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