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