1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>. 4 * Copyright 2016-2022, 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 BString webAppRepositorySourceCode; 554 555 { 556 BAutolock locker(&fLock); 557 packageName = package->Name(); 558 const DepotInfo* depot = DepotForName(package->DepotName()); 559 560 if (depot != NULL) { 561 webAppRepositoryCode = depot->WebAppRepositoryCode(); 562 webAppRepositorySourceCode 563 = depot->WebAppRepositorySourceCode(); 564 } 565 } 566 567 status_t status = fWebAppInterface 568 .RetreiveUserRatingsForPackageForDisplay(packageName, 569 webAppRepositoryCode, webAppRepositorySourceCode, 0, 570 PACKAGE_INFO_MAX_USER_RATINGS, info); 571 if (status == B_OK) { 572 // Parse message 573 BMessage result; 574 BMessage items; 575 if (info.FindMessage("result", &result) == B_OK 576 && result.FindMessage("items", &items) == B_OK) { 577 578 BAutolock locker(&fLock); 579 package->ClearUserRatings(); 580 581 int32 index = 0; 582 while (true) { 583 BString name; 584 name << index++; 585 586 BMessage item; 587 if (items.FindMessage(name, &item) != B_OK) 588 break; 589 590 BString code; 591 if (item.FindString("code", &code) != B_OK) { 592 HDERROR("corrupt user rating at index %" B_PRIi32, 593 index); 594 continue; 595 } 596 597 BString user; 598 BMessage userInfo; 599 if (item.FindMessage("user", &userInfo) != B_OK 600 || userInfo.FindString("nickname", &user) != B_OK) { 601 HDERROR("ignored user rating [%s] without a user " 602 "nickname", code.String()); 603 continue; 604 } 605 606 // Extract basic info, all items are optional 607 BString languageCode; 608 BString comment; 609 double rating; 610 item.FindString("naturalLanguageCode", &languageCode); 611 item.FindString("comment", &comment); 612 if (item.FindDouble("rating", &rating) != B_OK) 613 rating = -1; 614 if (comment.Length() == 0 && rating == -1) { 615 HDERROR("rating [%s] has no comment or rating so will" 616 " be ignored", code.String()); 617 continue; 618 } 619 620 // For which version of the package was the rating? 621 BString major = "?"; 622 BString minor = "?"; 623 BString micro = ""; 624 double revision = -1; 625 BString architectureCode = ""; 626 BMessage version; 627 if (item.FindMessage("pkgVersion", &version) == B_OK) { 628 version.FindString("major", &major); 629 version.FindString("minor", &minor); 630 version.FindString("micro", µ); 631 version.FindDouble("revision", &revision); 632 version.FindString("architectureCode", 633 &architectureCode); 634 } 635 BString versionString = major; 636 versionString << "."; 637 versionString << minor; 638 if (!micro.IsEmpty()) { 639 versionString << "."; 640 versionString << micro; 641 } 642 if (revision > 0) { 643 versionString << "-"; 644 versionString << (int) revision; 645 } 646 647 if (!architectureCode.IsEmpty()) { 648 versionString << " " << STR_MDASH << " "; 649 versionString << architectureCode; 650 } 651 652 double createTimestamp; 653 item.FindDouble("createTimestamp", &createTimestamp); 654 655 // Add the rating to the PackageInfo 656 UserRatingRef userRating(new UserRating( 657 UserInfo(user), rating, 658 comment, languageCode, versionString, 659 (uint64) createTimestamp), true); 660 package->AddUserRating(userRating); 661 HDDEBUG("rating [%s] retrieved from server", code.String()); 662 } 663 HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]", 664 index - 1, packageName.String()); 665 } else { 666 BString message; 667 message.SetToFormat("failure to retrieve user ratings for [%s]", 668 packageName.String()); 669 _MaybeLogJsonRpcError(info, message.String()); 670 } 671 } else 672 HDERROR("unable to retrieve user ratings"); 673 } 674 675 if ((flags & POPULATE_SCREEN_SHOTS) != 0) { 676 std::vector<ScreenshotInfoRef> screenshotInfos; 677 { 678 BAutolock locker(&fLock); 679 for (int32 i = 0; i < package->CountScreenshotInfos(); i++) 680 screenshotInfos.push_back(package->ScreenshotInfoAtIndex(i)); 681 package->ClearScreenshots(); 682 } 683 std::vector<ScreenshotInfoRef>::iterator it; 684 for (it = screenshotInfos.begin(); it != screenshotInfos.end(); it++) { 685 const ScreenshotInfoRef& info = *it; 686 _PopulatePackageScreenshot(package, info, 320, false); 687 } 688 } 689 } 690 691 692 void 693 Model::_PopulatePackageChangelog(const PackageInfoRef& package) 694 { 695 BMessage info; 696 BString packageName; 697 698 { 699 BAutolock locker(&fLock); 700 packageName = package->Name(); 701 } 702 703 status_t status = fWebAppInterface.GetChangelog(packageName, info); 704 705 if (status == B_OK) { 706 // Parse message 707 BMessage result; 708 BString content; 709 if (info.FindMessage("result", &result) == B_OK) { 710 if (result.FindString("content", &content) == B_OK 711 && 0 != content.Length()) { 712 BAutolock locker(&fLock); 713 package->SetChangelog(content); 714 HDDEBUG("changelog populated for [%s]", packageName.String()); 715 } else 716 HDDEBUG("no changelog present for [%s]", packageName.String()); 717 } else 718 _MaybeLogJsonRpcError(info, "populate package changelog"); 719 } else { 720 HDERROR("unable to obtain the changelog for the package [%s]", 721 packageName.String()); 722 } 723 } 724 725 726 static void 727 model_remove_key_for_user(const BString& nickname) 728 { 729 if (nickname.IsEmpty()) 730 return; 731 BKeyStore keyStore; 732 BPasswordKey key; 733 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 734 << nickname; 735 status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 736 passwordIdentifier, key); 737 738 switch (result) { 739 case B_OK: 740 result = keyStore.RemoveKey(kHaikuDepotKeyring, key); 741 if (result != B_OK) { 742 HDERROR("error occurred when removing password for nickname " 743 "[%s] : %s", nickname.String(), strerror(result)); 744 } 745 break; 746 case B_ENTRY_NOT_FOUND: 747 return; 748 default: 749 HDERROR("error occurred when finding password for nickname " 750 "[%s] : %s", nickname.String(), strerror(result)); 751 break; 752 } 753 } 754 755 756 void 757 Model::SetNickname(BString nickname) 758 { 759 BString password; 760 BString existingNickname = Nickname(); 761 762 // this happens when the user is logging out. Best to remove the password 763 // stored for the existing user since it is no longer required. 764 765 if (!existingNickname.IsEmpty() && nickname.IsEmpty()) 766 model_remove_key_for_user(existingNickname); 767 768 if (nickname.Length() > 0) { 769 BPasswordKey key; 770 BKeyStore keyStore; 771 BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 772 << nickname; 773 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, 774 passwordIdentifier, key) == B_OK) { 775 password = key.Password(); 776 } 777 if (password.IsEmpty()) 778 nickname = ""; 779 } 780 781 SetAuthorization(nickname, password, false); 782 } 783 784 785 const BString& 786 Model::Nickname() const 787 { 788 return fWebAppInterface.Nickname(); 789 } 790 791 792 void 793 Model::SetAuthorization(const BString& nickname, const BString& passwordClear, 794 bool storePassword) 795 { 796 BString existingNickname = Nickname(); 797 798 if (storePassword) { 799 // no point continuing to store the password for the previous user. 800 801 if (!existingNickname.IsEmpty()) 802 model_remove_key_for_user(existingNickname); 803 804 // adding a key that is already there does not seem to override the 805 // existing key so the old key needs to be removed first. 806 807 if (!nickname.IsEmpty()) 808 model_remove_key_for_user(nickname); 809 810 if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) { 811 BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) 812 << nickname; 813 BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier); 814 BKeyStore keyStore; 815 keyStore.AddKeyring(kHaikuDepotKeyring); 816 keyStore.AddKey(kHaikuDepotKeyring, key); 817 } 818 } 819 820 BAutolock locker(&fLock); 821 fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear)); 822 823 if (nickname != existingNickname) 824 _NotifyAuthorizationChanged(); 825 } 826 827 828 /*! When bulk repository data comes down from the server, it will 829 arrive as a json.gz payload. This is stored locally as a cache 830 and this method will provide the on-disk storage location for 831 this file. 832 */ 833 834 status_t 835 Model::DumpExportRepositoryDataPath(BPath& path) 836 { 837 BString leaf; 838 leaf.SetToFormat("repository-all_%s.json.gz", 839 Language()->PreferredLanguage()->Code()); 840 return StorageUtils::LocalWorkingFilesPath(leaf, path); 841 } 842 843 844 /*! When the system downloads reference data (eg; categories) from the server 845 then the downloaded data is stored and cached at the path defined by this 846 method. 847 */ 848 849 status_t 850 Model::DumpExportReferenceDataPath(BPath& path) 851 { 852 BString leaf; 853 leaf.SetToFormat("reference-all_%s.json.gz", 854 Language()->PreferredLanguage()->Code()); 855 return StorageUtils::LocalWorkingFilesPath(leaf, path); 856 } 857 858 859 status_t 860 Model::IconTarPath(BPath& path) const 861 { 862 return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path); 863 } 864 865 866 status_t 867 Model::DumpExportPkgDataPath(BPath& path, 868 const BString& repositorySourceCode) 869 { 870 BString leaf; 871 leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(), 872 Language()->PreferredLanguage()->Code()); 873 return StorageUtils::LocalWorkingFilesPath(leaf, path); 874 } 875 876 877 void 878 Model::_PopulatePackageScreenshot(const PackageInfoRef& package, 879 const ScreenshotInfoRef& info, int32 scaledWidth, bool fromCacheOnly) 880 { 881 // See if there is a cached screenshot 882 BFile screenshotFile; 883 BPath screenshotCachePath; 884 885 status_t result = StorageUtils::LocalWorkingDirectoryPath( 886 "Screenshots", screenshotCachePath); 887 888 if (result != B_OK) { 889 HDERROR("unable to get the screenshot dir - unable to proceed"); 890 return; 891 } 892 893 bool fileExists = false; 894 BString screenshotName(info->Code()); 895 screenshotName << "@" << scaledWidth; 896 screenshotName << ".png"; 897 time_t modifiedTime; 898 if (screenshotCachePath.Append(screenshotName) == B_OK) { 899 // Try opening the file in read-only mode, which will fail if its 900 // not a file or does not exist. 901 fileExists = screenshotFile.SetTo(screenshotCachePath.Path(), 902 B_READ_ONLY) == B_OK; 903 if (fileExists) 904 screenshotFile.GetModificationTime(&modifiedTime); 905 } 906 907 if (fileExists) { 908 time_t now; 909 time(&now); 910 if (fromCacheOnly || now - modifiedTime < 60 * 60) { 911 // Cache file is recent enough, just use it and return. 912 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile), 913 true); 914 BAutolock locker(&fLock); 915 package->AddScreenshot(bitmapRef); 916 return; 917 } 918 } 919 920 if (fromCacheOnly) 921 return; 922 923 // Retrieve screenshot from web-app 924 BMallocIO buffer; 925 926 int32 scaledHeight = scaledWidth * info->Height() / info->Width(); 927 928 status_t status = fWebAppInterface.RetrieveScreenshot(info->Code(), 929 scaledWidth, scaledHeight, &buffer); 930 if (status == B_OK) { 931 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 932 BAutolock locker(&fLock); 933 package->AddScreenshot(bitmapRef); 934 locker.Unlock(); 935 if (screenshotFile.SetTo(screenshotCachePath.Path(), 936 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 937 screenshotFile.Write(buffer.Buffer(), buffer.BufferLength()); 938 } 939 } else { 940 HDERROR("Failed to retrieve screenshot for code '%s' " 941 "at %" B_PRIi32 "x%" B_PRIi32 ".", info->Code().String(), 942 scaledWidth, scaledHeight); 943 } 944 } 945 946 947 // #pragma mark - listener notification methods 948 949 950 void 951 Model::_NotifyAuthorizationChanged() 952 { 953 std::vector<ModelListenerRef>::const_iterator it; 954 for (it = fListeners.begin(); it != fListeners.end(); it++) { 955 const ModelListenerRef& listener = *it; 956 if (listener.IsSet()) 957 listener->AuthorizationChanged(); 958 } 959 } 960 961 962 void 963 Model::_NotifyCategoryListChanged() 964 { 965 std::vector<ModelListenerRef>::const_iterator it; 966 for (it = fListeners.begin(); it != fListeners.end(); it++) { 967 const ModelListenerRef& listener = *it; 968 if (listener.IsSet()) 969 listener->CategoryListChanged(); 970 } 971 } 972 973 974 void 975 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload, 976 const char *sourceDescription) const 977 { 978 BMessage error; 979 BString errorMessage; 980 double errorCode; 981 982 if (responsePayload.FindMessage("error", &error) == B_OK 983 && error.FindString("message", &errorMessage) == B_OK 984 && error.FindDouble("code", &errorCode) == B_OK) { 985 HDERROR("[%s] --> error : [%s] (%f)", sourceDescription, 986 errorMessage.String(), errorCode); 987 } else 988 HDERROR("[%s] --> an undefined error has occurred", sourceDescription); 989 } 990 991 992 // #pragma mark - Rating Stabilities 993 994 995 int32 996 Model::CountRatingStabilities() const 997 { 998 return fRatingStabilities.size(); 999 } 1000 1001 1002 RatingStabilityRef 1003 Model::RatingStabilityByCode(BString& code) const 1004 { 1005 std::vector<RatingStabilityRef>::const_iterator it; 1006 for (it = fRatingStabilities.begin(); it != fRatingStabilities.end(); 1007 it++) { 1008 RatingStabilityRef aRatingStability = *it; 1009 if (aRatingStability->Code() == code) 1010 return aRatingStability; 1011 } 1012 return RatingStabilityRef(); 1013 } 1014 1015 1016 RatingStabilityRef 1017 Model::RatingStabilityAtIndex(int32 index) const 1018 { 1019 return fRatingStabilities[index]; 1020 } 1021 1022 1023 void 1024 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values) 1025 { 1026 std::vector<RatingStabilityRef>::const_iterator it; 1027 for (it = values.begin(); it != values.end(); it++) 1028 _AddRatingStability(*it); 1029 } 1030 1031 1032 void 1033 Model::_AddRatingStability(const RatingStabilityRef& value) 1034 { 1035 std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst 1036 = std::lower_bound( 1037 fRatingStabilities.begin(), 1038 fRatingStabilities.end(), 1039 value, 1040 &IsRatingStabilityBefore); 1041 std::vector<RatingStabilityRef>::iterator itInsertionPt = 1042 fRatingStabilities.begin() 1043 + (itInsertionPtConst - fRatingStabilities.begin()); 1044 1045 if (itInsertionPt != fRatingStabilities.end() 1046 && (*itInsertionPt)->Code() == value->Code()) { 1047 itInsertionPt = fRatingStabilities.erase(itInsertionPt); 1048 // replace the one with the same code. 1049 } 1050 1051 fRatingStabilities.insert(itInsertionPt, value); 1052 } 1053 1054 1055 // #pragma mark - Categories 1056 1057 1058 int32 1059 Model::CountCategories() const 1060 { 1061 return fCategories.size(); 1062 } 1063 1064 1065 CategoryRef 1066 Model::CategoryByCode(BString& code) const 1067 { 1068 std::vector<CategoryRef>::const_iterator it; 1069 for (it = fCategories.begin(); it != fCategories.end(); it++) { 1070 CategoryRef aCategory = *it; 1071 if (aCategory->Code() == code) 1072 return aCategory; 1073 } 1074 return CategoryRef(); 1075 } 1076 1077 1078 CategoryRef 1079 Model::CategoryAtIndex(int32 index) const 1080 { 1081 return fCategories[index]; 1082 } 1083 1084 1085 void 1086 Model::AddCategories(std::vector<CategoryRef>& values) 1087 { 1088 std::vector<CategoryRef>::iterator it; 1089 for (it = values.begin(); it != values.end(); it++) 1090 _AddCategory(*it); 1091 _NotifyCategoryListChanged(); 1092 } 1093 1094 /*! This will insert the category in order. 1095 */ 1096 1097 void 1098 Model::_AddCategory(const CategoryRef& category) 1099 { 1100 std::vector<CategoryRef>::const_iterator itInsertionPtConst 1101 = std::lower_bound( 1102 fCategories.begin(), 1103 fCategories.end(), 1104 category, 1105 &IsPackageCategoryBefore); 1106 std::vector<CategoryRef>::iterator itInsertionPt = 1107 fCategories.begin() + (itInsertionPtConst - fCategories.begin()); 1108 1109 if (itInsertionPt != fCategories.end() 1110 && (*itInsertionPt)->Code() == category->Code()) { 1111 itInsertionPt = fCategories.erase(itInsertionPt); 1112 // replace the one with the same code. 1113 } 1114 1115 fCategories.insert(itInsertionPt, category); 1116 } 1117