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