chromium/ash/app_list/app_list_metrics.cc

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

#include "ash/app_list/app_list_metrics.h"

#include <algorithm>
#include <map>
#include <string>

#include "ash/app_list/app_list_controller_impl.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/apps_collections_controller.h"
#include "ash/app_list/model/app_list_folder_item.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_item_list.h"
#include "ash/app_list/views/continue_section_view.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/app_menu_constants.h"
#include "ash/shell.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/strings/strcat.h"
#include "base/types/cxx23_to_underlying.h"
#include "ui/compositor/compositor.h"

namespace ash {

// The number of files removed from the continue section during this session.
int g_continue_file_removals_in_session = 0;

// The UMA histogram that logs smoothness of pagination animation.
constexpr char kPaginationTransitionAnimationSmoothnessInTablet[] =
    "Apps.PaginationTransition.AnimationSmoothness.TabletMode";

// The UMA histogram that logs smoothness of cardified animation.
constexpr char kCardifiedStateAnimationSmoothnessEnter[] =
    "Apps.AppList.CardifiedStateAnimation.AnimationSmoothness."
    "EnterCardifiedState";
constexpr char kCardifiedStateAnimationSmoothnessExit[] =
    "Apps.AppList.CardifiedStateAnimation.AnimationSmoothness."
    "ExitCardifiedState";

// The UMA histogram that logs user's decision (remove or cancel) for search
// result removal confirmation. Result removal is enabled outside zero state
// search.
constexpr char kSearchResultRemovalDialogDecisionHistogram[] =
    "Apps.AppList.SearchResultRemovalDecision";

// The base UMA histogram that logs app launches within the HomeLauncher (tablet
// mode AppList) and the Shelf.
constexpr char kAppListAppLaunched[] = "Apps.AppListAppLaunchedV2";

// UMA histograms that log launcher workflow actions (launching an app, search
// result, or a continue section task) in the app list UI. Split depending on
// whether tablet mode is active or not. Note that unlike `kAppListAppLaunched`
// histograms, these do not include actions from shelf, but do include non-app
// launch actions.
constexpr char kLauncherUserActionInTablet[] =
    "Apps.AppList.UserAction.TabletMode";
constexpr char kLauncherUserActionInClamshell[] =
    "Apps.AppList.UserAction.ClamshellMode";

// UMA histograms that log time elapsed from launcher getting shown at the time
// of an user taking a launcher workflow action (launching an app, search
// result, or a continue section task) in the app list UI. Split depending on
// whether tablet mode is active or not.
constexpr char kTimeToLauncherUserActionInTablet[] =
    "Apps.AppList.TimeToUserAction.TabletMode";
constexpr char kTimeToLauncherUserActionInClamshell[] =
    "Apps.AppList.TimeToUserAction.ClamshellMode";

// The UMA histograms that log app launches within the AppList, AppListBubble
// and Shelf. The app launches are divided by histogram for each of the the
// different AppList states.
constexpr char kAppListAppLaunchedBubbleAllApps[] =
    "Apps.AppListAppLaunchedV2.BubbleAllApps";
constexpr char kAppListAppLaunchedClosed[] = "Apps.AppListAppLaunchedV2.Closed";
constexpr char kAppListAppLaunchedFullscreenAllApps[] =
    "Apps.AppListAppLaunchedV2.FullscreenAllApps";
constexpr char kAppListAppLaunchedFullscreenSearch[] =
    "Apps.AppListAppLaunchedV2.FullscreenSearch";
constexpr char kAppListAppLaunchedHomecherClosed[] =
    "Apps.AppListAppLaunchedV2.HomecherClosed";
constexpr char kAppListAppLaunchedHomecherAllApps[] =
    "Apps.AppListAppLaunchedV2.HomecherAllApps";
constexpr char kAppListAppLaunchedHomecherSearch[] =
    "Apps.AppListAppLaunchedV2.HomecherSearch";

// UMA histograms for app list sort reorder.
constexpr char kClamshellReorderAnimationSmoothnessHistogram[] =
    "Apps.Launcher.ProductivityReorderAnimationSmoothness.ClamshellMode";
constexpr char kTabletReorderAnimationSmoothnessHistogram[] =
    "Apps.Launcher.ProductivityReorderAnimationSmoothness.TabletMode";
constexpr char kClamshellReorderActionHistogram[] =
    "Apps.Launcher.ProductivityReorderAction.ClamshellMode";
constexpr char kTabletReorderActionHistogram[] =
    "Apps.Launcher.ProductivityReorderAction.TabletMode";

// UMA histograms for app list drag reorder.
constexpr char kClamshellDragReorderAnimationSmoothnessHistogram[] =
    "Apps.Launcher.DragReorderAnimationSmoothness.ClamshellMode";
constexpr char kTabletDragReorderAnimationSmoothnessHistogram[] =
    "Apps.Launcher.DragReorderAnimationSmoothness.TabletMode";

// The prefix for all the variants that track how long the app list is kept
// open by open method. Suffix is decided in `GetAppListOpenMethod`
constexpr char kAppListOpenTimePrefix[] = "Apps.AppListOpenTime.";

constexpr char kContinueSectionFilesRemovedInSessionHistogram[] =
    "Apps.AppList.Search.ContinueSectionFilesRemovedPerSession";

constexpr char kSearchCategoryFilterMenuOpened[] =
    "Apps.AppList.Search.SearchCategoryFilterMenuOpenedCount";
constexpr char kSearchCategoriesEnableStateHeader[] =
    "Apps.AppList.Search.SearchCategoriesEnableState.";

std::string GetCategoryString(AppListSearchControlCategory category) {
  switch (category) {
    case AppListSearchControlCategory::kApps:
      return "Apps";
    case AppListSearchControlCategory::kAppShortcuts:
      return "AppShortcuts";
    case AppListSearchControlCategory::kFiles:
      return "Files";
    case AppListSearchControlCategory::kGames:
      return "Games";
    case AppListSearchControlCategory::kHelp:
      return "Helps";
    case AppListSearchControlCategory::kImages:
      return "Images";
    case AppListSearchControlCategory::kPlayStore:
      return "PlayStore";
    case AppListSearchControlCategory::kWeb:
      return "Web";
    case AppListSearchControlCategory::kCannotToggle:
      NOTREACHED();
  }
}

AppLaunchedMetricParams::AppLaunchedMetricParams() = default;

AppLaunchedMetricParams::AppLaunchedMetricParams(
    const AppLaunchedMetricParams&) = default;

AppLaunchedMetricParams& AppLaunchedMetricParams::operator=(
    const AppLaunchedMetricParams&) = default;

AppLaunchedMetricParams::AppLaunchedMetricParams(
    AppListLaunchedFrom launched_from,
    AppListLaunchType launch_type)
    : launched_from(launched_from), launch_type(launch_type) {}

AppLaunchedMetricParams::~AppLaunchedMetricParams() = default;

void AppListRecordPageSwitcherSourceByEventType(ui::EventType type) {
  AppListPageSwitcherSource source;

  switch (type) {
    case ui::EventType::kMousewheel:
      source = kMouseWheelScroll;
      break;
    case ui::EventType::kScroll:
      source = kMousePadScroll;
      break;
    case ui::EventType::kGestureScrollEnd:
      source = kSwipeAppGrid;
      break;
    case ui::EventType::kScrollFlingStart:
      source = kFlingAppGrid;
      break;
    case ui::EventType::kMouseReleased:
      source = kMouseDrag;
      break;
    default:
      NOTREACHED();
  }
  RecordPageSwitcherSource(source);
}

void RecordPageSwitcherSource(AppListPageSwitcherSource source) {
  UMA_HISTOGRAM_ENUMERATION("Apps.AppListPageSwitcherSource", source,
                            kMaxAppListPageSwitcherSource);
}

void RecordSearchResultRemovalDialogDecision(
    SearchResultRemovalConfirmation removal_decision) {
  base::UmaHistogramEnumeration(kSearchResultRemovalDialogDecisionHistogram,
                                removal_decision);
}

std::string GetAppListOpenMethod(AppListShowSource source) {
  // This switch determines which metric we submit for the Apps.AppListOpenTime
  // metric. Adding a string requires you update the apps histogram.xml as well.
  switch (source) {
    case AppListShowSource::kSearchKey:
    case AppListShowSource::kSearchKeyFullscreen_DEPRECATED:
      return "SearchKey";
    case AppListShowSource::kShelfButton:
    case AppListShowSource::kShelfButtonFullscreen_DEPRECATED:
      return "HomeButton";
    case AppListShowSource::kSwipeFromShelf:
      return "Swipe";
    case AppListShowSource::kScrollFromShelf:
      return "Scroll";
    case AppListShowSource::kTabletMode:
    case AppListShowSource::kAssistantEntryPoint:
    case AppListShowSource::kBrowser:
    case AppListShowSource::kWelcomeTour:
      return "Others";
  }
  NOTREACHED();
}

void RecordAppListUserJourneyTime(AppListShowSource source,
                                  base::TimeDelta time) {
  base::UmaHistogramMediumTimes(
      kAppListOpenTimePrefix + GetAppListOpenMethod(source), time);
}

void RecordPeriodicAppListMetrics() {
  int number_of_apps_in_launcher = 0;
  int number_of_root_level_items = 0;
  int number_of_folders = 0;
  int number_of_non_system_folders = 0;
  int number_of_apps_in_non_system_folders = 0;

  AppListModel* const model = AppListModelProvider::Get()->model();
  AppListItemList* const item_list = model->top_level_item_list();
  for (size_t i = 0; i < item_list->item_count(); ++i) {
    AppListItem* item = item_list->item_at(i);
    number_of_root_level_items++;

    // Item is a folder.
    if (item->GetItemType() == AppListFolderItem::kItemType) {
      AppListFolderItem* folder = static_cast<AppListFolderItem*>(item);
      number_of_apps_in_launcher += folder->item_list()->item_count();
      number_of_folders++;

      // Ignore the OEM folder and the "Linux apps" folder because those folders
      // are automatically created. The following metrics are trying to measure
      // how often users engage with folders that they created themselves.
      if (folder->IsSystemFolder())
        continue;
      number_of_apps_in_non_system_folders += folder->item_list()->item_count();
      number_of_non_system_folders++;
      continue;
    }

    // Item is an app that isn't in a folder.
    number_of_apps_in_launcher++;
  }

  UMA_HISTOGRAM_COUNTS_100("Apps.AppList.NumberOfApps",
                           number_of_apps_in_launcher);
  UMA_HISTOGRAM_COUNTS_100("Apps.AppList.NumberOfRootLevelItems",
                           number_of_root_level_items);
  UMA_HISTOGRAM_COUNTS_100("Apps.AppList.NumberOfFolders", number_of_folders);
  UMA_HISTOGRAM_COUNTS_100("Apps.AppList.NumberOfNonSystemFolders",
                           number_of_non_system_folders);
  UMA_HISTOGRAM_COUNTS_100("Apps.AppList.NumberOfAppsInNonSystemFolders",
                           number_of_apps_in_non_system_folders);
}

void RecordAppListByCollectionLaunched(AppCollection collection,
                                       bool is_apps_collections_page) {
  AppEntity app_entity = collection == AppCollection::kUnknown
                             ? AppEntity::kThirdPartyApp
                             : AppEntity::kDefaultApp;

  const std::string apps_collections_state =
      ash::AppsCollectionsController::Get()
          ->GetUserExperimentalArmAsHistogramSuffix();
  const std::string app_list_page =
      is_apps_collections_page ? "AppsCollectionsPage" : "AppsPage";

  base::UmaHistogramEnumeration(
      base::StrCat({"Apps.AppListBubble.", app_list_page,
                    ".AppLaunchesByEntity", apps_collections_state}),
      app_entity);
  base::UmaHistogramEnumeration(
      base::StrCat({"Apps.AppListBubble.", app_list_page,
                    ".AppLaunchesByCategory", apps_collections_state}),
      collection);
}

void RecordAppListAppLaunched(AppListLaunchedFrom launched_from,
                              AppListViewState app_list_state,
                              bool is_tablet_mode,
                              bool app_list_shown) {
  UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunched, launched_from);

