Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2014 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/app_mode/kiosk_external_updater.h" | |
| 6 | |
| 7 #include "base/bind.h" | |
| 8 #include "base/file_util.h" | |
| 9 #include "base/files/file_enumerator.h" | |
| 10 #include "base/json/json_file_value_serializer.h" | |
| 11 #include "base/location.h" | |
| 12 #include "base/logging.h" | |
| 13 #include "base/strings/utf_string_conversions.h" | |
| 14 #include "base/version.h" | |
| 15 #include "chrome/browser/chromeos/app_mode/kiosk_app_manager.h" | |
| 16 #include "chrome/browser/chromeos/ui/kiosk_external_update_notification.h" | |
| 17 #include "chrome/browser/extensions/sandboxed_unpacker.h" | |
| 18 #include "chrome/common/chrome_version_info.h" | |
| 19 #include "content/public/browser/browser_thread.h" | |
| 20 #include "extensions/common/extension.h" | |
| 21 #include "extensions/common/manifest_constants.h" | |
| 22 #include "grit/chromium_strings.h" | |
| 23 #include "grit/generated_resources.h" | |
| 24 #include "ui/base/l10n/l10n_util.h" | |
| 25 #include "ui/base/resource/resource_bundle.h" | |
| 26 | |
| 27 namespace chromeos { | |
| 28 | |
| 29 namespace { | |
| 30 | |
| 31 const char kExternalUpdateManifest[] = "external_update.json"; | |
| 32 const char kExternalCrx[] = "external_crx"; | |
| 33 const char kExternalVersion[] = "external_version"; | |
| 34 | |
| 35 } // namespace | |
| 36 | |
| 37 KioskExternalUpdater::ExternalUpdate::ExternalUpdate() { | |
| 38 } | |
| 39 | |
| 40 KioskExternalUpdater::KioskExternalUpdater( | |
| 41 const scoped_refptr<base::SequencedTaskRunner>& backend_task_runner, | |
| 42 const base::FilePath& crx_cache_dir, | |
| 43 const base::FilePath& crx_unpack_dir) | |
| 44 : backend_task_runner_(backend_task_runner), | |
| 45 crx_cache_dir_(crx_cache_dir), | |
| 46 crx_unpack_dir_(crx_unpack_dir) { | |
| 47 // Subscribe to DiskMountManager. | |
| 48 DCHECK(disks::DiskMountManager::GetInstance()); | |
| 49 disks::DiskMountManager::GetInstance()->AddObserver(this); | |
| 50 } | |
| 51 | |
| 52 KioskExternalUpdater::~KioskExternalUpdater() { | |
| 53 if (disks::DiskMountManager::GetInstance()) | |
| 54 disks::DiskMountManager::GetInstance()->RemoveObserver(this); | |
| 55 } | |
| 56 | |
| 57 void KioskExternalUpdater::OnDiskEvent( | |
| 58 disks::DiskMountManager::DiskEvent event, | |
| 59 const disks::DiskMountManager::Disk* disk) { | |
| 60 } | |
| 61 | |
| 62 void KioskExternalUpdater::OnDeviceEvent( | |
| 63 disks::DiskMountManager::DeviceEvent event, | |
| 64 const std::string& device_path) { | |
| 65 } | |
| 66 | |
| 67 void KioskExternalUpdater::OnMountEvent( | |
| 68 disks::DiskMountManager::MountEvent event, | |
| 69 MountError error_code, | |
| 70 const disks::DiskMountManager::MountPointInfo& mount_info) { | |
| 71 if (mount_info.mount_type != MOUNT_TYPE_DEVICE || | |
| 72 error_code != MOUNT_ERROR_NONE) { | |
| 73 return; | |
| 74 } | |
| 75 | |
| 76 if (event == disks::DiskMountManager::MOUNTING) { | |
| 77 // If multiple disks have been mounted, skip the rest of them if kiosk | |
| 78 // update has already been found. | |
| 79 if (!external_update_path_.empty()) { | |
| 80 LOG(WARNING) << "*** external update path already found, skip " | |
| 81 << mount_info.mount_path; | |
| 82 return; | |
| 83 } | |
| 84 | |
| 85 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 86 ShowKioskUpdateProgressOnUI( | |
| 87 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 88 IDS_KIOSK_EXTERNAL_UPDATE_IN_PROGRESS)); | |
| 89 | |
| 90 backend_task_runner_->PostTask( | |
| 91 FROM_HERE, | |
| 92 base::Bind(&KioskExternalUpdater::LoadExternalUpdateManifest, | |
|
xiyuan
2014/08/23 18:11:57
We can break LoadExternalUpdateManifest up into tw
| |
| 93 this, | |
| 94 base::FilePath(mount_info.mount_path))); | |
| 95 } else { // unmounting a removable device. | |
| 96 if (external_update_path_.value().empty()) { | |
| 97 // Clear any previously displayed message. | |
| 98 DismissKioskUpdateNotificationOnUIThread(); | |
| 99 } else if (external_update_path_.value() == mount_info.mount_path) { | |
| 100 DismissKioskUpdateNotificationOnUIThread(); | |
| 101 if (IsExternalUpdatePending()) { | |
| 102 LOG(ERROR) << "External kiosk update is not completed when the usb " | |
| 103 "stick is unmoutned."; | |
| 104 } | |
| 105 external_updates_.clear(); | |
| 106 external_update_path_.clear(); | |
| 107 } | |
| 108 } | |
| 109 } | |
| 110 | |
| 111 void KioskExternalUpdater::OnFormatEvent( | |
| 112 disks::DiskMountManager::FormatEvent event, | |
| 113 FormatError error_code, | |
| 114 const std::string& device_path) { | |
| 115 } | |
| 116 | |
| 117 void KioskExternalUpdater::OnExtenalUpdateUnpackSuccess( | |
| 118 const std::string& app_id, | |
| 119 const base::FilePath& temp_dir, | |
| 120 const extensions::Extension* extension) { | |
| 121 if (external_updates_.empty()) { | |
| 122 // This could happen if user pulls out the usb stick before the updating | |
| 123 // operation is completed. | |
| 124 LOG(ERROR) << "external_updates_ has been cleared before external " | |
| 125 << "updating completes."; | |
| 126 return; | |
| 127 } | |
| 128 | |
| 129 if (!ShouldDoExternalUpdate(app_id, extension)) { | |
| 130 external_updates_[app_id].update_status = FAILED; | |
| 131 MayBeNotifyKioskAppUpdate(); | |
| 132 return; | |
| 133 } | |
| 134 | |
| 135 // Cleans upt he temp_dir for unpacking. | |
|
xiyuan
2014/08/23 18:11:57
nit: upt he -> up the
jennyz
2014/08/27 00:58:41
Done.
| |
| 136 base::DeleteFile(temp_dir, true); | |
| 137 | |
| 138 // Copy the newer version from usb stick to cache. | |
| 139 std::string filename = app_id + "-" + extension->VersionString() + ".crx"; | |
| 140 base::FilePath new_cache_file = crx_cache_dir_.AppendASCII(filename); | |
| 141 CacheExternalCrx(app_id, new_cache_file); | |
| 142 } | |
| 143 | |
| 144 void KioskExternalUpdater::OnExternalUpdateUnpackFailure( | |
| 145 const std::string& app_id) { | |
| 146 if (external_updates_.empty()) { | |
| 147 // This could happen if user pulls out the usb stick before the updating | |
| 148 // operation is completed. | |
| 149 LOG(ERROR) << "external_updates_ has been cleared before external " | |
| 150 << "updating completes."; | |
| 151 return; | |
| 152 } | |
| 153 | |
| 154 external_updates_[app_id].update_status = FAILED; | |
| 155 external_updates_[app_id].error = | |
| 156 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 157 IDS_KIOSK_EXTERNAL_UPDATE_BAD_CRX); | |
| 158 MayBeNotifyKioskAppUpdate(); | |
| 159 } | |
| 160 | |
| 161 void KioskExternalUpdater::LoadExternalUpdateManifest( | |
| 162 const base::FilePath& external_update_dir) { | |
| 163 base::FilePath manifest = | |
| 164 external_update_dir.AppendASCII(kExternalUpdateManifest); | |
| 165 if (!base::PathExists(manifest)) { | |
| 166 LOG(WARNING) << "*** Can't find kiosk external update manifest file " | |
| 167 << manifest.value(); | |
| 168 ShowKioskUpdateProgressOnUI( | |
| 169 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 170 IDS_KIOSK_EXTERNAL_UPDATE_NO_MANIFEST)); | |
| 171 return; | |
| 172 } | |
| 173 | |
| 174 external_update_path_ = external_update_dir; | |
| 175 | |
| 176 JSONFileValueSerializer serializer(manifest); | |
| 177 std::string error_msg; | |
| 178 base::Value* extensions = serializer.Deserialize(NULL, &error_msg); | |
| 179 if (!extensions) { | |
| 180 ShowKioskUpdateProgressOnUI( | |
| 181 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 182 IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MANIFEST)); | |
| 183 LOG(ERROR) << "Unable to deserialize json data: " << error_msg | |
| 184 << " in manifest file " << manifest.value() << "."; | |
| 185 return; | |
| 186 } | |
| 187 | |
| 188 base::DictionaryValue* update_prefs = NULL; | |
| 189 if (!extensions->GetAsDictionary(&update_prefs)) { | |
| 190 ShowKioskUpdateProgressOnUI( | |
| 191 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 192 IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MANIFEST)); | |
| 193 LOG(ERROR) << "Invalid manifest file, expected dictionary data:" | |
| 194 << manifest.value(); | |
| 195 return; | |
| 196 } | |
| 197 | |
| 198 for (base::DictionaryValue::Iterator it(*update_prefs); !it.IsAtEnd(); | |
| 199 it.Advance()) { | |
| 200 std::string app_id = it.key(); | |
| 201 std::string cached_version_str; | |
| 202 base::FilePath cached_crx; | |
| 203 if (!KioskAppManager::Get()->GetCachedCrx( | |
| 204 app_id, &cached_crx, &cached_version_str)) { | |
| 205 LOG(WARNING) << "Can't find app in existing cache " << app_id; | |
| 206 continue; | |
| 207 } | |
| 208 | |
| 209 const base::DictionaryValue* extension = NULL; | |
| 210 if (!it.value().GetAsDictionary(&extension)) { | |
| 211 LOG(ERROR) << "Found bad entry in manifest type " << it.value().GetType(); | |
| 212 continue; | |
| 213 } | |
| 214 | |
| 215 std::string external_crx_str; | |
| 216 if (!extension->GetString(kExternalCrx, &external_crx_str)) { | |
| 217 LOG(ERROR) << "Can't find external crx in manifest " << app_id; | |
| 218 continue; | |
| 219 } | |
| 220 // Validate path first. | |
| 221 base::FilePath external_crx = | |
| 222 external_update_dir.AppendASCII(external_crx_str); | |
| 223 if (!base::PathExists(external_crx)) { | |
| 224 LOG(ERROR) << "External crx does not exist " << external_crx.value(); | |
| 225 continue; | |
| 226 } | |
| 227 | |
| 228 std::string external_version_str; | |
| 229 if (!extension->GetString(kExternalVersion, &external_version_str)) { | |
| 230 LOG(ERROR) << "Can't find external version in manifest " << app_id; | |
| 231 continue; | |
| 232 } | |
| 233 base::Version external_version(external_version_str); | |
| 234 base::Version cached_version(cached_version_str); | |
| 235 switch (cached_version.CompareTo(external_version)) { | |
| 236 case -1: // cached version is older, we should upgrade | |
| 237 break; | |
| 238 case 0: // cached version is same, do nothing | |
| 239 LOG(WARNING) << "External app " << app_id | |
| 240 << "is at the same version with manifest"; | |
| 241 continue; | |
| 242 case 1: // cached version is newer, do nothing. | |
| 243 LOG(WARNING) << "Found external version of extension " << app_id | |
| 244 << "that is older than current version. Current version " | |
| 245 << "is: " << cached_version_str << ". New " | |
| 246 << "version is: " << external_version_str | |
| 247 << ". Keeping current version."; | |
| 248 continue; | |
| 249 } | |
| 250 | |
| 251 ExternalUpdate update; | |
| 252 KioskAppManager::App app; | |
| 253 if (KioskAppManager::Get()->GetApp(app_id, &app)) { | |
| 254 update.app_name = app.name; | |
| 255 } else { | |
| 256 NOTREACHED(); | |
| 257 } | |
| 258 update.external_crx = external_crx; | |
| 259 update.update_status = PENDING; | |
| 260 external_updates_[app_id] = update; | |
| 261 } | |
| 262 | |
| 263 if (!external_updates_.size()) { | |
|
xiyuan
2014/08/23 18:11:57
nit: if (external_updates_.empty())
jennyz
2014/08/27 00:58:42
Done.
| |
| 264 ShowKioskUpdateProgressOnUI( | |
| 265 ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 266 IDS_KIOSK_EXTERNAL_UPDATE_NO_UPDATES)); | |
| 267 return; | |
| 268 } | |
| 269 | |
| 270 ValidateExternalUpdates(); | |
| 271 } | |
| 272 | |
| 273 void KioskExternalUpdater::ValidateExternalUpdates() { | |
| 274 DCHECK(backend_task_runner_->RunsTasksOnCurrentThread()); | |
| 275 for (ExternalUpdateMap::iterator it = external_updates_.begin(); | |
| 276 it != external_updates_.end(); | |
| 277 ++it) { | |
| 278 scoped_refptr<KioskExternalUpdateValidator> crx_validator = | |
|
xiyuan
2014/08/23 18:11:57
To not ref-counting updater, we should keep track
jennyz
2014/08/27 00:58:41
KioskExternalUpdater is not ref counted, and the v
| |
| 279 new KioskExternalUpdateValidator(backend_task_runner_, | |
| 280 it->first, | |
| 281 it->second.external_crx, | |
| 282 crx_unpack_dir_, | |
| 283 this); | |
| 284 crx_validator->Start(); | |
| 285 } | |
| 286 } | |
| 287 | |
| 288 void KioskExternalUpdater::CacheExternalCrx(const std::string& app_id, | |
| 289 const base::FilePath& target_file) { | |
| 290 if (!backend_task_runner_->RunsTasksOnCurrentThread()) { | |
| 291 if (!backend_task_runner_->PostTask( | |
| 292 FROM_HERE, | |
| 293 base::Bind(&KioskExternalUpdater::CacheExternalCrx, | |
| 294 this, | |
| 295 app_id, | |
| 296 target_file))) { | |
| 297 NOTREACHED(); | |
| 298 } | |
| 299 return; | |
| 300 } | |
| 301 | |
| 302 // User might pull out the usb stick before updating is completed. | |
| 303 if (external_updates_.empty()) { | |
| 304 LOG(ERROR) << "External_updates_ has been cleared before the external " | |
| 305 << "update completes."; | |
| 306 return; | |
| 307 } | |
| 308 | |
| 309 if (!base::CopyFile(external_updates_[app_id].external_crx, target_file)) { | |
| 310 external_updates_[app_id].update_status = FAILED; | |
| 311 external_updates_[app_id].error = l10n_util::GetStringFUTF16( | |
| 312 IDS_KIOSK_EXTERNAL_UPDATE_CANNOT_COPY_CRX, | |
| 313 base::UTF8ToUTF16(external_updates_[app_id].external_crx.value()), | |
| 314 base::UTF8ToUTF16(target_file.value())); | |
| 315 } else { | |
| 316 external_updates_[app_id].update_status = SUCCESS; | |
| 317 } | |
| 318 | |
| 319 MayBeNotifyKioskAppUpdate(); | |
| 320 } | |
| 321 | |
| 322 bool KioskExternalUpdater::IsExternalUpdatePending() { | |
| 323 for (ExternalUpdateMap::iterator it = external_updates_.begin(); | |
| 324 it != external_updates_.end(); | |
| 325 ++it) { | |
| 326 if (it->second.update_status == PENDING) { | |
| 327 return true; | |
| 328 } | |
| 329 } | |
| 330 return false; | |
| 331 } | |
| 332 | |
| 333 bool KioskExternalUpdater::ShouldDoExternalUpdate( | |
| 334 const std::string& app_id, | |
| 335 const extensions::Extension* extenral_extension) { | |
| 336 // User might have pulled out the usb stick before external updating is done. | |
| 337 if (external_updates_.empty()) { | |
| 338 LOG(ERROR) << "external_updates_ has been cleared before external " | |
| 339 << "updating completes."; | |
| 340 return false; | |
| 341 } | |
| 342 | |
| 343 std::string existing_version_str; | |
| 344 base::FilePath existing_path; | |
| 345 bool cached = KioskAppManager::Get()->GetCachedCrx( | |
| 346 app_id, &existing_path, &existing_version_str); | |
| 347 DCHECK(cached); | |
| 348 | |
| 349 // Compare app version. | |
| 350 const base::Version existing_version(existing_version_str); | |
| 351 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); | |
| 352 switch (existing_version.CompareTo(*extenral_extension->version())) { | |
| 353 case -1: // existing version is older, we should upgrade | |
| 354 break; | |
| 355 case 0: // existing version is same, no update | |
| 356 external_updates_[app_id].error = | |
| 357 rb.GetLocalizedString(IDS_KIOSK_EXTERNAL_UPDATE_SAME_APP_VERSION); | |
| 358 return false; | |
| 359 case 1: // existing version is newer, no update | |
| 360 external_updates_[app_id].error = l10n_util::GetStringFUTF16( | |
| 361 IDS_KIOSK_EXTERNAL_UPDATE_EXISTING_VERSION_NEWER, | |
| 362 base::UTF8ToUTF16(extenral_extension->VersionString()), | |
| 363 base::UTF8ToUTF16(existing_version_str)); | |
| 364 return false; | |
| 365 } | |
| 366 | |
| 367 // Check minimum browser version. | |
| 368 std::string minimum_version_string; | |
| 369 if (!extenral_extension->manifest()->GetString( | |
| 370 extensions::manifest_keys::kMinimumChromeVersion, | |
| 371 &minimum_version_string)) { | |
| 372 external_updates_[app_id].error = rb.GetLocalizedString( | |
| 373 IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MIN_BROWSER_VERSION); | |
| 374 return false; | |
| 375 } | |
| 376 | |
| 377 Version minimum_version(minimum_version_string); | |
| 378 if (!minimum_version.IsValid()) { | |
| 379 external_updates_[app_id].error = rb.GetLocalizedString( | |
| 380 IDS_KIOSK_EXTERNAL_UPDATE_INVALID_MIN_BROWSER_VERSION); | |
| 381 return false; | |
| 382 } | |
| 383 | |
| 384 chrome::VersionInfo current_version_info; | |
| 385 Version current_version(current_version_info.Version()); | |
| 386 if (!current_version.IsValid()) { | |
| 387 NOTREACHED(); | |
| 388 return false; | |
| 389 } | |
| 390 | |
| 391 base::string16 error; | |
| 392 if (current_version.CompareTo(minimum_version) < 0) { | |
| 393 external_updates_[app_id].error = l10n_util::GetStringFUTF16( | |
| 394 IDS_KIOSK_EXTERNAL_UPDATE_REQUIRE_HIGHER_BROWSER_VERSION, | |
| 395 l10n_util::GetStringUTF16(IDS_PRODUCT_NAME)); | |
| 396 return false; | |
| 397 } | |
| 398 | |
| 399 return true; | |
| 400 } | |
| 401 | |
| 402 void KioskExternalUpdater::ShowKioskUpdateProgressOnUI( | |
|
xiyuan
2014/08/23 18:11:57
nit: OnUI is misleading. Other places uses this su
jennyz
2014/08/27 00:58:41
Renamed.
| |
| 403 const base::string16& message) { | |
| 404 content::BrowserThread::PostTask( | |
| 405 content::BrowserThread::UI, | |
| 406 FROM_HERE, | |
| 407 base::Bind( | |
| 408 &KioskExternalUpdater::ShowKioskUpdateProgress, this, message)); | |
| 409 } | |
| 410 | |
| 411 void KioskExternalUpdater::MayBeNotifyKioskAppUpdate() { | |
| 412 if (IsExternalUpdatePending()) | |
| 413 return; | |
| 414 | |
| 415 ShowKioskUpdateProgressOnUI(GetUpdateReportMessage()); | |
| 416 | |
| 417 content::BrowserThread::PostTask( | |
| 418 content::BrowserThread::UI, | |
| 419 FROM_HERE, | |
| 420 base::Bind(&KioskExternalUpdater::NotifyKioskAppUpdateAvailable, this)); | |
| 421 } | |
| 422 | |
| 423 void KioskExternalUpdater::NotifyKioskAppUpdateAvailable() { | |
| 424 /// This should be done on UI thread??? | |
|
xiyuan
2014/08/23 18:11:58
Yes, this has to be called on UI.
jennyz
2014/08/27 00:58:42
Acknowledged.
| |
| 425 for (ExternalUpdateMap::iterator it = external_updates_.begin(); | |
| 426 it != external_updates_.end(); | |
| 427 ++it) { | |
| 428 if (it->second.update_status == SUCCESS) { | |
| 429 KioskAppManager::Get()->OnKioskAppCacheUpdated(it->first); | |
| 430 } | |
| 431 } | |
| 432 } | |
| 433 | |
| 434 void KioskExternalUpdater::ShowKioskUpdateProgress( | |
| 435 const base::string16& message) { | |
| 436 if (!notification_) | |
| 437 notification_.reset(new KioskExternalUpdateNotification(message)); | |
| 438 else | |
| 439 notification_->ShowMessage(message); | |
| 440 } | |
| 441 | |
| 442 void KioskExternalUpdater::DismissKioskUpdateNotificationOnUIThread() { | |
| 443 DCHECK_CURRENTLY_ON(content::BrowserThread::UI); | |
| 444 content::BrowserThread::PostTask( | |
| 445 content::BrowserThread::UI, | |
| 446 FROM_HERE, | |
| 447 base::Bind(&KioskExternalUpdater::DismissKioskUpdateNotification, this)); | |
| 448 } | |
| 449 | |
| 450 void KioskExternalUpdater::DismissKioskUpdateNotification() { | |
| 451 if (notification_.get()) { | |
| 452 notification_->Dismiss(); | |
| 453 notification_.reset(); | |
| 454 } | |
| 455 } | |
| 456 | |
| 457 base::string16 KioskExternalUpdater::GetUpdateReportMessage() { | |
| 458 DCHECK(!IsExternalUpdatePending()); | |
| 459 int updated = 0; | |
| 460 int failed = 0; | |
| 461 base::string16 updated_apps; | |
| 462 base::string16 failed_apps; | |
| 463 for (ExternalUpdateMap::iterator it = external_updates_.begin(); | |
| 464 it != external_updates_.end(); | |
| 465 ++it) { | |
| 466 base::string16 app_name = base::UTF8ToUTF16(it->second.app_name); | |
| 467 if (it->second.update_status == SUCCESS) { | |
| 468 ++updated; | |
| 469 if (updated_apps.empty()) | |
| 470 updated_apps = app_name; | |
| 471 else | |
| 472 updated_apps = updated_apps + base::UTF8ToUTF16(", ") + app_name; | |
| 473 } else { // FAILED | |
| 474 ++failed; | |
| 475 if (failed_apps.empty()) { | |
| 476 failed_apps = app_name + base::UTF8ToUTF16(": ") + it->second.error; | |
| 477 } else { | |
| 478 failed_apps = failed_apps + base::UTF8ToUTF16("\n") + app_name + | |
| 479 base::UTF8ToUTF16(": ") + it->second.error; | |
| 480 } | |
| 481 } | |
| 482 } | |
| 483 | |
| 484 base::string16 message; | |
| 485 message = ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 486 IDS_KIOSK_EXTERNAL_UPDATE_COMPLETE); | |
| 487 base::string16 success_app_msg; | |
| 488 if (updated) { | |
| 489 success_app_msg = l10n_util::GetStringFUTF16( | |
| 490 IDS_KIOSK_EXTERNAL_UPDATE_SUCCESSFUL_UPDATED_APPS, updated_apps); | |
| 491 message = message + base::UTF8ToUTF16("\n") + success_app_msg; | |
| 492 } | |
| 493 | |
| 494 base::string16 failed_app_msg; | |
| 495 if (failed) { | |
| 496 failed_app_msg = ui::ResourceBundle::GetSharedInstance().GetLocalizedString( | |
| 497 IDS_KIOSK_EXTERNAL_UPDATE_FAILED_UPDATED_APPS) + | |
| 498 base::UTF8ToUTF16("\n") + failed_apps; | |
| 499 message = message + base::UTF8ToUTF16("\n") + failed_app_msg; | |
| 500 } | |
| 501 return message; | |
| 502 } | |
| 503 | |
| 504 } // namespace chromeos | |
| OLD | NEW |