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