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->Name().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->Name(), term) 239 && !_TextContains(package->Title(), term) 240 && !_TextContains(package->Publisher().Name(), term) 241 && !_TextContains(package->ShortDescription(), term) 242 && !_TextContains(package->FullDescription(), term)) { 243 return false; 244 } 245 } 246 return true; 247 } 248 249 BString SearchTerms() const 250 { 251 BString searchTerms; 252 for (int32 i = 0; i < fSearchTerms.CountItems(); i++) { 253 const BString& term = fSearchTerms.ItemAtFast(i); 254 if (term.IsEmpty()) 255 continue; 256 if (!searchTerms.IsEmpty()) 257 searchTerms.Append(" "); 258 searchTerms.Append(term); 259 } 260 return searchTerms; 261 } 262 263 private: 264 bool _TextContains(BString text, const BString& string) const 265 { 266 text.ToLower(); 267 int32 index = text.FindFirst(string); 268 return index >= 0; 269 } 270 271 private: 272 StringList fSearchTerms; 273 }; 274 275 276 class IsFeaturedFilter : public PackageFilter { 277 public: 278 IsFeaturedFilter() 279 { 280 } 281 282 virtual bool AcceptsPackage(const PackageInfoRef& package) const 283 { 284 return package.Get() != NULL && package->IsProminent(); 285 } 286 }; 287 288 289 static inline bool 290 is_source_package(const PackageInfoRef& package) 291 { 292 const BString& packageName = package->Name(); 293 return packageName.EndsWith("_source"); 294 } 295 296 297 static inline bool 298 is_develop_package(const PackageInfoRef& package) 299 { 300 const BString& packageName = package->Name(); 301 return packageName.EndsWith("_devel"); 302 } 303 304 305 // #pragma mark - Model 306 307 308 Model::Model() 309 : 310 fDepots(), 311 312 fCategoryAudio(new PackageCategory( 313 BitmapRef(), 314 B_TRANSLATE("Audio"), "audio"), true), 315 fCategoryBusiness(new PackageCategory( 316 BitmapRef(), 317 B_TRANSLATE("Business"), "business"), true), 318 fCategoryDevelopment(new PackageCategory( 319 BitmapRef(), 320 B_TRANSLATE("Development"), "development"), true), 321 fCategoryEducation(new PackageCategory( 322 BitmapRef(), 323 B_TRANSLATE("Education"), "education"), true), 324 fCategoryGames(new PackageCategory( 325 BitmapRef(), 326 B_TRANSLATE("Games"), "games"), true), 327 fCategoryGraphics(new PackageCategory( 328 BitmapRef(), 329 B_TRANSLATE("Graphics"), "graphics"), true), 330 fCategoryInternetAndNetwork(new PackageCategory( 331 BitmapRef(), 332 B_TRANSLATE("Internet & Network"), "internetandnetwork"), true), 333 fCategoryProductivity(new PackageCategory( 334 BitmapRef(), 335 B_TRANSLATE("Productivity"), "productivity"), true), 336 fCategoryScienceAndMathematics(new PackageCategory( 337 BitmapRef(), 338 B_TRANSLATE("Science & Mathematics"), "scienceandmathematics"), true), 339 fCategorySystemAndUtilities(new PackageCategory( 340 BitmapRef(), 341 B_TRANSLATE("System & Utilities"), "systemandutilities"), true), 342 fCategoryVideo(new PackageCategory( 343 BitmapRef(), 344 B_TRANSLATE("Video"), "video"), true), 345 346 fCategoryFilter(PackageFilterRef(new AnyFilter(), true)), 347 fDepotFilter(""), 348 fSearchTermsFilter(PackageFilterRef(new AnyFilter(), true)), 349 fIsFeaturedFilter(), 350 351 fShowFeaturedPackages(true), 352 fShowAvailablePackages(true), 353 fShowInstalledPackages(true), 354 fShowSourcePackages(false), 355 fShowDevelopPackages(false), 356 357 fPopulateAllPackagesThread(-1), 358 fStopPopulatingAllPackages(false) 359 { 360 _UpdateIsFeaturedFilter(); 361 362 // Don't forget to add new categories to this list: 363 fCategories.Add(fCategoryGames); 364 fCategories.Add(fCategoryBusiness); 365 fCategories.Add(fCategoryAudio); 366 fCategories.Add(fCategoryVideo); 367 fCategories.Add(fCategoryGraphics); 368 fCategories.Add(fCategoryEducation); 369 fCategories.Add(fCategoryProductivity); 370 fCategories.Add(fCategorySystemAndUtilities); 371 fCategories.Add(fCategoryInternetAndNetwork); 372 fCategories.Add(fCategoryDevelopment); 373 fCategories.Add(fCategoryScienceAndMathematics); 374 // TODO: The server will eventually support an API to 375 // get the defined categories and their translated names. 376 // This should then be used instead of hard-coded 377 // categories and translations in the app. 378 379 fPreferredLanguage = "en"; 380 BLocaleRoster* localeRoster = BLocaleRoster::Default(); 381 if (localeRoster != NULL) { 382 BMessage preferredLanguages; 383 if (localeRoster->GetPreferredLanguages(&preferredLanguages) == B_OK) { 384 BString language; 385 if (preferredLanguages.FindString("language", 0, &language) == B_OK) 386 language.CopyInto(fPreferredLanguage, 0, 2); 387 } 388 } 389 390 // TODO: Fetch this from the web-app. 391 fSupportedLanguages.Add("en"); 392 fSupportedLanguages.Add("de"); 393 fSupportedLanguages.Add("fr"); 394 fSupportedLanguages.Add("ja"); 395 fSupportedLanguages.Add("es"); 396 fSupportedLanguages.Add("zh"); 397 fSupportedLanguages.Add("pt"); 398 fSupportedLanguages.Add("ru"); 399 400 if (!fSupportedLanguages.Contains(fPreferredLanguage)) { 401 // Force the preferred language to one of the currently supported 402 // ones, until the web application supports all ISO language codes. 403 printf("User preferred language '%s' not currently supported, " 404 "defaulting to 'en'.", fPreferredLanguage.String()); 405 fPreferredLanguage = "en"; 406 } 407 fWebAppInterface.SetPreferredLanguage(fPreferredLanguage); 408 } 409 410 411 Model::~Model() 412 { 413 StopPopulatingAllPackages(); 414 } 415 416 417 bool 418 Model::AddListener(const ModelListenerRef& listener) 419 { 420 return fListeners.Add(listener); 421 } 422 423 424 PackageList 425 Model::CreatePackageList() const 426 { 427 // Iterate all packages from all depots. 428 // If configured, restrict depot, filter by search terms, status, name ... 429 PackageList resultList; 430 431 for (int32 i = 0; i < fDepots.CountItems(); i++) { 432 const DepotInfo& depot = fDepots.ItemAtFast(i); 433 434 if (fDepotFilter.Length() > 0 && fDepotFilter != depot.Name()) 435 continue; 436 437 const PackageList& packages = depot.Packages(); 438 439 for (int32 j = 0; j < packages.CountItems(); j++) { 440 const PackageInfoRef& package = packages.ItemAtFast(j); 441 if (fCategoryFilter->AcceptsPackage(package) 442 && fSearchTermsFilter->AcceptsPackage(package) 443 && fIsFeaturedFilter->AcceptsPackage(package) 444 && (fShowAvailablePackages || package->State() != NONE) 445 && (fShowInstalledPackages || package->State() != ACTIVATED) 446 && (fShowSourcePackages || !is_source_package(package)) 447 && (fShowDevelopPackages || !is_develop_package(package))) { 448 resultList.Add(package); 449 } 450 } 451 } 452 453 return resultList; 454 } 455 456 457 bool 458 Model::AddDepot(const DepotInfo& depot) 459 { 460 return fDepots.Add(depot); 461 } 462 463 464 bool 465 Model::HasDepot(const BString& name) const 466 { 467 for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) { 468 if (fDepots.ItemAtFast(i).Name() == name) 469 return true; 470 } 471 return false; 472 } 473 474 475 bool 476 Model::SyncDepot(const DepotInfo& depot) 477 { 478 for (int32 i = fDepots.CountItems() - 1; i >= 0; i--) { 479 const DepotInfo& existingDepot = fDepots.ItemAtFast(i); 480 if (existingDepot.Name() == depot.Name()) { 481 DepotInfo mergedDepot(existingDepot); 482 mergedDepot.SyncPackages(depot.Packages()); 483 fDepots.Replace(i, mergedDepot); 484 return true; 485 } 486 } 487 return false; 488 } 489 490 491 void 492 Model::Clear() 493 { 494 fDepots.Clear(); 495 } 496 497 498 void 499 Model::SetPackageState(const PackageInfoRef& package, PackageState state) 500 { 501 switch (state) { 502 default: 503 case NONE: 504 fInstalledPackages.Remove(package); 505 fActivatedPackages.Remove(package); 506 fUninstalledPackages.Remove(package); 507 break; 508 case INSTALLED: 509 if (!fInstalledPackages.Contains(package)) 510 fInstalledPackages.Add(package); 511 fActivatedPackages.Remove(package); 512 fUninstalledPackages.Remove(package); 513 break; 514 case ACTIVATED: 515 if (!fInstalledPackages.Contains(package)) 516 fInstalledPackages.Add(package); 517 if (!fActivatedPackages.Contains(package)) 518 fActivatedPackages.Add(package); 519 fUninstalledPackages.Remove(package); 520 break; 521 case UNINSTALLED: 522 fInstalledPackages.Remove(package); 523 fActivatedPackages.Remove(package); 524 if (!fUninstalledPackages.Contains(package)) 525 fUninstalledPackages.Add(package); 526 break; 527 } 528 529 package->SetState(state); 530 } 531 532 533 // #pragma mark - filters 534 535 536 void 537 Model::SetCategory(const BString& category) 538 { 539 PackageFilter* filter; 540 541 if (category.Length() == 0) 542 filter = new AnyFilter(); 543 else 544 filter = new CategoryFilter(category); 545 546 fCategoryFilter.SetTo(filter, true); 547 } 548 549 550 BString 551 Model::Category() const 552 { 553 CategoryFilter* filter 554 = dynamic_cast<CategoryFilter*>(fCategoryFilter.Get()); 555 if (filter == NULL) 556 return ""; 557 return filter->Category(); 558 } 559 560 561 void 562 Model::SetDepot(const BString& depot) 563 { 564 fDepotFilter = depot; 565 } 566 567 568 BString 569 Model::Depot() const 570 { 571 return fDepotFilter; 572 } 573 574 575 void 576 Model::SetSearchTerms(const BString& searchTerms) 577 { 578 PackageFilter* filter; 579 580 if (searchTerms.Length() == 0) 581 filter = new AnyFilter(); 582 else 583 filter = new SearchTermsFilter(searchTerms); 584 585 fSearchTermsFilter.SetTo(filter, true); 586 _UpdateIsFeaturedFilter(); 587 } 588 589 590 BString 591 Model::SearchTerms() const 592 { 593 SearchTermsFilter* filter 594 = dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get()); 595 if (filter == NULL) 596 return ""; 597 return filter->SearchTerms(); 598 } 599 600 601 void 602 Model::SetShowFeaturedPackages(bool show) 603 { 604 fShowFeaturedPackages = show; 605 _UpdateIsFeaturedFilter(); 606 } 607 608 609 void 610 Model::SetShowAvailablePackages(bool show) 611 { 612 fShowAvailablePackages = show; 613 } 614 615 616 void 617 Model::SetShowInstalledPackages(bool show) 618 { 619 fShowInstalledPackages = show; 620 } 621 622 623 void 624 Model::SetShowSourcePackages(bool show) 625 { 626 fShowSourcePackages = show; 627 } 628 629 630 void 631 Model::SetShowDevelopPackages(bool show) 632 { 633 fShowDevelopPackages = show; 634 } 635 636 637 // #pragma mark - information retrival 638 639 640 void 641 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) 642 { 643 // TODO: There should probably also be a way to "unpopulate" the 644 // package information. Maybe a cache of populated packages, so that 645 // packages loose their extra information after a certain amount of 646 // time when they have not been accessed/displayed in the UI. Otherwise 647 // HaikuDepot will consume more and more resources in the packages. 648 // Especially screen-shots will be a problem eventually. 649 { 650 BAutolock locker(&fLock); 651 bool alreadyPopulated = fPopulatedPackages.Contains(package); 652 if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated) 653 return; 654 if (!alreadyPopulated) 655 fPopulatedPackages.Add(package); 656 } 657 658 if ((flags & POPULATE_USER_RATINGS) != 0) { 659 // Retrieve info from web-app 660 BMessage info; 661 662 BString packageName; 663 BString architecture; 664 { 665 BAutolock locker(&fLock); 666 packageName = package->Name(); 667 architecture = package->Architecture(); 668 } 669 670 status_t status = fWebAppInterface.RetrieveUserRatings(packageName, 671 architecture, 0, 50, info); 672 if (status == B_OK) { 673 // Parse message 674 BMessage result; 675 BMessage items; 676 if (info.FindMessage("result", &result) == B_OK 677 && result.FindMessage("items", &items) == B_OK) { 678 679 BAutolock locker(&fLock); 680 package->ClearUserRatings(); 681 682 int index = 0; 683 while (true) { 684 BString name; 685 name << index++; 686 687 BMessage item; 688 if (items.FindMessage(name, &item) != B_OK) 689 break; 690 // item.PrintToStream(); 691 692 BString user; 693 BMessage userInfo; 694 if (item.FindMessage("user", &userInfo) != B_OK 695 || userInfo.FindString("nickname", &user) != B_OK) { 696 // Ignore, we need the user name 697 continue; 698 } 699 700 // Extract basic info, all items are optional 701 BString languageCode; 702 BString comment; 703 double rating; 704 item.FindString("naturalLanguageCode", &languageCode); 705 item.FindString("comment", &comment); 706 if (item.FindDouble("rating", &rating) != B_OK) 707 rating = -1; 708 if (comment.Length() == 0 && rating == -1) { 709 // No useful information given. 710 continue; 711 } 712 713 // For which version of the package was the rating? 714 BString major = "?"; 715 BString minor = "?"; 716 BString micro = ""; 717 BMessage version; 718 if (item.FindMessage("pkgVersion", &version) == B_OK) { 719 version.FindString("major", &major); 720 version.FindString("minor", &minor); 721 version.FindString("micro", µ); 722 } 723 BString versionString = major; 724 versionString << "."; 725 versionString << minor; 726 if (micro.Length() > 0) { 727 versionString << "."; 728 versionString << micro; 729 } 730 // Add the rating to the PackageInfo 731 package->AddUserRating( 732 UserRating(UserInfo(user), rating, 733 comment, languageCode, versionString, 0, 0) 734 ); 735 } 736 } else if (info.FindMessage("error", &result) == B_OK) { 737 result.PrintToStream(); 738 } 739 } 740 } 741 742 if ((flags & POPULATE_SCREEN_SHOTS) != 0) { 743 ScreenshotInfoList screenshotInfos; 744 { 745 BAutolock locker(&fLock); 746 screenshotInfos = package->ScreenshotInfos(); 747 package->ClearScreenshots(); 748 } 749 for (int i = 0; i < screenshotInfos.CountItems(); i++) { 750 const ScreenshotInfo& info = screenshotInfos.ItemAtFast(i); 751 _PopulatePackageScreenshot(package, info, 320, false); 752 } 753 } 754 } 755 756 757 void 758 Model::PopulateAllPackages() 759 { 760 StopPopulatingAllPackages(); 761 762 fStopPopulatingAllPackages = false; 763 764 fPopulateAllPackagesThread = spawn_thread(&_PopulateAllPackagesEntry, 765 "Package populator", B_NORMAL_PRIORITY, this); 766 if (fPopulateAllPackagesThread >= 0) 767 resume_thread(fPopulateAllPackagesThread); 768 } 769 770 771 void 772 Model::StopPopulatingAllPackages() 773 { 774 if (fPopulateAllPackagesThread < 0) 775 return; 776 777 fStopPopulatingAllPackages = true; 778 wait_for_thread(fPopulateAllPackagesThread, NULL); 779 fPopulateAllPackagesThread = -1; 780 } 781 782 783 void 784 Model::SetUsername(BString username) 785 { 786 BString password; 787 if (username.Length() > 0) { 788 BPasswordKey key; 789 BKeyStore keyStore; 790 if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, username, 791 key) == B_OK) { 792 password = key.Password(); 793 } else { 794 username = ""; 795 } 796 } 797 SetAuthorization(username, password, false); 798 } 799 800 801 const BString& 802 Model::Username() const 803 { 804 return fWebAppInterface.Username(); 805 } 806 807 808 void 809 Model::SetAuthorization(const BString& username, const BString& password, 810 bool storePassword) 811 { 812 if (storePassword && username.Length() > 0 && password.Length() > 0) { 813 BPasswordKey key(password, B_KEY_PURPOSE_WEB, username); 814 BKeyStore keyStore; 815 keyStore.AddKeyring(kHaikuDepotKeyring); 816 keyStore.AddKey(kHaikuDepotKeyring, key); 817 } 818 819 BAutolock locker(&fLock); 820 fWebAppInterface.SetAuthorization(username, password); 821 822 _NotifyAuthorizationChanged(); 823 } 824 825 826 // #pragma mark - private 827 828 829 void 830 Model::_UpdateIsFeaturedFilter() 831 { 832 if (fShowFeaturedPackages && SearchTerms().IsEmpty()) 833 fIsFeaturedFilter = PackageFilterRef(new IsFeaturedFilter(), true); 834 else 835 fIsFeaturedFilter = PackageFilterRef(new AnyFilter(), true); 836 } 837 838 839 int32 840 Model::_PopulateAllPackagesEntry(void* cookie) 841 { 842 Model* model = static_cast<Model*>(cookie); 843 model->_PopulateAllPackagesThread(true); 844 model->_PopulateAllPackagesThread(false); 845 return 0; 846 } 847 848 849 void 850 Model::_PopulateAllPackagesThread(bool fromCacheOnly) 851 { 852 int32 depotIndex = 0; 853 int32 packageIndex = 0; 854 PackageList bulkPackageList; 855 PackageList packagesWithIconsList; 856 857 while (!fStopPopulatingAllPackages) { 858 // Obtain PackageInfoRef while keeping the depot and package lists 859 // locked. 860 PackageInfoRef package; 861 { 862 BAutolock locker(&fLock); 863 864 if (depotIndex >= fDepots.CountItems()) 865 break; 866 const DepotInfo& depot = fDepots.ItemAt(depotIndex); 867 868 const PackageList& packages = depot.Packages(); 869 if (packageIndex >= packages.CountItems()) { 870 // Need the next depot 871 packageIndex = 0; 872 depotIndex++; 873 continue; 874 } 875 876 package = packages.ItemAt(packageIndex); 877 packageIndex++; 878 } 879 880 if (package.Get() == NULL) 881 continue; 882 883 //_PopulatePackageInfo(package, fromCacheOnly); 884 bulkPackageList.Add(package); 885 if (bulkPackageList.CountItems() == 50) { 886 _PopulatePackageInfos(bulkPackageList, fromCacheOnly, 887 packagesWithIconsList); 888 bulkPackageList.Clear(); 889 } 890 if (fromCacheOnly) 891 _PopulatePackageIcon(package, fromCacheOnly); 892 // TODO: Average user rating. It needs to be shown in the 893 // list view, so without the user clicking the package. 894 } 895 896 if (bulkPackageList.CountItems() > 0) { 897 _PopulatePackageInfos(bulkPackageList, fromCacheOnly, 898 packagesWithIconsList); 899 } 900 901 if (!fromCacheOnly) { 902 for (int i = packagesWithIconsList.CountItems() - 1; i >= 0; i--) { 903 if (fStopPopulatingAllPackages) 904 break; 905 const PackageInfoRef& package = packagesWithIconsList.ItemAtFast(i); 906 printf("Getting/Updating native icon for %s\n", 907 package->Name().String()); 908 _PopulatePackageIcon(package, fromCacheOnly); 909 } 910 } 911 } 912 913 914 bool 915 Model::_GetCacheFile(BPath& path, BFile& file, directory_which directory, 916 const char* relativeLocation, const char* fileName, uint32 openMode) const 917 { 918 if (find_directory(directory, &path) == B_OK 919 && path.Append(relativeLocation) == B_OK 920 && create_directory(path.Path(), 0777) == B_OK 921 && path.Append(fileName) == B_OK) { 922 // Try opening the file which will fail if its 923 // not a file or does not exist. 924 return file.SetTo(path.Path(), openMode) == B_OK; 925 } 926 return false; 927 } 928 929 930 bool 931 Model::_GetCacheFile(BPath& path, BFile& file, directory_which directory, 932 const char* relativeLocation, const char* fileName, 933 bool ignoreAge, time_t maxAge) const 934 { 935 if (!_GetCacheFile(path, file, directory, relativeLocation, fileName, 936 B_READ_ONLY)) { 937 return false; 938 } 939 940 if (ignoreAge) 941 return true; 942 943 time_t modifiedTime; 944 file.GetModificationTime(&modifiedTime); 945 time_t now; 946 time(&now); 947 return now - modifiedTime < maxAge; 948 } 949 950 951 void 952 Model::_PopulatePackageInfos(PackageList& packages, bool fromCacheOnly, 953 PackageList& packagesWithIcons) 954 { 955 if (fStopPopulatingAllPackages) 956 return; 957 958 // See if there are cached info files 959 for (int i = packages.CountItems() - 1; i >= 0; i--) { 960 if (fStopPopulatingAllPackages) 961 return; 962 963 const PackageInfoRef& package = packages.ItemAtFast(i); 964 965 BFile file; 966 BPath path; 967 BString name(package->Name()); 968 name << ".info"; 969 if (_GetCacheFile(path, file, B_USER_CACHE_DIRECTORY, 970 "HaikuDepot", name, fromCacheOnly, 60 * 60)) { 971 // Cache file is recent enough, just use it and return. 972 BMessage pkgInfo; 973 if (pkgInfo.Unflatten(&file) == B_OK) { 974 _PopulatePackageInfo(package, pkgInfo); 975 if (_HasNativeIcon(pkgInfo)) 976 packagesWithIcons.Add(package); 977 packages.Remove(i); 978 } 979 } 980 } 981 982 if (fromCacheOnly || packages.IsEmpty()) 983 return; 984 985 // Retrieve info from web-app 986 BMessage info; 987 988 StringList packageNames; 989 StringList packageArchitectures; 990 for (int i = 0; i < packages.CountItems(); i++) { 991 const PackageInfoRef& package = packages.ItemAtFast(i); 992 packageNames.Add(package->Name()); 993 packageArchitectures.Add(package->Architecture()); 994 } 995 996 status_t status = fWebAppInterface.RetrieveBulkPackageInfo(packageNames, 997 packageArchitectures, info); 998 if (status == B_OK) { 999 // Parse message 1000 // info.PrintToStream(); 1001 BMessage result; 1002 BMessage pkgs; 1003 if (info.FindMessage("result", &result) == B_OK 1004 && result.FindMessage("pkgs", &pkgs) == B_OK) { 1005 int32 index = 0; 1006 while (true) { 1007 if (fStopPopulatingAllPackages) 1008 return; 1009 BString name; 1010 name << index++; 1011 BMessage pkgInfo; 1012 if (pkgs.FindMessage(name, &pkgInfo) != B_OK) 1013 break; 1014 1015 BString pkgName; 1016 if (pkgInfo.FindString("name", &pkgName) != B_OK) 1017 continue; 1018 1019 // Find the PackageInfoRef 1020 bool found = false; 1021 for (int i = 0; i < packages.CountItems(); i++) { 1022 const PackageInfoRef& package = packages.ItemAtFast(i); 1023 if (pkgName == package->Name()) { 1024 _PopulatePackageInfo(package, pkgInfo); 1025 if (_HasNativeIcon(pkgInfo)) 1026 packagesWithIcons.Add(package); 1027 1028 // Store in cache 1029 BFile file; 1030 BPath path; 1031 BString fileName(package->Name()); 1032 fileName << ".info"; 1033 if (_GetCacheFile(path, file, B_USER_CACHE_DIRECTORY, 1034 "HaikuDepot", fileName, 1035 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE)) { 1036 pkgInfo.Flatten(&file); 1037 } 1038 1039 packages.Remove(i); 1040 found = true; 1041 break; 1042 } 1043 } 1044 if (!found) 1045 printf("No matching package for %s\n", pkgName.String()); 1046 } 1047 } 1048 } else { 1049 printf("Error sending request: %s\n", strerror(status)); 1050 int count = packages.CountItems(); 1051 if (count >= 4) { 1052 // Retry in smaller chunks 1053 PackageList firstHalf; 1054 PackageList secondHalf; 1055 for (int i = 0; i < count / 2; i++) 1056 firstHalf.Add(packages.ItemAtFast(i)); 1057 for (int i = count / 2; i < count; i++) 1058 secondHalf.Add(packages.ItemAtFast(i)); 1059 packages.Clear(); 1060 _PopulatePackageInfos(firstHalf, fromCacheOnly, packagesWithIcons); 1061 _PopulatePackageInfos(secondHalf, fromCacheOnly, packagesWithIcons); 1062 } else { 1063 while (packages.CountItems() > 0) { 1064 const PackageInfoRef& package = packages.ItemAtFast(0); 1065 _PopulatePackageInfo(package, fromCacheOnly); 1066 packages.Remove(0); 1067 } 1068 } 1069 } 1070 1071 if (packages.CountItems() > 0) { 1072 for (int i = 0; i < packages.CountItems(); i++) { 1073 const PackageInfoRef& package = packages.ItemAtFast(i); 1074 printf("No package info for %s\n", package->Name().String()); 1075 } 1076 } 1077 } 1078 1079 1080 void 1081 Model::_PopulatePackageInfo(const PackageInfoRef& package, bool fromCacheOnly) 1082 { 1083 if (fromCacheOnly) 1084 return; 1085 1086 // Retrieve info from web-app 1087 BMessage info; 1088 1089 status_t status = fWebAppInterface.RetrievePackageInfo(package->Name(), 1090 package->Architecture(), info); 1091 if (status == B_OK) { 1092 // Parse message 1093 // info.PrintToStream(); 1094 BMessage result; 1095 if (info.FindMessage("result", &result) == B_OK) 1096 _PopulatePackageInfo(package, result); 1097 } 1098 } 1099 1100 1101 static void 1102 append_word_list(BString& words, const char* word) 1103 { 1104 if (words.Length() > 0) 1105 words << ", "; 1106 words << word; 1107 } 1108 1109 1110 void 1111 Model::_PopulatePackageInfo(const PackageInfoRef& package, const BMessage& data) 1112 { 1113 BAutolock locker(&fLock); 1114 1115 BString foundInfo; 1116 1117 BMessage versions; 1118 if (data.FindMessage("versions", &versions) == B_OK) { 1119 // Search a summary and description in the preferred language 1120 int32 index = 0; 1121 while (true) { 1122 BString name; 1123 name << index++; 1124 BMessage version; 1125 if (versions.FindMessage(name, &version) != B_OK) 1126 break; 1127 1128 BString title; 1129 if (version.FindString("title", &title) == B_OK) { 1130 package->SetTitle(title); 1131 append_word_list(foundInfo, "title"); 1132 } 1133 BString summary; 1134 if (version.FindString("summary", &summary) == B_OK) { 1135 package->SetShortDescription(summary); 1136 append_word_list(foundInfo, "summary"); 1137 } 1138 BString description; 1139 if (version.FindString("description", &description) == B_OK) { 1140 package->SetFullDescription(description); 1141 append_word_list(foundInfo, "description"); 1142 } 1143 double payloadLength; 1144 if (version.FindDouble("payloadLength", &payloadLength) == B_OK) { 1145 package->SetSize((int64)payloadLength); 1146 append_word_list(foundInfo, "size"); 1147 } 1148 break; 1149 } 1150 } 1151 1152 BMessage categories; 1153 if (data.FindMessage("pkgCategoryCodes", &categories) == B_OK) { 1154 bool foundCategory = false; 1155 int32 index = 0; 1156 while (true) { 1157 BString name; 1158 name << index++; 1159 BString category; 1160 if (categories.FindString(name, &category) != B_OK) 1161 break; 1162 1163 package->ClearCategories(); 1164 for (int i = fCategories.CountItems() - 1; i >= 0; i--) { 1165 const CategoryRef& categoryRef = fCategories.ItemAtFast(i); 1166 if (categoryRef->Name() == category) { 1167 package->AddCategory(categoryRef); 1168 foundCategory = true; 1169 break; 1170 } 1171 } 1172 } 1173 if (foundCategory) 1174 append_word_list(foundInfo, "categories"); 1175 } 1176 1177 double derivedRating; 1178 if (data.FindDouble("derivedRating", &derivedRating) == B_OK) { 1179 RatingSummary summary; 1180 summary.averageRating = derivedRating; 1181 package->SetRatingSummary(summary); 1182 1183 append_word_list(foundInfo, "rating"); 1184 } 1185 1186 double prominenceOrdering; 1187 if (data.FindDouble("prominenceOrdering", &prominenceOrdering) == B_OK) { 1188 package->SetProminence(prominenceOrdering); 1189 1190 append_word_list(foundInfo, "prominence"); 1191 } 1192 1193 BMessage screenshots; 1194 if (data.FindMessage("pkgScreenshots", &screenshots) == B_OK) { 1195 package->ClearScreenshotInfos(); 1196 bool foundScreenshot = false; 1197 int32 index = 0; 1198 while (true) { 1199 BString name; 1200 name << index++; 1201 1202 BMessage screenshot; 1203 if (screenshots.FindMessage(name, &screenshot) != B_OK) 1204 break; 1205 1206 BString code; 1207 double width; 1208 double height; 1209 double dataSize; 1210 if (screenshot.FindString("code", &code) == B_OK 1211 && screenshot.FindDouble("width", &width) == B_OK 1212 && screenshot.FindDouble("height", &height) == B_OK 1213 && screenshot.FindDouble("length", &dataSize) == B_OK) { 1214 package->AddScreenshotInfo(ScreenshotInfo(code, (int32)width, 1215 (int32)height, (int32)dataSize)); 1216 foundScreenshot = true; 1217 } 1218 } 1219 if (foundScreenshot) 1220 append_word_list(foundInfo, "screenshots"); 1221 } 1222 1223 if (foundInfo.Length() > 0) { 1224 printf("Populated package info for %s: %s\n", 1225 package->Name().String(), foundInfo.String()); 1226 } 1227 1228 // If the user already clicked this package, remove it from the 1229 // list of populated packages, so that clicking it again will 1230 // populate any additional information. 1231 // TODO: Trigger re-populating if the package is currently showing. 1232 fPopulatedPackages.Remove(package); 1233 } 1234 1235 1236 void 1237 Model::_PopulatePackageIcon(const PackageInfoRef& package, bool fromCacheOnly) 1238 { 1239 // See if there is a cached icon file 1240 BFile iconFile; 1241 BPath iconCachePath; 1242 BString iconName(package->Name()); 1243 iconName << ".hvif"; 1244 if (_GetCacheFile(iconCachePath, iconFile, B_USER_CACHE_DIRECTORY, 1245 "HaikuDepot", iconName, fromCacheOnly, 60 * 60)) { 1246 // Cache file is recent enough, just use it and return. 1247 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(iconFile), true); 1248 BAutolock locker(&fLock); 1249 package->SetIcon(bitmapRef); 1250 return; 1251 } 1252 1253 if (fromCacheOnly) 1254 return; 1255 1256 // Retrieve icon from web-app 1257 BMallocIO buffer; 1258 1259 status_t status = fWebAppInterface.RetrievePackageIcon(package->Name(), 1260 &buffer); 1261 if (status == B_OK) { 1262 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 1263 BAutolock locker(&fLock); 1264 package->SetIcon(bitmapRef); 1265 locker.Unlock(); 1266 if (iconFile.SetTo(iconCachePath.Path(), 1267 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 1268 iconFile.Write(buffer.Buffer(), buffer.BufferLength()); 1269 } 1270 } 1271 } 1272 1273 1274 void 1275 Model::_PopulatePackageScreenshot(const PackageInfoRef& package, 1276 const ScreenshotInfo& info, int32 scaledWidth, bool fromCacheOnly) 1277 { 1278 // See if there is a cached screenshot 1279 BFile screenshotFile; 1280 BPath screenshotCachePath; 1281 bool fileExists = false; 1282 BString screenshotName(info.Code()); 1283 screenshotName << "@" << scaledWidth; 1284 screenshotName << ".png"; 1285 time_t modifiedTime; 1286 if (find_directory(B_USER_CACHE_DIRECTORY, &screenshotCachePath) == B_OK 1287 && screenshotCachePath.Append("HaikuDepot/Screenshots") == B_OK 1288 && create_directory(screenshotCachePath.Path(), 0777) == B_OK 1289 && screenshotCachePath.Append(screenshotName) == B_OK) { 1290 // Try opening the file in read-only mode, which will fail if its 1291 // not a file or does not exist. 1292 fileExists = screenshotFile.SetTo(screenshotCachePath.Path(), 1293 B_READ_ONLY) == B_OK; 1294 if (fileExists) 1295 screenshotFile.GetModificationTime(&modifiedTime); 1296 } 1297 1298 if (fileExists) { 1299 time_t now; 1300 time(&now); 1301 if (fromCacheOnly || now - modifiedTime < 60 * 60) { 1302 // Cache file is recent enough, just use it and return. 1303 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile), 1304 true); 1305 BAutolock locker(&fLock); 1306 package->AddScreenshot(bitmapRef); 1307 return; 1308 } 1309 } 1310 1311 if (fromCacheOnly) 1312 return; 1313 1314 // Retrieve screenshot from web-app 1315 BMallocIO buffer; 1316 1317 int32 scaledHeight = scaledWidth * info.Height() / info.Width(); 1318 1319 status_t status = fWebAppInterface.RetrieveScreenshot(info.Code(), 1320 scaledWidth, scaledHeight, &buffer); 1321 if (status == B_OK) { 1322 BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true); 1323 BAutolock locker(&fLock); 1324 package->AddScreenshot(bitmapRef); 1325 locker.Unlock(); 1326 if (screenshotFile.SetTo(screenshotCachePath.Path(), 1327 B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) { 1328 screenshotFile.Write(buffer.Buffer(), buffer.BufferLength()); 1329 } 1330 } else { 1331 fprintf(stderr, "Failed to retrieve screenshot for code '%s' " 1332 "at %" B_PRIi32 "x%" B_PRIi32 ".\n", info.Code().String(), 1333 scaledWidth, scaledHeight); 1334 } 1335 } 1336 1337 1338 bool 1339 Model::_HasNativeIcon(const BMessage& message) const 1340 { 1341 BMessage pkgIcons; 1342 if (message.FindMessage("pkgIcons", &pkgIcons) != B_OK) 1343 return false; 1344 1345 int32 index = 0; 1346 while (true) { 1347 BString name; 1348 name << index++; 1349 1350 BMessage typeCodeInfo; 1351 if (pkgIcons.FindMessage(name, &typeCodeInfo) != B_OK) 1352 break; 1353 1354 BString mediaTypeCode; 1355 if (typeCodeInfo.FindString("mediaTypeCode", &mediaTypeCode) == B_OK 1356 && mediaTypeCode == "application/x-vnd.haiku-icon") { 1357 return true; 1358 } 1359 } 1360 return false; 1361 } 1362 1363 1364 // #pragma mark - listener notification methods 1365 1366 1367 void 1368 Model::_NotifyAuthorizationChanged() 1369 { 1370 for (int32 i = fListeners.CountItems() - 1; i >= 0; i--) { 1371 const ModelListenerRef& listener = fListeners.ItemAtFast(i); 1372 if (listener.Get() != NULL) 1373 listener->AuthorizationChanged(); 1374 } 1375 } 1376 1377