1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>. 4 * All rights reserved. Distributed under the terms of the MIT License. 5 */ 6 7 #include "Model.h" 8 9 #include <ctime> 10 #include <stdarg.h> 11 #include <stdio.h> 12 #include <time.h> 13 14 #include <Autolock.h> 15 #include <Catalog.h> 16 #include <Directory.h> 17 #include <Entry.h> 18 #include <File.h> 19 #include <KeyStore.h> 20 #include <LocaleRoster.h> 21 #include <Message.h> 22 #include <Path.h> 23 24 25 #undef B_TRANSLATION_CONTEXT 26 #define B_TRANSLATION_CONTEXT "Model" 27 28 29 static const char* kHaikuDepotKeyring = "HaikuDepot"; 30 31 32 PackageFilter::~PackageFilter() 33 { 34 } 35 36 37 ModelListener::~ModelListener() 38 { 39 } 40 41 42 // #pragma mark - PackageFilters 43 44 45 class AnyFilter : public PackageFilter { 46 public: 47 virtual bool AcceptsPackage(const PackageInfoRef& package) const 48 { 49 return true; 50 } 51 }; 52 53 54 class DepotFilter : public PackageFilter { 55 public: 56 DepotFilter(const DepotInfo& depot) 57 : 58 fDepot(depot) 59 { 60 } 61 62 virtual bool AcceptsPackage(const PackageInfoRef& package) const 63 { 64 // TODO: Maybe a PackageInfo ought to know the Depot it came from? 65 // But right now the same package could theoretically be provided 66 // from different depots and the filter would work correctly. 67 // Also the PackageList could actually contain references to packages 68 // instead of the packages as objects. The equal operator is quite 69 // expensive as is. 70 return fDepot.Packages().Contains(package); 71 } 72 73 const BString& Depot() const 74 { 75 return fDepot.Name(); 76 } 77 78 private: 79 DepotInfo fDepot; 80 }; 81 82 83 class CategoryFilter : public PackageFilter { 84 public: 85 CategoryFilter(const BString& category) 86 : 87 fCategory(category) 88 { 89 } 90 91 virtual bool AcceptsPackage(const PackageInfoRef& package) const 92 { 93 if (package.Get() == NULL) 94 return false; 95 96 const CategoryList& categories = package->Categories(); 97 for (int i = categories.CountItems() - 1; i >= 0; i--) { 98 const CategoryRef& category = categories.ItemAtFast(i); 99 if (category.Get() == NULL) 100 continue; 101 if (category->Name() == fCategory) 102 return true; 103 } 104 return false; 105 } 106 107 const BString& Category() const 108 { 109 return fCategory; 110 } 111 112 private: 113 BString fCategory; 114 }; 115 116 117 class ContainedInFilter : public PackageFilter { 118 public: 119 ContainedInFilter(const PackageList& packageList) 120 : 121 fPackageList(packageList) 122 { 123 } 124 125 virtual bool AcceptsPackage(const PackageInfoRef& package) const 126 { 127 return fPackageList.Contains(package); 128 } 129 130 private: 131 const PackageList& fPackageList; 132 }; 133 134 135 class ContainedInEitherFilter : public PackageFilter { 136 public: 137 ContainedInEitherFilter(const PackageList& packageListA, 138 const PackageList& packageListB) 139 : 140 fPackageListA(packageListA), 141 fPackageListB(packageListB) 142 { 143 } 144 145 virtual bool AcceptsPackage(const PackageInfoRef& package) const 146 { 147 return fPackageListA.Contains(package) 148 || fPackageListB.Contains(package); 149 } 150 151 private: 152 const PackageList& fPackageListA; 153 const PackageList& fPackageListB; 154 }; 155 156 157 class NotContainedInFilter : public PackageFilter { 158 public: 159 NotContainedInFilter(const PackageList* packageList, ...) 160 { 161 va_list args; 162 va_start(args, packageList); 163 while (true) { 164 const PackageList* packageList = va_arg(args, const PackageList*); 165 if (packageList == NULL) 166 break; 167 fPackageLists.Add(packageList); 168 } 169 va_end(args); 170 } 171 172 virtual bool AcceptsPackage(const PackageInfoRef& package) const 173 { 174 if (package.Get()==NULL) 175 return false; 176 177 printf("TEST %s\n", package->Title().String()); 178 179 for (int32 i = 0; i < fPackageLists.CountItems(); i++) { 180 if (fPackageLists.ItemAtFast(i)->Contains(package)) { 181 printf(" contained in %" B_PRId32 "\n", i); 182 return false; 183 } 184 } 185 return true; 186 } 187 188 private: 189 List<const PackageList*, true> fPackageLists; 190 }; 191 192 193 class StateFilter : public PackageFilter { 194 public: 195 StateFilter(PackageState state) 196 : 197 fState(state) 198 { 199 } 200 201 virtual bool AcceptsPackage(const PackageInfoRef& package) const 202 { 203 return package->State() == NONE; 204 } 205 206 private: 207 PackageState fState; 208 }; 209 210 211 class SearchTermsFilter : public PackageFilter { 212 public: 213 SearchTermsFilter(const BString& searchTerms) 214 { 215 // Separate the string into terms at spaces 216 int32 index = 0; 217 while (index < searchTerms.Length()) { 218 int32 nextSpace = searchTerms.FindFirst(" ", index); 219 if (nextSpace < 0) 220 nextSpace = searchTerms.Length(); 221 if (nextSpace > index) { 222 BString term; 223 searchTerms.CopyInto(term, index, nextSpace - index); 224 term.ToLower(); 225 fSearchTerms.Add(term); 226 } 227 index = nextSpace + 1; 228 } 229 } 230 231 virtual bool AcceptsPackage(const PackageInfoRef& package) const 232 { 233 if (package.Get() == NULL) 234 return false; 235 // Every search term must be found in one of the package texts 236 for (int32 i = fSearchTerms.CountItems() - 1; i >= 0; i--) { 237 const BString& term = fSearchTerms.ItemAtFast(i); 238 if (!_TextContains(package->Title(), term) 239 && !_TextContains(package->Publisher().Name(), term) 240 && !_TextContains(package->ShortDescription(), term) 241 && !_TextContains(package->FullDescription(), term)) { 242 return false; 243 } 244 } 245 return true; 246 } 247 248 BString SearchTerms() const 249 { 250 BString searchTerms; 251 for (int32 i = 0; i < fSearchTerms.CountItems(); i++) { 252 const BString& term = fSearchTerms.ItemAtFast(i); 253 if (term.IsEmpty()) 254 continue; 255 if (!searchTerms.IsEmpty()) 256 searchTerms.Append(" "); 257 searchTerms.Append(term); 258 } 259 return searchTerms; 260 } 261 262 private: 263 bool _TextContains(BString text, const BString& string) const 264 { 265 text.ToLower(); 266 int32 index = text.FindFirst(string); 267 return index >= 0; 268 } 269 270 private: 271 StringList fSearchTerms; 272 }; 273 274 275 static inline bool 276 is_source_package(const PackageInfoRef& package) 277 { 278 const BString& packageName = package->Title(); 279 return packageName.EndsWith("_source"); 280 } 281 282 283 static inline bool 284 is_develop_package(const PackageInfoRef& package) 285 { 286 const BString& packageName = package->Title(); 287 return packageName.EndsWith("_devel"); 288 } 289 290 291 // #pragma mark - Model 292 293 294 Model::Model() 295 : 296 fDepots(), 297 298 fCategoryAudio(new PackageCategory( 299 BitmapRef(), 300 B_TRANSLATE("Audio"), "audio"), true), 301 fCategoryBusiness(new PackageCategory( 302 BitmapRef(), 303 B_TRANSLATE("Business"), "business"), true), 304 fCategoryDevelopment(new PackageCategory( 305 BitmapRef(), 306 B_TRANSLATE("Development"), "development"), true), 307 fCategoryEducation(new PackageCategory( 308 BitmapRef(), 309 B_TRANSLATE("Education"), "education"), true), 310 fCategoryGames(new PackageCategory( 311 BitmapRef(), 312 B_TRANSLATE("Games"), "games"), true), 313 fCategoryGraphics(new PackageCategory( 314 BitmapRef(), 315 B_TRANSLATE("Graphics"), "graphics"), true), 316 fCategoryInternetAndNetwork(new PackageCategory( 317 BitmapRef(), 318 B_TRANSLATE("Internet & Network"), "internetandnetwork"), true), 319 fCategoryProductivity(new PackageCategory( 320 BitmapRef(), 321 B_TRANSLATE("Productivity"), "productivity"), true), 322 fCategoryScienceAndMathematics(new PackageCategory( 323 BitmapRef(), 324 B_TRANSLATE("Science & Mathematics"), "scienceandmathematics"), true), 325 fCategorySystemAndUtilities(new PackageCategory( 326 BitmapRef(), 327 B_TRANSLATE("System & Utilities"), "systemandutilities"), true), 328 fCategoryVideo(new PackageCategory( 329 BitmapRef(), 330 B_TRANSLATE("Video"), "video"), true), 331 332 fCategoryFilter(PackageFilterRef(new AnyFilter(), true)), 333 fDepotFilter(""), 334 fSearchTermsFilter(PackageFilterRef(new AnyFilter(), true)), 335 336 fShowAvailablePackages(true), 337 fShowInstalledPackages(false), 338 fShowSourcePackages(false), 339 fShowDevelopPackages(false), 340 341 fPopulateAllPackagesThread(-1), 342 fStopPopulatingAllPackages(false) 343 { 344 // Don't forget to add new categories to this list: 345 fCategories.Add(fCategoryGames); 346 fCategories.Add(fCategoryBusiness); 347 fCategories.Add(fCategoryAudio); 348 fCategories.Add(fCategoryVideo); 349 fCategories.Add(fCategoryGraphics); 350 fCategories.Add(fCategoryEducation); 351 fCategories.Add(fCategoryProductivity); 352 fCategories.Add(fCategorySystemAndUtilities); 353 fCategories.Add(fCategoryInternetAndNetwork); 354 fCategories.Add(fCategoryDevelopment); 355 fCategories.Add(fCategoryScienceAndMathematics); 356 // TODO: The server will eventually support an API to 357 // get the defined categories and their translated names. 358 // This should then be used instead of hard-coded 359 // categories and translations in the app. 360 361 fPreferredLanguage = "en"; 362 BLocaleRoster* localeRoster = BLocaleRoster::Default(); 363 if (localeRoster != NULL) { 364 BMessage preferredLanguages; 365 if (localeRoster->GetPreferredLanguages(&preferredLanguages) == B_OK) { 366 BString language; 367 if (preferredLanguages.FindString("language", 0, &language) == B_OK) 368 language.CopyInto(fPreferredLanguage, 0, 2); 369 } 370 } 371 372 // TODO: Fetch this from the web-app. 373 fSupportedLanguages.Add("en"); 374 fSupportedLanguages.Add("de"); 375 fSupportedLanguages.Add("fr"); 376 fSupportedLanguages.Add("ja"); 377 fSupportedLanguages.Add("es"); 378 fSupportedLanguages.Add("zh"); 379 fSupportedLanguages.Add("pt"); 380 fSupportedLanguages.Add("ru"); 381 382 if (!fSupportedLanguages.Contains(fPreferredLanguage)) { 383 // Force the preferred language to one of the currently supported 384 // ones, until the web application supports all ISO language codes. 385 printf("User preferred language '%s' not currently supported, " 386 "defaulting to 'en'.", fPreferredLanguage.String()); 387 fPreferredLanguage = "en"; 388 } 389 fWebAppInterface.SetPreferredLanguage(fPreferredLanguage); 390 } 391 392 393 Model::~Model() 394 { 395 StopPopulatingAllPackages(); 396 } 397 398 399 bool 400 Model::AddListener(const ModelListenerRef& listener) 401 { 402 return fListeners.Add(listener); 403 } 404 405 406 PackageList 407 Model::CreatePackageList() const 408 { 409 // Iterate all packages from all depots. 410 // If configured, restrict depot, filter by search terms, status, name ... 411 PackageList resultList; 412 413 for (int32 i = 0; i < fDepots.CountItems(); i++) { 414 const DepotInfo& depot = fDepots.ItemAtFast(i); 415 416 if (fDepotFilter.Length() > 0 && fDepotFilter != depot.Name()) 417 continue; 418 419 const PackageList& packages = depot.Packages(); 420 421 for (int32 j = 0; j < packages.CountItems(); j++) { 422 const PackageInfoRef& package = packages.ItemAtFast(j); 423 if (fCategoryFilter->AcceptsPackage(package) 424 && fSearchTermsFilter->AcceptsPackage(package) 425 && (fShowAvailablePackages || package->State() != NONE) 426 && (fShowInstalledPackages || package->State() != ACTIVATED) 427 && (fShowSourcePackages || !is_source_package(package)) 428 && (fShowDevelopPackages || !is_develop_package(package))) { 429 resultList.Add(package); 430 } 431 } 432 } 433 434 return resultList; 435 } 436 437 438 bool 439 Model::AddDepot(const DepotInfo& depot) 440 { 441 return fDepots.Add(depot); 442 } 443 444 445 void 446 Model::Clear() 447 { 448 fDepots.Clear(); 449 } 450 451 452 void 453 Model::SetPackageState(const PackageInfoRef& package, PackageState state) 454 { 455 switch (state) { 456 default: 457 case NONE: 458 fInstalledPackages.Remove(package); 459 fActivatedPackages.Remove(package); 460 fUninstalledPackages.Remove(package); 461 break; 462 case INSTALLED: 463 if (!fInstalledPackages.Contains(package)) 464 fInstalledPackages.Add(package); 465 fActivatedPackages.Remove(package); 466 fUninstalledPackages.Remove(package); 467 break; 468 case ACTIVATED: 469 if (!fInstalledPackages.Contains(package)) 470 fInstalledPackages.Add(package); 471 if (!fActivatedPackages.Contains(package)) 472 fActivatedPackages.Add(package); 473 fUninstalledPackages.Remove(package); 474 break; 475 case UNINSTALLED: 476 fInstalledPackages.Remove(package); 477 fActivatedPackages.Remove(package); 478 if (!fUninstalledPackages.Contains(package)) 479 fUninstalledPackages.Add(package); 480 break; 481 } 482 483 package->SetState(state); 484 } 485 486 487 // #pragma mark - filters 488 489 490 void 491 Model::SetCategory(const BString& category) 492 { 493 PackageFilter* filter; 494 495 if (category.Length() == 0) 496 filter = new AnyFilter(); 497 else 498 filter = new CategoryFilter(category); 499 500 fCategoryFilter.SetTo(filter, true); 501 } 502 503 504 BString 505 Model::Category() const 506 { 507 CategoryFilter* filter 508 = dynamic_cast<CategoryFilter*>(fCategoryFilter.Get()); 509 if (filter == NULL) 510 return ""; 511 return filter->Category(); 512 } 513 514 515 void 516 Model::SetDepot(const BString& depot) 517 { 518 fDepotFilter = depot; 519 } 520 521 522 BString 523 Model::Depot() const 524 { 525 return fDepotFilter; 526 } 527 528 529 void 530 Model::SetSearchTerms(const BString& searchTerms) 531 { 532 PackageFilter* filter; 533 534 if (searchTerms.Length() == 0) 535 filter = new AnyFilter(); 536 else 537 filter = new SearchTermsFilter(searchTerms); 538 539 fSearchTermsFilter.SetTo(filter, true); 540 } 541 542 543 BString 544 Model::SearchTerms() const 545 { 546 SearchTermsFilter* filter 547 = dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get()); 548 if (filter == NULL) 549 return ""; 550 return filter->SearchTerms(); 551 } 552 553 554 void 555 Model::SetShowAvailablePackages(bool show) 556 { 557 fShowAvailablePackages = show; 558 } 559 560 561 void 562 Model::SetShowInstalledPackages(bool show) 563 { 564 fShowInstalledPackages = show; 565 } 566 567 568 void 569 Model::SetShowSourcePackages(bool show) 570 { 571 fShowSourcePackages = show; 572 } 573 574 575 void 576 Model::SetShowDevelopPackages(bool show) 577 { 578 fShowDevelopPackages = show; 579 } 580 581 582 // #pragma mark - information retrival 583 584 585 void 586 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) 587 { 588 // TODO: There should probably also be a way to "unpopulate" the 589 // package information. Maybe a cache of populated packages, so that 590 // packages loose their extra information after a certain amount of 591 // time when they have not been accessed/displayed in the UI. Otherwise 592 // HaikuDepot will consume more and more resources in the packages. 593 // Especially screen-shots will be a problem eventually. 594 { 595 BAutolock locker(&fLock); 596 if (fPopulatedPackages.Contains(package)) 597 return; 598 fPopulatedPackages.Add(package); 599 } 600 601 if ((flags & POPULATE_USER_RATINGS) != 0) { 602 // Retrieve info from web-app 603 BMessage info; 604 605 BString packageName; 606 BString architecture; 607 { 608 BAutolock locker(&fLock); 609 packageName = package->Title(); 610 architecture = package->Architecture(); 611 } 612 613 status_t status = fWebAppInterface.RetrieveUserRatings(packageName, 614 architecture, 0, 50, info); 615 if (status == B_OK) { 616 // Parse message 617 BMessage result; 618 BMessage items; 619 if (info.FindMessage("result", &result) == B_OK 620 && result.FindMessage("items", &items) == B_OK) { 621 622 BAutolock locker(&fLock); 623 package->ClearUserRatings(); 624 625 int index = 0; 626 while (true) { 627 BString name; 628 name << index++; 629 630 BMessage item; 631 if (items.FindMessage(name, &item) != B_OK) 632 break; 633 // item.PrintToStream(); 634 635 BString user; 636 BMessage userInfo; 637 if (item.FindMessage("user", &userInfo) != B_OK 638 || userInfo.FindString("nickname", &user) != B_OK) { 639 // Ignore, we need the user name 640 continue; 641 } 642 643 // Extract basic info, all items are optional 644 BString languageCode; 645 BString comment; 646 double rating; 647 item.FindString("naturalLanguageCode", &languageCode); 648 item.FindString("comment", &comment); 649 if (item.FindDouble("rating", &rating) != B_OK) 650 rating = -1; 651 if (comment.Length() == 0 && rating == -1) { 652 // No useful information given. 653 continue; 654 } 655 656 // For which version of the package was the rating? 657 BString major = "?"; 658 BString minor = "?"; 659 BString micro = ""; 660 BMessage version; 661 if (item.FindMessage("pkgVersion", &version) == B_OK) { 662 version.FindString("major", &major); 663 version.FindString("minor", &minor); 664 version.FindString("micro", µ); 665 } 666 BString versionString = major; 667 versionString << "."; 668 versionString << minor; 669 if (micro.Length() > 0) { 670 versionString << "."; 671 versionString << micro; 672 } 673 // Add the rating to the PackageInfo 674 package->AddUserRating( 675 UserRating(UserInfo(user), rating, 676 comment, languageCode, versionString, 0, 0) 677 ); 678 } 679 } else if (info.FindMessage("error", &result) == B_OK) { 680 result.PrintToStream(); 681 } 682 } 683 } 684 685 if ((flags & POPULATE_SCREEN_SHOTS) != 0) { 686 ScreenshotInfoList screenshotInfos; 687 { 688 BAutolock locker(&fLock); 689 screenshotInfos = package->ScreenshotInfos(); 690 package->ClearScreenshots(); 691 } 692 for (int i = 0; i < screenshotInfos.CountItems(); i++) { 693 const ScreenshotInfo& info = screenshotInfos.ItemAtFast(i); 694 _PopulatePackageScreenshot(package, info, 320, false); 695 } 696 } 697 } 698 699 700 void 701 Model::PopulateAllPackages() 702 { 703 StopPopulatingAllPackages(); 704 705 fStopPopulatingAllPackages = false; 706 707 fPopulateAllPackagesThread = spawn_thread(&_PopulateAllPackagesEntry, 708 "Package populator", B_NORMAL_PRIORITY, this); 709 if (fPopulateAllPackagesThread >= 0) 710 resume_thread(fPopulateAllPackagesThread); 711 } 712 713 714 void 715 Model::StopPopulatingAllPackages() 716 { 717 if (fPopulateAllPackagesThread < 0) 718 return; 719 720 fStopPopulatingAllPackages = true; 721 wait_for_thread(fPopulateAllPackagesThread, NULL); 722 fPopulateAllPackagesThread = -1; 723 } 724 725 726 void 727 Model::SetUsername(BString username) 728 { 729 BString password; 730 if (username.Length() > 0) { 731 BPasswordKey key; 732 BKeyStore keyStore; 733 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, username, 734 key) == B_OK) { 735 password = key.Password(); 736 } else { 737 username = ""; 738 } 739 } 740 SetAuthorization(username, password, false); 741 } 742 743 744 const BString& 745 Model::Username() const 746 { 747 return fWebAppInterface.Username(); 748 } 749 750 751 void 752 Model::SetAuthorization(const BString& username, const BString& password, 753 bool storePassword) 754 { 755 if (storePassword && username.Length() > 0 && password.Length() > 0) { 756 BPasswordKey key(password, B_KEY_PURPOSE_WEB, username); 757 BKeyStore keyStore; 758 keyStore.AddKeyring(kHaikuDepotKeyring); 759 keyStore.AddKey(kHaikuDepotKeyring, key); 760 } 761 762 BAutolock locker(&fLock); 763 fWebAppInterface.SetAuthorization(username, password); 764 765 _NotifyAuthorizationChanged(); 766 } 767 768 769 // #pragma mark - private 770 771 772 int32 773 Model::_PopulateAllPackagesEntry(void* cookie) 774 { 775 Model* model = static_cast<Model*>(cookie); 776 model->_PopulateAllPackagesThread(true); 777 model->_PopulateAllPackagesThread(false); 778 return 0; 779 } 780 781 782 void 783 Model::_PopulateAllPackagesThread(bool fromCacheOnly) 784 { 785 int32 depotIndex = 0; 786 int32 packageIndex = 0; 787 PackageList bulkPackageList; 788 PackageList packagesWithIconsList; 789 790 while (!fStopPopulatingAllPackages) { 791 // Obtain PackageInfoRef while keeping the depot and package lists 792 // locked. 793 PackageInfoRef package; 794 { 795 BAutolock locker(&fLock); 796 797 if (depotIndex >= fDepots.CountItems()) 798 break; 799 const DepotInfo& depot = fDepots.ItemAt(depotIndex); 800 801 const PackageList& packages = depot.Packages(); 802 if (packageIndex >= packages.CountItems()) { 803 // Need the next depot 804 packageIndex = 0; 805 depotIndex++; 806 continue; 807 } 808 809 package = packages.ItemAt(packageIndex); 810 packageIndex++; 811 } 812 813 if (package.Get() == NULL) 814 continue; 815 816 //_PopulatePackageInfo(package, fromCacheOnly); 817 bulkPackageList.Add(package); 818 if (bulkPackageList.CountItems() == 50) { 819 _PopulatePackageInfos(bulkPackageList, fromCacheOnly, 820 packagesWithIconsList); 821 bulkPackageList.Clear(); 822 } 823 if (fromCacheOnly) 824 _PopulatePackageIcon(package, fromCacheOnly); 825 // TODO: Average user rating. It needs to be shown in the 826 // list view, so without the user clicking the package. 827 } 828 829 if (bulkPackageList.CountItems() > 0) { 830 _PopulatePackageInfos(bulkPackageList, fromCacheOnly, 831 packagesWithIconsList); 832 } 833 834 if (!fromCacheOnly) { 835 for (int i = packagesWithIconsList.CountItems() - 1; i >= 0; i--) { 836 if (fStopPopulatingAllPackages) 837 break; 838 const PackageInfoRef& package = packagesWithIconsList.ItemAtFast(i); 839 printf("Getting/Updating native icon for %s\n", 840 package->Title().String()); 841 _PopulatePackageIcon(package, fromCacheOnly); 842 } 843 } 844 } 845 846 847 bool 848 Model::_GetCacheFile(BPath& path, BFile& file, directory_which directory, 849 const char* relativeLocation, const char* fileName, uint32 openMode) const 850 { 851 if (find_directory(directory, &path) == B_OK 852 && path.Append(relativeLocation) == B_OK 853 && create_directory(path.Path(), 0777) == B_OK 854 && path.Append(fileName) == B_OK) { 855 // Try opening the file which will fail if its 856 // not a file or does not exist. 857 return file.SetTo(path.Path(), openMode) == B_OK; 858 } 859 return false; 860 } 861 862 863 bool 864 Model::_GetCacheFile(BPath& path, BFile& file, directory_which directory, 865 const char* relativeLocation, const char* fileName, 866 bool ignoreAge, time_t maxAge) const 867 { 868 if (!_GetCacheFile(path, file, directory, relativeLocation, fileName, 869 B_READ_ONLY)) { 870 return false; 871 } 872 873 if (ignoreAge) 874 return true; 875 876 time_t modifiedTime; 877 file.GetModificationTime(&modifiedTime); 878 time_t now; 879 time(&now); 880 return now - modifiedTime < maxAge; 881 } 882 883 884 void 885 Model::_PopulatePackageInfos(PackageList& packages, bool fromCacheOnly, 886 PackageList& packagesWithIcons) 887 { 888 if (fStopPopulatingAllPackages) 889 return; 890 891 // See if there are cached info files 892 for (int i = packages.CountItems() - 1; i >= 0; i--) { 893 if (fStopPopulatingAllPackages) 894 return; 895 896 const PackageInfoRef& package = packages.ItemAtFast(i); 897 898 BFile file; 899 BPath path; 900 BString name(package->Title()); 901 name << ".info"; 902 if (_GetCacheFile(path, file, B_USER_CACHE_DIRECTORY, 903 "HaikuDepot", name, fromCacheOnly, 60 * 60)) { 904 // Cache file is recent enough, just use it and return. 905 BMessage pkgInfo; 906 if (pkgInfo.Unflatten(&file) == B_OK) { 907 _PopulatePackageInfo(package, pkgInfo); 908 if (_HasNativeIcon(pkgInfo)) 909 packagesWithIcons.Add(package); 910 packages.Remove(i); 911 } 912 } 913 } 914 915 if (fromCacheOnly || packages.IsEmpty()) 916 return; 917 918 // Retrieve info from web-app 919 BMessage info; 920 921 StringList packageNames; 922 StringList packageArchitectures; 923 for (int i = 0; i < packages.CountItems(); i++) { 924 const PackageInfoRef& package = packages.ItemAtFast(i); 925 packageNames.Add(package->Title()); 926 packageArchitectures.Add(package->Architecture()); 927 } 928 929 status_t status = fWebAppInterface.RetrieveBulkPackageInfo(packageNames, 930 packageArchitectures, info); 931 if (status == B_OK) { 932 // Parse message 933 // info.PrintToStream(); 934 BMessage result; 935 BMessage pkgs; 936 if (info.FindMessage("result", &result) == B_OK 937 && result.FindMessage("pkgs", &pkgs) == B_OK) { 938 int32 index = 0; 939 while (true) { 940 if (fStopPopulatingAllPackages) 941 return; 942 BString name; 943 name << index++; 944 BMessage pkgInfo; 945 if (pkgs.FindMessage(name, &pkgInfo) != B_OK) 946 break; 947 948 BString pkgName; 949 if (pkgInfo.FindString("name", &pkgName) != B_OK) 950 continue; 951 952 // Find the PackageInfoRef 953 bool found = false; 954 for (int i = 0; i < packages.CountItems(); i++) { 955 const PackageInfoRef& package = packages.ItemAtFast(i); 956 if (pkgName == package->Title()) { 957 _PopulatePackageInfo(package, pkgInfo); 958 if (_HasNativeIcon(pkgInfo)) 959 packagesWithIcons.Add(package); 960 961 // Store in cache 962 BFile file; 963 BPath path; 964 BString fileName(package->Title()); 965 fileName << ".info"; 966 if (_GetCacheFile(path, file, B_USER_CACHE_DIRECTORY, 967 "HaikuDepot", fileName, 968 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE)) { 969 pkgInfo.Flatten(&file); 970 } 971 972 packages.Remove(i); 973 found = true; 974 break; 975 } 976 } 977 if (!found) 978 printf("No matching package for %s\n", pkgName.String()); 979 } 980 } 981 } else { 982 printf("Error sending request: %s\n", strerror(status)); 983 int count = packages.CountItems(); 984 if (count >= 4) { 985 // Retry in smaller chunks 986 PackageList firstHalf; 987 PackageList secondHalf; 988 for (int i = 0; i < count / 2; i++) 989 firstHalf.Add(packages.ItemAtFast(i)); 990 for (int i = count / 2; i < count; i++) 991 secondHalf.Add(packages.ItemAtFast(i)); 992 packages.Clear(); 993 _PopulatePackageInfos(firstHalf, fromCacheOnly, packagesWithIcons); 994 _PopulatePackageInfos(secondHalf, fromCacheOnly, packagesWithIcons); 995 } else { 996 while (packages.CountItems() > 0) { 997 const PackageInfoRef& package = packages.ItemAtFast(0); 998 _PopulatePackageInfo(package, fromCacheOnly); 999 packages.Remove(0); 1000 } 1001 } 1002 } 1003 1004 if (packages.CountItems() > 0) { 1005 for (int i = 0; i < packages.CountItems(); i++) { 1006 const PackageInfoRef& package = packages.ItemAtFast(i); 1007 printf("No package info for %s\n", package->Title().String()); 1008 } 1009 } 1010 } 1011 1012 1013 void 1014 Model::_PopulatePackageInfo(const PackageInfoRef& package, bool fromCacheOnly) 1015 { 1016 if (fromCacheOnly) 1017 return; 1018 1019 // Retrieve info from web-app 1020 BMessage info; 1021 1022 status_t status = fWebAppInterface.RetrievePackageInfo(package->Title(), 1023 package->Architecture(), info); 1024 if (status == B_OK) { 1025 // Parse message 1026 // info.PrintToStream(); 1027 BMessage result; 1028 if (info.FindMessage("result", &result) == B_OK) 1029 _PopulatePackageInfo(package, result); 1030 } 1031 } 1032 1033 1034 static void 1035 append_word_list(BString& words, const char* word) 1036 { 1037 if (words.Length() > 0) 1038 words << ", "; 1039 words << word; 1040 } 1041 1042 1043 void 1044 Model::_PopulatePackageInfo(const PackageInfoRef& package, const BMessage& data) 1045 { 1046 BAutolock locker(&fLock); 1047 1048 BString foundInfo; 1049 1050 BMessage versions; 1051 if (data.FindMessage("versions", &versions) == B_OK) { 1052 // Search a summary and description in the preferred language 1053 int32 index = 0; 1054 while (true) { 1055 BString name; 1056 name << index++; 1057 BMessage version; 1058 if (versions.FindMessage(name, &version) != B_OK) 1059 break; 1060 BString languageCode; 1061 if (version.FindString("naturalLanguageCode", 1062 &languageCode) != B_OK 1063 || languageCode != fPreferredLanguage) { 1064 continue; 1065 } 1066 1067 BString summary; 1068 if (version.FindString("summary", &summary) == B_OK) { 1069 package->SetShortDescription(summary); 1070 append_word_list(foundInfo, "summary"); 1071 } 1072 BString description; 1073 if (version.FindString("description", &description) == B_OK) { 1074 package->SetFullDescription(description); 1075 append_word_list(foundInfo, "description"); 1076 } 1077 break; 1078 } 1079 } 1080 1081 BMessage categories; 1082 if (data.FindMessage("pkgCategoryCodes", &categories) == B_OK) { 1083 bool foundCategory = false; 1084 int32 index = 0; 1085 while (true) { 1086 BString name; 1087 name << index++; 1088 BString category; 1089 if (categories.FindString(name, &category) != B_OK) 1090 break; 1091 1092 package->ClearCategories(); 1093 for (int i = fCategories.CountItems() - 1; i >= 0; i--) { 1094 const CategoryRef& categoryRef = fCategories.ItemAtFast(i); 1095 if (categoryRef->Name() == category) { 1096 package->AddCategory(categoryRef); 1097 foundCategory = true; 1098 break; 1099 } 1100 } 1101 } 1102 if (foundCategory) 1103 append_word_list(foundInfo, "categories"); 1104 } 1105 1106 double derivedRating; 1107 if (data.FindDouble("derivedRating", &derivedRating) == B_OK) { 1108 RatingSummary summary; 1109 summary.averageRating = derivedRating; 1110 package->SetRatingSummary(summary); 1111 1112 append_word_list(foundInfo, "rating"); 1113 } 1114 1115 double prominenceOrdering; 1116 if (data.FindDouble("prominenceOrdering", &prominenceOrdering) == B_OK) { 1117 package->SetProminence(prominenceOrdering); 1118 1119 append_word_list(foundInfo, "prominence"); 1120 } 1121 1122 BMessage screenshots; 1123 if (data.FindMessage("pkgScreenshots", &screenshots) == B_OK) { 1124 package->ClearScreenshotInfos(); 1125 bool foundScreenshot = false; 1126 int32 index = 0; 1127 while (true) { 1128 BString name; 1129 name << index++; 1130 1131 BMessage screenshot; 1132 if (screenshots.FindMessage(name, &screenshot) != B_OK) 1133 break; 1134 1135 BString code; 1136 double width; 1137 double height; 1138 double dataSize; 1139 if (screenshot.FindString("code", &code) == B_OK 1140 && screenshot.FindDouble("width", &width) == B_OK 1141 && screenshot.FindDouble("height", &height) == B_OK 1142 && screenshot.FindDouble("length", &dataSize) == B_OK) { 1143 package->AddScreenshotInfo(ScreenshotInfo(code, (int32)width, 1144 (int32)height, (int32)dataSize)); 1145 foundScreenshot = true; 1146 } 1147 } 1148 if (foundScreenshot) 1149 append_word_list(foundInfo, "screenshots"); 1150 } 1151 1152 if (foundInfo.Length() > 0) { 1153 printf("Populated package info for %s: %s\n", 1154 package->Title().String(), foundInfo.String()); 1155 } 1156 1157 // If the user already clicked this package, remove it from the 1158 // list of populated packages, so that clicking it again will 1159 // populate any additional information. 1160 // TODO: Trigger re-populating if the package is currently showing. 1161 fPopulatedPackages.Remove(package); 1162 } 1163 1164 1165 void 1166 Model::_PopulatePackageIcon(const PackageInfoRef& package, bool fromCacheOnly) 1167 { 1168 // See if there is a cached icon file 1169 BFile iconFile; 1170 BPath iconCachePath; 1171 BString iconName(package->Title()); 1172 iconName << ".hvif"; 1173 if (_GetCacheFile(iconCachePath, iconFile, B_USER_CACHE_DIRECTORY, 1174 "HaikuDepot", iconName, fromCacheOnly, 60 * 60)) { 1175 // Cache file is recent enough, just use it and return. 1176 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(iconFile), true); 1177 BAutolock locker(&fLock); 1178 package->SetIcon(bitmapRef); 1179 return; 1180 } 1181 1182 if (fromCacheOnly) 1183 return; 1184 1185 // Retrieve icon from web-app 1186 BMallocIO buffer; 1187 1188 status_t status = fWebAppInterface.RetrievePackageIcon(package->Title(), 1189 &buffer); 1190 if (status == B_OK) { 1191 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 1192 BAutolock locker(&fLock); 1193 package->SetIcon(bitmapRef); 1194 locker.Unlock(); 1195 if (iconFile.SetTo(iconCachePath.Path(), 1196 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 1197 iconFile.Write(buffer.Buffer(), buffer.BufferLength()); 1198 } 1199 } 1200 } 1201 1202 1203 void 1204 Model::_PopulatePackageScreenshot(const PackageInfoRef& package, 1205 const ScreenshotInfo& info, int32 scaledWidth, bool fromCacheOnly) 1206 { 1207 // See if there is a cached screenshot 1208 BFile screenshotFile; 1209 BPath screenshotCachePath; 1210 bool fileExists = false; 1211 BString screenshotName(info.Code()); 1212 screenshotName << "@" << scaledWidth; 1213 screenshotName << ".png"; 1214 time_t modifiedTime; 1215 if (find_directory(B_USER_CACHE_DIRECTORY, &screenshotCachePath) == B_OK 1216 && screenshotCachePath.Append("HaikuDepot/Screenshots") == B_OK 1217 && create_directory(screenshotCachePath.Path(), 0777) == B_OK 1218 && screenshotCachePath.Append(screenshotName) == B_OK) { 1219 // Try opening the file in read-only mode, which will fail if its 1220 // not a file or does not exist. 1221 fileExists = screenshotFile.SetTo(screenshotCachePath.Path(), 1222 B_READ_ONLY) == B_OK; 1223 if (fileExists) 1224 screenshotFile.GetModificationTime(&modifiedTime); 1225 } 1226 1227 if (fileExists) { 1228 time_t now; 1229 time(&now); 1230 if (fromCacheOnly || now - modifiedTime < 60 * 60) { 1231 // Cache file is recent enough, just use it and return. 1232 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile), 1233 true); 1234 BAutolock locker(&fLock); 1235 package->AddScreenshot(bitmapRef); 1236 return; 1237 } 1238 } 1239 1240 if (fromCacheOnly) 1241 return; 1242 1243 // Retrieve screenshot from web-app 1244 BMallocIO buffer; 1245 1246 int32 scaledHeight = scaledWidth * info.Height() / info.Width(); 1247 1248 status_t status = fWebAppInterface.RetrieveScreenshot(info.Code(), 1249 scaledWidth, scaledHeight, &buffer); 1250 if (status == B_OK) { 1251 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 1252 BAutolock locker(&fLock); 1253 package->AddScreenshot(bitmapRef); 1254 locker.Unlock(); 1255 if (screenshotFile.SetTo(screenshotCachePath.Path(), 1256 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 1257 screenshotFile.Write(buffer.Buffer(), buffer.BufferLength()); 1258 } 1259 } else { 1260 fprintf(stderr, "Failed to retrieve screenshot for code '%s' " 1261 "at %" B_PRIi32 "x%" B_PRIi32 ".\n", info.Code().String(), 1262 scaledWidth, scaledHeight); 1263 } 1264 } 1265 1266 1267 bool 1268 Model::_HasNativeIcon(const BMessage& message) const 1269 { 1270 BMessage pkgIcons; 1271 if (message.FindMessage("pkgIcons", &pkgIcons) != B_OK) 1272 return false; 1273 1274 int32 index = 0; 1275 while (true) { 1276 BString name; 1277 name << index++; 1278 1279 BMessage typeCodeInfo; 1280 if (pkgIcons.FindMessage(name, &typeCodeInfo) != B_OK) 1281 break; 1282 1283 BString mediaTypeCode; 1284 if (typeCodeInfo.FindString("mediaTypeCode", &mediaTypeCode) == B_OK 1285 && mediaTypeCode == "application/x-vnd.haiku-icon") { 1286 return true; 1287 } 1288 } 1289 return false; 1290 } 1291 1292 1293 // #pragma mark - listener notification methods 1294 1295 1296 void 1297 Model::_NotifyAuthorizationChanged() 1298 { 1299 for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) { 1300 const ModelListenerRef& listener = fListeners.ItemAtFast(i); 1301 if (listener.Get() != NULL) 1302 listener->AuthorizationChanged(); 1303 } 1304 } 1305 1306