chromium/chrome/browser/ash/note_taking/note_taking_helper.cc

// 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.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chrome/browser/ash/note_taking/note_taking_helper.h"

#include <stddef.h>

#include <atomic>
#include <map>
#include <ostream>
#include <utility>

#include "apps/launcher.h"
#include "ash/components/arc/metrics/arc_metrics_constants.h"
#include "ash/components/arc/metrics/arc_metrics_service.h"
#include "ash/components/arc/mojom/file_system.mojom-forward.h"
#include "ash/components/arc/mojom/file_system.mojom.h"
#include "ash/components/arc/mojom/intent_common.mojom-forward.h"
#include "ash/components/arc/mojom/intent_common.mojom-shared.h"
#include "ash/components/arc/mojom/intent_common.mojom.h"
#include "ash/components/arc/mojom/intent_helper.mojom.h"
#include "ash/components/arc/session/arc_bridge_service.h"
#include "ash/components/arc/session/arc_service_manager.h"
#include "ash/components/arc/session/connection_holder.h"
#include "ash/constants/ash_switches.h"
#include "ash/public/cpp/stylus_utils.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/debug/dump_without_crashing.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/string_split.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/apps/app_service/app_service_proxy_forward.h"
#include "chrome/browser/apps/app_service/launch_utils.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/arc/session/arc_session_manager.h"
#include "chrome/browser/ash/lock_screen_apps/lock_screen_apps.h"
#include "chrome/browser/ash/note_taking/note_taking_controller_client.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/web_applications/web_app_id_constants.h"
#include "chrome/common/pref_names.h"
#include "components/arc/intent_helper/arc_intent_helper_bridge.h"
#include "components/prefs/pref_service.h"
#include "components/services/app_service/public/cpp/app_launch_util.h"
#include "components/services/app_service/public/cpp/app_registry_cache.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "components/services/app_service/public/cpp/intent.h"
#include "components/services/app_service/public/cpp/intent_filter.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/common/api/app_runtime.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/manifest_handlers/action_handlers_handler.h"
#include "mojo/public/cpp/bindings/struct_ptr.h"
#include "ui/display/display.h"
#include "ui/display/util/display_util.h"
#include "ui/events/event_constants.h"
#include "url/gurl.h"

namespace ash {
namespace {

namespace app_runtime = ::extensions::api::app_runtime;

// Pointer to singleton instance.
NoteTakingHelper* g_helper = nullptr;

// Allowed note-taking app IDs. These will be treated as note-taking apps
// regardless of the app metadata, and will be shown in this order at the top of
// the list of note-taking apps.
const char* const kDefaultAllowedAppIds[] = {
    web_app::kCursiveAppId,
    NoteTakingHelper::kDevKeepExtensionId,
    NoteTakingHelper::kProdKeepExtensionId,
    NoteTakingHelper::kNoteTakingWebAppIdTest,
};

// Types of App Service apps that support note taking. Note that Note Taking
// Chrome Apps are not supported in Lacros, so kStandaloneBrowserChromeApp is
// not included.
// TODO (crbug.com/1336120): Add Android here.
const apps::AppType kNoteTakingAppTypes[] = {apps::AppType::kWeb,
                                             apps::AppType::kChromeApp};

// Returns whether `app_id` looks like it's probably an Android package name
// rather than a Chrome extension ID or web app ID.
bool LooksLikeAndroidPackageName(const std::string& app_id) {
  // Android package names are required to contain at least one period (see
  // validateName() in PackageParser.java), while Chrome extension IDs and web
  // app IDs contain only characters in [a-p].
  return base::Contains(app_id, '.');
}

bool IsInstalledApp(const std::string& app_id, Profile* profile) {
  if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile))
    return false;
  auto& cache =
      apps::AppServiceProxyFactory::GetForProfile(profile)->AppRegistryCache();

  bool result = false;
  cache.ForOneApp(app_id, [&result](const apps::AppUpdate& update) {
    if (apps_util::IsInstalled(update.Readiness())) {
      result = true;
    }
  });
  return result;
}

