1 // Copyright 2013 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include "chrome/browser/chromeos/login/default_pinned_apps_field_trial.h" 6 7 #include "base/basictypes.h" 8 #include "base/command_line.h" 9 #include "base/memory/ref_counted.h" 10 #include "base/metrics/field_trial.h" 11 #include "base/metrics/histogram.h" 12 #include "base/prefs/pref_registry_simple.h" 13 #include "base/prefs/pref_service.h" 14 #include "base/values.h" 15 #include "chrome/browser/browser_process.h" 16 #include "chrome/browser/chromeos/login/user_manager.h" 17 #include "chrome/browser/prefs/pref_service_syncable.h" 18 #include "chrome/browser/prefs/scoped_user_pref_update.h" 19 #include "chrome/browser/profiles/profile.h" 20 #include "chrome/browser/profiles/profile_manager.h" 21 #include "chrome/browser/ui/ash/chrome_launcher_prefs.h" 22 #include "chrome/common/chrome_switches.h" 23 #include "chrome/common/extensions/extension_constants.h" 24 #include "chrome/common/pref_names.h" 25 26 namespace chromeos { 27 namespace default_pinned_apps_field_trial { 28 29 namespace { 30 31 // Name of the default pinned app experiment. 32 const char kExperimentName[] = "DefaultPinnedApps"; 33 34 // Name of a local state pref to store the list of users that participate 35 // the experiment. 36 const char kExperimentUsers[] = "DefaultPinnedAppsExperimentUsers"; 37 38 // Alternate default pinned apps. 39 const char* kAlternateDefaultPinnedApps[] = { 40 extension_misc::kGmailAppId, 41 extension_misc::kGoogleDocAppId, 42 extension_misc::kGoogleSheetsAppId, 43 extension_misc::kGoogleSlidesAppId, 44 extension_misc::kGooglePlayMusicAppId, 45 }; 46 47 // Global state of trial setup. 48 bool trial_configured = false; 49 int alternate_group = base::FieldTrial::kNotFinalized; 50 51 // Returns true if user participates the experiment. 52 bool IsUserInExperiment(const std::string& username) { 53 const base::ListValue* users = 54 g_browser_process->local_state()->GetList(kExperimentUsers); 55 return users && users->Find(base::StringValue(username)) != users->end(); 56 } 57 58 // Adds user to the experiment user list. 59 void AddUserToExperiment(const std::string& username) { 60 ListPrefUpdate users(g_browser_process->local_state(), kExperimentUsers); 61 users->AppendString(username); 62 } 63 64 // Removes user from the experiment user list. 65 void RemoveUserFromExperiment(const std::string& username) { 66 ListPrefUpdate users(g_browser_process->local_state(), kExperimentUsers); 67 users->Remove(base::StringValue(username), NULL); 68 } 69 70 // Gets click target type for given app id. Returns false if the app id is 71 // not interesting. 72 bool GetClickTargetForApp(const std::string& app_id, 73 ClickTarget* click_target) { 74 static const struct AppIdToClickTarget { 75 const char* app_id; 76 ClickTarget click_target; 77 } kApps[] = { 78 { extension_misc::kChromeAppId, CHROME }, 79 { extension_misc::kGmailAppId, GMAIL }, 80 { extension_misc::kGoogleSearchAppId, SEARCH }, 81 { extension_misc::kYoutubeAppId, YOUTUBE }, 82 { extension_misc::kGoogleDocAppId, DOC }, 83 { extension_misc::kGoogleSheetsAppId, SHEETS }, 84 { extension_misc::kGoogleSlidesAppId, SLIDES }, 85 { extension_misc::kGooglePlayMusicAppId, PLAY_MUSIC }, 86 }; 87 88 // kApps should define an app id for all click types except APP_LAUNCHER. 89 COMPILE_ASSERT(ARRAYSIZE_UNSAFE(kApps) == CLICK_TARGET_COUNT - 1, 90 app_to_click_target_incorrect_size); 91 92 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kApps); ++i) { 93 if (app_id == kApps[i].app_id) { 94 *click_target = kApps[i].click_target; 95 return true; 96 } 97 } 98 99 return false; 100 } 101 102 // Check if the current user has default pinned apps pulled from sync. If that 103 // is the case, kick the user out of the trial and returns true. 104 bool ProcessIfUserHasSyncedAppPins() { 105 static bool has_synced_pref = false; 106 if (has_synced_pref) 107 return true; 108 109 Profile* user_profile = ProfileManager::GetDefaultProfile(); 110 has_synced_pref = PrefServiceSyncable::FromProfile(user_profile) 111 ->IsPrefSynced(prefs::kPinnedLauncherApps); 112 if (has_synced_pref) { 113 const std::string username = UserManager::Get()->GetActiveUser()->email(); 114 RemoveUserFromExperiment(username); 115 } 116 117 return has_synced_pref; 118 } 119 120 } // namespace 121 122 void RegisterPrefs(PrefRegistrySimple* registry) { 123 registry->RegisterListPref(kExperimentUsers); 124 } 125 126 void SetupTrial() { 127 // No trial if multiple profiles are enabled. 128 if (CommandLine::ForCurrentProcess()->HasSwitch(::switches::kMultiProfiles)) 129 return; 130 131 // SetupForUser should only be called once for single profile mode. 132 DCHECK(!trial_configured); 133 134 const base::FieldTrial::Probability kDivisor = 100; 135 scoped_refptr<base::FieldTrial> trial = 136 base::FieldTrialList::FactoryGetFieldTrial( 137 kExperimentName, kDivisor, "Existing", 2013, 12, 31, 138 base::FieldTrial::ONE_TIME_RANDOMIZED, NULL); 139 140 // Split 50/50 between "Control" and "Alternamte" group for new user. 141 // Existing users already have their default pinned apps and have the trial 142 // disabled to go to "Existing" group. 143 trial->AppendGroup("Control", 50); 144 alternate_group = trial->AppendGroup("Alternate", 50); 145 } 146 147 void SetupForUser(const std::string& username, bool is_new_user) { 148 base::FieldTrial* trial = base::FieldTrialList::Find(kExperimentName); 149 if (!trial) 150 return; 151 152 trial_configured = true; 153 154 if (is_new_user) { 155 AddUserToExperiment(username); 156 return; 157 } 158 159 if (!IsUserInExperiment(username)) 160 trial->Disable(); 161 } 162 163 base::ListValue* GetAlternateDefaultPinnedApps() { 164 // This function is called for login manager profile, which happens 165 // before any user signs in. Returns NULL in this case. The case is covered 166 // by NULL trial check below but checking a boolean flag is faster. 167 if (!trial_configured) 168 return NULL; 169 170 base::FieldTrial* trial = base::FieldTrialList::Find(kExperimentName); 171 if (!trial || trial->group() != alternate_group) 172 return NULL; 173 174 scoped_ptr<base::ListValue> apps(new base::ListValue); 175 for (size_t i = 0; i < arraysize(kAlternateDefaultPinnedApps); ++i) 176 apps->Append(ash::CreateAppDict(kAlternateDefaultPinnedApps[i])); 177 178 return apps.release(); 179 } 180 181 void RecordShelfClick(ClickTarget click_target) { 182 // The experiment does not include certain user types, such as public account, 183 // retail mode user, local user and ephemeral user. 184 if (!trial_configured) 185 return; 186 187 if (ProcessIfUserHasSyncedAppPins()) 188 return; 189 190 UMA_HISTOGRAM_ENUMERATION("Cros.ClickOnShelf", 191 click_target, 192 CLICK_TARGET_COUNT); 193 } 194 195 void RecordShelfAppClick(const std::string& app_id) { 196 ClickTarget click_target = CLICK_TARGET_COUNT; 197 if (GetClickTargetForApp(app_id, &click_target)) 198 RecordShelfClick(click_target); 199 } 200 201 void ResetStateForTest() { 202 trial_configured = false; 203 alternate_group = base::FieldTrial::kNotFinalized; 204 } 205 206 } // namespace default_pinned_apps_field_trial 207 } // namespace chromeos 208