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