bool IsInstalledWebApp(const std::string& app_id, Profile* profile) {
  if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile))
    return false;
  auto& cache =
      apps::AppServiceProxyFactory::GetForProfile(profile)->AppRegistryCache();

  bool result = false;
  cache.ForOneApp(app_id, [&result](const apps::AppUpdate& update) {
    if (apps_util::IsInstalled(update.Readiness()) &&
        update.AppType() == apps::AppType::kWeb) {
      result = true;
    }
  });
  return result;
}

// Creates a new Mojo IntentInfo struct for launching an Android note-taking app
// with an optional ClipData URI.
arc::mojom::IntentInfoPtr CreateIntentInfo(const GURL& clip_data_uri) {
  arc::mojom::IntentInfoPtr intent = arc::mojom::IntentInfo::New();
  intent->action = NoteTakingHelper::kIntentAction;
  if (!clip_data_uri.is_empty())
    intent->clip_data_uri = clip_data_uri.spec();
  return intent;
}

// Returns the name of the installed app with the given `app_id`.
std::string GetAppName(Profile* profile, const std::string& app_id) {
  DCHECK(!app_id.empty());
  std::string name;
  if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile))
    return name;
  auto& cache =
      apps::AppServiceProxyFactory::GetForProfile(profile)->AppRegistryCache();

  cache.ForOneApp(app_id, [&name](const apps::AppUpdate& update) {
    if (apps_util::IsInstalled(update.Readiness()))
      name = update.Name();
  });

  if (!name.empty())
    return name;

  // TODO(crbug.com/40758396): Remove once Chrome Apps are gone or Lacros
  // launches, as note-taking Chrome Apps will not be supported in Lacros.
  const extensions::Extension* chrome_app =
      extensions::ExtensionRegistry::Get(profile)->enabled_extensions().GetByID(
          app_id);
  DCHECK(chrome_app) << "app_id must be a valid app";
  name = chrome_app->name();

  DCHECK(!name.empty()) << "app_id must be a valid app";
  return name;
}

bool IsNoteTakingIntentFilter(const apps::IntentFilterPtr& filter) {
  for (const auto& condition : filter->conditions) {
    if (condition->condition_type != apps::ConditionType::kAction)
      continue;

    for (const auto& condition_value : condition->condition_values) {
      if (condition_value->value == apps_util::kIntentActionCreateNote)
        return true;
    }
  }
  return false;
}

bool HasNoteTakingIntentFilter(
    const std::vector<apps::IntentFilterPtr>& filters) {
  for (const apps::IntentFilterPtr& filter : filters) {
    if (IsNoteTakingIntentFilter(filter))
      return true;
  }
  return false;
}

NoteTakingHelper::LaunchResult LaunchWebAppInternal(const std::string& app_id,
                                                    Profile* profile) {
  // IsInstalledWebApp must be called before trying to launch. It also ensures
  // App Service is available.
  DCHECK(IsInstalledWebApp(app_id, profile));
  DCHECK(
      apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile));
  auto& cache =
      apps::AppServiceProxyFactory::GetForProfile(profile)->AppRegistryCache();

  bool has_note_taking_intent_filter = false;
  cache.ForOneApp(
      app_id, [&has_note_taking_intent_filter](const apps::AppUpdate& update) {
        if (HasNoteTakingIntentFilter(update.IntentFilters()))
          has_note_taking_intent_filter = true;
      });

  // Apps in 'kDefaultAllowedAppIds' might not have a note-taking intent filter.
  // They can just launch without the intent.
  if (has_note_taking_intent_filter) {
    apps::AppServiceProxyFactory::GetForProfile(profile)->LaunchAppWithIntent(
        app_id, ui::EF_NONE, apps_util::CreateCreateNoteIntent(),
        apps::LaunchSource::kFromShelf, nullptr, base::DoNothing());
  } else {
    apps::AppServiceProxyFactory::GetForProfile(profile)->Launch(
        app_id, ui::EF_NONE, apps::LaunchSource::kFromShelf);
  }

  return NoteTakingHelper::LaunchResult::WEB_APP_SUCCESS;
}

}  // namespace

const char NoteTakingHelper::kIntentAction[] =
    "org.chromium.arc.intent.action.CREATE_NOTE";
// ID of a Keep Chrome App used for dev and testing.
const char NoteTakingHelper::kDevKeepExtensionId[] =
    "ogfjaccbdfhecploibfbhighmebiffla";
