Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(296)

Side by Side Diff: apps/launcher.cc

Issue 300063006: Files.app: Let Files.app pass mutliple files to file handlers. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Fixed. Created 6 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « apps/launcher.h ('k') | chrome/browser/chromeos/file_manager/file_tasks.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 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 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "apps/launcher.h" 5 #include "apps/launcher.h"
6 6
7 #include "apps/browser/api/app_runtime/app_runtime_api.h" 7 #include "apps/browser/api/app_runtime/app_runtime_api.h"
8 #include "apps/browser/file_handler_util.h" 8 #include "apps/browser/file_handler_util.h"
9 #include "apps/common/api/app_runtime.h" 9 #include "apps/common/api/app_runtime.h"
10 #include "base/command_line.h" 10 #include "base/command_line.h"
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
82 } 82 }
83 83
84 // Helper method to launch the platform app |extension| with no data. This 84 // Helper method to launch the platform app |extension| with no data. This
85 // should be called in the fallback case, where it has been impossible to 85 // should be called in the fallback case, where it has been impossible to
86 // load or obtain file launch data. 86 // load or obtain file launch data.
87 void LaunchPlatformAppWithNoData(Profile* profile, const Extension* extension) { 87 void LaunchPlatformAppWithNoData(Profile* profile, const Extension* extension) {
88 DCHECK_CURRENTLY_ON(BrowserThread::UI); 88 DCHECK_CURRENTLY_ON(BrowserThread::UI);
89 AppEventRouter::DispatchOnLaunchedEvent(profile, extension); 89 AppEventRouter::DispatchOnLaunchedEvent(profile, extension);
90 } 90 }
91 91
92 // Class to handle launching of platform apps to open a specific path. 92 // Class to handle launching of platform apps to open specific paths.
93 // An instance of this class is created for each launch. The lifetime of these 93 // An instance of this class is created for each launch. The lifetime of these
94 // instances is managed by reference counted pointers. As long as an instance 94 // instances is managed by reference counted pointers. As long as an instance
95 // has outstanding tasks on a message queue it will be retained; once all 95 // has outstanding tasks on a message queue it will be retained; once all
96 // outstanding tasks are completed it will be deleted. 96 // outstanding tasks are completed it will be deleted.
97 class PlatformAppPathLauncher 97 class PlatformAppPathLauncher
98 : public base::RefCountedThreadSafe<PlatformAppPathLauncher> { 98 : public base::RefCountedThreadSafe<PlatformAppPathLauncher> {
99 public: 99 public:
100 PlatformAppPathLauncher(Profile* profile, 100 PlatformAppPathLauncher(Profile* profile,
101 const Extension* extension, 101 const Extension* extension,
102 const std::vector<base::FilePath>& file_paths)
103 : profile_(profile), extension_(extension), file_paths_(file_paths) {}
104
105 PlatformAppPathLauncher(Profile* profile,
106 const Extension* extension,
102 const base::FilePath& file_path) 107 const base::FilePath& file_path)
103 : profile_(profile), extension_(extension), file_path_(file_path) {} 108 : profile_(profile), extension_(extension) {
109 if (!file_path.empty())
110 file_paths_.push_back(file_path);
111 }
104 112
105 void Launch() { 113 void Launch() {
106 DCHECK_CURRENTLY_ON(BrowserThread::UI); 114 DCHECK_CURRENTLY_ON(BrowserThread::UI);
107 if (file_path_.empty()) { 115 if (file_paths_.empty()) {
108 LaunchPlatformAppWithNoData(profile_, extension_); 116 LaunchPlatformAppWithNoData(profile_, extension_);
109 return; 117 return;
110 } 118 }
111 119
112 DCHECK(file_path_.IsAbsolute()); 120 for (size_t i = 0; i < file_paths_.size(); ++i) {
121 DCHECK(file_paths_[i].IsAbsolute());
122 }
113 123
114 if (HasFileSystemWritePermission(extension_)) { 124 if (HasFileSystemWritePermission(extension_)) {
115 std::vector<base::FilePath> paths;
116 paths.push_back(file_path_);
117 CheckWritableFiles( 125 CheckWritableFiles(
118 paths, 126 file_paths_,
119 profile_, 127 profile_,
120 false, 128 false,
121 base::Bind(&PlatformAppPathLauncher::OnFileValid, this), 129 base::Bind(&PlatformAppPathLauncher::OnFileValid, this),
122 base::Bind(&PlatformAppPathLauncher::OnFileInvalid, this)); 130 base::Bind(&PlatformAppPathLauncher::OnFileInvalid, this));
123 return; 131 return;
124 } 132 }
125 133
126 OnFileValid(); 134 OnFileValid();
127 } 135 }
128 136
(...skipping 12 matching lines...) Expand all
141 } 149 }
142 150
143 private: 151 private:
144 friend class base::RefCountedThreadSafe<PlatformAppPathLauncher>; 152 friend class base::RefCountedThreadSafe<PlatformAppPathLauncher>;
145 153
146 virtual ~PlatformAppPathLauncher() {} 154 virtual ~PlatformAppPathLauncher() {}
147 155
148 void MakePathAbsolute(const base::FilePath& current_directory) { 156 void MakePathAbsolute(const base::FilePath& current_directory) {
149 DCHECK_CURRENTLY_ON(BrowserThread::FILE); 157 DCHECK_CURRENTLY_ON(BrowserThread::FILE);
150 158
151 if (!DoMakePathAbsolute(current_directory, &file_path_)) { 159 for (std::vector<base::FilePath>::iterator it = file_paths_.begin();
152 LOG(WARNING) << "Cannot make absolute path from " << file_path_.value(); 160 it != file_paths_.end();
153 file_path_ = base::FilePath(); 161 ++it) {
162 if (!DoMakePathAbsolute(current_directory, &*it)) {
163 LOG(WARNING) << "Cannot make absolute path from " << it->value();
164 BrowserThread::PostTask(
165 BrowserThread::UI,
166 FROM_HERE,
167 base::Bind(&PlatformAppPathLauncher::LaunchWithNoLaunchData, this));
168 return;
169 }
154 } 170 }
155 171
156 BrowserThread::PostTask(BrowserThread::UI, 172 BrowserThread::PostTask(BrowserThread::UI,
157 FROM_HERE, 173 FROM_HERE,
158 base::Bind(&PlatformAppPathLauncher::Launch, this)); 174 base::Bind(&PlatformAppPathLauncher::Launch, this));
159 } 175 }
160 176
161 void OnFileValid() { 177 void OnFileValid() {
162 #if defined(OS_CHROMEOS) 178 mime_types_.resize(file_paths_.size());
163 if (file_manager::util::IsUnderNonNativeLocalPath(profile_, file_path_)) { 179 StepToFillMimeTypes();
benwells 2014/05/30 00:59:35 Sorry to be so nitpicky. I'm finding this logic pr
hirono 2014/05/30 02:53:44 Currently the mixed case should not happen, but it
benwells 2014/05/30 06:33:57 Ah, OK. Hmm. Maybe with some comments / changed na
hirono 2014/06/02 03:33:25 Done.
164 file_manager::util::GetNonNativeLocalPathMimeType(
165 profile_,
166 file_path_,
167 base::Bind(&PlatformAppPathLauncher::OnGotMimeType, this));
168 return;
169 }
170 #endif
171
172 BrowserThread::PostTask(
173 BrowserThread::FILE,
174 FROM_HERE,
175 base::Bind(&PlatformAppPathLauncher::GetMimeTypeAndLaunch, this));
176 } 180 }
177 181
178 void OnFileInvalid(const base::FilePath& /* error_path */) { 182 void OnFileInvalid(const base::FilePath& /* error_path */) {
179 LaunchWithNoLaunchData(); 183 LaunchWithNoLaunchData();
180 } 184 }
181 185
186 void StepToFillMimeTypes() {
benwells 2014/05/30 06:33:57 I think the name GetNextNonNativeMimeType would be
hirono 2014/06/02 03:33:25 Done.
187 DCHECK_CURRENTLY_ON(BrowserThread::UI);
188
189 bool filled = true;
benwells 2014/05/30 06:33:57 Changing filled to any_native_files would be clear
hirono 2014/06/02 03:33:25 Done.
190
191 for (size_t i = 0; i < mime_types_.size(); ++i) {
192 if (!mime_types_[i].empty())
193 continue;
194 const base::FilePath& file_path = file_paths_[i];
195 #if defined(OS_CHROMEOS)
196 if (file_manager::util::IsUnderNonNativeLocalPath(profile_, file_path)) {
197 file_manager::util::GetNonNativeLocalPathMimeType(
198 profile_,
199 file_path,
200 base::Bind(&PlatformAppPathLauncher::OnGotMimeType, this, i));
201 return;
202 }
203 #endif
204 filled = false;
205 }
206
207 if (!filled) {
208 BrowserThread::PostTask(
209 BrowserThread::FILE,
210 FROM_HERE,
211 base::Bind(&PlatformAppPathLauncher::GetMimeTypeAndLaunch, this));
212 return;
213 }
214
215 LaunchWithMimeType();
216 }
217
182 void GetMimeTypeAndLaunch() { 218 void GetMimeTypeAndLaunch() {
benwells 2014/05/30 00:59:35 Nit: GetMimeTypesAndLaunch
hirono 2014/06/02 03:33:25 Done.
183 DCHECK_CURRENTLY_ON(BrowserThread::FILE); 219 DCHECK_CURRENTLY_ON(BrowserThread::FILE);
184 220
185 // If the file doesn't exist, or is a directory, launch with no launch data. 221 for (size_t i = 0; i < mime_types_.size(); ++i) {
186 if (!base::PathExists(file_path_) || 222 if (!this->mime_types_[i].empty())
benwells 2014/05/30 00:59:35 I think this would be a programming error. Maybe D
hirono 2014/05/30 02:53:44 It can happen, if the given files are mixed (nativ
benwells 2014/05/30 06:33:57 OK, great, thanks for explaining.
187 base::DirectoryExists(file_path_)) { 223 continue;
188 LOG(WARNING) << "No file exists with path " << file_path_.value(); 224 const base::FilePath& file_path = file_paths_[i];
189 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind( 225
190 &PlatformAppPathLauncher::LaunchWithNoLaunchData, this)); 226 // If the file doesn't exist, or is a directory, launch with no launch
191 return; 227 // data.
228 if (!base::PathExists(file_path) || base::DirectoryExists(file_path)) {
229 LOG(WARNING) << "No file exists with path " << file_path.value();
230 BrowserThread::PostTask(
231 BrowserThread::UI,
232 FROM_HERE,
233 base::Bind(&PlatformAppPathLauncher::LaunchWithNoLaunchData, this));
234 return;
235 }
236
237 std::string mime_type;
238 if (!net::GetMimeTypeFromFile(file_path, &mime_type)) {
239 // If MIME type of the file can't be determined by its path,
240 // try to sniff it by its content.
241 std::vector<char> content(net::kMaxBytesToSniff);
242 int bytes_read = base::ReadFile(file_path, &content[0], content.size());
243 if (bytes_read >= 0) {
244 net::SniffMimeType(&content[0],
245 bytes_read,
246 net::FilePathToFileURL(file_path),
247 std::string(), // type_hint (passes no hint)
248 &mime_type);
249 }
250 if (mime_type.empty())
251 mime_type = kFallbackMimeType;
252 }
253 mime_types_[i] = mime_type;
192 } 254 }
193 255
194 std::string mime_type; 256 BrowserThread::PostTask(
195 if (!net::GetMimeTypeFromFile(file_path_, &mime_type)) { 257 BrowserThread::UI,
196 // If MIME type of the file can't be determined by its path, 258 FROM_HERE,
197 // try to sniff it by its content. 259 base::Bind(&PlatformAppPathLauncher::LaunchWithMimeType, this));
198 std::vector<char> content(net::kMaxBytesToSniff);
199 int bytes_read = base::ReadFile(file_path_, &content[0], content.size());
200 if (bytes_read >= 0) {
201 net::SniffMimeType(&content[0],
202 bytes_read,
203 net::FilePathToFileURL(file_path_),
204 std::string(), // type_hint (passes no hint)
205 &mime_type);
206 }
207 if (mime_type.empty())
208 mime_type = kFallbackMimeType;
209 }
210
211 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, base::Bind(
212 &PlatformAppPathLauncher::LaunchWithMimeType, this, mime_type));
213 } 260 }
214 261
215 #if defined(OS_CHROMEOS) 262 #if defined(OS_CHROMEOS)
216 void OnGotMimeType(bool success, const std::string& mime_type) { 263 void OnGotMimeType(size_t index, bool success, const std::string& mime_type) {
217 if (!success) { 264 if (!success) {
218 LaunchWithNoLaunchData(); 265 LaunchWithNoLaunchData();
219 return; 266 return;
220 } 267 }
221 LaunchWithMimeType(mime_type.empty() ? kFallbackMimeType : mime_type); 268 mime_types_[index] = mime_type.empty() ? kFallbackMimeType : mime_type;
269 StepToFillMimeTypes();
222 } 270 }
223 #endif 271 #endif
224 272
225 void LaunchWithNoLaunchData() { 273 void LaunchWithNoLaunchData() {
226 // This method is required as an entry point on the UI thread. 274 // This method is required as an entry point on the UI thread.
227 LaunchPlatformAppWithNoData(profile_, extension_); 275 LaunchPlatformAppWithNoData(profile_, extension_);
228 } 276 }
229 277
230 void LaunchWithMimeType(const std::string& mime_type) { 278 void LaunchWithMimeType() {
benwells 2014/05/30 00:59:35 Nit: LaunchWithMimeTypes
hirono 2014/06/02 03:33:25 Done.
279 DCHECK(file_paths_.size() == mime_types_.size());
280
231 // Find file handler from the platform app for the file being opened. 281 // Find file handler from the platform app for the file being opened.
232 const extensions::FileHandlerInfo* handler = NULL; 282 const extensions::FileHandlerInfo* handler = NULL;
233 if (!handler_id_.empty()) 283 if (!handler_id_.empty()) {
234 handler = FileHandlerForId(*extension_, handler_id_); 284 handler = FileHandlerForId(*extension_, handler_id_);
235 else 285 if (handler) {
236 handler = FirstFileHandlerForFile(*extension_, mime_type, file_path_); 286 for (size_t i = 0; i < file_paths_.size(); ++i) {
237 if (handler && !FileHandlerCanHandleFile(*handler, mime_type, file_path_)) { 287 if (!FileHandlerCanHandleFile(
238 LOG(WARNING) << "Extension does not provide a valid file handler for " 288 *handler, mime_types_[i], file_paths_[i])) {
239 << file_path_.value(); 289 LOG(WARNING)
240 LaunchWithNoLaunchData(); 290 << "Extension does not provide a valid file handler for "
241 return; 291 << file_paths_[i].value();
292 handler = NULL;
293 break;
294 }
295 }
296 }
297 } else {
298 std::set<std::pair<base::FilePath, std::string> > path_and_file_type_set;
299 for (size_t i = 0; i < file_paths_.size(); ++i) {
300 path_and_file_type_set.insert(
301 std::make_pair(file_paths_[i], mime_types_[i]));
302 }
303 const std::vector<const extensions::FileHandlerInfo*>& handlers =
304 extensions::app_file_handler_util::FindFileHandlersForFiles(
305 *extension_, path_and_file_type_set);
306 if (!handlers.empty())
307 handler = handlers[0];
242 } 308 }
243 309
244 // If this app doesn't have a file handler that supports the file, launch 310 // If this app doesn't have a file handler that supports the file, launch
245 // with no launch data. 311 // with no launch data.
246 if (!handler) { 312 if (!handler) {
247 LOG(WARNING) << "Extension does not provide a valid file handler for " 313 LOG(WARNING) << "Extension does not provide a valid file handler.";
248 << file_path_.value();
249 LaunchWithNoLaunchData(); 314 LaunchWithNoLaunchData();
250 return; 315 return;
251 } 316 }
252 317
253 if (handler_id_.empty()) 318 if (handler_id_.empty())
254 handler_id_ = handler->id; 319 handler_id_ = handler->id;
255 320
256 // Access needs to be granted to the file for the process associated with 321 // Access needs to be granted to the file for the process associated with
257 // the extension. To do this the ExtensionHost is needed. This might not be 322 // the extension. To do this the ExtensionHost is needed. This might not be
258 // available, or it might be in the process of being unloaded, in which case 323 // available, or it might be in the process of being unloaded, in which case
259 // the lazy background task queue is used to load the extension and then 324 // the lazy background task queue is used to load the extension and then
260 // call back to us. 325 // call back to us.
261 extensions::LazyBackgroundTaskQueue* queue = 326 extensions::LazyBackgroundTaskQueue* const queue =
262 ExtensionSystem::Get(profile_)->lazy_background_task_queue(); 327 ExtensionSystem::Get(profile_)->lazy_background_task_queue();
263 if (queue->ShouldEnqueueTask(profile_, extension_)) { 328 if (queue->ShouldEnqueueTask(profile_, extension_)) {
264 queue->AddPendingTask(profile_, extension_->id(), base::Bind( 329 queue->AddPendingTask(
265 &PlatformAppPathLauncher::GrantAccessToFileAndLaunch, 330 profile_,
266 this, mime_type)); 331 extension_->id(),
332 base::Bind(&PlatformAppPathLauncher::GrantAccessToFilesAndLaunch,
333 this));
267 return; 334 return;
268 } 335 }
269 336
270 extensions::ProcessManager* process_manager = 337 extensions::ProcessManager* const process_manager =
271 ExtensionSystem::Get(profile_)->process_manager(); 338 ExtensionSystem::Get(profile_)->process_manager();
272 ExtensionHost* host = 339 ExtensionHost* const host =
273 process_manager->GetBackgroundHostForExtension(extension_->id()); 340 process_manager->GetBackgroundHostForExtension(extension_->id());
274 DCHECK(host); 341 DCHECK(host);
275 GrantAccessToFileAndLaunch(mime_type, host); 342 GrantAccessToFilesAndLaunch(host);
276 } 343 }
277 344
278 void GrantAccessToFileAndLaunch(const std::string& mime_type, 345 void GrantAccessToFilesAndLaunch(ExtensionHost* host) {
279 ExtensionHost* host) {
280 // If there was an error loading the app page, |host| will be NULL. 346 // If there was an error loading the app page, |host| will be NULL.
281 if (!host) { 347 if (!host) {
282 LOG(ERROR) << "Could not load app page for " << extension_->id(); 348 LOG(ERROR) << "Could not load app page for " << extension_->id();
283 return; 349 return;
284 } 350 }
285 351
286 GrantedFileEntry file_entry = 352 std::vector<GrantedFileEntry> file_entries;
287 CreateFileEntry(profile_, 353 for (size_t i = 0; i < file_paths_.size(); ++i) {
288 extension_, 354 file_entries.push_back(
289 host->render_process_host()->GetID(), 355 CreateFileEntry(profile_,
290 file_path_, 356 extension_,
291 false); 357 host->render_process_host()->GetID(),
292 AppEventRouter::DispatchOnLaunchedEventWithFileEntry( 358 file_paths_[i],
293 profile_, extension_, handler_id_, mime_type, file_entry); 359 false));
360 }
361
362 AppEventRouter::DispatchOnLaunchedEventWithFileEntries(
363 profile_, extension_, handler_id_, mime_types_, file_entries);
294 } 364 }
295 365
296 // The profile the app should be run in. 366 // The profile the app should be run in.
297 Profile* profile_; 367 Profile* profile_;
298 // The extension providing the app. 368 // The extension providing the app.
299 // TODO(benwells): Hold onto the extension ID instead of a pointer as it 369 // TODO(benwells): Hold onto the extension ID instead of a pointer as it
300 // is possible the extension will be unloaded while we're doing our thing. 370 // is possible the extension will be unloaded while we're doing our thing.
301 // See http://crbug.com/372270 for details. 371 // See http://crbug.com/372270 for details.
302 const Extension* extension_; 372 const Extension* extension_;
303 // The path to be passed through to the app. 373 // The path to be passed through to the app.
304 base::FilePath file_path_; 374 std::vector<base::FilePath> file_paths_;
375 std::vector<std::string> mime_types_;
305 // The ID of the file handler used to launch the app. 376 // The ID of the file handler used to launch the app.
306 std::string handler_id_; 377 std::string handler_id_;
307 378
308 DISALLOW_COPY_AND_ASSIGN(PlatformAppPathLauncher); 379 DISALLOW_COPY_AND_ASSIGN(PlatformAppPathLauncher);
309 }; 380 };
310 381
311 } // namespace 382 } // namespace
312 383
313 void LaunchPlatformAppWithCommandLine(Profile* profile, 384 void LaunchPlatformAppWithCommandLine(Profile* profile,
314 const Extension* extension, 385 const Extension* extension,
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
362 launcher->Launch(); 433 launcher->Launch();
363 } 434 }
364 435
365 void LaunchPlatformApp(Profile* profile, const Extension* extension) { 436 void LaunchPlatformApp(Profile* profile, const Extension* extension) {
366 LaunchPlatformAppWithCommandLine(profile, 437 LaunchPlatformAppWithCommandLine(profile,
367 extension, 438 extension,
368 CommandLine(CommandLine::NO_PROGRAM), 439 CommandLine(CommandLine::NO_PROGRAM),
369 base::FilePath()); 440 base::FilePath());
370 } 441 }
371 442
372 void LaunchPlatformAppWithFileHandler(Profile* profile, 443 void LaunchPlatformAppWithFileHandler(
373 const Extension* extension, 444 Profile* profile,
374 const std::string& handler_id, 445 const Extension* extension,
375 const base::FilePath& file_path) { 446 const std::string& handler_id,
447 const std::vector<base::FilePath>& file_paths) {
376 scoped_refptr<PlatformAppPathLauncher> launcher = 448 scoped_refptr<PlatformAppPathLauncher> launcher =
377 new PlatformAppPathLauncher(profile, extension, file_path); 449 new PlatformAppPathLauncher(profile, extension, file_paths);
378 launcher->LaunchWithHandler(handler_id); 450 launcher->LaunchWithHandler(handler_id);
379 } 451 }
380 452
381 void RestartPlatformApp(Profile* profile, const Extension* extension) { 453 void RestartPlatformApp(Profile* profile, const Extension* extension) {
382 EventRouter* event_router = EventRouter::Get(profile); 454 EventRouter* event_router = EventRouter::Get(profile);
383 bool listening_to_restart = event_router-> 455 bool listening_to_restart = event_router->
384 ExtensionHasEventListener(extension->id(), 456 ExtensionHasEventListener(extension->id(),
385 app_runtime::OnRestarted::kEventName); 457 app_runtime::OnRestarted::kEventName);
386 458
387 if (listening_to_restart) { 459 if (listening_to_restart) {
(...skipping 16 matching lines...) Expand all
404 void LaunchPlatformAppWithUrl(Profile* profile, 476 void LaunchPlatformAppWithUrl(Profile* profile,
405 const Extension* extension, 477 const Extension* extension,
406 const std::string& handler_id, 478 const std::string& handler_id,
407 const GURL& url, 479 const GURL& url,
408 const GURL& referrer_url) { 480 const GURL& referrer_url) {
409 AppEventRouter::DispatchOnLaunchedEventWithUrl( 481 AppEventRouter::DispatchOnLaunchedEventWithUrl(
410 profile, extension, handler_id, url, referrer_url); 482 profile, extension, handler_id, url, referrer_url);
411 } 483 }
412 484
413 } // namespace apps 485 } // namespace apps
OLDNEW
« no previous file with comments | « apps/launcher.h ('k') | chrome/browser/chromeos/file_manager/file_tasks.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698