  if (!is_tablet_mode) {
    if (!app_list_shown) {
      UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedClosed, launched_from);
    } else {
      // TODO(newcomer): Handle the case where search is open.
      UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedBubbleAllApps,
                                launched_from);
    }
    return;
  }

  switch (app_list_state) {
    case AppListViewState::kClosed:
      // The app list state may be set to closed while the device is animating
      // to tablet mode. While this transition is running, a user may be able to
      // launch an app.
      DCHECK_EQ(launched_from, AppListLaunchedFrom::kLaunchedFromShelf);
      UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedClosed, launched_from);
      break;
    case AppListViewState::kFullscreenAllApps:
      if (is_tablet_mode) {
        if (app_list_shown) {
          UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedHomecherAllApps,
                                    launched_from);
        } else {
          UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedHomecherClosed,
                                    launched_from);
        }
      } else {
        UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedFullscreenAllApps,
                                  launched_from);
      }
      break;
    case AppListViewState::kFullscreenSearch:
      if (is_tablet_mode) {
        if (app_list_shown) {
          UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedHomecherSearch,
                                    launched_from);
        } else {
          // (http://crbug.com/947729) Search box still expanded when opening
          // launcher in tablet mode
          UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedHomecherClosed,
                                    launched_from);
        }
      } else {
        UMA_HISTOGRAM_ENUMERATION(kAppListAppLaunchedFullscreenSearch,
                                  launched_from);
      }
      break;
  }
}