const char NoteTakingHelper::kProdKeepExtensionId[] =
    "hmjkmjkepdijhoojdojkdfohbdgmmhki";
// ID of a test web app (https://yielding-large-chef.glitch.me/).
const char NoteTakingHelper::kNoteTakingWebAppIdTest[] =
    "clikhfibhokkkabhcgdhcccofienkkhj";
const char NoteTakingHelper::kPreferredLaunchResultHistogramName[] =
    "Apps.NoteTakingApp.PreferredLaunchResult";
const char NoteTakingHelper::kDefaultLaunchResultHistogramName[] =
    "Apps.NoteTakingApp.DefaultLaunchResult";

// static
void NoteTakingHelper::Initialize() {
  DCHECK(!g_helper);
  g_helper = new NoteTakingHelper();
}

// static
void NoteTakingHelper::Shutdown() {
  DCHECK(g_helper);
  delete g_helper;
  g_helper = nullptr;
}

// static
NoteTakingHelper* NoteTakingHelper::Get() {
  DCHECK(g_helper);
  return g_helper;
}

void NoteTakingHelper::AddObserver(Observer* observer) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(observer);
  observers_.AddObserver(observer);
}

void NoteTakingHelper::RemoveObserver(Observer* observer) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(observer);
  observers_.RemoveObserver(observer);
}

// TODO(crbug.com/40227659): Remove this method and observe LockScreenHelper for
// app updates instead.
void NoteTakingHelper::NotifyAppUpdated(Profile* profile,
                                        const std::string& app_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (app_id == GetPreferredAppId(profile)) {
    for (Observer& observer : observers_) {
      observer.OnPreferredNoteTakingAppUpdated(profile);
    }
  }
}

std::vector<NoteTakingAppInfo> NoteTakingHelper::GetAvailableApps(
    Profile* profile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(profile);
  std::vector<NoteTakingAppInfo> infos;

  std::vector<std::string> app_ids = GetNoteTakingAppIds(profile);
  for (const auto& app_id : app_ids) {
    LockScreenAppSupport lock_screen_support =
        LockScreenApps::GetSupport(profile, app_id);
    infos.push_back(NoteTakingAppInfo{GetAppName(profile, app_id), app_id,
                                      /*preferred=*/false,
                                      lock_screen_support});
  }

  if (arc::IsArcAllowedForProfile(profile))
    infos.insert(infos.end(), android_apps_.begin(), android_apps_.end());

  // Determine which app, if any, is selected as the preferred note taking app.
  const std::string pref_app_id =
      profile->GetPrefs()->GetString(prefs::kNoteTakingAppId);
  for (auto& info : infos) {
    if (info.app_id == pref_app_id) {
      info.preferred = true;
      break;
    }
  }

  return infos;
}

std::string NoteTakingHelper::GetPreferredAppId(Profile* profile) {
  std::string app_id = profile->GetPrefs()->GetString(prefs::kNoteTakingAppId);
  if (IsInstalledApp(app_id, profile))
    return app_id;
  return std::string();
}

void NoteTakingHelper::SetPreferredApp(Profile* profile,
                                       const std::string& app_id) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(profile);

  if (app_id == profile->GetPrefs()->GetString(prefs::kNoteTakingAppId))
    return;

  profile->GetPrefs()->SetString(prefs::kNoteTakingAppId, app_id);

  for (Observer& observer : observers_)
    observer.OnPreferredNoteTakingAppUpdated(profile);
}

bool NoteTakingHelper::SetPreferredAppEnabledOnLockScreen(Profile* profile,
                                                          bool enabled) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(profile);

  std::string app_id = profile->GetPrefs()->GetString(prefs::kNoteTakingAppId);
  if (app_id.empty())
    return false;

  LockScreenApps* lock_screen_apps =
      LockScreenAppsFactory::GetInstance()->Get(profile);
  if (!lock_screen_apps)
    return false;

  bool changed = lock_screen_apps->SetAppEnabledOnLockScreen(app_id, enabled);
  if (!changed)
    return false;

  for (Observer& observer : observers_)
    observer.OnPreferredNoteTakingAppUpdated(profile);

  return true;
}

