xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision 79d6f0870e70cb72a2bbd7910e05cb531335e9b7)
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", &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