ASH_EXPORT void RecordLauncherWorkflowMetrics(
    AppListUserAction action,
    bool is_tablet_mode,
    std::optional<base::TimeTicks> launcher_show_time) {
  if (is_tablet_mode) {
    base::UmaHistogramEnumeration(kLauncherUserActionInTablet, action);

    if (launcher_show_time) {
      base::UmaHistogramMediumTimes(
          kTimeToLauncherUserActionInTablet,
          base::TimeTicks::Now() - *launcher_show_time);
    }
  } else {
    base::UmaHistogramEnumeration(kLauncherUserActionInClamshell, action);

    if (launcher_show_time) {
      base::UmaHistogramMediumTimes(
          kTimeToLauncherUserActionInClamshell,
          base::TimeTicks::Now() - *launcher_show_time);
    }
  }
}

bool IsCommandIdAnAppLaunch(int command_id_number) {
  CommandId command_id = static_cast<CommandId>(command_id_number);

  // Consider all platform app menu options as launches.
  if (command_id >= CommandId::EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
      command_id < CommandId::EXTENSIONS_CONTEXT_CUSTOM_LAST) {
    return true;
  }

  // Consider all arc app shortcut options as launches.
  if (command_id >= CommandId::LAUNCH_APP_SHORTCUT_FIRST &&
      command_id < CommandId::LAUNCH_APP_SHORTCUT_LAST) {
    return true;
  }

  // All app menu items in a ShelfApplicationMenuModel are not launches.
  if (command_id >= CommandId::APP_MENU_ITEM_ID_FIRST &&
      command_id < CommandId::APP_MENU_ITEM_ID_LAST) {
    return false;
  }

  switch (command_id) {
    // Used by AppContextMenu and/or ShelfContextMenu.
    case CommandId::LAUNCH_NEW:
    case CommandId::SHOW_APP_INFO:
    case CommandId::OPTIONS:
    case CommandId::APP_CONTEXT_MENU_NEW_WINDOW:
    case CommandId::APP_CONTEXT_MENU_NEW_INCOGNITO_WINDOW:
    case CommandId::SETTINGS:
    // Used by both AppContextMenu and ShelfContextMenu for app shortcuts.
    case CommandId::LAUNCH_APP_SHORTCUT_FIRST:
    case CommandId::LAUNCH_APP_SHORTCUT_LAST:
      return true;

    // Used by ShelfContextMenu (shelf).
    case CommandId::MENU_CLOSE:
    case CommandId::SWAP_WITH_NEXT:
    case CommandId::SWAP_WITH_PREVIOUS:
    // Used by AppMenuModelAdapter
    case CommandId::NOTIFICATION_CONTAINER:
    // Used by CrostiniShelfContextMenu.
    case CommandId::CROSTINI_USE_LOW_DENSITY:
    case CommandId::CROSTINI_USE_HIGH_DENSITY:
    // Used by AppContextMenu.
    case CommandId::TOGGLE_PIN:
    case CommandId::UNINSTALL:
    case CommandId::REMOVE_FROM_FOLDER:
    case CommandId::INSTALL:
    case CommandId::USE_LAUNCH_TYPE_REGULAR:
    case CommandId::USE_LAUNCH_TYPE_WINDOW:
    case CommandId::USE_LAUNCH_TYPE_TABBED_WINDOW:
    case CommandId::USE_LAUNCH_TYPE_COMMAND_END:
    case CommandId::REORDER_SUBMENU:
    case CommandId::REORDER_BY_NAME_ALPHABETICAL:
    case CommandId::REORDER_BY_NAME_REVERSE_ALPHABETICAL:
    case CommandId::REORDER_BY_COLOR:
    case CommandId::SHUTDOWN_GUEST_OS:
    case CommandId::SHUTDOWN_BRUSCHETTA_OS:
    case CommandId::EXTENSIONS_CONTEXT_CUSTOM_FIRST:
    case CommandId::EXTENSIONS_CONTEXT_CUSTOM_LAST:
    case CommandId::COMMAND_ID_COUNT:
    // Used by ShelfApplicationMenuModel.
    case CommandId::APP_MENU_ITEM_ID_FIRST:
    case CommandId::APP_MENU_ITEM_ID_LAST:
      return false;
    case CommandId::DEPRECATED_MENU_OPEN_NEW:
    case CommandId::DEPRECATED_MENU_PIN:
    case CommandId::DEPRECATED_MENU_NEW_WINDOW:
    case CommandId::DEPRECATED_MENU_NEW_INCOGNITO_WINDOW:
    case CommandId::DEPRECATED_LAUNCH_TYPE_PINNED_TAB:
    case CommandId::DEPRECATED_LAUNCH_TYPE_REGULAR_TAB:
    case CommandId::DEPRECATED_LAUNCH_TYPE_WINDOW:
    case CommandId::DEPRECATED_LAUNCH_TYPE_TABBED_WINDOW:
    case CommandId::DEPRECATED_LAUNCH_TYPE_FULLSCREEN:
    case CommandId::DEPRECATED_USE_LAUNCH_TYPE_PINNED:
    case CommandId::DEPRECATED_USE_LAUNCH_TYPE_FULLSCREEN:
      NOTREACHED();
  }
  NOTREACHED();
}

