blob: 37132bc17b1c8b3e6cbc41fce2167616dd2c14e4 [file] [log] [blame]
// Copyright (c) 2012 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.
#include "chrome/browser/ui/ash/chrome_launcher_prefs.h"
#include <stddef.h>
#include <memory>
#include <utility>
#include "ash/public/cpp/app_list/internal_app_id_constants.h"
#include "ash/public/cpp/ash_pref_names.h"
#include "base/values.h"
#include "chrome/browser/chromeos/login/demo_mode/demo_session.h"
#include "chrome/browser/prefs/pref_service_syncable_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sync/profile_sync_service_factory.h"
#include "chrome/browser/ui/app_list/app_list_syncable_service.h"
#include "chrome/browser/ui/app_list/app_list_syncable_service_factory.h"
#include "chrome/browser/ui/app_list/arc/arc_app_list_prefs.h"
#include "chrome/browser/ui/app_list/arc/arc_app_utils.h"
#include "chrome/browser/ui/ash/launcher/chrome_launcher_controller_util.h"
#include "chrome/browser/ui/ash/launcher/launcher_controller_helper.h"
#include "chrome/browser/web_applications/components/app_registrar.h"
#include "chrome/browser/web_applications/components/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "components/crx_file/id_util.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/sync/base/user_selectable_type.h"
#include "components/sync/driver/sync_service.h"
#include "components/sync/driver/sync_user_settings.h"
#include "components/sync/model/string_ordinal.h"
#include "components/sync_preferences/pref_service_syncable.h"
#include "extensions/common/constants.h"
namespace {
// Chrome is pinned explicitly.
const char* kDefaultPinnedApps[] = {
extension_misc::kGmailAppId, extension_misc::kGoogleDocAppId,
extension_misc::kYoutubeAppId, arc::kPlayStoreAppId};
const char* kDefaultPinnedApps7Apps[] = {
extension_misc::kGmailAppId, extension_misc::kGoogleDocAppId,
extension_misc::kGooglePhotosAppId, extension_misc::kFilesManagerAppId,
extension_misc::kYoutubeAppId, arc::kPlayStoreAppId};
const char* kDefaultPinnedApps10Apps[] = {extension_misc::kGmailAppId,
extension_misc::kCalendarAppId,
extension_misc::kGoogleDocAppId,
extension_misc::kGoogleSheetsAppId,
extension_misc::kGoogleSlidesAppId,
extension_misc::kFilesManagerAppId,
app_list::kInternalAppIdCamera,
extension_misc::kGooglePhotosAppId,
arc::kPlayStoreAppId};
const char kDefaultPinnedAppsKey[] = "default";
const char kDefaultPinnedApps7AppsKey[] = "7apps";
const char kDefaultPinnedApps10AppsKey[] = "10apps";
bool IsAppIdArcPackage(const std::string& app_id) {
return app_id.find('.') != app_id.npos;
}
std::vector<std::string> GetActivitiesForPackage(
const std::string& package,
const std::vector<std::string>& all_arc_app_ids,
const ArcAppListPrefs& app_list_pref) {
std::vector<std::string> activities;
for (const std::string& app_id : all_arc_app_ids) {
const std::unique_ptr<ArcAppListPrefs::AppInfo> app_info =
app_list_pref.GetApp(app_id);
if (app_info->package_name == package) {
activities.push_back(app_info->activity);
}
}
return activities;
}
std::vector<ash::ShelfID> AppIdsToShelfIDs(
const std::vector<std::string> app_ids) {
std::vector<ash::ShelfID> shelf_ids(app_ids.size());
for (size_t i = 0; i < app_ids.size(); ++i)
shelf_ids[i] = ash::ShelfID(app_ids[i]);
return shelf_ids;
}
struct PinInfo {
PinInfo(const std::string& app_id, const syncer::StringOrdinal& item_ordinal)
: app_id(app_id), item_ordinal(item_ordinal) {}
std::string app_id;
syncer::StringOrdinal item_ordinal;
};
struct ComparePinInfo {
bool operator()(const PinInfo& pin1, const PinInfo& pin2) {
return pin1.item_ordinal.LessThan(pin2.item_ordinal);
}
};
// Returns true in case some configuration was rolled.
bool IsAnyDefaultPinLayoutRolled(Profile* profile) {
const auto* layouts_rolled =
profile->GetPrefs()->GetList(prefs::kShelfDefaultPinLayoutRolls);
return layouts_rolled && !layouts_rolled->GetList().empty();
}
// Returns true in case default pin layout |default_pin_layout| was already
// rolled.
bool IsDefaultPinLayoutRolled(Profile* profile,
const std::string& default_pin_layout) {
const auto* layouts_rolled =
profile->GetPrefs()->GetList(prefs::kShelfDefaultPinLayoutRolls);
if (!layouts_rolled)
return false;
for (const auto& layout_rolled : layouts_rolled->GetList()) {
if (layout_rolled.GetString() == default_pin_layout)
return true;
}
return false;
}
// Marks that default pin layout |default_pin_layout| is rolled.
void MarkDefaultPinLayoutRolled(Profile* profile,
const std::string& default_pin_layout) {
DCHECK(!IsDefaultPinLayoutRolled(profile, default_pin_layout));
ListPrefUpdate update(profile->GetPrefs(),
prefs::kShelfDefaultPinLayoutRolls);
update->AppendString(default_pin_layout);
}
// Returns true in case default pin layout configuration could be applied
// safely. That means all required components are synced or worked in local
// mode.
bool IsSafeToApplyDefaultPinLayout(Profile* profile) {
const syncer::SyncService* sync_service =
ProfileSyncServiceFactory::GetForProfile(profile);
// No |sync_service| in incognito mode.
if (!sync_service)
return true;
const syncer::UserSelectableTypeSet selected_sync =
sync_service->GetUserSettings()->GetSelectedTypes();
// If App sync is not yet started, don't apply default pin apps once synced
// apps is likely override it. There is a case when App sync is disabled and
// in last case local cache is available immediately.
if (selected_sync.Has(syncer::UserSelectableType::kApps) &&
!app_list::AppListSyncableServiceFactory::GetForProfile(profile)
->IsSyncing()) {
return false;
}
// If shelf pin layout rolls preference is not started yet then we cannot say
// if we rolled layout or not.
if (selected_sync.Has(syncer::UserSelectableType::kPreferences) &&
!PrefServiceSyncableFromProfile(profile)->IsSyncing()) {
return false;
}
return true;
}
// Returns true in case |pins_from_sync_raw| is empty or represents default app
// set |kDefaultPinnedApps| plus Chrome app.
bool IsCurrentDefaultOrEmpty(const std::set<std::string>& pins_from_sync_raw) {
if (pins_from_sync_raw.empty())
return true;
// Chrome is explicitly pinned regardless of configuration.
if (pins_from_sync_raw.size() != base::size(kDefaultPinnedApps) + 1)
return false;
if (!pins_from_sync_raw.count(extension_misc::kChromeAppId))
return false;
for (const char* default_app_id : kDefaultPinnedApps) {
if (!pins_from_sync_raw.count(default_app_id))
return false;
}
return true;
}
} // namespace
const char kPinnedAppsPrefAppIDKey[] = "id";
const base::Feature kEnableExtendedShelfLayout{
"EnableExtendedShelfLayout", base::FEATURE_DISABLED_BY_DEFAULT};
// Parameter for the finch experiment with number of default apps on the shelf.
// Possible values:
// 7 - activates |kDefaultPinnedApps7Apps|, see cc file.
// 10 - activates |kDefaultPinnedApps10Apps|, see cc file.
// 0 - by default.
constexpr base::FeatureParam<int> kEnableExtendedShelfLayoutParam(
&kEnableExtendedShelfLayout,
"app_count",
0);
void RegisterChromeLauncherUserPrefs(
user_prefs::PrefRegistrySyncable* registry) {
registry->RegisterListPref(prefs::kPolicyPinnedLauncherApps);
registry->RegisterListPref(
prefs::kShelfDefaultPinLayoutRolls,
user_prefs::PrefRegistrySyncable::SYNCABLE_PRIORITY_PREF);
}
void InitLocalPref(PrefService* prefs, const char* local, const char* synced) {
// Ash's prefs *should* have been propagated to Chrome by now, but maybe not.
// This belongs in Ash, but it can't observe syncing changes: crbug.com/774657
if (prefs->FindPreference(local) && prefs->FindPreference(synced) &&
!prefs->FindPreference(local)->HasUserSetting()) {
prefs->SetString(local, prefs->GetString(synced));
}
}
// Helper that extracts app list from policy preferences.
std::vector<std::string> GetAppsPinnedByPolicy(
LauncherControllerHelper* helper) {
const PrefService* prefs = helper->profile()->GetPrefs();
std::vector<std::string> result;
const base::Value* policy_apps =
prefs->GetList(prefs::kPolicyPinnedLauncherApps);
if (!policy_apps)
return result;
// Obtain here all ids of ARC apps because it takes linear time, and getting
// them in the loop bellow would lead to quadratic complexity.
const ArcAppListPrefs* const arc_app_list_pref = helper->GetArcAppListPrefs();
const std::vector<std::string> all_arc_app_ids(
arc_app_list_pref ? arc_app_list_pref->GetAppIds()
: std::vector<std::string>());
for (const auto& policy_dict_entry : policy_apps->GetList()) {
const std::string* policy_entry =
policy_dict_entry.is_dict()
? policy_dict_entry.FindStringKey(kPinnedAppsPrefAppIDKey)
: nullptr;
if (!policy_entry) {
LOG(ERROR) << "Cannot extract policy app info from prefs.";
continue;
}
if (chromeos::DemoSession::Get() &&
chromeos::DemoSession::Get()->ShouldIgnorePinPolicy(*policy_entry)) {
continue;
}
// Handle Chrome App ids
if (crx_file::id_util::IdIsValid(*policy_entry)) {
result.emplace_back(*policy_entry);
continue;
}
// Handle Web App ids
const GURL web_app_url(*policy_entry);
if (web_app_url.is_valid()) {
base::Optional<web_app::AppId> web_app_id =
web_app::WebAppProvider::Get(helper->profile())
->registrar()
.LookupExternalAppId(web_app_url);
if (web_app_id.has_value())
result.emplace_back(web_app_id.value());
continue;
}
// Handle Arc++ App ids
if (IsAppIdArcPackage(*policy_entry)) {
if (!arc_app_list_pref)
continue;
// We are dealing with package name, not with 32 characters ID.
const std::string& arc_package = *policy_entry;
const std::vector<std::string> activities = GetActivitiesForPackage(
arc_package, all_arc_app_ids, *arc_app_list_pref);
for (const auto& activity : activities) {
const std::string arc_app_id =
ArcAppListPrefs::GetAppId(arc_package, activity);
result.emplace_back(arc_app_id);
}
continue;
}
}
return result;
}
// Returns pinned app position even if app is not currently visible on device
// that is leftmost item on the shelf. If |exclude_chrome| is true then Chrome
// app is not processed. if nothing pinned found, returns an invalid ordinal.
syncer::StringOrdinal GetFirstPinnedAppPosition(Profile* profile,
bool exclude_chrome) {
syncer::StringOrdinal position;
app_list::AppListSyncableService* syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile);
for (const auto& sync_peer : syncable_service->sync_items()) {
if (!sync_peer.second->item_pin_ordinal.IsValid())
continue;
if (exclude_chrome && sync_peer.first == extension_misc::kChromeAppId)
continue;
if (!position.IsValid() ||
sync_peer.second->item_pin_ordinal.LessThan(position)) {
position = sync_peer.second->item_pin_ordinal;
}
}
return position;
}
// Helper to create pin position that stays before any synced app, even if
// app is not currently visible on a device.
syncer::StringOrdinal CreateFirstPinPosition(Profile* profile) {
const syncer::StringOrdinal position =
GetFirstPinnedAppPosition(profile, false /* exclude_chrome */);
return position.IsValid() ? position.CreateBefore()
: syncer::StringOrdinal::CreateInitialOrdinal();
}
// Helper to creates pin position that stays before any synced app, even if
// app is not currently visible on a device.
syncer::StringOrdinal CreateLastPinPosition(Profile* profile) {
syncer::StringOrdinal position;
app_list::AppListSyncableService* syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile);
for (const auto& sync_peer : syncable_service->sync_items()) {
if (!sync_peer.second->item_pin_ordinal.IsValid())
continue;
if (!position.IsValid() ||
sync_peer.second->item_pin_ordinal.GreaterThan(position)) {
position = sync_peer.second->item_pin_ordinal;
}
}
return position.IsValid() ? position.CreateAfter()
: syncer::StringOrdinal::CreateInitialOrdinal();
}
// Helper to create and insert pins on the shelf for the set of apps defined in
// |app_ids| after Chrome in the first position and before any other pinned app.
// If Chrome is not the first pinned app then apps are pinned before any other
// app.
void InsertPinsAfterChromeAndBeforeFirstPinnedApp(
LauncherControllerHelper* helper,
app_list::AppListSyncableService* syncable_service,
const std::vector<std::string>& app_ids,
std::vector<PinInfo>* pin_infos) {
if (app_ids.empty())
return;
// Chrome must be pinned at this point.
const syncer::StringOrdinal chrome_position =
syncable_service->GetPinPosition(extension_misc::kChromeAppId);
DCHECK(chrome_position.IsValid());
// New pins are inserted after this position.
syncer::StringOrdinal after;
// New pins are inserted before this position.
syncer::StringOrdinal before =
GetFirstPinnedAppPosition(helper->profile(), true /* exclude_chrome */);
if (before.IsValid() && before.GreaterThan(chrome_position)) {
// Perfect case, Chrome is the first pinned app and we have next pinned app.
after = chrome_position;
} else {
if (before.IsValid()) {
// Chrome is not first. Generate after as position before |before|.
after = before.CreateBefore();
} else {
// Chrome is first and no more other pinned apps or apps are
// conflicting with Chrome pin position.
after = chrome_position;
before = chrome_position.CreateAfter();
}
}
for (const auto& app_id : app_ids) {
// Check if we already processed the current app.
if (syncable_service->GetPinPosition(app_id).IsValid())
continue;
const syncer::StringOrdinal position = after.CreateBetween(before);
syncable_service->SetPinPosition(app_id, position);
// Even if app is not currently visible, its pin is pre-created.
if (helper->IsValidIDForCurrentUser(app_id))
pin_infos->emplace_back(PinInfo(app_id, position));
// Shift after position, next policy pin position will be created after
// current item.
after = position;
}
}
std::vector<ash::ShelfID> GetPinnedAppsFromSync(
LauncherControllerHelper* helper) {
const PrefService* prefs = helper->profile()->GetPrefs();
app_list::AppListSyncableService* const syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(helper->profile());
// Some unit tests may not have it or service may not be initialized.
if (!syncable_service || !syncable_service->IsInitialized())
return std::vector<ash::ShelfID>();
std::vector<PinInfo> pin_infos;
// Empty pins indicates that sync based pin model is used for the first
// time. In the normal workflow we have at least Chrome browser pin info.
// Contains pins from sync regardless either real app available on device or
// not.
std::set<std::string> pins_from_sync_raw;
for (const auto& sync_peer : syncable_service->sync_items()) {
if (!sync_peer.second->item_pin_ordinal.IsValid())
continue;
pins_from_sync_raw.insert(sync_peer.first);
// Don't include apps that currently do not exist on device.
if (sync_peer.first != extension_misc::kChromeAppId &&
!helper->IsValidIDForCurrentUser(sync_peer.first)) {
continue;
}
// Prevent old app camera pinning.
if (IsCameraApp(sync_peer.first))
continue;
pin_infos.emplace_back(
PinInfo(sync_peer.first, sync_peer.second->item_pin_ordinal));
}
// Make sure Chrome is always pinned.
syncer::StringOrdinal chrome_position =
syncable_service->GetPinPosition(extension_misc::kChromeAppId);
if (!chrome_position.IsValid()) {
chrome_position = CreateFirstPinPosition(helper->profile());
syncable_service->SetPinPosition(extension_misc::kChromeAppId,
chrome_position);
pin_infos.emplace_back(
PinInfo(extension_misc::kChromeAppId, chrome_position));
}
// Apply default apps in case profile syncing is done. Otherwise there is a
// risk that applied default apps would be overwritten by sync once it is
// completed. prefs::kPolicyPinnedLauncherApps overrides any default layout.
// This also limits applying experimental configuration only for users who
// have the default pin layout specified by |kDefaultPinnedApps| or for
// fresh users who have no pin information at all. Default configuration is
// not applied if any of experimental layout was rolled.
std::string shelf_layout = IsAnyDefaultPinLayoutRolled(helper->profile())
? std::string()
: kDefaultPinnedAppsKey;
// Set to true in case default configuration has to be reset in order to let
// new layout takes effect.
bool reset_default_configuration = false;
if (base::FeatureList::IsEnabled(kEnableExtendedShelfLayout) &&
IsCurrentDefaultOrEmpty(pins_from_sync_raw)) {
const int forced_shelf_layout_app_count =
kEnableExtendedShelfLayoutParam.Get();
switch (forced_shelf_layout_app_count) {
case 0:
shelf_layout = kDefaultPinnedAppsKey;
break;
case 7:
reset_default_configuration = true;
shelf_layout = kDefaultPinnedApps7AppsKey;
break;
case 10:
reset_default_configuration = true;
shelf_layout = kDefaultPinnedApps10AppsKey;
break;
default:
LOG(ERROR) << "Wrong default shelf pin layout "
<< forced_shelf_layout_app_count;
}
}
if (!prefs->HasPrefPath(prefs::kPolicyPinnedLauncherApps) &&
IsSafeToApplyDefaultPinLayout(helper->profile()) &&
!shelf_layout.empty() &&
!IsDefaultPinLayoutRolled(helper->profile(), shelf_layout)) {
VLOG(1) << "Roll default shelf pin layout " << shelf_layout;
if (reset_default_configuration) {
VLOG(1) << "Reset previous default configuration";
pin_infos.clear();
pin_infos.emplace_back(
PinInfo(extension_misc::kChromeAppId, chrome_position));
for (const char* default_app_id : kDefaultPinnedApps) {
syncable_service->SetPinPosition(default_app_id,
syncer::StringOrdinal());
}
}
std::vector<std::string> default_app_ids;
if (shelf_layout == kDefaultPinnedApps7AppsKey) {
for (const char* default_app_id : kDefaultPinnedApps7Apps)
default_app_ids.push_back(default_app_id);
} else if (shelf_layout == kDefaultPinnedApps10AppsKey) {
for (const char* default_app_id : kDefaultPinnedApps10Apps)
default_app_ids.push_back(default_app_id);
} else {
for (const char* default_app_id : kDefaultPinnedApps)
default_app_ids.push_back(default_app_id);
}
InsertPinsAfterChromeAndBeforeFirstPinnedApp(helper, syncable_service,
default_app_ids, &pin_infos);
MarkDefaultPinLayoutRolled(helper->profile(), shelf_layout);
}
// Handle pins, forced by policy. In case Chrome is first app they are added
// after Chrome, otherwise they are added to the front. Note, we handle apps
// that may not be currently on device. At this case pin position would be
// preallocated and apps will appear on shelf in deterministic order, even if
// their install order differ.
InsertPinsAfterChromeAndBeforeFirstPinnedApp(
helper, syncable_service, GetAppsPinnedByPolicy(helper), &pin_infos);
// Sort pins according their ordinals.
std::sort(pin_infos.begin(), pin_infos.end(), ComparePinInfo());
// Convert to ShelfID array.
std::vector<std::string> pins(pin_infos.size());
for (size_t i = 0; i < pin_infos.size(); ++i)
pins[i] = pin_infos[i].app_id;
return AppIdsToShelfIDs(pins);
}
void RemovePinPosition(Profile* profile, const ash::ShelfID& shelf_id) {
DCHECK(profile);
const std::string& app_id = shelf_id.app_id;
if (!shelf_id.launch_id.empty()) {
VLOG(2) << "Syncing remove pin for '" << app_id
<< "' with non-empty launch id '" << shelf_id.launch_id
<< "' is not supported.";
return;
}
DCHECK(!app_id.empty());
app_list::AppListSyncableService* syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile);
syncable_service->SetPinPosition(app_id, syncer::StringOrdinal());
}
void SetPinPosition(Profile* profile,
const ash::ShelfID& shelf_id,
const ash::ShelfID& shelf_id_before,
const std::vector<ash::ShelfID>& shelf_ids_after) {
DCHECK(profile);
// Camera apps are mapped to the internal app.
DCHECK(!IsCameraApp(shelf_id.app_id));
const std::string& app_id = shelf_id.app_id;
if (!shelf_id.launch_id.empty()) {
VLOG(2) << "Syncing set pin for '" << app_id
<< "' with non-empty launch id '" << shelf_id.launch_id
<< "' is not supported.";
return;
}
const std::string& app_id_before = shelf_id_before.app_id;
DCHECK(!app_id.empty());
DCHECK_NE(app_id, app_id_before);
app_list::AppListSyncableService* syncable_service =
app_list::AppListSyncableServiceFactory::GetForProfile(profile);
// Some unit tests may not have this service.
if (!syncable_service)
return;
syncer::StringOrdinal position_before =
app_id_before.empty() ? syncer::StringOrdinal()
: syncable_service->GetPinPosition(app_id_before);
syncer::StringOrdinal position_after;
for (const auto& shelf_id_after : shelf_ids_after) {
const std::string& app_id_after = shelf_id_after.app_id;
DCHECK_NE(app_id_after, app_id);
DCHECK_NE(app_id_after, app_id_before);
syncer::StringOrdinal position =
syncable_service->GetPinPosition(app_id_after);
DCHECK(position.IsValid());
if (!position.IsValid()) {
LOG(ERROR) << "Sync pin position was not found for " << app_id_after;
continue;
}
if (!position_before.IsValid() || !position.Equals(position_before)) {
position_after = position;
break;
}
}
syncer::StringOrdinal pin_position;
if (position_before.IsValid() && position_after.IsValid())
pin_position = position_before.CreateBetween(position_after);
else if (position_before.IsValid())
pin_position = position_before.CreateAfter();
else if (position_after.IsValid())
pin_position = position_after.CreateBefore();
else
pin_position = syncer::StringOrdinal::CreateInitialOrdinal();
syncable_service->SetPinPosition(app_id, pin_position);
}