1 // Copyright 2020 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/ui/ash/holding_space/holding_space_keyed_service.h"
6
7 #include "ash/public/cpp/holding_space/holding_space_controller.h"
8 #include "ash/public/cpp/holding_space/holding_space_image.h"
9 #include "ash/public/cpp/holding_space/holding_space_item.h"
10 #include "ash/public/cpp/holding_space/holding_space_metrics.h"
11 #include "ash/public/cpp/holding_space/holding_space_prefs.h"
12 #include "base/files/file_path.h"
13 #include "chrome/browser/browser_process.h"
14 #include "chrome/browser/chromeos/file_manager/app_id.h"
15 #include "chrome/browser/chromeos/file_manager/fileapi_util.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/ui/ash/holding_space/holding_space_downloads_delegate.h"
18 #include "chrome/browser/ui/ash/holding_space/holding_space_file_system_delegate.h"
19 #include "chrome/browser/ui/ash/holding_space/holding_space_persistence_delegate.h"
20 #include "chrome/browser/ui/ash/holding_space/holding_space_util.h"
21 #include "components/account_id/account_id.h"
22 #include "components/prefs/scoped_user_pref_update.h"
23 #include "storage/browser/file_system/file_system_url.h"
24
25 namespace ash {
26
27 namespace {
28
29 // Helpers ---------------------------------------------------------------------
30
31 // TODO(crbug.com/1131266): Track alternative type in `HoldingSpaceItem`.
32 // Returns a holding space item other than the one provided which is backed by
33 // the same file path in the specified `model`.
GetAlternativeHoldingSpaceItem(const HoldingSpaceModel & model,const HoldingSpaceItem * item)34 base::Optional<const HoldingSpaceItem*> GetAlternativeHoldingSpaceItem(
35 const HoldingSpaceModel& model,
36 const HoldingSpaceItem* item) {
37 for (const auto& candidate_item : model.items()) {
38 if (candidate_item.get() == item)
39 continue;
40 if (candidate_item->file_path() == item->file_path())
41 return candidate_item.get();
42 }
43 return base::nullopt;
44 }
45
46 // Returns the singleton profile manager for the browser process.
GetProfileManager()47 ProfileManager* GetProfileManager() {
48 return g_browser_process->profile_manager();
49 }
50
51 // Records the time from the first availability of the holding space feature
52 // to the time of the first item being added into holding space.
RecordTimeFromFirstAvailabilityToFirstAdd(Profile * profile)53 void RecordTimeFromFirstAvailabilityToFirstAdd(Profile* profile) {
54 base::Time time_of_first_availability =
55 holding_space_prefs::GetTimeOfFirstAvailability(profile->GetPrefs())
56 .value();
57 base::Time time_of_first_add =
58 holding_space_prefs::GetTimeOfFirstAdd(profile->GetPrefs()).value();
59 holding_space_metrics::RecordTimeFromFirstAvailabilityToFirstAdd(
60 time_of_first_add - time_of_first_availability);
61 }
62
63 // Records the time from the first entry to the first pin into holding space.
64 // Note that this time may be zero if the user pinned their first file before
65 // having ever entered holding space.
RecordTimeFromFirstEntryToFirstPin(Profile * profile)66 void RecordTimeFromFirstEntryToFirstPin(Profile* profile) {
67 base::Time time_of_first_pin =
68 holding_space_prefs::GetTimeOfFirstPin(profile->GetPrefs()).value();
69 base::Time time_of_first_entry =
70 holding_space_prefs::GetTimeOfFirstEntry(profile->GetPrefs())
71 .value_or(time_of_first_pin);
72 holding_space_metrics::RecordTimeFromFirstEntryToFirstPin(
73 time_of_first_pin - time_of_first_entry);
74 }
75
76 } // namespace
77
78 // HoldingSpaceKeyedService ----------------------------------------------------
79
HoldingSpaceKeyedService(Profile * profile,const AccountId & account_id)80 HoldingSpaceKeyedService::HoldingSpaceKeyedService(Profile* profile,
81 const AccountId& account_id)
82 : profile_(profile),
83 account_id_(account_id),
84 holding_space_client_(profile),
85 thumbnail_loader_(profile) {
86 // Mark when the holding space feature first became available. If this is not
87 // the first time that holding space became available, this will no-op.
88 holding_space_prefs::MarkTimeOfFirstAvailability(profile_->GetPrefs());
89
90 // The associated profile may not be ready yet. If it is, we can immediately
91 // proceed with profile dependent initialization.
92 ProfileManager* const profile_manager = GetProfileManager();
93 if (profile_manager->IsValidProfile(profile)) {
94 OnProfileReady();
95 return;
96 }
97
98 // Otherwise we need to wait for the profile to be added.
99 profile_manager_observer_.Add(profile_manager);
100 }
101
~HoldingSpaceKeyedService()102 HoldingSpaceKeyedService::~HoldingSpaceKeyedService() {
103 if (HoldingSpaceController::Get()) {
104 // For BrowserWithTestWindowTest that releases profile and its keyed
105 // services before ash Shell.
106 HoldingSpaceController::Get()->RegisterClientAndModelForUser(
107 account_id_, /*client=*/nullptr, /*model=*/nullptr);
108 }
109 }
110
111 // static
RegisterProfilePrefs(user_prefs::PrefRegistrySyncable * registry)112 void HoldingSpaceKeyedService::RegisterProfilePrefs(
113 user_prefs::PrefRegistrySyncable* registry) {
114 // TODO(crbug.com/1131266): Move to `ash::holding_space_prefs`.
115 HoldingSpacePersistenceDelegate::RegisterProfilePrefs(registry);
116 }
117
AddPinnedFile(const storage::FileSystemURL & file_system_url)118 void HoldingSpaceKeyedService::AddPinnedFile(
119 const storage::FileSystemURL& file_system_url) {
120 if (holding_space_model_.GetItem(HoldingSpaceItem::GetFileBackedItemId(
121 HoldingSpaceItem::Type::kPinnedFile, file_system_url.path()))) {
122 return;
123 }
124
125 // Mark when the first pin to holding space occurred. If this is not the first
126 // pin to holding space, this will no-op. If this is the first pin, record the
127 // amount of time from first entry to first pin into holding space.
128 if (holding_space_prefs::MarkTimeOfFirstPin(profile_->GetPrefs()))
129 RecordTimeFromFirstEntryToFirstPin(profile_);
130
131 std::unique_ptr<HoldingSpaceItem> holding_space_item =
132 HoldingSpaceItem::CreateFileBackedItem(
133 HoldingSpaceItem::Type::kPinnedFile, file_system_url.path(),
134 file_system_url.ToGURL(),
135 holding_space_util::ResolveImage(&thumbnail_loader_,
136 HoldingSpaceItem::Type::kPinnedFile,
137 file_system_url.path()));
138
139 // When pinning an item which already exists in holding space, the pin action
140 // should be recorded on the alternative item backed by the same file path if
141 // such an item exists. Otherwise the only type of holding space item pinned
142 // will be thought to be `kPinnedFile`.
143 const HoldingSpaceItem* holding_space_item_to_record =
144 GetAlternativeHoldingSpaceItem(holding_space_model_,
145 holding_space_item.get())
146 .value_or(holding_space_item.get());
147
148 holding_space_metrics::RecordItemAction(
149 {holding_space_item_to_record}, holding_space_metrics::ItemAction::kPin);
150
151 AddItem(std::move(holding_space_item));
152 }
153
RemovePinnedFile(const storage::FileSystemURL & file_system_url)154 void HoldingSpaceKeyedService::RemovePinnedFile(
155 const storage::FileSystemURL& file_system_url) {
156 const HoldingSpaceItem* holding_space_item =
157 holding_space_model_.GetItem(HoldingSpaceItem::GetFileBackedItemId(
158 HoldingSpaceItem::Type::kPinnedFile, file_system_url.path()));
159 if (!holding_space_item)
160 return;
161
162 // When removing a pinned item, the unpin action should be recorded on the
163 // alternative item backed by the same file path if such an item exists. This
164 // will give more insight as to what types of items are being unpinned than
165 // would otherwise be known if only `kPinnedFile` was recorded.
166 const HoldingSpaceItem* holding_space_item_to_record =
167 GetAlternativeHoldingSpaceItem(holding_space_model_, holding_space_item)
168 .value_or(holding_space_item);
169
170 holding_space_metrics::RecordItemAction(
171 {holding_space_item_to_record},
172 holding_space_metrics::ItemAction::kUnpin);
173
174 holding_space_model_.RemoveItem(holding_space_item->id());
175 }
176
ContainsPinnedFile(const storage::FileSystemURL & file_system_url) const177 bool HoldingSpaceKeyedService::ContainsPinnedFile(
178 const storage::FileSystemURL& file_system_url) const {
179 return holding_space_model_.GetItem(HoldingSpaceItem::GetFileBackedItemId(
180 HoldingSpaceItem::Type::kPinnedFile, file_system_url.path()));
181 }
182
GetPinnedFiles() const183 std::vector<GURL> HoldingSpaceKeyedService::GetPinnedFiles() const {
184 std::vector<GURL> pinned_files;
185 for (const auto& item : holding_space_model_.items()) {
186 if (item->type() == HoldingSpaceItem::Type::kPinnedFile)
187 pinned_files.push_back(item->file_system_url());
188 }
189 return pinned_files;
190 }
191
AddScreenshot(const base::FilePath & screenshot_file)192 void HoldingSpaceKeyedService::AddScreenshot(
193 const base::FilePath& screenshot_file) {
194 GURL file_system_url =
195 holding_space_util::ResolveFileSystemUrl(profile_, screenshot_file);
196 if (file_system_url.is_empty())
197 return;
198
199 AddItem(HoldingSpaceItem::CreateFileBackedItem(
200 HoldingSpaceItem::Type::kScreenshot, screenshot_file, file_system_url,
201 holding_space_util::ResolveImage(&thumbnail_loader_,
202 HoldingSpaceItem::Type::kScreenshot,
203 screenshot_file)));
204 }
205
AddDownload(const base::FilePath & download_file)206 void HoldingSpaceKeyedService::AddDownload(
207 const base::FilePath& download_file) {
208 const bool already_exists =
209 holding_space_model_.GetItem(HoldingSpaceItem::GetFileBackedItemId(
210 HoldingSpaceItem::Type::kDownload, download_file));
211 if (already_exists)
212 return;
213
214 GURL file_system_url =
215 holding_space_util::ResolveFileSystemUrl(profile_, download_file);
216 if (file_system_url.is_empty())
217 return;
218
219 AddItem(HoldingSpaceItem::CreateFileBackedItem(
220 HoldingSpaceItem::Type::kDownload, download_file, file_system_url,
221 holding_space_util::ResolveImage(&thumbnail_loader_,
222 HoldingSpaceItem::Type::kDownload,
223 download_file)));
224 }
225
AddNearbyShare(const base::FilePath & nearby_share_path)226 void HoldingSpaceKeyedService::AddNearbyShare(
227 const base::FilePath& nearby_share_path) {
228 const bool already_exists =
229 holding_space_model_.GetItem(HoldingSpaceItem::GetFileBackedItemId(
230 HoldingSpaceItem::Type::kNearbyShare, nearby_share_path));
231 if (already_exists)
232 return;
233
234 GURL file_system_url =
235 holding_space_util::ResolveFileSystemUrl(profile_, nearby_share_path);
236 if (file_system_url.is_empty())
237 return;
238
239 AddItem(HoldingSpaceItem::CreateFileBackedItem(
240 HoldingSpaceItem::Type::kNearbyShare, nearby_share_path, file_system_url,
241 holding_space_util::ResolveImage(&thumbnail_loader_,
242 HoldingSpaceItem::Type::kNearbyShare,
243 nearby_share_path)));
244 }
245
AddScreenRecording(const base::FilePath & screen_recording_file)246 void HoldingSpaceKeyedService::AddScreenRecording(
247 const base::FilePath& screen_recording_file) {
248 GURL file_system_url =
249 holding_space_util::ResolveFileSystemUrl(profile_, screen_recording_file);
250 if (file_system_url.is_empty())
251 return;
252
253 AddItem(HoldingSpaceItem::CreateFileBackedItem(
254 HoldingSpaceItem::Type::kScreenRecording, screen_recording_file,
255 file_system_url,
256 holding_space_util::ResolveImage(&thumbnail_loader_,
257 HoldingSpaceItem::Type::kScreenRecording,
258 screen_recording_file)));
259 }
260
AddItem(std::unique_ptr<HoldingSpaceItem> item)261 void HoldingSpaceKeyedService::AddItem(std::unique_ptr<HoldingSpaceItem> item) {
262 // Mark the time when the user's first item was added to holding space. Note
263 // that true is returned iff this is in fact the user's first add and, if so,
264 // the time it took for the user to add their first item should be recorded.
265 if (holding_space_prefs::MarkTimeOfFirstAdd(profile_->GetPrefs()))
266 RecordTimeFromFirstAvailabilityToFirstAdd(profile_);
267
268 holding_space_model_.AddItem(std::move(item));
269 }
270
Shutdown()271 void HoldingSpaceKeyedService::Shutdown() {
272 for (auto& delegate : delegates_)
273 delegate->Shutdown();
274 }
275
OnProfileAdded(Profile * profile)276 void HoldingSpaceKeyedService::OnProfileAdded(Profile* profile) {
277 if (profile == profile_) {
278 profile_manager_observer_.Remove(GetProfileManager());
279 OnProfileReady();
280 }
281 }
282
OnProfileReady()283 void HoldingSpaceKeyedService::OnProfileReady() {
284 // The `HoldingSpaceDownloadsDelegate` monitors the status of downloads.
285 delegates_.push_back(std::make_unique<HoldingSpaceDownloadsDelegate>(
286 profile_, &holding_space_model_,
287 /*item_downloaded_callback=*/
288 base::BindRepeating(&HoldingSpaceKeyedService::AddDownload,
289 weak_factory_.GetWeakPtr())));
290
291 // The `HoldingSpaceFileSystemDelegate` monitors the file system for changes.
292 delegates_.push_back(std::make_unique<HoldingSpaceFileSystemDelegate>(
293 profile_, &holding_space_model_));
294
295 // The `HoldingSpacePersistenceDelegate` manages holding space persistence.
296 delegates_.push_back(std::make_unique<HoldingSpacePersistenceDelegate>(
297 profile_, &holding_space_model_, &thumbnail_loader_,
298 /*item_restored_callback=*/
299 base::BindRepeating(&HoldingSpaceKeyedService::AddItem,
300 weak_factory_.GetWeakPtr()),
301 /*persistence_restored_callback=*/
302 base::BindOnce(&HoldingSpaceKeyedService::OnPersistenceRestored,
303 weak_factory_.GetWeakPtr())));
304
305 // Initialize all delegates only after they have been added to our collection.
306 // Delegates should not fire their respective callbacks during construction
307 // but once they have been initialized they are free to do so.
308 for (auto& delegate : delegates_)
309 delegate->Init();
310 }
311
OnPersistenceRestored()312 void HoldingSpaceKeyedService::OnPersistenceRestored() {
313 for (auto& delegate : delegates_)
314 delegate->NotifyPersistenceRestored();
315
316 HoldingSpaceController::Get()->RegisterClientAndModelForUser(
317 account_id_, &holding_space_client_, &holding_space_model_);
318 }
319
320 } // namespace ash
321