void ReportPaginationSmoothness(int smoothness) {
  UMA_HISTOGRAM_PERCENTAGE(kPaginationTransitionAnimationSmoothnessInTablet,
                           smoothness);
}

void ReportCardifiedSmoothness(bool is_entering_cardified, int smoothness) {
  if (is_entering_cardified) {
    UMA_HISTOGRAM_PERCENTAGE(kCardifiedStateAnimationSmoothnessEnter,
                             smoothness);
  } else {
    UMA_HISTOGRAM_PERCENTAGE(kCardifiedStateAnimationSmoothnessExit,
                             smoothness);
  }
}

// Reports reorder animation smoothness.
void ReportReorderAnimationSmoothness(bool in_tablet, int smoothness) {
  if (in_tablet) {
    base::UmaHistogramPercentage(kTabletReorderAnimationSmoothnessHistogram,
                                 smoothness);
  } else {
    base::UmaHistogramPercentage(kClamshellReorderAnimationSmoothnessHistogram,
                                 smoothness);
  }
}

void RecordAppListSortAction(AppListSortOrder new_order, bool in_tablet) {
  // NOTE: (1) kNameReverseAlphabetical is not used for now; (2) Resetting the
  // sort order is not recorded here.
  DCHECK(new_order != AppListSortOrder::kNameReverseAlphabetical &&
         new_order != AppListSortOrder::kCustom);

  if (in_tablet)
    base::UmaHistogramEnumeration(kTabletReorderActionHistogram, new_order);
  else
    base::UmaHistogramEnumeration(kClamshellReorderActionHistogram, new_order);
}

