1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>. 4 * Copyright 2016-2021, Andrew Lindesay <apl@lindesay.co.nz>. 5 * All rights reserved. Distributed under the terms of the MIT License. 6 */ 7 8 #include "Model.h" 9 10 #include <algorithm> 11 #include <ctime> 12 #include <vector> 13 14 #include <stdarg.h> 15 #include <time.h> 16 17 #include <Autolock.h> 18 #include <Catalog.h> 19 #include <Directory.h> 20 #include <Entry.h> 21 #include <File.h> 22 #include <KeyStore.h> 23 #include <Locale.h> 24 #include <LocaleRoster.h> 25 #include <Message.h> 26 #include <Path.h> 27 28 #include "HaikuDepotConstants.h" 29 #include "Logger.h" 30 #include "LocaleUtils.h" 31 #include "StorageUtils.h" 32 #include "RepositoryUrlUtils.h" 33 34 35 #undef B_TRANSLATION_CONTEXT 36 #define B_TRANSLATION_CONTEXT "Model" 37 38 39 #define KEY_STORE_IDENTIFIER_PREFIX "hds.password." 40 // this prefix is added before the nickname in the keystore 41 // so that HDS username/password pairs can be identified. 42 43 static const char* kHaikuDepotKeyring = "HaikuDepot"; 44 45 46 PackageFilter::~PackageFilter() 47 { 48 } 49 50 51 ModelListener::~ModelListener() 52 { 53 } 54 55 56 // #pragma mark - PackageFilters 57 58 59 class AnyFilter : public PackageFilter { 60 public: 61 virtual bool AcceptsPackage(const PackageInfoRef& package) const 62 { 63 return true; 64 } 65 }; 66 67 68 class CategoryFilter : public PackageFilter { 69 public: 70 CategoryFilter(const BString& category) 71 : 72 fCategory(category) 73 { 74 } 75 76 virtual bool AcceptsPackage(const PackageInfoRef& package) const 77 { 78 if (!package.IsSet()) 79 return false; 80 81 for (int i = package->CountCategories() - 1; i >= 0; i--) { 82 const CategoryRef& category = package->CategoryAtIndex(i); 83 if (!category.IsSet()) 84 continue; 85 if (category->Code() == fCategory) 86 return true; 87 } 88 return false; 89 } 90 91 const BString& Category() const 92 { 93 return fCategory; 94 } 95 96 private: 97 BString fCategory; 98 }; 99 100 101 class StateFilter : public PackageFilter { 102 public: 103 StateFilter(PackageState state) 104 : 105 fState(state) 106 { 107 } 108 109 virtual bool AcceptsPackage(const PackageInfoRef& package) const 110 { 111 return package->State() == NONE; 112 } 113 114 private: 115 PackageState fState; 116 }; 117 118 119 class SearchTermsFilter : public PackageFilter { 120 public: 121 SearchTermsFilter(const BString& searchTerms) 122 { 123 // Separate the string into terms at spaces 124 int32 index = 0; 125 while (index < searchTerms.Length()) { 126 int32 nextSpace = searchTerms.FindFirst(" ", index); 127 if (nextSpace < 0) 128 nextSpace = searchTerms.Length(); 129 if (nextSpace > index) { 130 BString term; 131 searchTerms.CopyInto(term, index, nextSpace - index); 132 term.ToLower(); 133 fSearchTerms.Add(term); 134 } 135 index = nextSpace + 1; 136 } 137 } 138 139 virtual bool AcceptsPackage(const PackageInfoRef& package) const 140 { 141 if (!package.IsSet()) 142 return false; 143 // Every search term must be found in one of the package texts 144 for (int32 i = fSearchTerms.CountStrings() - 1; i >= 0; i--) { 145 const BString& term = fSearchTerms.StringAt(i); 146 if (!_TextContains(package->Name(), term) 147 && !_TextContains(package->Title(), term) 148 && !_TextContains(package->Publisher().Name(), term) 149 && !_TextContains(package->ShortDescription(), term) 150 && !_TextContains(package->FullDescription(), term)) { 151 return false; 152 } 153 } 154 return true; 155 } 156 157 BString SearchTerms() const 158 { 159 BString searchTerms; 160 for (int32 i = 0; i < fSearchTerms.CountStrings(); i++) { 161 const BString& term = fSearchTerms.StringAt(i); 162 if (term.IsEmpty()) 163 continue; 164 if (!searchTerms.IsEmpty()) 165 searchTerms.Append(" "); 166 searchTerms.Append(term); 167 } 168 return searchTerms; 169 } 170 171 private: 172 bool _TextContains(BString text, const BString& string) const 173 { 174 text.ToLower(); 175 int32 index = text.FindFirst(string); 176 return index >= 0; 177 } 178 179 private: 180 BStringList fSearchTerms; 181 }; 182 183 184 static inline bool 185 is_source_package(const PackageInfoRef& package) 186 { 187 const BString& packageName = package->Name(); 188 return packageName.EndsWith("_source"); 189 } 190 191 192 static inline bool 193 is_develop_package(const PackageInfoRef& package) 194 { 195 const BString& packageName = package->Name(); 196 return packageName.EndsWith("_devel") 197 || packageName.EndsWith("_debuginfo"); 198 } 199 200 201 // #pragma mark - Model 202 203 204 Model::Model() 205 : 206 fDepots(), 207 fCategories(), 208 fCategoryFilter(PackageFilterRef(new AnyFilter(), true)), 209 fDepotFilter(""), 210 fSearchTermsFilter(PackageFilterRef(new AnyFilter(), true)), 211 fPackageListViewMode(PROMINENT), 212 fShowAvailablePackages(true), 213 fShowInstalledPackages(true), 214 fShowSourcePackages(false), 215 fShowDevelopPackages(false), 216 fCanShareAnonymousUsageData(false) 217 { 218 } 219 220 221 Model::~Model() 222 { 223 } 224 225 226 LanguageModel* 227 Model::Language() 228 { 229 return &fLanguageModel; 230 } 231 232 233 PackageIconRepository& 234 Model::GetPackageIconRepository() 235 { 236 return fPackageIconRepository; 237 } 238 239 240 status_t 241 Model::InitPackageIconRepository() 242 { 243 BPath tarPath; 244 status_t result = IconTarPath(tarPath); 245 if (result == B_OK) 246 result = fPackageIconRepository.Init(tarPath); 247 return result; 248 } 249 250 251 void 252 Model::AddListener(const ModelListenerRef& listener) 253 { 254 fListeners.push_back(listener); 255 } 256 257 258 // TODO; part of a wider change; cope with the package being in more than one 259 // depot 260 PackageInfoRef 261 Model::PackageForName(const BString& name) 262 { 263 std::vector<DepotInfoRef>::iterator it; 264 for (it = fDepots.begin(); it != fDepots.end(); it++) { 265 DepotInfoRef depotInfoRef = *it; 266 PackageInfoRef packageInfoRef = depotInfoRef->PackageByName(name); 267 if (packageInfoRef.Get() != NULL) 268 return packageInfoRef; 269 } 270 return PackageInfoRef(); 271 } 272 273 274 bool 275 Model::MatchesFilter(const PackageInfoRef& package) const 276 { 277 return fCategoryFilter->AcceptsPackage(package) 278 && fSearchTermsFilter->AcceptsPackage(package) 279 && (fDepotFilter.IsEmpty() || fDepotFilter == package->DepotName()) 280 && (fShowAvailablePackages || package->State() != NONE) 281 && (fShowInstalledPackages || package->State() != ACTIVATED) 282 && (fShowSourcePackages || !is_source_package(package)) 283 && (fShowDevelopPackages || !is_develop_package(package)); 284 } 285 286 287 void 288 Model::MergeOrAddDepot(const DepotInfoRef& depot) 289 { 290 BString depotName = depot->Name(); 291 for(uint32 i = 0; i < fDepots.size(); i++) { 292 if (fDepots[i]->Name() == depotName) { 293 DepotInfoRef ersatzDepot(new DepotInfo(*(fDepots[i].Get())), true); 294 ersatzDepot->SyncPackagesFromDepot(depot); 295 fDepots[i] = ersatzDepot; 296 return; 297 } 298 } 299 fDepots.push_back(depot); 300 } 301 302 303 bool 304 Model::HasDepot(const BString& name) const 305 { 306 return NULL != DepotForName(name).Get(); 307 } 308 309 310 const DepotInfoRef 311 Model::DepotForName(const BString& name) const 312 { 313 std::vector<DepotInfoRef>::const_iterator it; 314 for (it = fDepots.begin(); it != fDepots.end(); it++) { 315 DepotInfoRef aDepot = *it; 316 if (aDepot->Name() == name) 317 return aDepot; 318 } 319 return DepotInfoRef(); 320 } 321 322 323 int32 324 Model::CountDepots() const 325 { 326 return fDepots.size(); 327 } 328 329 330 DepotInfoRef 331 Model::DepotAtIndex(int32 index) const 332 { 333 return fDepots[index]; 334 } 335 336 337 bool 338 Model::HasAnyProminentPackages() 339 { 340 std::vector<DepotInfoRef>::iterator it; 341 for (it = fDepots.begin(); it != fDepots.end(); it++) { 342 DepotInfoRef aDepot = *it; 343 if (aDepot->HasAnyProminentPackages()) 344 return true; 345 } 346 return false; 347 } 348 349 350 void 351 Model::Clear() 352 { 353 GetPackageIconRepository().Clear(); 354 fDepots.clear(); 355 fPopulatedPackageNames.MakeEmpty(); 356 } 357 358 359 void 360 Model::SetPackageState(const PackageInfoRef& package, PackageState state) 361 { 362 package->SetState(state); 363 } 364 365 366 // #pragma mark - filters 367 368 369 void 370 Model::SetCategory(const BString& category) 371 { 372 PackageFilter* filter; 373 374 if (category.Length() == 0) 375 filter = new AnyFilter(); 376 else 377 filter = new CategoryFilter(category); 378 379 fCategoryFilter.SetTo(filter, true); 380 } 381 382 383 BString 384 Model::Category() const 385 { 386 CategoryFilter* filter 387 = dynamic_cast<CategoryFilter*>(fCategoryFilter.Get()); 388 if (filter == NULL) 389 return ""; 390 return filter->Category(); 391 } 392 393 394 void 395 Model::SetDepot(const BString& depot) 396 { 397 fDepotFilter = depot; 398 } 399 400 401 BString 402 Model::Depot() const 403 { 404 return fDepotFilter; 405 } 406 407 408 void 409 Model::SetSearchTerms(const BString& searchTerms) 410 { 411 PackageFilter* filter; 412 413 if (searchTerms.Length() == 0) 414 filter = new AnyFilter(); 415 else 416 filter = new SearchTermsFilter(searchTerms); 417 418 fSearchTermsFilter.SetTo(filter, true); 419 } 420 421 422 BString 423 Model::SearchTerms() const 424 { 425 SearchTermsFilter* filter 426 = dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get()); 427 if (filter == NULL) 428 return ""; 429 return filter->SearchTerms(); 430 } 431 432 433 void 434 Model::SetPackageListViewMode(package_list_view_mode mode) 435 { 436 fPackageListViewMode = mode; 437 } 438 439 440 void 441 Model::SetCanShareAnonymousUsageData(bool value) 442 { 443 fCanShareAnonymousUsageData = value; 444 } 445 446 447 void 448 Model::SetShowAvailablePackages(bool show) 449 { 450 fShowAvailablePackages = show; 451 } 452 453 454 void 455 Model::SetShowInstalledPackages(bool show) 456 { 457 fShowInstalledPackages = show; 458 } 459 460 461 void 462 Model::SetShowSourcePackages(bool show) 463 { 464 fShowSourcePackages = show; 465 } 466 467 468 void 469 Model::SetShowDevelopPackages(bool show) 470 { 471 fShowDevelopPackages = show; 472 } 473 474 475 // #pragma mark - information retrieval 476 477 /*! It may transpire that the package has no corresponding record on the 478 server side because the repository is not represented in the server. 479 In such a case, there is little point in communicating with the server 480 only to hear back that the package does not exist. 481 */ 482 483 bool 484 Model::CanPopulatePackage(const PackageInfoRef& package) 485 { 486 const BString& depotName = package->DepotName(); 487 488 if (depotName.IsEmpty()) 489 return false; 490 491 const DepotInfoRef& depot = DepotForName(depotName); 492 493 if (depot.Get() == NULL) 494 return false; 495 496 return !depot->WebAppRepositoryCode().IsEmpty(); 497 } 498 499 500 /*! Initially only superficial data is loaded from the server into the data 501 model of the packages. When the package is viewed, additional data needs 502 to be populated including ratings. This method takes care of that. 503 */ 504 505 void 506 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) 507 { 508 if (!CanPopulatePackage(package)) { 509 HDINFO("unable to populate package [%s]", package->Name().String()); 510 return; 511 } 512 513 // TODO: There should probably also be a way to "unpopulate" the 514 // package information. Maybe a cache of populated packages, so that 515 // packages loose their extra information after a certain amount of 516 // time when they have not been accessed/displayed in the UI. Otherwise 517 // HaikuDepot will consume more and more resources in the packages. 518 // Especially screen-shots will be a problem eventually. 519 { 520 BAutolock locker(&fLock); 521 bool alreadyPopulated = fPopulatedPackageNames.HasString( 522 package->Name()); 523 if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated) 524 return; 525 if (!alreadyPopulated) 526 fPopulatedPackageNames.Add(package->Name()); 527 } 528 529 if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) { 530 _PopulatePackageChangelog(package); 531 } 532 533 if ((flags & POPULATE_USER_RATINGS) != 0) { 534 // Retrieve info from web-app 535 BMessage info; 536 537 BString packageName; 538 BString webAppRepositoryCode; 539 { 540 BAutolock locker(&fLock); 541 packageName = package->Name(); 542 const DepotInfo* depot = DepotForName(package->DepotName()); 543 544 if (depot != NULL) 545 webAppRepositoryCode = depot->WebAppRepositoryCode(); 546 } 547 548 status_t status = fWebAppInterface 549 .RetreiveUserRatingsForPackageForDisplay(packageName, 550 webAppRepositoryCode, 0, PACKAGE_INFO_MAX_USER_RATINGS, 551 info); 552 if (status == B_OK) { 553 // Parse message 554 BMessage result; 555 BMessage items; 556 if (info.FindMessage("result", &result) == B_OK 557 && result.FindMessage("items", &items) == B_OK) { 558 559 BAutolock locker(&fLock); 560 package->ClearUserRatings(); 561 562 int32 index = 0; 563 while (true) { 564 BString name; 565 name << index++; 566 567 BMessage item; 568 if (items.FindMessage(name, &item) != B_OK) 569 break; 570 571 BString code; 572 if (item.FindString("code", &code) != B_OK) { 573 HDERROR("corrupt user rating at index %" B_PRIi32, 574 index); 575 continue; 576 } 577 578 BString user; 579 BMessage userInfo; 580 if (item.FindMessage("user", &userInfo) != B_OK 581 || userInfo.FindString("nickname", &user) != B_OK) { 582 HDERROR("ignored user rating [%s] without a user " 583 "nickname", code.String()); 584 continue; 585 } 586 587 // Extract basic info, all items are optional 588 BString languageCode; 589 BString comment; 590 double rating; 591 item.FindString("naturalLanguageCode", &languageCode); 592 item.FindString("comment", &comment); 593 if (item.FindDouble("rating", &rating) != B_OK) 594 rating = -1; 595 if (comment.Length() == 0 && rating == -1) { 596 HDERROR("rating [%s] has no comment or rating so will" 597 " be ignored", code.String()); 598 continue; 599 } 600 601 // For which version of the package was the rating? 602 BString major = "?"; 603 BString minor = "?"; 604 BString micro = ""; 605 double revision = -1; 606 BString architectureCode = ""; 607 BMessage version; 608 if (item.FindMessage("pkgVersion", &version) == B_OK) { 609 version.FindString("major", &major); 610 version.FindString("minor", &minor); 611 version.FindString("micro", µ); 612 version.FindDouble("revision", &revision); 613 version.FindString("architectureCode", 614 &architectureCode); 615 } 616 BString versionString = major; 617 versionString << "."; 618 versionString << minor; 619 if (!micro.IsEmpty()) { 620 versionString << "."; 621 versionString << micro; 622 } 623 if (revision > 0) { 624 versionString << "-"; 625 versionString << (int) revision; 626 } 627 628 if (!architectureCode.IsEmpty()) { 629 versionString << " " << STR_MDASH << " "; 630 versionString << architectureCode; 631 } 632 633 double createTimestamp; 634 item.FindDouble("createTimestamp", &createTimestamp); 635 636 // Add the rating to the PackageInfo 637 UserRatingRef userRating(new UserRating( 638 UserInfo(user), rating, 639 comment, languageCode, versionString, 640 (uint64) createTimestamp), true); 641 package->AddUserRating(userRating); 642 HDDEBUG("rating [%s] retrieved from server", code.String()); 643 } 644 HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]", 645 index - 1, packageName.String()); 646 } else { 647 BString message; 648 message.SetToFormat("failure to retrieve user ratings for [%s]", 649 packageName.String()); 650 _MaybeLogJsonRpcError(info, message.String()); 651 } 652 } else 653 HDERROR("unable to retrieve user ratings"); 654 } 655 656 if ((flags & POPULATE_SCREEN_SHOTS) != 0) { 657 std::vector<ScreenshotInfoRef> screenshotInfos; 658 { 659 BAutolock locker(&fLock); 660 for (int32 i = 0; i < package->CountScreenshotInfos(); i++) 661 screenshotInfos.push_back(package->ScreenshotInfoAtIndex(i)); 662 package->ClearScreenshots(); 663 } 664 std::vector<ScreenshotInfoRef>::iterator it; 665 for (it = screenshotInfos.begin(); it != screenshotInfos.end(); it++) { 666 const ScreenshotInfoRef& info = *it; 667 _PopulatePackageScreenshot(package, info, 320, false); 668 } 669 } 670 } 671 672 673 void 674 Model::_PopulatePackageChangelog(const PackageInfoRef& package) 675 { 676 BMessage info; 677 BString packageName; 678 679 { 680 BAutolock locker(&fLock); 681 packageName = package->Name(); 682 } 683 684 status_t status = fWebAppInterface.GetChangelog(packageName, info); 685 686 if (status == B_OK) { 687 // Parse message 688 BMessage result; 689 BString content; 690 if (info.FindMessage("result", &result) == B_OK) { 691 if (result.FindString("content", &content) == B_OK 692 && 0 != content.Length()) { 693 BAutolock locker(&fLock); 694 package->SetChangelog(content); 695 HDDEBUG("changelog populated for [%s]", packageName.String()); 696 } else 697 HDDEBUG("no changelog present for [%s]", packageName.String()); 698 } else 699 _MaybeLogJsonRpcError(info, "populate package changelog"); 700 } else { 701 HDERROR("unable to obtain the changelog for the package [%s]", 702 packageName.String()); 703 } 704 } 705 706 707 static void 708 model_remove_key_for_user(const BString& nickname) 709 { 710 if (nickname.IsEmpty()) 711 return; 712 BKeyStore keyStore; 713 BPasswordKey key; 714 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 715 << nickname; 716 status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 717 passwordIdentifier, key); 718 719 switch (result) { 720 case B_OK: 721 result = keyStore.RemoveKey(kHaikuDepotKeyring, key); 722 if (result != B_OK) { 723 HDERROR("error occurred when removing password for nickname " 724 "[%s] : %s", nickname.String(), strerror(result)); 725 } 726 break; 727 case B_ENTRY_NOT_FOUND: 728 return; 729 default: 730 HDERROR("error occurred when finding password for nickname " 731 "[%s] : %s", nickname.String(), strerror(result)); 732 break; 733 } 734 } 735 736 737 void 738 Model::SetNickname(BString nickname) 739 { 740 BString password; 741 BString existingNickname = Nickname(); 742 743 // this happens when the user is logging out. Best to remove the password 744 // stored for the existing user since it is no longer required. 745 746 if (!existingNickname.IsEmpty() && nickname.IsEmpty()) 747 model_remove_key_for_user(existingNickname); 748 749 if (nickname.Length() > 0) { 750 BPasswordKey key; 751 BKeyStore keyStore; 752 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 753 << nickname; 754 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 755 passwordIdentifier, key) == B_OK) { 756 password = key.Password(); 757 } 758 if (password.IsEmpty()) 759 nickname = ""; 760 } 761 762 SetAuthorization(nickname, password, false); 763 } 764 765 766 const BString& 767 Model::Nickname() const 768 { 769 return fWebAppInterface.Nickname(); 770 } 771 772 773 void 774 Model::SetAuthorization(const BString& nickname, const BString& passwordClear, 775 bool storePassword) 776 { 777 BString existingNickname = Nickname(); 778 779 if (storePassword) { 780 // no point continuing to store the password for the previous user. 781 782 if (!existingNickname.IsEmpty()) 783 model_remove_key_for_user(existingNickname); 784 785 // adding a key that is already there does not seem to override the 786 // existing key so the old key needs to be removed first. 787 788 if (!nickname.IsEmpty()) 789 model_remove_key_for_user(nickname); 790 791 if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) { 792 BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 793 << nickname; 794 BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier); 795 BKeyStore keyStore; 796 keyStore.AddKeyring(kHaikuDepotKeyring); 797 keyStore.AddKey(kHaikuDepotKeyring, key); 798 } 799 } 800 801 BAutolock locker(&fLock); 802 fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear)); 803 804 if (nickname != existingNickname) 805 _NotifyAuthorizationChanged(); 806 } 807 808 809 /*! When bulk repository data comes down from the server, it will 810 arrive as a json.gz payload. This is stored locally as a cache 811 and this method will provide the on-disk storage location for 812 this file. 813 */ 814 815 status_t 816 Model::DumpExportRepositoryDataPath(BPath& path) 817 { 818 BString leaf; 819 leaf.SetToFormat("repository-all_%s.json.gz", 820 Language()->PreferredLanguage()->Code()); 821 return StorageUtils::LocalWorkingFilesPath(leaf, path); 822 } 823 824 825 /*! When the system downloads reference data (eg; categories) from the server 826 then the downloaded data is stored and cached at the path defined by this 827 method. 828 */ 829 830 status_t 831 Model::DumpExportReferenceDataPath(BPath& path) 832 { 833 BString leaf; 834 leaf.SetToFormat("reference-all_%s.json.gz", 835 Language()->PreferredLanguage()->Code()); 836 return StorageUtils::LocalWorkingFilesPath(leaf, path); 837 } 838 839 840 status_t 841 Model::IconTarPath(BPath& path) const 842 { 843 return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path); 844 } 845 846 847 status_t 848 Model::DumpExportPkgDataPath(BPath& path, 849 const BString& repositorySourceCode) 850 { 851 BString leaf; 852 leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(), 853 Language()->PreferredLanguage()->Code()); 854 return StorageUtils::LocalWorkingFilesPath(leaf, path); 855 } 856 857 858 void 859 Model::_PopulatePackageScreenshot(const PackageInfoRef& package, 860 const ScreenshotInfoRef& info, int32 scaledWidth, bool fromCacheOnly) 861 { 862 // See if there is a cached screenshot 863 BFile screenshotFile; 864 BPath screenshotCachePath; 865 866 status_t result = StorageUtils::LocalWorkingDirectoryPath( 867 "Screenshots", screenshotCachePath); 868 869 if (result != B_OK) { 870 HDERROR("unable to get the screenshot dir - unable to proceed"); 871 return; 872 } 873 874 bool fileExists = false; 875 BString screenshotName(info->Code()); 876 screenshotName << "@" << scaledWidth; 877 screenshotName << ".png"; 878 time_t modifiedTime; 879 if (screenshotCachePath.Append(screenshotName) == B_OK) { 880 // Try opening the file in read-only mode, which will fail if its 881 // not a file or does not exist. 882 fileExists = screenshotFile.SetTo(screenshotCachePath.Path(), 883 B_READ_ONLY) == B_OK; 884 if (fileExists) 885 screenshotFile.GetModificationTime(&modifiedTime); 886 } 887 888 if (fileExists) { 889 time_t now; 890 time(&now); 891 if (fromCacheOnly || now - modifiedTime < 60 * 60) { 892 // Cache file is recent enough, just use it and return. 893 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile), 894 true); 895 BAutolock locker(&fLock); 896 package->AddScreenshot(bitmapRef); 897 return; 898 } 899 } 900 901 if (fromCacheOnly) 902 return; 903 904 // Retrieve screenshot from web-app 905 BMallocIO buffer; 906 907 int32 scaledHeight = scaledWidth * info->Height() / info->Width(); 908 909 status_t status = fWebAppInterface.RetrieveScreenshot(info->Code(), 910 scaledWidth, scaledHeight, &buffer); 911 if (status == B_OK) { 912 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 913 BAutolock locker(&fLock); 914 package->AddScreenshot(bitmapRef); 915 locker.Unlock(); 916 if (screenshotFile.SetTo(screenshotCachePath.Path(), 917 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 918 screenshotFile.Write(buffer.Buffer(), buffer.BufferLength()); 919 } 920 } else { 921 HDERROR("Failed to retrieve screenshot for code '%s' " 922 "at %" B_PRIi32 "x%" B_PRIi32 ".", info->Code().String(), 923 scaledWidth, scaledHeight); 924 } 925 } 926 927 928 // #pragma mark - listener notification methods 929 930 931 void 932 Model::_NotifyAuthorizationChanged() 933 { 934 std::vector<ModelListenerRef>::const_iterator it; 935 for (it = fListeners.begin(); it != fListeners.end(); it++) { 936 const ModelListenerRef& listener = *it; 937 if (listener.IsSet()) 938 listener->AuthorizationChanged(); 939 } 940 } 941 942 943 void 944 Model::_NotifyCategoryListChanged() 945 { 946 std::vector<ModelListenerRef>::const_iterator it; 947 for (it = fListeners.begin(); it != fListeners.end(); it++) { 948 const ModelListenerRef& listener = *it; 949 if (listener.IsSet()) 950 listener->CategoryListChanged(); 951 } 952 } 953 954 955 void 956 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload, 957 const char *sourceDescription) const 958 { 959 BMessage error; 960 BString errorMessage; 961 double errorCode; 962 963 if (responsePayload.FindMessage("error", &error) == B_OK 964 && error.FindString("message", &errorMessage) == B_OK 965 && error.FindDouble("code", &errorCode) == B_OK) { 966 HDERROR("[%s] --> error : [%s] (%f)", sourceDescription, 967 errorMessage.String(), errorCode); 968 } else 969 HDERROR("[%s] --> an undefined error has occurred", sourceDescription); 970 } 971 972 973 // #pragma mark - Rating Stabilities 974 975 976 int32 977 Model::CountRatingStabilities() const 978 { 979 return fRatingStabilities.size(); 980 } 981 982 983 RatingStabilityRef 984 Model::RatingStabilityByCode(BString& code) const 985 { 986 std::vector<RatingStabilityRef>::const_iterator it; 987 for (it = fRatingStabilities.begin(); it != fRatingStabilities.end(); 988 it++) { 989 RatingStabilityRef aRatingStability = *it; 990 if (aRatingStability->Code() == code) 991 return aRatingStability; 992 } 993 return RatingStabilityRef(); 994 } 995 996 997 RatingStabilityRef 998 Model::RatingStabilityAtIndex(int32 index) const 999 { 1000 return fRatingStabilities[index]; 1001 } 1002 1003 1004 void 1005 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values) 1006 { 1007 std::vector<RatingStabilityRef>::const_iterator it; 1008 for (it = values.begin(); it != values.end(); it++) 1009 _AddRatingStability(*it); 1010 } 1011 1012 1013 void 1014 Model::_AddRatingStability(const RatingStabilityRef& value) 1015 { 1016 std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst 1017 = std::lower_bound( 1018 fRatingStabilities.begin(), 1019 fRatingStabilities.end(), 1020 value, 1021 &IsRatingStabilityBefore); 1022 std::vector<RatingStabilityRef>::iterator itInsertionPt = 1023 fRatingStabilities.begin() 1024 + (itInsertionPtConst - fRatingStabilities.begin()); 1025 1026 if (itInsertionPt != fRatingStabilities.end() 1027 && (*itInsertionPt)->Code() == value->Code()) { 1028 itInsertionPt = fRatingStabilities.erase(itInsertionPt); 1029 // replace the one with the same code. 1030 } 1031 1032 fRatingStabilities.insert(itInsertionPt, value); 1033 } 1034 1035 1036 // #pragma mark - Categories 1037 1038 1039 int32 1040 Model::CountCategories() const 1041 { 1042 return fCategories.size(); 1043 } 1044 1045 1046 CategoryRef 1047 Model::CategoryByCode(BString& code) const 1048 { 1049 std::vector<CategoryRef>::const_iterator it; 1050 for (it = fCategories.begin(); it != fCategories.end(); it++) { 1051 CategoryRef aCategory = *it; 1052 if (aCategory->Code() == code) 1053 return aCategory; 1054 } 1055 return CategoryRef(); 1056 } 1057 1058 1059 CategoryRef 1060 Model::CategoryAtIndex(int32 index) const 1061 { 1062 return fCategories[index]; 1063 } 1064 1065 1066 void 1067 Model::AddCategories(std::vector<CategoryRef>& values) 1068 { 1069 std::vector<CategoryRef>::iterator it; 1070 for (it = values.begin(); it != values.end(); it++) 1071 _AddCategory(*it); 1072 _NotifyCategoryListChanged(); 1073 } 1074 1075 /*! This will insert the category in order. 1076 */ 1077 1078 void 1079 Model::_AddCategory(const CategoryRef& category) 1080 { 1081 std::vector<CategoryRef>::const_iterator itInsertionPtConst 1082 = std::lower_bound( 1083 fCategories.begin(), 1084 fCategories.end(), 1085 category, 1086 &IsPackageCategoryBefore); 1087 std::vector<CategoryRef>::iterator itInsertionPt = 1088 fCategories.begin() + (itInsertionPtConst - fCategories.begin()); 1089 1090 if (itInsertionPt != fCategories.end() 1091 && (*itInsertionPt)->Code() == category->Code()) { 1092 itInsertionPt = fCategories.erase(itInsertionPt); 1093 // replace the one with the same code. 1094 } 1095 1096 fCategories.insert(itInsertionPt, category); 1097 } 1098