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