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