bool NoteTakingHelper::IsAppAvailable(Profile* profile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(profile);
  return stylus_utils::HasStylusInput() && !GetAvailableApps(profile).empty();
}

void NoteTakingHelper::LaunchAppForNewNote(Profile* profile) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  DCHECK(profile);

  LaunchResult result = LaunchResult::NO_APP_SPECIFIED;
  std::string app_id = profile->GetPrefs()->GetString(prefs::kNoteTakingAppId);
  if (!app_id.empty())
    result = LaunchAppInternal(profile, app_id);
  UMA_HISTOGRAM_ENUMERATION(kPreferredLaunchResultHistogramName,
                            static_cast<int>(result),
                            static_cast<int>(LaunchResult::MAX));
  if (result == LaunchResult::CHROME_SUCCESS ||
      result == LaunchResult::WEB_APP_SUCCESS ||
      result == LaunchResult::ANDROID_SUCCESS) {
    return;
  }

  // If the user hasn't chosen an app or we were unable to launch the one that
  // they've chosen, just launch the first one we see.
  result = LaunchResult::NO_APPS_AVAILABLE;
  std::vector<NoteTakingAppInfo> infos = GetAvailableApps(profile);
  if (infos.empty()) {
    LOG(WARNING) << "Unable to launch note-taking app; none available";
  } else {
    result = LaunchAppInternal(profile, infos[0].app_id);
  }
  UMA_HISTOGRAM_ENUMERATION(kDefaultLaunchResultHistogramName,
                            static_cast<int>(result),
                            static_cast<int>(LaunchResult::MAX));
}

void NoteTakingHelper::OnIntentFiltersUpdated(
    const std::optional<std::string>& package_name) {
  if (play_store_enabled_)
    UpdateAndroidApps();
}

void NoteTakingHelper::OnArcPlayStoreEnabledChanged(bool enabled) {
  play_store_enabled_ = enabled;
  if (!enabled) {
    android_apps_.clear();
    android_apps_received_ = false;
  }
  for (Observer& observer : observers_)
    observer.OnAvailableNoteTakingAppsUpdated();
}

void NoteTakingHelper::OnProfileAdded(Profile* profile) {
  if (apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile)) {
    auto& cache = apps::AppServiceProxyFactory::GetForProfile(profile)
                      ->AppRegistryCache();
    if (app_registry_observations_.IsObservingSource(&cache)) {
      base::debug::DumpWithoutCrashing();
    } else {
      app_registry_observations_.AddObservation(&cache);
    }
  }

  if (!play_store_enabled_ && arc::IsArcPlayStoreEnabledForProfile(profile)) {
    play_store_enabled_ = true;
    for (Observer& observer : observers_)
      observer.OnAvailableNoteTakingAppsUpdated();
  }

  auto* bridge = arc::ArcIntentHelperBridge::GetForBrowserContext(profile);
  if (bridge) {
    if (arc_intent_helper_observations_.IsObservingSource(bridge)) {
      base::debug::DumpWithoutCrashing();
    } else {
      arc_intent_helper_observations_.AddObservation(bridge);
    }
  }
}

void NoteTakingHelper::OnProfileManagerDestroying() {
  profile_manager_observation_.Reset();
}

