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::SetStateForPackagesByName(BStringList& packageNames, PackageState state) 361 { 362 for (int32 i = 0; i < packageNames.CountStrings(); i++) { 363 BString packageName = packageNames.StringAt(i); 364 PackageInfoRef packageInfo = PackageForName(packageName); 365 366 if (packageInfo.IsSet()) { 367 packageInfo->SetState(state); 368 HDINFO("did update package [%s] with state [%s]", 369 packageName.String(), package_state_to_string(state)); 370 } 371 else { 372 HDINFO("was unable to find package [%s] so was not possible to set" 373 " the state to [%s]", packageName.String(), 374 package_state_to_string(state)); 375 } 376 } 377 } 378 379 380 // #pragma mark - filters 381 382 383 void 384 Model::SetCategory(const BString& category) 385 { 386 PackageFilter* filter; 387 388 if (category.Length() == 0) 389 filter = new AnyFilter(); 390 else 391 filter = new CategoryFilter(category); 392 393 fCategoryFilter.SetTo(filter, true); 394 } 395 396 397 BString 398 Model::Category() const 399 { 400 CategoryFilter* filter 401 = dynamic_cast<CategoryFilter*>(fCategoryFilter.Get()); 402 if (filter == NULL) 403 return ""; 404 return filter->Category(); 405 } 406 407 408 void 409 Model::SetDepot(const BString& depot) 410 { 411 fDepotFilter = depot; 412 } 413 414 415 BString 416 Model::Depot() const 417 { 418 return fDepotFilter; 419 } 420 421 422 void 423 Model::SetSearchTerms(const BString& searchTerms) 424 { 425 PackageFilter* filter; 426 427 if (searchTerms.Length() == 0) 428 filter = new AnyFilter(); 429 else 430 filter = new SearchTermsFilter(searchTerms); 431 432 fSearchTermsFilter.SetTo(filter, true); 433 } 434 435 436 BString 437 Model::SearchTerms() const 438 { 439 SearchTermsFilter* filter 440 = dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get()); 441 if (filter == NULL) 442 return ""; 443 return filter->SearchTerms(); 444 } 445 446 447 void 448 Model::SetPackageListViewMode(package_list_view_mode mode) 449 { 450 fPackageListViewMode = mode; 451 } 452 453 454 void 455 Model::SetCanShareAnonymousUsageData(bool value) 456 { 457 fCanShareAnonymousUsageData = value; 458 } 459 460 461 void 462 Model::SetShowAvailablePackages(bool show) 463 { 464 fShowAvailablePackages = show; 465 } 466 467 468 void 469 Model::SetShowInstalledPackages(bool show) 470 { 471 fShowInstalledPackages = show; 472 } 473 474 475 void 476 Model::SetShowSourcePackages(bool show) 477 { 478 fShowSourcePackages = show; 479 } 480 481 482 void 483 Model::SetShowDevelopPackages(bool show) 484 { 485 fShowDevelopPackages = show; 486 } 487 488 489 // #pragma mark - information retrieval 490 491 /*! It may transpire that the package has no corresponding record on the 492 server side because the repository is not represented in the server. 493 In such a case, there is little point in communicating with the server 494 only to hear back that the package does not exist. 495 */ 496 497 bool 498 Model::CanPopulatePackage(const PackageInfoRef& package) 499 { 500 const BString& depotName = package->DepotName(); 501 502 if (depotName.IsEmpty()) 503 return false; 504 505 const DepotInfoRef& depot = DepotForName(depotName); 506 507 if (depot.Get() == NULL) 508 return false; 509 510 return !depot->WebAppRepositoryCode().IsEmpty(); 511 } 512 513 514 /*! Initially only superficial data is loaded from the server into the data 515 model of the packages. When the package is viewed, additional data needs 516 to be populated including ratings. This method takes care of that. 517 */ 518 519 void 520 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) 521 { 522 if (!CanPopulatePackage(package)) { 523 HDINFO("unable to populate package [%s]", package->Name().String()); 524 return; 525 } 526 527 // TODO: There should probably also be a way to "unpopulate" the 528 // package information. Maybe a cache of populated packages, so that 529 // packages loose their extra information after a certain amount of 530 // time when they have not been accessed/displayed in the UI. Otherwise 531 // HaikuDepot will consume more and more resources in the packages. 532 // Especially screen-shots will be a problem eventually. 533 { 534 BAutolock locker(&fLock); 535 bool alreadyPopulated = fPopulatedPackageNames.HasString( 536 package->Name()); 537 if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated) 538 return; 539 if (!alreadyPopulated) 540 fPopulatedPackageNames.Add(package->Name()); 541 } 542 543 if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) { 544 _PopulatePackageChangelog(package); 545 } 546 547 if ((flags & POPULATE_USER_RATINGS) != 0) { 548 // Retrieve info from web-app 549 BMessage info; 550 551 BString packageName; 552 BString webAppRepositoryCode; 553 { 554 BAutolock locker(&fLock); 555 packageName = package->Name(); 556 const DepotInfo* depot = DepotForName(package->DepotName()); 557 558 if (depot != NULL) 559 webAppRepositoryCode = depot->WebAppRepositoryCode(); 560 } 561 562 status_t status = fWebAppInterface 563 .RetreiveUserRatingsForPackageForDisplay(packageName, 564 webAppRepositoryCode, 0, PACKAGE_INFO_MAX_USER_RATINGS, 565 info); 566 if (status == B_OK) { 567 // Parse message 568 BMessage result; 569 BMessage items; 570 if (info.FindMessage("result", &result) == B_OK 571 && result.FindMessage("items", &items) == B_OK) { 572 573 BAutolock locker(&fLock); 574 package->ClearUserRatings(); 575 576 int32 index = 0; 577 while (true) { 578 BString name; 579 name << index++; 580 581 BMessage item; 582 if (items.FindMessage(name, &item) != B_OK) 583 break; 584 585 BString code; 586 if (item.FindString("code", &code) != B_OK) { 587 HDERROR("corrupt user rating at index %" B_PRIi32, 588 index); 589 continue; 590 } 591 592 BString user; 593 BMessage userInfo; 594 if (item.FindMessage("user", &userInfo) != B_OK 595 || userInfo.FindString("nickname", &user) != B_OK) { 596 HDERROR("ignored user rating [%s] without a user " 597 "nickname", code.String()); 598 continue; 599 } 600 601 // Extract basic info, all items are optional 602 BString languageCode; 603 BString comment; 604 double rating; 605 item.FindString("naturalLanguageCode", &languageCode); 606 item.FindString("comment", &comment); 607 if (item.FindDouble("rating", &rating) != B_OK) 608 rating = -1; 609 if (comment.Length() == 0 && rating == -1) { 610 HDERROR("rating [%s] has no comment or rating so will" 611 " be ignored", code.String()); 612 continue; 613 } 614 615 // For which version of the package was the rating? 616 BString major = "?"; 617 BString minor = "?"; 618 BString micro = ""; 619 double revision = -1; 620 BString architectureCode = ""; 621 BMessage version; 622 if (item.FindMessage("pkgVersion", &version) == B_OK) { 623 version.FindString("major", &major); 624 version.FindString("minor", &minor); 625 version.FindString("micro", µ); 626 version.FindDouble("revision", &revision); 627 version.FindString("architectureCode", 628 &architectureCode); 629 } 630 BString versionString = major; 631 versionString << "."; 632 versionString << minor; 633 if (!micro.IsEmpty()) { 634 versionString << "."; 635 versionString << micro; 636 } 637 if (revision > 0) { 638 versionString << "-"; 639 versionString << (int) revision; 640 } 641 642 if (!architectureCode.IsEmpty()) { 643 versionString << " " << STR_MDASH << " "; 644 versionString << architectureCode; 645 } 646 647 double createTimestamp; 648 item.FindDouble("createTimestamp", &createTimestamp); 649 650 // Add the rating to the PackageInfo 651 UserRatingRef userRating(new UserRating( 652 UserInfo(user), rating, 653 comment, languageCode, versionString, 654 (uint64) createTimestamp), true); 655 package->AddUserRating(userRating); 656 HDDEBUG("rating [%s] retrieved from server", code.String()); 657 } 658 HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]", 659 index - 1, packageName.String()); 660 } else { 661 BString message; 662 message.SetToFormat("failure to retrieve user ratings for [%s]", 663 packageName.String()); 664 _MaybeLogJsonRpcError(info, message.String()); 665 } 666 } else 667 HDERROR("unable to retrieve user ratings"); 668 } 669 670 if ((flags & POPULATE_SCREEN_SHOTS) != 0) { 671 std::vector<ScreenshotInfoRef> screenshotInfos; 672 { 673 BAutolock locker(&fLock); 674 for (int32 i = 0; i < package->CountScreenshotInfos(); i++) 675 screenshotInfos.push_back(package->ScreenshotInfoAtIndex(i)); 676 package->ClearScreenshots(); 677 } 678 std::vector<ScreenshotInfoRef>::iterator it; 679 for (it = screenshotInfos.begin(); it != screenshotInfos.end(); it++) { 680 const ScreenshotInfoRef& info = *it; 681 _PopulatePackageScreenshot(package, info, 320, false); 682 } 683 } 684 } 685 686 687 void 688 Model::_PopulatePackageChangelog(const PackageInfoRef& package) 689 { 690 BMessage info; 691 BString packageName; 692 693 { 694 BAutolock locker(&fLock); 695 packageName = package->Name(); 696 } 697 698 status_t status = fWebAppInterface.GetChangelog(packageName, info); 699 700 if (status == B_OK) { 701 // Parse message 702 BMessage result; 703 BString content; 704 if (info.FindMessage("result", &result) == B_OK) { 705 if (result.FindString("content", &content) == B_OK 706 && 0 != content.Length()) { 707 BAutolock locker(&fLock); 708 package->SetChangelog(content); 709 HDDEBUG("changelog populated for [%s]", packageName.String()); 710 } else 711 HDDEBUG("no changelog present for [%s]", packageName.String()); 712 } else 713 _MaybeLogJsonRpcError(info, "populate package changelog"); 714 } else { 715 HDERROR("unable to obtain the changelog for the package [%s]", 716 packageName.String()); 717 } 718 } 719 720 721 static void 722 model_remove_key_for_user(const BString& nickname) 723 { 724 if (nickname.IsEmpty()) 725 return; 726 BKeyStore keyStore; 727 BPasswordKey key; 728 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 729 << nickname; 730 status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 731 passwordIdentifier, key); 732 733 switch (result) { 734 case B_OK: 735 result = keyStore.RemoveKey(kHaikuDepotKeyring, key); 736 if (result != B_OK) { 737 HDERROR("error occurred when removing password for nickname " 738 "[%s] : %s", nickname.String(), strerror(result)); 739 } 740 break; 741 case B_ENTRY_NOT_FOUND: 742 return; 743 default: 744 HDERROR("error occurred when finding password for nickname " 745 "[%s] : %s", nickname.String(), strerror(result)); 746 break; 747 } 748 } 749 750 751 void 752 Model::SetNickname(BString nickname) 753 { 754 BString password; 755 BString existingNickname = Nickname(); 756 757 // this happens when the user is logging out. Best to remove the password 758 // stored for the existing user since it is no longer required. 759 760 if (!existingNickname.IsEmpty() && nickname.IsEmpty()) 761 model_remove_key_for_user(existingNickname); 762 763 if (nickname.Length() > 0) { 764 BPasswordKey key; 765 BKeyStore keyStore; 766 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 767 << nickname; 768 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 769 passwordIdentifier, key) == B_OK) { 770 password = key.Password(); 771 } 772 if (password.IsEmpty()) 773 nickname = ""; 774 } 775 776 SetAuthorization(nickname, password, false); 777 } 778 779 780 const BString& 781 Model::Nickname() const 782 { 783 return fWebAppInterface.Nickname(); 784 } 785 786 787 void 788 Model::SetAuthorization(const BString& nickname, const BString& passwordClear, 789 bool storePassword) 790 { 791 BString existingNickname = Nickname(); 792 793 if (storePassword) { 794 // no point continuing to store the password for the previous user. 795 796 if (!existingNickname.IsEmpty()) 797 model_remove_key_for_user(existingNickname); 798 799 // adding a key that is already there does not seem to override the 800 // existing key so the old key needs to be removed first. 801 802 if (!nickname.IsEmpty()) 803 model_remove_key_for_user(nickname); 804 805 if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) { 806 BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 807 << nickname; 808 BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier); 809 BKeyStore keyStore; 810 keyStore.AddKeyring(kHaikuDepotKeyring); 811 keyStore.AddKey(kHaikuDepotKeyring, key); 812 } 813 } 814 815 BAutolock locker(&fLock); 816 fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear)); 817 818 if (nickname != existingNickname) 819 _NotifyAuthorizationChanged(); 820 } 821 822 823 /*! When bulk repository data comes down from the server, it will 824 arrive as a json.gz payload. This is stored locally as a cache 825 and this method will provide the on-disk storage location for 826 this file. 827 */ 828 829 status_t 830 Model::DumpExportRepositoryDataPath(BPath& path) 831 { 832 BString leaf; 833 leaf.SetToFormat("repository-all_%s.json.gz", 834 Language()->PreferredLanguage()->Code()); 835 return StorageUtils::LocalWorkingFilesPath(leaf, path); 836 } 837 838 839 /*! When the system downloads reference data (eg; categories) from the server 840 then the downloaded data is stored and cached at the path defined by this 841 method. 842 */ 843 844 status_t 845 Model::DumpExportReferenceDataPath(BPath& path) 846 { 847 BString leaf; 848 leaf.SetToFormat("reference-all_%s.json.gz", 849 Language()->PreferredLanguage()->Code()); 850 return StorageUtils::LocalWorkingFilesPath(leaf, path); 851 } 852 853 854 status_t 855 Model::IconTarPath(BPath& path) const 856 { 857 return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path); 858 } 859 860 861 status_t 862 Model::DumpExportPkgDataPath(BPath& path, 863 const BString& repositorySourceCode) 864 { 865 BString leaf; 866 leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(), 867 Language()->PreferredLanguage()->Code()); 868 return StorageUtils::LocalWorkingFilesPath(leaf, path); 869 } 870 871 872 void 873 Model::_PopulatePackageScreenshot(const PackageInfoRef& package, 874 const ScreenshotInfoRef& info, int32 scaledWidth, bool fromCacheOnly) 875 { 876 // See if there is a cached screenshot 877 BFile screenshotFile; 878 BPath screenshotCachePath; 879 880 status_t result = StorageUtils::LocalWorkingDirectoryPath( 881 "Screenshots", screenshotCachePath); 882 883 if (result != B_OK) { 884 HDERROR("unable to get the screenshot dir - unable to proceed"); 885 return; 886 } 887 888 bool fileExists = false; 889 BString screenshotName(info->Code()); 890 screenshotName << "@" << scaledWidth; 891 screenshotName << ".png"; 892 time_t modifiedTime; 893 if (screenshotCachePath.Append(screenshotName) == B_OK) { 894 // Try opening the file in read-only mode, which will fail if its 895 // not a file or does not exist. 896 fileExists = screenshotFile.SetTo(screenshotCachePath.Path(), 897 B_READ_ONLY) == B_OK; 898 if (fileExists) 899 screenshotFile.GetModificationTime(&modifiedTime); 900 } 901 902 if (fileExists) { 903 time_t now; 904 time(&now); 905 if (fromCacheOnly || now - modifiedTime < 60 * 60) { 906 // Cache file is recent enough, just use it and return. 907 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile), 908 true); 909 BAutolock locker(&fLock); 910 package->AddScreenshot(bitmapRef); 911 return; 912 } 913 } 914 915 if (fromCacheOnly) 916 return; 917 918 // Retrieve screenshot from web-app 919 BMallocIO buffer; 920 921 int32 scaledHeight = scaledWidth * info->Height() / info->Width(); 922 923 status_t status = fWebAppInterface.RetrieveScreenshot(info->Code(), 924 scaledWidth, scaledHeight, &buffer); 925 if (status == B_OK) { 926 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 927 BAutolock locker(&fLock); 928 package->AddScreenshot(bitmapRef); 929 locker.Unlock(); 930 if (screenshotFile.SetTo(screenshotCachePath.Path(), 931 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 932 screenshotFile.Write(buffer.Buffer(), buffer.BufferLength()); 933 } 934 } else { 935 HDERROR("Failed to retrieve screenshot for code '%s' " 936 "at %" B_PRIi32 "x%" B_PRIi32 ".", info->Code().String(), 937 scaledWidth, scaledHeight); 938 } 939 } 940 941 942 // #pragma mark - listener notification methods 943 944 945 void 946 Model::_NotifyAuthorizationChanged() 947 { 948 std::vector<ModelListenerRef>::const_iterator it; 949 for (it = fListeners.begin(); it != fListeners.end(); it++) { 950 const ModelListenerRef& listener = *it; 951 if (listener.IsSet()) 952 listener->AuthorizationChanged(); 953 } 954 } 955 956 957 void 958 Model::_NotifyCategoryListChanged() 959 { 960 std::vector<ModelListenerRef>::const_iterator it; 961 for (it = fListeners.begin(); it != fListeners.end(); it++) { 962 const ModelListenerRef& listener = *it; 963 if (listener.IsSet()) 964 listener->CategoryListChanged(); 965 } 966 } 967 968 969 void 970 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload, 971 const char *sourceDescription) const 972 { 973 BMessage error; 974 BString errorMessage; 975 double errorCode; 976 977 if (responsePayload.FindMessage("error", &error) == B_OK 978 && error.FindString("message", &errorMessage) == B_OK 979 && error.FindDouble("code", &errorCode) == B_OK) { 980 HDERROR("[%s] --> error : [%s] (%f)", sourceDescription, 981 errorMessage.String(), errorCode); 982 } else 983 HDERROR("[%s] --> an undefined error has occurred", sourceDescription); 984 } 985 986 987 // #pragma mark - Rating Stabilities 988 989 990 int32 991 Model::CountRatingStabilities() const 992 { 993 return fRatingStabilities.size(); 994 } 995 996 997 RatingStabilityRef 998 Model::RatingStabilityByCode(BString& code) const 999 { 1000 std::vector<RatingStabilityRef>::const_iterator it; 1001 for (it = fRatingStabilities.begin(); it != fRatingStabilities.end(); 1002 it++) { 1003 RatingStabilityRef aRatingStability = *it; 1004 if (aRatingStability->Code() == code) 1005 return aRatingStability; 1006 } 1007 return RatingStabilityRef(); 1008 } 1009 1010 1011 RatingStabilityRef 1012 Model::RatingStabilityAtIndex(int32 index) const 1013 { 1014 return fRatingStabilities[index]; 1015 } 1016 1017 1018 void 1019 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values) 1020 { 1021 std::vector<RatingStabilityRef>::const_iterator it; 1022 for (it = values.begin(); it != values.end(); it++) 1023 _AddRatingStability(*it); 1024 } 1025 1026 1027 void 1028 Model::_AddRatingStability(const RatingStabilityRef& value) 1029 { 1030 std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst 1031 = std::lower_bound( 1032 fRatingStabilities.begin(), 1033 fRatingStabilities.end(), 1034 value, 1035 &IsRatingStabilityBefore); 1036 std::vector<RatingStabilityRef>::iterator itInsertionPt = 1037 fRatingStabilities.begin() 1038 + (itInsertionPtConst - fRatingStabilities.begin()); 1039 1040 if (itInsertionPt != fRatingStabilities.end() 1041 && (*itInsertionPt)->Code() == value->Code()) { 1042 itInsertionPt = fRatingStabilities.erase(itInsertionPt); 1043 // replace the one with the same code. 1044 } 1045 1046 fRatingStabilities.insert(itInsertionPt, value); 1047 } 1048 1049 1050 // #pragma mark - Categories 1051 1052 1053 int32 1054 Model::CountCategories() const 1055 { 1056 return fCategories.size(); 1057 } 1058 1059 1060 CategoryRef 1061 Model::CategoryByCode(BString& code) const 1062 { 1063 std::vector<CategoryRef>::const_iterator it; 1064 for (it = fCategories.begin(); it != fCategories.end(); it++) { 1065 CategoryRef aCategory = *it; 1066 if (aCategory->Code() == code) 1067 return aCategory; 1068 } 1069 return CategoryRef(); 1070 } 1071 1072 1073 CategoryRef 1074 Model::CategoryAtIndex(int32 index) const 1075 { 1076 return fCategories[index]; 1077 } 1078 1079 1080 void 1081 Model::AddCategories(std::vector<CategoryRef>& values) 1082 { 1083 std::vector<CategoryRef>::iterator it; 1084 for (it = values.begin(); it != values.end(); it++) 1085 _AddCategory(*it); 1086 _NotifyCategoryListChanged(); 1087 } 1088 1089 /*! This will insert the category in order. 1090 */ 1091 1092 void 1093 Model::_AddCategory(const CategoryRef& category) 1094 { 1095 std::vector<CategoryRef>::const_iterator itInsertionPtConst 1096 = std::lower_bound( 1097 fCategories.begin(), 1098 fCategories.end(), 1099 category, 1100 &IsPackageCategoryBefore); 1101 std::vector<CategoryRef>::iterator itInsertionPt = 1102 fCategories.begin() + (itInsertionPtConst - fCategories.begin()); 1103 1104 if (itInsertionPt != fCategories.end() 1105 && (*itInsertionPt)->Code() == category->Code()) { 1106 itInsertionPt = fCategories.erase(itInsertionPt); 1107 // replace the one with the same code. 1108 } 1109 1110 fCategories.insert(itInsertionPt, category); 1111 } 1112