xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision 4c07199d8201fcf267e90be0d24b76799d03cea6)
1 /*
2  * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>.
4  * Copyright 2016-2023, Andrew Lindesay <apl@lindesay.co.nz>.
5  * All rights reserved. Distributed under the terms of the MIT License.
6  */
7 
8 #include "Model.h"
9 
10 #include <algorithm>
11 #include <ctime>
12 #include <vector>
13 
14 #include <stdarg.h>
15 #include <time.h>
16 
17 #include <Autolock.h>
18 #include <Catalog.h>
19 #include <Directory.h>
20 #include <Entry.h>
21 #include <File.h>
22 #include <KeyStore.h>
23 #include <Locale.h>
24 #include <LocaleRoster.h>
25 #include <Message.h>
26 #include <Path.h>
27 
28 #include "HaikuDepotConstants.h"
29 #include "Logger.h"
30 #include "LocaleUtils.h"
31 #include "StorageUtils.h"
32 
33 
34 #undef B_TRANSLATION_CONTEXT
35 #define B_TRANSLATION_CONTEXT "Model"
36 
37 
38 #define KEY_STORE_IDENTIFIER_PREFIX "hds.password."
39 	// this prefix is added before the nickname in the keystore
40 	// so that HDS username/password pairs can be identified.
41 
42 static const char* kHaikuDepotKeyring = "HaikuDepot";
43 
44 
45 PackageFilter::~PackageFilter()
46 {
47 }
48 
49 
50 ModelListener::~ModelListener()
51 {
52 }
53 
54 
55 // #pragma mark - PackageFilters
56 
57 
58 class AnyFilter : public PackageFilter {
59 public:
60 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
61 	{
62 		return true;
63 	}
64 };
65 
66 
67 class CategoryFilter : public PackageFilter {
68 public:
69 	CategoryFilter(const BString& category)
70 		:
71 		fCategory(category)
72 	{
73 	}
74 
75 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
76 	{
77 		if (!package.IsSet())
78 			return false;
79 
80 		for (int i = package->CountCategories() - 1; i >= 0; i--) {
81 			const CategoryRef& category = package->CategoryAtIndex(i);
82 			if (!category.IsSet())
83 				continue;
84 			if (category->Code() == fCategory)
85 				return true;
86 		}
87 		return false;
88 	}
89 
90 	const BString& Category() const
91 	{
92 		return fCategory;
93 	}
94 
95 private:
96 	BString		fCategory;
97 };
98 
99 
100 class StateFilter : public PackageFilter {
101 public:
102 	StateFilter(PackageState state)
103 		:
104 		fState(state)
105 	{
106 	}
107 
108 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
109 	{
110 		return package->State() == NONE;
111 	}
112 
113 private:
114 	PackageState	fState;
115 };
116 
117 
118 class SearchTermsFilter : public PackageFilter {
119 public:
120 	SearchTermsFilter(const BString& searchTerms)
121 	{
122 		// Separate the string into terms at spaces
123 		int32 index = 0;
124 		while (index < searchTerms.Length()) {
125 			int32 nextSpace = searchTerms.FindFirst(" ", index);
126 			if (nextSpace < 0)
127 				nextSpace = searchTerms.Length();
128 			if (nextSpace > index) {
129 				BString term;
130 				searchTerms.CopyInto(term, index, nextSpace - index);
131 				term.ToLower();
132 				fSearchTerms.Add(term);
133 			}
134 			index = nextSpace + 1;
135 		}
136 	}
137 
138 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
139 	{
140 		if (!package.IsSet())
141 			return false;
142 		// Every search term must be found in one of the package texts
143 		for (int32 i = fSearchTerms.CountStrings() - 1; i >= 0; i--) {
144 			const BString& term = fSearchTerms.StringAt(i);
145 			if (!_TextContains(package->Name(), term)
146 				&& !_TextContains(package->Title(), term)
147 				&& !_TextContains(package->Publisher().Name(), term)
148 				&& !_TextContains(package->ShortDescription(), term)
149 				&& !_TextContains(package->FullDescription(), term)) {
150 				return false;
151 			}
152 		}
153 		return true;
154 	}
155 
156 	BString SearchTerms() const
157 	{
158 		BString searchTerms;
159 		for (int32 i = 0; i < fSearchTerms.CountStrings(); i++) {
160 			const BString& term = fSearchTerms.StringAt(i);
161 			if (term.IsEmpty())
162 				continue;
163 			if (!searchTerms.IsEmpty())
164 				searchTerms.Append(" ");
165 			searchTerms.Append(term);
166 		}
167 		return searchTerms;
168 	}
169 
170 private:
171 	bool _TextContains(BString text, const BString& string) const
172 	{
173 		text.ToLower();
174 		int32 index = text.FindFirst(string);
175 		return index >= 0;
176 	}
177 
178 private:
179 	BStringList fSearchTerms;
180 };
181 
182 
183 static inline bool
184 is_source_package(const PackageInfoRef& package)
185 {
186 	const BString& packageName = package->Name();
187 	return packageName.EndsWith("_source");
188 }
189 
190 
191 static inline bool
192 is_develop_package(const PackageInfoRef& package)
193 {
194 	const BString& packageName = package->Name();
195 	return packageName.EndsWith("_devel")
196 		|| packageName.EndsWith("_debuginfo");
197 }
198 
199 
200 // #pragma mark - Model
201 
202 
203 Model::Model()
204 	:
205 	fDepots(),
206 	fCategories(),
207 	fCategoryFilter(PackageFilterRef(new AnyFilter(), true)),
208 	fDepotFilter(""),
209 	fSearchTermsFilter(PackageFilterRef(new AnyFilter(), true)),
210 	fPackageListViewMode(PROMINENT),
211 	fShowAvailablePackages(true),
212 	fShowInstalledPackages(true),
213 	fShowSourcePackages(false),
214 	fShowDevelopPackages(false),
215 	fCanShareAnonymousUsageData(false)
216 {
217 }
218 
219 
220 Model::~Model()
221 {
222 }
223 
224 
225 LanguageModel*
226 Model::Language()
227 {
228 	return &fLanguageModel;
229 }
230 
231 
232 PackageIconRepository&
233 Model::GetPackageIconRepository()
234 {
235 	return fPackageIconRepository;
236 }
237 
238 
239 status_t
240 Model::InitPackageIconRepository()
241 {
242 	BPath tarPath;
243 	status_t result = IconTarPath(tarPath);
244 	if (result == B_OK)
245 		result = fPackageIconRepository.Init(tarPath);
246 	return result;
247 }
248 
249 
250 void
251 Model::AddListener(const ModelListenerRef& listener)
252 {
253 	fListeners.push_back(listener);
254 }
255 
256 
257 // TODO; part of a wider change; cope with the package being in more than one
258 // depot
259 PackageInfoRef
260 Model::PackageForName(const BString& name)
261 {
262 	std::vector<DepotInfoRef>::iterator it;
263 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
264 		DepotInfoRef depotInfoRef = *it;
265 		PackageInfoRef packageInfoRef = depotInfoRef->PackageByName(name);
266 		if (packageInfoRef.Get() != NULL)
267 			return packageInfoRef;
268 	}
269 	return PackageInfoRef();
270 }
271 
272 
273 bool
274 Model::MatchesFilter(const PackageInfoRef& package) const
275 {
276 	return fCategoryFilter->AcceptsPackage(package)
277 			&& fSearchTermsFilter->AcceptsPackage(package)
278 			&& (fDepotFilter.IsEmpty() || fDepotFilter == package->DepotName())
279 			&& (fShowAvailablePackages || package->State() != NONE)
280 			&& (fShowInstalledPackages || package->State() != ACTIVATED)
281 			&& (fShowSourcePackages || !is_source_package(package))
282 			&& (fShowDevelopPackages || !is_develop_package(package));
283 }
284 
285 
286 void
287 Model::MergeOrAddDepot(const DepotInfoRef& depot)
288 {
289 	BString depotName = depot->Name();
290 	for(uint32 i = 0; i < fDepots.size(); i++) {
291 		if (fDepots[i]->Name() == depotName) {
292 			DepotInfoRef ersatzDepot(new DepotInfo(*(fDepots[i].Get())), true);
293 			ersatzDepot->SyncPackagesFromDepot(depot);
294 			fDepots[i] = ersatzDepot;
295 			return;
296 		}
297 	}
298 	fDepots.push_back(depot);
299 }
300 
301 
302 bool
303 Model::HasDepot(const BString& name) const
304 {
305 	return NULL != DepotForName(name).Get();
306 }
307 
308 
309 const DepotInfoRef
310 Model::DepotForName(const BString& name) const
311 {
312 	std::vector<DepotInfoRef>::const_iterator it;
313 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
314 		DepotInfoRef aDepot = *it;
315 		if (aDepot->Name() == name)
316 			return aDepot;
317 	}
318 	return DepotInfoRef();
319 }
320 
321 
322 int32
323 Model::CountDepots() const
324 {
325 	return fDepots.size();
326 }
327 
328 
329 DepotInfoRef
330 Model::DepotAtIndex(int32 index) const
331 {
332 	return fDepots[index];
333 }
334 
335 
336 bool
337 Model::HasAnyProminentPackages()
338 {
339 	std::vector<DepotInfoRef>::iterator it;
340 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
341 		DepotInfoRef aDepot = *it;
342 		if (aDepot->HasAnyProminentPackages())
343 			return true;
344 	}
345 	return false;
346 }
347 
348 
349 void
350 Model::Clear()
351 {
352 	GetPackageIconRepository().Clear();
353 	fDepots.clear();
354 	fPopulatedPackageNames.MakeEmpty();
355 }
356 
357 
358 void
359 Model::SetStateForPackagesByName(BStringList& packageNames, PackageState state)
360 {
361 	for (int32 i = 0; i < packageNames.CountStrings(); i++) {
362 		BString packageName = packageNames.StringAt(i);
363 		PackageInfoRef packageInfo = PackageForName(packageName);
364 
365 		if (packageInfo.IsSet()) {
366 			packageInfo->SetState(state);
367 			HDINFO("did update package [%s] with state [%s]",
368 				packageName.String(), package_state_to_string(state));
369 		}
370 		else {
371 			HDINFO("was unable to find package [%s] so was not possible to set"
372 				" the state to [%s]", packageName.String(),
373 				package_state_to_string(state));
374 		}
375 	}
376 }
377 
378 
379 // #pragma mark - filters
380 
381 
382 void
383 Model::SetCategory(const BString& category)
384 {
385 	PackageFilter* filter;
386 
387 	if (category.Length() == 0)
388 		filter = new AnyFilter();
389 	else
390 		filter = new CategoryFilter(category);
391 
392 	fCategoryFilter.SetTo(filter, true);
393 }
394 
395 
396 BString
397 Model::Category() const
398 {
399 	CategoryFilter* filter
400 		= dynamic_cast<CategoryFilter*>(fCategoryFilter.Get());
401 	if (filter == NULL)
402 		return "";
403 	return filter->Category();
404 }
405 
406 
407 void
408 Model::SetDepot(const BString& depot)
409 {
410 	fDepotFilter = depot;
411 }
412 
413 
414 BString
415 Model::Depot() const
416 {
417 	return fDepotFilter;
418 }
419 
420 
421 void
422 Model::SetSearchTerms(const BString& searchTerms)
423 {
424 	PackageFilter* filter;
425 
426 	if (searchTerms.Length() == 0)
427 		filter = new AnyFilter();
428 	else
429 		filter = new SearchTermsFilter(searchTerms);
430 
431 	fSearchTermsFilter.SetTo(filter, true);
432 }
433 
434 
435 BString
436 Model::SearchTerms() const
437 {
438 	SearchTermsFilter* filter
439 		= dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get());
440 	if (filter == NULL)
441 		return "";
442 	return filter->SearchTerms();
443 }
444 
445 
446 void
447 Model::SetPackageListViewMode(package_list_view_mode mode)
448 {
449 	fPackageListViewMode = mode;
450 }
451 
452 
453 void
454 Model::SetCanShareAnonymousUsageData(bool value)
455 {
456 	fCanShareAnonymousUsageData = value;
457 }
458 
459 
460 void
461 Model::SetShowAvailablePackages(bool show)
462 {
463 	fShowAvailablePackages = show;
464 }
465 
466 
467 void
468 Model::SetShowInstalledPackages(bool show)
469 {
470 	fShowInstalledPackages = show;
471 }
472 
473 
474 void
475 Model::SetShowSourcePackages(bool show)
476 {
477 	fShowSourcePackages = show;
478 }
479 
480 
481 void
482 Model::SetShowDevelopPackages(bool show)
483 {
484 	fShowDevelopPackages = show;
485 }
486 
487 
488 // #pragma mark - information retrieval
489 
490 /*!	It may transpire that the package has no corresponding record on the
491 	server side because the repository is not represented in the server.
492 	In such a case, there is little point in communicating with the server
493 	only to hear back that the package does not exist.
494 */
495 
496 bool
497 Model::CanPopulatePackage(const PackageInfoRef& package)
498 {
499 	const BString& depotName = package->DepotName();
500 
501 	if (depotName.IsEmpty())
502 		return false;
503 
504 	const DepotInfoRef& depot = DepotForName(depotName);
505 
506 	if (depot.Get() == NULL)
507 		return false;
508 
509 	return !depot->WebAppRepositoryCode().IsEmpty();
510 }
511 
512 
513 /*! Initially only superficial data is loaded from the server into the data
514     model of the packages.  When the package is viewed, additional data needs
515     to be populated including ratings.  This method takes care of that.
516 */
517 
518 void
519 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags)
520 {
521 	HDTRACE("will populate package for [%s]", package->Name().String());
522 
523 	if (!CanPopulatePackage(package)) {
524 		HDINFO("unable to populate package [%s]", package->Name().String());
525 		return;
526 	}
527 
528 	// TODO: There should probably also be a way to "unpopulate" the
529 	// package information. Maybe a cache of populated packages, so that
530 	// packages loose their extra information after a certain amount of
531 	// time when they have not been accessed/displayed in the UI. Otherwise
532 	// HaikuDepot will consume more and more resources in the packages.
533 	// Especially screen-shots will be a problem eventually.
534 	{
535 		BAutolock locker(&fLock);
536 		bool alreadyPopulated = fPopulatedPackageNames.HasString(
537 			package->Name());
538 		if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated)
539 			return;
540 		if (!alreadyPopulated)
541 			fPopulatedPackageNames.Add(package->Name());
542 	}
543 
544 	if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) {
545 		_PopulatePackageChangelog(package);
546 	}
547 
548 	if ((flags & POPULATE_USER_RATINGS) != 0) {
549 		// Retrieve info from web-app
550 		BMessage info;
551 
552 		BString packageName;
553 		BString webAppRepositoryCode;
554 		BString webAppRepositorySourceCode;
555 
556 		{
557 			BAutolock locker(&fLock);
558 			packageName = package->Name();
559 			const DepotInfo* depot = DepotForName(package->DepotName());
560 
561 			if (depot != NULL) {
562 				webAppRepositoryCode = depot->WebAppRepositoryCode();
563 				webAppRepositorySourceCode
564 					= depot->WebAppRepositorySourceCode();
565 			}
566 		}
567 
568 		status_t status = fWebAppInterface
569 			.RetrieveUserRatingsForPackageForDisplay(packageName,
570 				webAppRepositoryCode, webAppRepositorySourceCode, 0,
571 				PACKAGE_INFO_MAX_USER_RATINGS, info);
572 		if (status == B_OK) {
573 			// Parse message
574 			BMessage result;
575 			BMessage items;
576 			if (info.FindMessage("result", &result) == B_OK
577 				&& result.FindMessage("items", &items) == B_OK) {
578 
579 				BAutolock locker(&fLock);
580 				package->ClearUserRatings();
581 
582 				int32 index = 0;
583 				while (true) {
584 					BString name;
585 					name << index++;
586 
587 					BMessage item;
588 					if (items.FindMessage(name, &item) != B_OK)
589 						break;
590 
591 					BString code;
592 					if (item.FindString("code", &code) != B_OK) {
593 						HDERROR("corrupt user rating at index %" B_PRIi32,
594 							index);
595 						continue;
596 					}
597 
598 					BString user;
599 					BMessage userInfo;
600 					if (item.FindMessage("user", &userInfo) != B_OK
601 							|| userInfo.FindString("nickname", &user) != B_OK) {
602 						HDERROR("ignored user rating [%s] without a user "
603 							"nickname", code.String());
604 						continue;
605 					}
606 
607 					// Extract basic info, all items are optional
608 					BString languageCode;
609 					BString comment;
610 					double rating;
611 					item.FindString("naturalLanguageCode", &languageCode);
612 					item.FindString("comment", &comment);
613 					if (item.FindDouble("rating", &rating) != B_OK)
614 						rating = -1;
615 					if (comment.Length() == 0 && rating == -1) {
616 						HDERROR("rating [%s] has no comment or rating so will"
617 							" be ignored", code.String());
618 						continue;
619 					}
620 
621 					// For which version of the package was the rating?
622 					BString major = "?";
623 					BString minor = "?";
624 					BString micro = "";
625 					double revision = -1;
626 					BString architectureCode = "";
627 					BMessage version;
628 					if (item.FindMessage("pkgVersion", &version) == B_OK) {
629 						version.FindString("major", &major);
630 						version.FindString("minor", &minor);
631 						version.FindString("micro", &micro);
632 						version.FindDouble("revision", &revision);
633 						version.FindString("architectureCode",
634 							&architectureCode);
635 					}
636 					BString versionString = major;
637 					versionString << ".";
638 					versionString << minor;
639 					if (!micro.IsEmpty()) {
640 						versionString << ".";
641 						versionString << micro;
642 					}
643 					if (revision > 0) {
644 						versionString << "-";
645 						versionString << (int) revision;
646 					}
647 
648 					if (!architectureCode.IsEmpty()) {
649 						versionString << " " << STR_MDASH << " ";
650 						versionString << architectureCode;
651 					}
652 
653 					double createTimestamp;
654 					item.FindDouble("createTimestamp", &createTimestamp);
655 
656 					// Add the rating to the PackageInfo
657 					UserRatingRef userRating(new UserRating(
658 						UserInfo(user), rating,
659 						comment, languageCode, versionString,
660 						(uint64) createTimestamp), true);
661 					package->AddUserRating(userRating);
662 					HDDEBUG("rating [%s] retrieved from server", code.String());
663 				}
664 				HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]",
665 						index - 1, packageName.String());
666 			} else {
667 				BString message;
668 				message.SetToFormat("failure to retrieve user ratings for [%s]",
669 					packageName.String());
670 				_MaybeLogJsonRpcError(info, message.String());
671 			}
672 		} else
673 			HDERROR("unable to retrieve user ratings");
674 	}
675 
676 	if ((flags & POPULATE_SCREEN_SHOTS) != 0) {
677 		std::vector<ScreenshotInfoRef> screenshotInfos;
678 		{
679 			BAutolock locker(&fLock);
680 			for (int32 i = 0; i < package->CountScreenshotInfos(); i++)
681 				screenshotInfos.push_back(package->ScreenshotInfoAtIndex(i));
682 			package->ClearScreenshots();
683 		}
684 		std::vector<ScreenshotInfoRef>::iterator it;
685 		for (it = screenshotInfos.begin(); it != screenshotInfos.end(); it++) {
686 			const ScreenshotInfoRef& info = *it;
687 			_PopulatePackageScreenshot(package, info, 320, false);
688 		}
689 	}
690 }
691 
692 
693 void
694 Model::_PopulatePackageChangelog(const PackageInfoRef& package)
695 {
696 	BMessage info;
697 	BString packageName;
698 
699 	{
700 		BAutolock locker(&fLock);
701 		packageName = package->Name();
702 	}
703 
704 	status_t status = fWebAppInterface.GetChangelog(packageName, info);
705 
706 	if (status == B_OK) {
707 		// Parse message
708 		BMessage result;
709 		BString content;
710 		if (info.FindMessage("result", &result) == B_OK) {
711 			if (result.FindString("content", &content) == B_OK
712 				&& 0 != content.Length()) {
713 				BAutolock locker(&fLock);
714 				package->SetChangelog(content);
715 				HDDEBUG("changelog populated for [%s]", packageName.String());
716 			} else
717 				HDDEBUG("no changelog present for [%s]", packageName.String());
718 		} else
719 			_MaybeLogJsonRpcError(info, "populate package changelog");
720 	} else {
721 		HDERROR("unable to obtain the changelog for the package [%s]",
722 			packageName.String());
723 	}
724 }
725 
726 
727 static void
728 model_remove_key_for_user(const BString& nickname)
729 {
730 	if (nickname.IsEmpty())
731 		return;
732 	BKeyStore keyStore;
733 	BPasswordKey key;
734 	BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
735 		<< nickname;
736 	status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
737 			passwordIdentifier, key);
738 
739 	switch (result) {
740 		case B_OK:
741 			result = keyStore.RemoveKey(kHaikuDepotKeyring, key);
742 			if (result != B_OK) {
743 				HDERROR("error occurred when removing password for nickname "
744 					"[%s] : %s", nickname.String(), strerror(result));
745 			}
746 			break;
747 		case B_ENTRY_NOT_FOUND:
748 			return;
749 		default:
750 			HDERROR("error occurred when finding password for nickname "
751 				"[%s] : %s", nickname.String(), strerror(result));
752 			break;
753 	}
754 }
755 
756 
757 void
758 Model::SetNickname(BString nickname)
759 {
760 	BString password;
761 	BString existingNickname = Nickname();
762 
763 	// this happens when the user is logging out.  Best to remove the password
764 	// stored for the existing user since it is no longer required.
765 
766 	if (!existingNickname.IsEmpty() && nickname.IsEmpty())
767 		model_remove_key_for_user(existingNickname);
768 
769 	if (nickname.Length() > 0) {
770 		BPasswordKey key;
771 		BKeyStore keyStore;
772 		BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
773 			<< nickname;
774 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
775 				passwordIdentifier, key) == B_OK) {
776 			password = key.Password();
777 		}
778 		if (password.IsEmpty())
779 			nickname = "";
780 	}
781 
782 	SetCredentials(nickname, password, false);
783 }
784 
785 
786 const BString&
787 Model::Nickname()
788 {
789 	return fWebAppInterface.Nickname();
790 }
791 
792 
793 void
794 Model::SetCredentials(const BString& nickname, const BString& passwordClear,
795 	bool storePassword)
796 {
797 	BString existingNickname = Nickname();
798 
799 	if (storePassword) {
800 		// no point continuing to store the password for the previous user.
801 
802 		if (!existingNickname.IsEmpty())
803 			model_remove_key_for_user(existingNickname);
804 
805 		// adding a key that is already there does not seem to override the
806 		// existing key so the old key needs to be removed first.
807 
808 		if (!nickname.IsEmpty())
809 			model_remove_key_for_user(nickname);
810 
811 		if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) {
812 			BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
813 				<< nickname;
814 			BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier);
815 			BKeyStore keyStore;
816 			keyStore.AddKeyring(kHaikuDepotKeyring);
817 			keyStore.AddKey(kHaikuDepotKeyring, key);
818 		}
819 	}
820 
821 	BAutolock locker(&fLock);
822 	fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear));
823 
824 	if (nickname != existingNickname)
825 		_NotifyAuthorizationChanged();
826 }
827 
828 
829 /*! When bulk repository data comes down from the server, it will
830     arrive as a json.gz payload.  This is stored locally as a cache
831     and this method will provide the on-disk storage location for
832     this file.
833 */
834 
835 status_t
836 Model::DumpExportRepositoryDataPath(BPath& path)
837 {
838 	BString leaf;
839 	leaf.SetToFormat("repository-all_%s.json.gz",
840 		Language()->PreferredLanguage()->Code());
841 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
842 }
843 
844 
845 /*! When the system downloads reference data (eg; categories) from the server
846     then the downloaded data is stored and cached at the path defined by this
847     method.
848 */
849 
850 status_t
851 Model::DumpExportReferenceDataPath(BPath& path)
852 {
853 	BString leaf;
854 	leaf.SetToFormat("reference-all_%s.json.gz",
855 		Language()->PreferredLanguage()->Code());
856 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
857 }
858 
859 
860 status_t
861 Model::IconTarPath(BPath& path) const
862 {
863 	return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path);
864 }
865 
866 
867 status_t
868 Model::DumpExportPkgDataPath(BPath& path,
869 	const BString& repositorySourceCode)
870 {
871 	BString leaf;
872 	leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
873 		Language()->PreferredLanguage()->Code());
874 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
875 }
876 
877 
878 void
879 Model::_PopulatePackageScreenshot(const PackageInfoRef& package,
880 	const ScreenshotInfoRef& info, int32 scaledWidth, bool fromCacheOnly)
881 {
882 	// See if there is a cached screenshot
883 	BFile screenshotFile;
884 	BPath screenshotCachePath;
885 
886 	status_t result = StorageUtils::LocalWorkingDirectoryPath(
887 		"Screenshots", screenshotCachePath);
888 
889 	if (result != B_OK) {
890 		HDERROR("unable to get the screenshot dir - unable to proceed");
891 		return;
892 	}
893 
894 	bool fileExists = false;
895 	BString screenshotName(info->Code());
896 	screenshotName << "@" << scaledWidth;
897 	screenshotName << ".png";
898 	time_t modifiedTime;
899 	if (screenshotCachePath.Append(screenshotName) == B_OK) {
900 		// Try opening the file in read-only mode, which will fail if its
901 		// not a file or does not exist.
902 		fileExists = screenshotFile.SetTo(screenshotCachePath.Path(),
903 			B_READ_ONLY) == B_OK;
904 		if (fileExists)
905 			screenshotFile.GetModificationTime(&modifiedTime);
906 	}
907 
908 	if (fileExists) {
909 		time_t now;
910 		time(&now);
911 		if (fromCacheOnly || now - modifiedTime < 60 * 60) {
912 			// Cache file is recent enough, just use it and return.
913 			BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile),
914 				true);
915 			BAutolock locker(&fLock);
916 			package->AddScreenshot(bitmapRef);
917 			return;
918 		}
919 	}
920 
921 	if (fromCacheOnly)
922 		return;
923 
924 	// Retrieve screenshot from web-app
925 	BMallocIO buffer;
926 
927 	int32 scaledHeight = scaledWidth * info->Height() / info->Width();
928 
929 	status_t status = fWebAppInterface.RetrieveScreenshot(info->Code(),
930 		scaledWidth, scaledHeight, &buffer);
931 	if (status == B_OK) {
932 		BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true);
933 		BAutolock locker(&fLock);
934 		package->AddScreenshot(bitmapRef);
935 		locker.Unlock();
936 		if (screenshotFile.SetTo(screenshotCachePath.Path(),
937 				B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) {
938 			screenshotFile.Write(buffer.Buffer(), buffer.BufferLength());
939 		}
940 	} else {
941 		HDERROR("Failed to retrieve screenshot for code '%s' "
942 			"at %" B_PRIi32 "x%" B_PRIi32 ".", info->Code().String(),
943 			scaledWidth, scaledHeight);
944 	}
945 }
946 
947 
948 // #pragma mark - listener notification methods
949 
950 
951 void
952 Model::_NotifyAuthorizationChanged()
953 {
954 	std::vector<ModelListenerRef>::const_iterator it;
955 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
956 		const ModelListenerRef& listener = *it;
957 		if (listener.IsSet())
958 			listener->AuthorizationChanged();
959 	}
960 }
961 
962 
963 void
964 Model::_NotifyCategoryListChanged()
965 {
966 	std::vector<ModelListenerRef>::const_iterator it;
967 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
968 		const ModelListenerRef& listener = *it;
969 		if (listener.IsSet())
970 			listener->CategoryListChanged();
971 	}
972 }
973 
974 
975 void
976 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
977 	const char *sourceDescription) const
978 {
979 	BMessage error;
980 	BString errorMessage;
981 	double errorCode;
982 
983 	if (responsePayload.FindMessage("error", &error) == B_OK
984 		&& error.FindString("message", &errorMessage) == B_OK
985 		&& error.FindDouble("code", &errorCode) == B_OK) {
986 		HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
987 			errorMessage.String(), errorCode);
988 	} else
989 		HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
990 }
991 
992 
993 // #pragma mark - Rating Stabilities
994 
995 
996 int32
997 Model::CountRatingStabilities() const
998 {
999 	return fRatingStabilities.size();
1000 }
1001 
1002 
1003 RatingStabilityRef
1004 Model::RatingStabilityByCode(BString& code) const
1005 {
1006 	std::vector<RatingStabilityRef>::const_iterator it;
1007 	for (it = fRatingStabilities.begin(); it != fRatingStabilities.end();
1008 			it++) {
1009 		RatingStabilityRef aRatingStability = *it;
1010 		if (aRatingStability->Code() == code)
1011 			return aRatingStability;
1012 	}
1013 	return RatingStabilityRef();
1014 }
1015 
1016 
1017 RatingStabilityRef
1018 Model::RatingStabilityAtIndex(int32 index) const
1019 {
1020 	return fRatingStabilities[index];
1021 }
1022 
1023 
1024 void
1025 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values)
1026 {
1027 	std::vector<RatingStabilityRef>::const_iterator it;
1028 	for (it = values.begin(); it != values.end(); it++)
1029 		_AddRatingStability(*it);
1030 }
1031 
1032 
1033 void
1034 Model::_AddRatingStability(const RatingStabilityRef& value)
1035 {
1036 	std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst
1037 		= std::lower_bound(
1038 			fRatingStabilities.begin(),
1039 			fRatingStabilities.end(),
1040 			value,
1041 			&IsRatingStabilityBefore);
1042 	std::vector<RatingStabilityRef>::iterator itInsertionPt =
1043 		fRatingStabilities.begin()
1044 			+ (itInsertionPtConst - fRatingStabilities.begin());
1045 
1046 	if (itInsertionPt != fRatingStabilities.end()
1047 		&& (*itInsertionPt)->Code() == value->Code()) {
1048 		itInsertionPt = fRatingStabilities.erase(itInsertionPt);
1049 			// replace the one with the same code.
1050 	}
1051 
1052 	fRatingStabilities.insert(itInsertionPt, value);
1053 }
1054 
1055 
1056 // #pragma mark - Categories
1057 
1058 
1059 int32
1060 Model::CountCategories() const
1061 {
1062 	return fCategories.size();
1063 }
1064 
1065 
1066 CategoryRef
1067 Model::CategoryByCode(BString& code) const
1068 {
1069 	std::vector<CategoryRef>::const_iterator it;
1070 	for (it = fCategories.begin(); it != fCategories.end(); it++) {
1071 		CategoryRef aCategory = *it;
1072 		if (aCategory->Code() == code)
1073 			return aCategory;
1074 	}
1075 	return CategoryRef();
1076 }
1077 
1078 
1079 CategoryRef
1080 Model::CategoryAtIndex(int32 index) const
1081 {
1082 	return fCategories[index];
1083 }
1084 
1085 
1086 void
1087 Model::AddCategories(std::vector<CategoryRef>& values)
1088 {
1089 	std::vector<CategoryRef>::iterator it;
1090 	for (it = values.begin(); it != values.end(); it++)
1091 		_AddCategory(*it);
1092 	_NotifyCategoryListChanged();
1093 }
1094 
1095 /*! This will insert the category in order.
1096  */
1097 
1098 void
1099 Model::_AddCategory(const CategoryRef& category)
1100 {
1101 	std::vector<CategoryRef>::const_iterator itInsertionPtConst
1102 		= std::lower_bound(
1103 			fCategories.begin(),
1104 			fCategories.end(),
1105 			category,
1106 			&IsPackageCategoryBefore);
1107 	std::vector<CategoryRef>::iterator itInsertionPt =
1108 		fCategories.begin() + (itInsertionPtConst - fCategories.begin());
1109 
1110 	if (itInsertionPt != fCategories.end()
1111 		&& (*itInsertionPt)->Code() == category->Code()) {
1112 		itInsertionPt = fCategories.erase(itInsertionPt);
1113 			// replace the one with the same code.
1114 	}
1115 
1116 	fCategories.insert(itInsertionPt, category);
1117 }
1118