NoteTakingHelper::NoteTakingHelper()
    : launch_chrome_app_callback_(
          base::BindRepeating(&apps::LaunchPlatformAppWithAction)),
      note_taking_controller_client_(
          std::make_unique<NoteTakingControllerClient>(this)) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  const std::string switch_value =
      base::CommandLine::ForCurrentProcess()->GetSwitchValueASCII(
          switches::kNoteTakingAppIds);
  if (!switch_value.empty()) {
    force_allowed_app_ids_ = base::SplitString(
        switch_value, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
  }
  force_allowed_app_ids_.insert(
      force_allowed_app_ids_.end(), kDefaultAllowedAppIds,
      kDefaultAllowedAppIds + std::size(kDefaultAllowedAppIds));

  // Track profiles so we can observe their app registries.
  profile_manager_observation_.Observe(g_browser_process->profile_manager());
  play_store_enabled_ = false;
  for (Profile* profile :
       g_browser_process->profile_manager()->GetLoadedProfiles()) {
    if (apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
            profile)) {
      auto& cache = apps::AppServiceProxyFactory::GetForProfile(profile)
                        ->AppRegistryCache();
      if (app_registry_observations_.IsObservingSource(&cache)) {
        base::debug::DumpWithoutCrashing();
      } else {
        app_registry_observations_.AddObservation(&cache);
      }
    }

    // Check if the profile has already enabled Google Play Store.
    // IsArcPlayStoreEnabledForProfile() can return true only for the primary
    // profile.
    play_store_enabled_ |= arc::IsArcPlayStoreEnabledForProfile(profile);

    // ArcIntentHelperBridge will notify us about changes to the list of
    // available Android apps.
    auto* bridge = arc::ArcIntentHelperBridge::GetForBrowserContext(profile);
    if (bridge) {
      if (arc_intent_helper_observations_.IsObservingSource(bridge)) {
        base::debug::DumpWithoutCrashing();
      } else {
        arc_intent_helper_observations_.AddObservation(bridge);
      }
    }
  }

  // Watch for changes of Google Play Store enabled state.
  auto* session_manager = arc::ArcSessionManager::Get();
  session_manager->AddObserver(this);

  // If the ARC intent helper is ready, get the Android apps. Otherwise,
  // UpdateAndroidApps() will be called when ArcServiceManager calls
  // OnIntentFiltersUpdated().
  if (play_store_enabled_ && arc::ArcServiceManager::Get()
                                 ->arc_bridge_service()
                                 ->intent_helper()
                                 ->IsConnected()) {
    UpdateAndroidApps();
  }
}

NoteTakingHelper::~NoteTakingHelper() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  // ArcSessionManagerTest shuts down ARC before NoteTakingHelper.
  if (arc::ArcSessionManager::Get())
    arc::ArcSessionManager::Get()->RemoveObserver(this);
}

std::vector<std::string> NoteTakingHelper::GetNoteTakingAppIds(
    Profile* profile) const {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(profile))
    return {};

  auto& cache =
      apps::AppServiceProxyFactory::GetForProfile(profile)->AppRegistryCache();

  std::vector<std::string> app_ids;
  for (const auto& id : force_allowed_app_ids_) {
    cache.ForOneApp(id, [&app_ids](const apps::AppUpdate& update) {
      if (!apps_util::IsInstalled(update.Readiness()))
        return;
      if (!base::Contains(kNoteTakingAppTypes, update.AppType()))
        return;
      DCHECK(!base::Contains(app_ids, update.AppId()));
      app_ids.push_back(update.AppId());
    });
  }

  cache.ForEachApp([&app_ids](const apps::AppUpdate& update) {
    if (!apps_util::IsInstalled(update.Readiness()))
      return;
    if (base::Contains(app_ids, update.AppId()))
      return;
    if (!base::Contains(kNoteTakingAppTypes, update.AppType()))
      return;
    if (HasNoteTakingIntentFilter(update.IntentFilters())) {
      app_ids.push_back(update.AppId());
    }
  });

  return app_ids;
}

void NoteTakingHelper::UpdateAndroidApps() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  auto* helper = ARC_GET_INSTANCE_FOR_METHOD(
      arc::ArcServiceManager::Get()->arc_bridge_service()->intent_helper(),
      RequestIntentHandlerList);
  if (!helper)
    return;
  helper->RequestIntentHandlerList(
      CreateIntentInfo(GURL()),
      base::BindOnce(&NoteTakingHelper::OnGotAndroidApps,
                     weak_ptr_factory_.GetWeakPtr()));
}

arc::mojom::ActivityNamePtr AppIdToActivityName(const std::string& id) {
  auto name = arc::mojom::ActivityName::New();

  const size_t separator = id.find('/');
  if (separator == std::string::npos) {
    name->package_name = id;
    name->activity_name = std::string();
  } else {
    name->package_name = id.substr(0, separator);
    name->activity_name = id.substr(separator + 1);
  }
  return name;
}

void NoteTakingHelper::OnGotAndroidApps(
    std::vector<arc::mojom::IntentHandlerInfoPtr> handlers) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!play_store_enabled_)
    return;

  android_apps_.clear();
  android_apps_.reserve(handlers.size());
  for (const auto& it : handlers) {
    android_apps_.emplace_back(
        NoteTakingAppInfo{it->name, it->package_name, false,
                          LockScreenAppSupport::kNotSupported});
  }
  android_apps_received_ = true;

  for (Observer& observer : observers_)
    observer.OnAvailableNoteTakingAppsUpdated();
}

