1/*
2 * Copyright 2019-2021, Andrew Lindesay <apl@lindesay.co.nz>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5
6#include "UserUsageConditionsWindow.h"
7
8#include <Button.h>
9#include <Catalog.h>
10#include <Font.h>
11#include <LayoutBuilder.h>
12#include <ScrollView.h>
13#include <StringFormat.h>
14#include <StringView.h>
15
16#include "AppUtils.h"
17#include "BarberPole.h"
18#include "HaikuDepotConstants.h"
19#include "LocaleUtils.h"
20#include "Logger.h"
21#include "MarkupTextView.h"
22#include "Model.h"
23#include "UserUsageConditions.h"
24#include "ServerHelper.h"
25#include "TextView.h"
26#include "WebAppInterface.h"
27
28
29#undef B_TRANSLATION_CONTEXT
30#define B_TRANSLATION_CONTEXT "UserUsageConditions"
31
32#define PLACEHOLDER_TEXT "..."
33
34#define INTRODUCTION_TEXT_LATEST "HaikuDepot communicates with a " \
35	"server component called HaikuDepotServer. These are the latest " \
36	"usage conditions for use of the HaikuDepotServer service."
37
38#define INTRODUCTION_TEXT_USER "HaikuDepot communicates with a " \
39	"server component called HaikuDepotServer. These are the usage " \
40	"conditions that the user '%Nickname%' agreed to at %AgreedToTimestamp% "\
41	"in relation to the use of the HaikuDepotServer service."
42
43#define KEY_USER_USAGE_CONDITIONS	"userUsageConditions"
44#define KEY_USER_DETAIL				"userDetail"
45
46/*!	This is the anticipated number of lines of test that appear in the
47	introduction.
48*/
49
50#define LINES_INTRODUCTION_TEXT 2
51
52
53UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model,
54	UserUsageConditions& userUsageConditions)
55	:
56	BWindow(BRect(), B_TRANSLATE("Usage conditions"),
57			B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
58			B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
59				| B_NOT_RESIZABLE | B_NOT_ZOOMABLE),
60	fMode(FIXED),
61	fModel(model),
62	fIntroductionTextView(NULL),
63	fWorkerThread(-1)
64{
65	_InitUiControls();
66
67	font_height fontHeight;
68	be_plain_font->GetHeight(&fontHeight);
69	const float lineHeight = fontHeight.ascent + fontHeight.descent;
70
71	BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView,
72		0, false, true, B_PLAIN_BORDER);
73	scrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight * 6));
74	BButton* okButton = new BButton("ok", B_TRANSLATE("OK"),
75		new BMessage(B_QUIT_REQUESTED));
76
77	BLayoutBuilder::Group<>(this, B_VERTICAL)
78		.SetInsets(B_USE_WINDOW_INSETS)
79		.Add(fVersionStringView, 1)
80		.Add(scrollView, 97)
81		.Add(fAgeNoteStringView, 1)
82		.AddGroup(B_HORIZONTAL, 1)
83			.AddGlue()
84			.Add(okButton)
85			.End()
86		.End();
87
88	GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET));
89	ResizeToPreferred();
90	CenterOnScreen();
91
92	UserDetail userDetail;
93		// invalid user detail
94	_DisplayData(userDetail, userUsageConditions);
95}
96
97UserUsageConditionsWindow::UserUsageConditionsWindow(
98	Model& model, UserUsageConditionsSelectionMode mode)
99	:
100	BWindow(BRect(), B_TRANSLATE("Usage conditions"),
101			B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
102			B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
103				| B_NOT_RESIZABLE | B_NOT_ZOOMABLE),
104	fMode(mode),
105	fModel(model),
106	fWorkerThread(-1)
107{
108	_InitUiControls();
109
110	font_height fontHeight;
111	be_plain_font->GetHeight(&fontHeight);
112	const float lineHeight = fontHeight.ascent + fontHeight.descent;
113
114	fWorkerIndicator = new BarberPole("fetch data worker indicator");
115	BSize workerIndicatorSize;
116	workerIndicatorSize.SetHeight(lineHeight);
117	fWorkerIndicator->SetExplicitSize(workerIndicatorSize);
118
119	fIntroductionTextView = new TextView("introduction text view");
120	UserDetail userDetail;
121	fIntroductionTextView->SetText(_IntroductionTextForMode(mode, userDetail));
122
123	BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView,
124		0, false, true, B_PLAIN_BORDER);
125	scrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight * 6));
126	BButton* okButton = new BButton("ok", B_TRANSLATE("OK"),
127		new BMessage(B_QUIT_REQUESTED));
128
129	BLayoutBuilder::Group<>(this, B_VERTICAL)
130		.SetInsets(B_USE_WINDOW_INSETS)
131		.Add(fIntroductionTextView, 1)
132		.AddGlue()
133		.Add(fVersionStringView, 1)
134		.Add(scrollView, 95)
135		.Add(fAgeNoteStringView, 1)
136		.AddGroup(B_HORIZONTAL, 1)
137			.AddGlue()
138			.Add(okButton)
139			.End()
140		.Add(fWorkerIndicator, 1)
141		.End();
142
143	GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET));
144	ResizeToPreferred();
145	CenterOnScreen();
146
147	_FetchData();
148		// start a new thread to pull down the user usage conditions data.
149}
150
151
152UserUsageConditionsWindow::~UserUsageConditionsWindow()
153{
154}
155
156
157/*! This sets up the UI controls / interface elements that are not specific to
158    a given mode of viewing.
159*/
160
161void
162UserUsageConditionsWindow::_InitUiControls()
163{
164	fCopyView = new MarkupTextView("copy view");
165	fCopyView->SetViewUIColor(B_NO_COLOR);
166	fCopyView->SetLowColor(RGB_COLOR_WHITE);
167	fCopyView->SetInsets(8.0f);
168
169	fAgeNoteStringView = new BStringView("age note string view",
170		PLACEHOLDER_TEXT);
171	fAgeNoteStringView->AdoptSystemColors();
172
173	BFont versionFont(be_plain_font);
174	versionFont.SetSize(versionFont.Size() * 0.75f);
175
176	fVersionStringView = new BStringView("version string view",
177		PLACEHOLDER_TEXT);
178	fVersionStringView->AdoptSystemColors();
179	fVersionStringView->SetFont(&versionFont);
180	fVersionStringView->SetAlignment(B_ALIGN_RIGHT);
181	fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT);
182}
183
184
185void
186UserUsageConditionsWindow::MessageReceived(BMessage* message)
187{
188	switch (message->what) {
189		case MSG_USER_USAGE_CONDITIONS_DATA:
190		{
191			BMessage userDetailMessage;
192			BMessage userUsageConditionsMessage;
193			message->FindMessage(KEY_USER_DETAIL, &userDetailMessage);
194			message->FindMessage(KEY_USER_USAGE_CONDITIONS,
195				&userUsageConditionsMessage);
196			UserDetail userDetail(&userDetailMessage);
197			UserUsageConditions userUsageConditions(&userUsageConditionsMessage);
198			_DisplayData(userDetail, userUsageConditions);
199			fWorkerIndicator->Stop();
200			break;
201		}
202		default:
203			BWindow::MessageReceived(message);
204			break;
205	}
206}
207
208
209bool
210UserUsageConditionsWindow::QuitRequested()
211{
212	// for now we just don't allow the quit when the background thread
213	// is processing.  In the future it would be good if the HTTP
214	// requests were re-organized such that cancellations were easier to
215	// implement.
216
217	if (fWorkerThread == -1)
218		return true;
219	HDINFO("unable to quit when the user usage "
220		"conditions window is still fetching data");
221	return false;
222}
223
224
225/*!	This method is called on the main thread in order to initiate the background
226	processing to obtain the user usage conditions data.  It will take
227	responsibility for coordinating the creation of the thread and starting the
228	thread etc...
229*/
230
231void
232UserUsageConditionsWindow::_FetchData()
233{
234	if (-1 != fWorkerThread)
235		debugger("illegal state - attempt to fetch, but fetch in progress");
236	thread_id thread = spawn_thread(&_FetchDataThreadEntry,
237		"Fetch usage conditions data", B_NORMAL_PRIORITY, this);
238	if (thread >= 0) {
239		fWorkerIndicator->Start();
240		_SetWorkerThread(thread);
241		resume_thread(fWorkerThread);
242	} else {
243		debugger("unable to start a thread to fetch the user usage "
244			"conditions.");
245	}
246}
247
248
249/*!	This method is called from the thread in order to start the thread; it is
250	the entry-point for the background processing to obtain the user usage
251	conditions.
252*/
253
254/*static*/ int32
255UserUsageConditionsWindow::_FetchDataThreadEntry(void* data)
256{
257	UserUsageConditionsWindow* win
258		= reinterpret_cast<UserUsageConditionsWindow*>(data);
259	win->_FetchDataPerform();
260	return 0;
261}
262
263
264/*!	This method will perform the task of obtaining data about the user usage
265	conditions.
266*/
267
268void
269UserUsageConditionsWindow::_FetchDataPerform()
270{
271	UserDetail userDetail;
272	UserUsageConditions conditions;
273	WebAppInterface* interface = fModel.GetWebAppInterface();
274	BString code;
275	status_t status = _FetchUserUsageConditionsCodePerform(userDetail, code);
276
277	if (status == B_OK) {
278		if (fMode == USER && code.IsEmpty()) {
279			BString message = B_TRANSLATE(
280				"The user '%Nickname%' has not agreed to any usage "
281				"conditions.");
282			message.ReplaceAll("%Nickname%", userDetail.Nickname());
283			AppUtils::NotifySimpleError(B_TRANSLATE("No usage conditions"),
284				message);
285			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
286			status = B_BAD_DATA;
287		}
288	} else {
289		_NotifyFetchProblem();
290		BMessenger(this).SendMessage(B_QUIT_REQUESTED);
291	}
292
293	if (status == B_OK) {
294		if (interface->RetrieveUserUsageConditions(code, conditions) == B_OK) {
295			BMessage userUsageConditionsMessage;
296			BMessage userDetailMessage;
297			conditions.Archive(&userUsageConditionsMessage, true);
298			userDetail.Archive(&userDetailMessage, true);
299			BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA);
300			dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS,
301				&userUsageConditionsMessage);
302			dataMessage.AddMessage(KEY_USER_DETAIL, &userDetailMessage);
303			BMessenger(this).SendMessage(&dataMessage);
304		} else {
305			_NotifyFetchProblem();
306			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
307		}
308	}
309
310	_SetWorkerThread(-1);
311}
312
313
314status_t
315UserUsageConditionsWindow::_FetchUserUsageConditionsCodePerform(
316	UserDetail& userDetail, BString& code)
317{
318	switch (fMode) {
319		case LATEST:
320			code.SetTo("");
321				// no code in order to get the latest
322			return B_OK;
323		case USER:
324			return _FetchUserUsageConditionsCodeForUserPerform(
325				userDetail, code);
326		default:
327			debugger("unhanded mode");
328			return B_ERROR;
329	}
330}
331
332
333status_t
334UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform(
335	UserDetail& userDetail, BString& code)
336{
337	WebAppInterface* interface = fModel.GetWebAppInterface();
338
339	if (interface->Nickname().IsEmpty())
340		debugger("attempt to get user details for the current user, but"
341			" there is no current user");
342
343	BMessage responseEnvelopeMessage;
344	status_t result = interface->RetrieveCurrentUserDetail(responseEnvelopeMessage);
345
346	if (result == B_OK) {
347		// could be an error or could be a valid response envelope
348		// containing data.
349		switch (WebAppInterface::ErrorCodeFromResponse(responseEnvelopeMessage)) {
350			case ERROR_CODE_NONE:
351				result = WebAppInterface::UnpackUserDetail(
352					responseEnvelopeMessage, userDetail);
353				break;
354			default:
355				ServerHelper::NotifyServerJsonRpcError(responseEnvelopeMessage);
356				result = B_ERROR;
357					// just any old error to stop
358				break;
359		}
360	} else {
361		HDERROR("an error has arisen communicating with the"
362			" server to obtain data for a user's user usage conditions"
363			" [%s]", strerror(result));
364		ServerHelper::NotifyTransportError(result);
365	}
366
367	if (result == B_OK) {
368		BString userUsageConditionsCode = userDetail.Agreement().Code();
369		HDDEBUG("the user [%s] has agreed to uuc [%s]",
370			interface->Nickname().String(),
371			userUsageConditionsCode.String());
372		code.SetTo(userUsageConditionsCode);
373	} else {
374		HDDEBUG("unable to get details of the user [%s]",
375			interface->Nickname().String());
376	}
377
378	return result;
379}
380
381
382void
383UserUsageConditionsWindow::_NotifyFetchProblem()
384{
385	AppUtils::NotifySimpleError(
386		B_TRANSLATE("Usage conditions download problem"),
387		B_TRANSLATE("An error has arisen downloading the usage "
388			"conditions. Check the log for details and try again. "
389			ALERT_MSG_LOGS_USER_GUIDE));
390}
391
392
393void
394UserUsageConditionsWindow::_SetWorkerThread(thread_id thread)
395{
396	if (!Lock())
397		HDERROR("failed to lock window");
398	else {
399		fWorkerThread = thread;
400		Unlock();
401	}
402}
403
404
405void
406UserUsageConditionsWindow::_DisplayData(
407	const UserDetail& userDetail,
408	const UserUsageConditions& userUsageConditions)
409{
410	fCopyView->SetText(userUsageConditions.CopyMarkdown());
411	fAgeNoteStringView->SetText(_MinimumAgeText(
412		userUsageConditions.MinimumAge()));
413	fVersionStringView->SetText(_VersionText(userUsageConditions.Code()));
414	if (fIntroductionTextView != NULL) {
415		fIntroductionTextView->SetText(
416			_IntroductionTextForMode(fMode, userDetail));
417	}
418}
419
420
421/*static*/ const BString
422UserUsageConditionsWindow::_VersionText(const BString& code)
423{
424	BString versionText(
425		B_TRANSLATE("Version %Code%"));
426	versionText.ReplaceAll("%Code%", code);
427	return versionText;
428}
429
430
431/*static*/ const BString
432UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge)
433{
434	BString ageNoteText;
435	static BStringFormat formatText(B_TRANSLATE("Users are required to be "
436		"{0, plural, one{# year of age} other{# years of age}} or older."));
437	formatText.Format(ageNoteText, minimumAge);
438	return ageNoteText;
439}
440
441
442/*static*/ const BString
443UserUsageConditionsWindow::_IntroductionTextForMode(
444	UserUsageConditionsSelectionMode mode,
445	const UserDetail& userDetail)
446{
447	switch (mode) {
448		case LATEST:
449			return B_TRANSLATE(INTRODUCTION_TEXT_LATEST);
450		case USER:
451		{
452			BString nicknamePresentation = PLACEHOLDER_TEXT;
453			BString agreedToTimestampPresentation = PLACEHOLDER_TEXT;
454
455			if (!userDetail.Nickname().IsEmpty())
456				nicknamePresentation = userDetail.Nickname();
457
458			uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed();
459
460			if (timestampAgreed > 0) {
461				agreedToTimestampPresentation =
462					LocaleUtils::TimestampToDateTimeString(timestampAgreed);
463			}
464
465			BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER);
466			text.ReplaceAll("%Nickname%", nicknamePresentation);
467			text.ReplaceAll("%AgreedToTimestamp%",
468				agreedToTimestampPresentation);
469			return text;
470		}
471		default:
472			return "???";
473	}
474}
475