xref: /haiku/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp (revision 1757f197bd98997256fea2918bfa4603bf81b748)
1 /*
2  * Copyright 2019-2020, 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 #include <TextView.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 "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 	"sever 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 	"sever 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 #define WINDOW_FRAME BRect(0, 0, 500, 400)
52 
53 
54 UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model,
55 	UserUsageConditions& userUsageConditions)
56 	:
57 	BWindow(WINDOW_FRAME, B_TRANSLATE("Usage conditions"),
58 			B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
59 			B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
60 				| B_NOT_RESIZABLE | B_NOT_ZOOMABLE),
61 	fMode(FIXED),
62 	fModel(model),
63 	fIntroductionTextView(NULL),
64 	fWorkerThread(-1)
65 {
66 	_InitUiControls();
67 
68 	BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView,
69 		0, false, true, B_PLAIN_BORDER);
70 	BButton* okButton = new BButton("ok", B_TRANSLATE("OK"),
71 		new BMessage(B_QUIT_REQUESTED));
72 
73 	BLayoutBuilder::Group<>(this, B_VERTICAL)
74 		.SetInsets(B_USE_WINDOW_INSETS)
75 		.Add(fVersionStringView, 1)
76 		.Add(scrollView, 97)
77 		.Add(fAgeNoteStringView, 1)
78 		.AddGroup(B_HORIZONTAL, 1)
79 			.AddGlue()
80 			.Add(okButton)
81 			.End()
82 		.End();
83 
84 	CenterOnScreen();
85 
86 	UserDetail userDetail;
87 		// invalid user detail
88 	_DisplayData(userDetail, userUsageConditions);
89 }
90 
91 UserUsageConditionsWindow::UserUsageConditionsWindow(
92 	Model& model, UserUsageConditionsSelectionMode mode)
93 	:
94 	BWindow(WINDOW_FRAME, B_TRANSLATE("Usage conditions"),
95 			B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
96 			B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
97 				| B_NOT_RESIZABLE | B_NOT_ZOOMABLE),
98 	fMode(mode),
99 	fModel(model),
100 	fWorkerThread(-1)
101 {
102 	_InitUiControls();
103 
104 	fWorkerIndicator = new BarberPole("fetch data worker indicator");
105 	BSize workerIndicatorSize;
106 	workerIndicatorSize.SetHeight(20);
107 	fWorkerIndicator->SetExplicitMinSize(workerIndicatorSize);
108 
109 	fIntroductionTextView = new BTextView("introduction text view");
110 	fIntroductionTextView->AdoptSystemColors();
111 	fIntroductionTextView->MakeEditable(false);
112 	fIntroductionTextView->MakeSelectable(false);
113 	UserDetail userDetail;
114 	fIntroductionTextView->SetText(_IntroductionTextForMode(mode, userDetail));
115 
116 	BSize introductionSize;
117 	introductionSize.SetHeight(
118 		_ExpectedIntroductionTextHeight(fIntroductionTextView));
119 	fIntroductionTextView->SetExplicitPreferredSize(introductionSize);
120 
121 	BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView,
122 		0, false, true, B_PLAIN_BORDER);
123 	BButton* okButton = new BButton("ok", B_TRANSLATE("OK"),
124 		new BMessage(B_QUIT_REQUESTED));
125 
126 	BLayoutBuilder::Group<>(this, B_VERTICAL)
127 		.SetInsets(B_USE_WINDOW_INSETS)
128 		.Add(fIntroductionTextView, 1)
129 		.AddGlue()
130 		.Add(fVersionStringView, 1)
131 		.Add(scrollView, 95)
132 		.Add(fAgeNoteStringView, 1)
133 		.AddGroup(B_HORIZONTAL, 1)
134 			.AddGlue()
135 			.Add(okButton)
136 			.End()
137 		.Add(fWorkerIndicator, 1)
138 		.End();
139 
140 	CenterOnScreen();
141 
142 	_FetchData();
143 		// start a new thread to pull down the user usage conditions data.
144 }
145 
146 
147 UserUsageConditionsWindow::~UserUsageConditionsWindow()
148 {
149 }
150 
151 
152 /*! This sets up the UI controls / interface elements that are not specific to
153     a given mode of viewing.
154 */
155 
156 void
157 UserUsageConditionsWindow::_InitUiControls()
158 {
159 	fCopyView = new MarkupTextView("copy view");
160 	fCopyView->SetViewUIColor(B_NO_COLOR);
161 	fCopyView->SetLowColor(RGB_COLOR_WHITE);
162 	fCopyView->SetInsets(8.0f);
163 
164 	fAgeNoteStringView = new BStringView("age note string view",
165 		PLACEHOLDER_TEXT);
166 	fAgeNoteStringView->AdoptSystemColors();
167 	fAgeNoteStringView->SetExplicitMaxSize(
168 		BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
169 
170 	BFont versionFont(be_plain_font);
171 	versionFont.SetSize(9.0);
172 
173 	fVersionStringView = new BStringView("version string view",
174 		PLACEHOLDER_TEXT);
175 	fVersionStringView->AdoptSystemColors();
176 	fVersionStringView->SetFont(&versionFont);
177 	fVersionStringView->SetAlignment(B_ALIGN_RIGHT);
178 	fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT);
179 	fVersionStringView->SetExplicitMaxSize(
180 		BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
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 	if (Logger::IsInfoEnabled()) {
219 		fprintf(stderr, "unable to quit when the user usage "
220 			"conditions window is still fetching data\n");
221 	}
222 	return false;
223 }
224 
225 
226 /*!	This method is called on the main thread in order to initiate the background
227 	processing to obtain the user usage conditions data.  It will take
228 	responsibility for coordinating the creation of the thread and starting the
229 	thread etc...
230 */
231 
232 void
233 UserUsageConditionsWindow::_FetchData()
234 {
235 	if (-1 != fWorkerThread)
236 		debugger("illegal state - attempt to fetch, but fetch in progress");
237 	thread_id thread = spawn_thread(&_FetchDataThreadEntry,
238 		"Fetch usage conditions data", B_NORMAL_PRIORITY, this);
239 	if (thread >= 0) {
240 		fWorkerIndicator->Start();
241 		_SetWorkerThread(thread);
242 		resume_thread(fWorkerThread);
243 	} else {
244 		debugger("unable to start a thread to fetch the user usage "
245 			"conditions.");
246 	}
247 }
248 
249 
250 /*!	This method is called from the thread in order to start the thread; it is
251 	the entry-point for the background processing to obtain the user usage
252 	conditions.
253 */
254 
255 /*static*/ int32
256 UserUsageConditionsWindow::_FetchDataThreadEntry(void* data)
257 {
258 	UserUsageConditionsWindow* win
259 		= reinterpret_cast<UserUsageConditionsWindow*>(data);
260 	win->_FetchDataPerform();
261 	return 0;
262 }
263 
264 
265 /*!	This method will perform the task of obtaining data about the user usage
266 	conditions.
267 */
268 
269 void
270 UserUsageConditionsWindow::_FetchDataPerform()
271 {
272 	UserDetail userDetail;
273 	UserUsageConditions conditions;
274 	WebAppInterface interface = fModel.GetWebAppInterface();
275 	BString code;
276 	status_t status = _FetchUserUsageConditionsCodePerform(userDetail, code);
277 
278 	if (status == B_OK) {
279 		if (fMode == USER && code.IsEmpty()) {
280 			BString message = B_TRANSLATE(
281 				"The user '%Nickname%' has not agreed to any usage "
282 				"conditions.");
283 			message.ReplaceAll("%Nickname%", userDetail.Nickname());
284 			AppUtils::NotifySimpleError(B_TRANSLATE("No usage conditions"),
285 				message);
286 			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
287 			status = B_BAD_DATA;
288 		}
289 	} else {
290 		_NotifyFetchProblem();
291 		BMessenger(this).SendMessage(B_QUIT_REQUESTED);
292 	}
293 
294 	if (status == B_OK) {
295 		if (interface.RetrieveUserUsageConditions(code, conditions) == B_OK) {
296 			BMessage userUsageConditionsMessage;
297 			BMessage userDetailMessage;
298 			conditions.Archive(&userUsageConditionsMessage, true);
299 			userDetail.Archive(&userDetailMessage, true);
300 			BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA);
301 			dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS,
302 				&userUsageConditionsMessage);
303 			dataMessage.AddMessage(KEY_USER_DETAIL, &userDetailMessage);
304 			BMessenger(this).SendMessage(&dataMessage);
305 		} else {
306 			_NotifyFetchProblem();
307 			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
308 		}
309 	}
310 
311 	_SetWorkerThread(-1);
312 }
313 
314 
315 status_t
316 UserUsageConditionsWindow::_FetchUserUsageConditionsCodePerform(
317 	UserDetail& userDetail, BString& code)
318 {
319 	switch (fMode) {
320 		case LATEST:
321 			code.SetTo("");
322 				// no code in order to get the latest
323 			return B_OK;
324 		case USER:
325 			return _FetchUserUsageConditionsCodeForUserPerform(
326 				userDetail, code);
327 		default:
328 			debugger("unhanded mode");
329 			return B_ERROR;
330 	}
331 }
332 
333 
334 status_t
335 UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform(
336 	UserDetail& userDetail, BString& code)
337 {
338 	WebAppInterface interface = fModel.GetWebAppInterface();
339 
340 	if (interface.Nickname().IsEmpty())
341 		debugger("attempt to get user details for the current user, but"
342 			" there is no current user");
343 
344 	BMessage responseEnvelopeMessage;
345 	status_t result = interface.RetrieveCurrentUserDetail(
346 		responseEnvelopeMessage);
347 
348 	if (result == B_OK) {
349 		// could be an error or could be a valid response envelope
350 		// containing data.
351 		switch (interface.ErrorCodeFromResponse(responseEnvelopeMessage)) {
352 			case ERROR_CODE_NONE:
353 				result = WebAppInterface::UnpackUserDetail(
354 					responseEnvelopeMessage, userDetail);
355 				break;
356 			default:
357 				ServerHelper::NotifyServerJsonRpcError(responseEnvelopeMessage);
358 				result = B_ERROR;
359 					// just any old error to stop
360 				break;
361 		}
362 	} else {
363 		fprintf(stderr, "an error has arisen communicating with the"
364 			" server to obtain data for a user's user usage conditions"
365 			" [%s]\n", strerror(result));
366 		ServerHelper::NotifyTransportError(result);
367 	}
368 
369 	if (result == B_OK) {
370 		BString userUsageConditionsCode = userDetail.Agreement().Code();
371 		if (Logger::IsDebugEnabled()) {
372 			printf("the user [%s] has agreed to uuc [%s]\n",
373 				interface.Nickname().String(),
374 				userUsageConditionsCode.String());
375 		}
376 		code.SetTo(userUsageConditionsCode);
377 	} else {
378 		if (Logger::IsDebugEnabled()) {
379 			printf("unable to get details of the user [%s]\n",
380 				interface.Nickname().String());
381 		}
382 	}
383 
384 	return result;
385 }
386 
387 
388 void
389 UserUsageConditionsWindow::_NotifyFetchProblem()
390 {
391 	AppUtils::NotifySimpleError(
392 		B_TRANSLATE("Usage conditions download problem"),
393 		B_TRANSLATE("An error has arisen downloading the usage "
394 			"conditions. Check the log for details and try again. "
395 			ALERT_MSG_LOGS_USER_GUIDE));
396 }
397 
398 
399 void
400 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread)
401 {
402 	if (!Lock()) {
403 		if (Logger::IsInfoEnabled())
404 			fprintf(stderr, "failed to lock window\n");
405 	} else {
406 		fWorkerThread = thread;
407 		Unlock();
408 	}
409 }
410 
411 
412 void
413 UserUsageConditionsWindow::_DisplayData(
414 	const UserDetail& userDetail,
415 	const UserUsageConditions& userUsageConditions)
416 {
417 	fCopyView->SetText(userUsageConditions.CopyMarkdown());
418 	fAgeNoteStringView->SetText(_MinimumAgeText(
419 		userUsageConditions.MinimumAge()));
420 	fVersionStringView->SetText(_VersionText(userUsageConditions.Code()));
421 	if (fIntroductionTextView != NULL) {
422 		fIntroductionTextView->SetText(
423 			_IntroductionTextForMode(fMode, userDetail));
424 	}
425 }
426 
427 
428 /*static*/ const BString
429 UserUsageConditionsWindow::_VersionText(const BString& code)
430 {
431 	BString versionText(
432 		B_TRANSLATE("Version %Code%"));
433 	versionText.ReplaceAll("%Code%", code);
434 	return versionText;
435 }
436 
437 
438 /*static*/ const BString
439 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge)
440 {
441 	BString minimumAgeString;
442 	minimumAgeString.SetToFormat("%" B_PRId8, minimumAge);
443 	BString ageNoteText(
444 		B_TRANSLATE("Users are required to be %AgeYears% years of age or "
445 			"older."));
446 	ageNoteText.ReplaceAll("%AgeYears%", minimumAgeString);
447 	return ageNoteText;
448 }
449 
450 
451 /*static*/ const BString
452 UserUsageConditionsWindow::_IntroductionTextForMode(
453 	UserUsageConditionsSelectionMode mode,
454 	const UserDetail& userDetail)
455 {
456 	switch (mode) {
457 		case LATEST:
458 			return B_TRANSLATE(INTRODUCTION_TEXT_LATEST);
459 		case USER:
460 		{
461 			BString nicknamePresentation = PLACEHOLDER_TEXT;
462 			BString agreedToTimestampPresentation = PLACEHOLDER_TEXT;
463 
464 			if (!userDetail.Nickname().IsEmpty())
465 				nicknamePresentation = userDetail.Nickname();
466 
467 			uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed();
468 
469 			if (timestampAgreed > 0) {
470 				agreedToTimestampPresentation =
471 					LocaleUtils::TimestampToDateTimeString(timestampAgreed);
472 			}
473 
474 			BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER);
475 			text.ReplaceAll("%Nickname%", nicknamePresentation);
476 			text.ReplaceAll("%AgreedToTimestamp%",
477 				agreedToTimestampPresentation);
478 			return text;
479 		}
480 		default:
481 			return "???";
482 	}
483 }
484 
485 
486 /*static*/ float
487 UserUsageConditionsWindow::_ExpectedIntroductionTextHeight(
488 	BTextView* introductionTextView)
489 {
490 	float insetTop;
491 	float insetBottom;
492 	introductionTextView->GetInsets(NULL, &insetTop, NULL, &insetBottom);
493 
494 	BSize introductionSize;
495 	font_height fh;
496 	be_plain_font->GetHeight(&fh);
497 	return ((fh.ascent + fh.descent + fh.leading) * LINES_INTRODUCTION_TEXT)
498 		+ insetTop + insetBottom;
499 }
500