arc::mojom::OpenUrlsRequestPtr CreateArcNoteRequest(const std::string& app_id) {
  auto request = arc::mojom::OpenUrlsRequest::New();
  request->action_type = arc::mojom::ActionType::CREATE_NOTE;
  request->activity_name = AppIdToActivityName(app_id);
  return request;
}

NoteTakingHelper::LaunchResult NoteTakingHelper::LaunchAppInternal(
    Profile* profile,
    const std::string& app_id) {
  DCHECK(profile);

  // Android app.
  if (LooksLikeAndroidPackageName(app_id)) {
    // Android app.
    if (!arc::IsArcAllowedForProfile(profile)) {
      LOG(WARNING) << "Can't launch Android app " << app_id << " for profile";
      return LaunchResult::ANDROID_NOT_SUPPORTED_BY_PROFILE;
    }
    auto* helper = ARC_GET_INSTANCE_FOR_METHOD(
        arc::ArcServiceManager::Get()->arc_bridge_service()->intent_helper(),
        HandleIntent);
    if (!helper)
      return LaunchResult::ANDROID_NOT_RUNNING;

    // Only set the package name: leaving the activity name unset enables the
    // app to rename its activities.
    arc::mojom::ActivityNamePtr activity = arc::mojom::ActivityName::New();
    activity->package_name = app_id;

    auto request = CreateArcNoteRequest(app_id);
    arc::mojom::FileSystemInstance* arc_file_system =
        ARC_GET_INSTANCE_FOR_METHOD(
            arc::ArcServiceManager::Get()->arc_bridge_service()->file_system(),
            OpenUrlsWithPermissionAndWindowInfo);
    if (!arc_file_system)
      return LaunchResult::ANDROID_NOT_RUNNING;

    if (!display::HasInternalDisplay()) {
      LOG(WARNING) << "Cannot find an internal display!";
      return LaunchResult::NO_INTERNAL_DISPLAY_FOUND;
    }
    apps::WindowInfoPtr window_info = std::make_unique<apps::WindowInfo>(
        display::Display::InternalDisplayId());
    arc_file_system->OpenUrlsWithPermissionAndWindowInfo(
        std::move(request), apps::MakeArcWindowInfo(std::move(window_info)),
        base::DoNothing());

    arc::ArcMetricsService::RecordArcUserInteraction(
        profile, arc::UserInteractionType::APP_STARTED_FROM_STYLUS_TOOLS);

    return LaunchResult::ANDROID_SUCCESS;
  }

  // Web app.
  if (IsInstalledWebApp(app_id, profile))
    return LaunchWebAppInternal(app_id, profile);

  // Chrome app.
  const extensions::ExtensionRegistry* extension_registry =
      extensions::ExtensionRegistry::Get(profile);
  const extensions::Extension* app =
      extension_registry->enabled_extensions().GetByID(app_id);
  if (!app) {
    LOG(WARNING) << "Failed to find note-taking app " << app_id;
    return LaunchResult::CHROME_APP_MISSING;
  }
  app_runtime::ActionData action_data;
  action_data.action_type = app_runtime::ActionType::kNewNote;
  launch_chrome_app_callback_.Run(profile, app, std::move(action_data));
  return LaunchResult::CHROME_SUCCESS;
}

void NoteTakingHelper::OnAppUpdate(const apps::AppUpdate& update) {
  if (!base::Contains(kNoteTakingAppTypes, update.AppType()))
    return;
  // App was added, removed, enabled, or disabled.
  if (!update.ReadinessChanged())
    return;
  if (!base::Contains(force_allowed_app_ids_, update.AppId()) &&
      !HasNoteTakingIntentFilter(update.IntentFilters())) {
    return;
  }

  // Ok to send false positive notifications to observers.
  for (Observer& observer : observers_)
    observer.OnAvailableNoteTakingAppsUpdated();
}

void NoteTakingHelper::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  app_registry_observations_.RemoveObservation(cache);
}

}  // namespace ash