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