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