void ReportItemDragReorderAnimationSmoothness(bool in_tablet, int smoothness) {
  if (in_tablet) {
    base::UmaHistogramPercentage(kTabletDragReorderAnimationSmoothnessHistogram,
                                 smoothness);
  } else {
    base::UmaHistogramPercentage(
        kClamshellDragReorderAnimationSmoothnessHistogram, smoothness);
  }
}

void RecordMetricsOnSessionEnd() {
  if (ContinueSectionView::EnableContinueSectionFileRemovalMetrics() &&
      g_continue_file_removals_in_session == 0) {
    base::UmaHistogramCounts100(kContinueSectionFilesRemovedInSessionHistogram,
                                0);
  }
}

void RecordCumulativeContinueSectionResultRemovedNumber() {
  base::UmaHistogramCounts100(kContinueSectionFilesRemovedInSessionHistogram,
                              ++g_continue_file_removals_in_session);
}

void ResetContinueSectionFileRemovedCountForTest() {
  g_continue_file_removals_in_session = 0;
}

void RecordHideContinueSectionMetric() {
  const bool hide_continue_section =
      Shell::Get()->app_list_controller()->ShouldHideContinueSection();
  if (Shell::Get()->IsInTabletMode()) {
    base::UmaHistogramBoolean(
        "Apps.AppList.ContinueSectionHiddenByUser.TabletMode",
        hide_continue_section);
  } else {
    base::UmaHistogramBoolean(
        "Apps.AppList.ContinueSectionHiddenByUser.ClamshellMode",
        hide_continue_section);
  }
}

void RecordSearchCategoryFilterMenuOpened() {
  base::UmaHistogramCounts100(kSearchCategoryFilterMenuOpened, 1);
}

void RecordSearchCategoryEnableState(
    const CategoryEnableStateMap& category_to_state) {
  for (auto category_state_pair : category_to_state) {
    std::string histogram =
        base::StrCat({kSearchCategoriesEnableStateHeader,
                      GetCategoryString(category_state_pair.first)});
    base::UmaHistogramEnumeration(histogram, category_state_pair.second);
  }
}

}  // namespace ash