xref: /haiku/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp (revision 15fb7d88e971c4d6c787c6a3a5c159afb1ebf77b)
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 }
396 
397 
398 void
399 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread)
400 {
401 	if (!Lock()) {
402 		if (Logger::IsInfoEnabled())
403 			fprintf(stderr, "failed to lock window\n");
404 	} else {
405 		fWorkerThread = thread;
406 		Unlock();
407 	}
408 }
409 
410 
411 void
412 UserUsageConditionsWindow::_DisplayData(
413 	const UserDetail& userDetail,
414 	const UserUsageConditions& userUsageConditions)
415 {
416 	fCopyView->SetText(userUsageConditions.CopyMarkdown());
417 	fAgeNoteStringView->SetText(_MinimumAgeText(
418 		userUsageConditions.MinimumAge()));
419 	fVersionStringView->SetText(_VersionText(userUsageConditions.Code()));
420 	if (fIntroductionTextView != NULL) {
421 		fIntroductionTextView->SetText(
422 			_IntroductionTextForMode(fMode, userDetail));
423 	}
424 }
425 
426 
427 /*static*/ const BString
428 UserUsageConditionsWindow::_VersionText(const BString& code)
429 {
430 	BString versionText(
431 		B_TRANSLATE("Version %Code%"));
432 	versionText.ReplaceAll("%Code%", code);
433 	return versionText;
434 }
435 
436 
437 /*static*/ const BString
438 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge)
439 {
440 	BString minimumAgeString;
441 	minimumAgeString.SetToFormat("%" B_PRId8, minimumAge);
442 	BString ageNoteText(
443 		B_TRANSLATE("Users are required to be %AgeYears% years of age or "
444 			"older."));
445 	ageNoteText.ReplaceAll("%AgeYears%", minimumAgeString);
446 	return ageNoteText;
447 }
448 
449 
450 /*static*/ const BString
451 UserUsageConditionsWindow::_IntroductionTextForMode(
452 	UserUsageConditionsSelectionMode mode,
453 	const UserDetail& userDetail)
454 {
455 	switch (mode) {
456 		case LATEST:
457 			return B_TRANSLATE(INTRODUCTION_TEXT_LATEST);
458 		case USER:
459 		{
460 			BString nicknamePresentation = PLACEHOLDER_TEXT;
461 			BString agreedToTimestampPresentation = PLACEHOLDER_TEXT;
462 
463 			if (!userDetail.Nickname().IsEmpty())
464 				nicknamePresentation = userDetail.Nickname();
465 
466 			uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed();
467 
468 			if (timestampAgreed > 0) {
469 				agreedToTimestampPresentation =
470 					LocaleUtils::TimestampToDateTimeString(timestampAgreed);
471 			}
472 
473 			BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER);
474 			text.ReplaceAll("%Nickname%", nicknamePresentation);
475 			text.ReplaceAll("%AgreedToTimestamp%",
476 				agreedToTimestampPresentation);
477 			return text;
478 		}
479 		default:
480 			return "???";
481 	}
482 }
483 
484 
485 /*static*/ float
486 UserUsageConditionsWindow::_ExpectedIntroductionTextHeight(
487 	BTextView* introductionTextView)
488 {
489 	float insetTop;
490 	float insetBottom;
491 	introductionTextView->GetInsets(NULL, &insetTop, NULL, &insetBottom);
492 
493 	BSize introductionSize;
494 	font_height fh;
495 	be_plain_font->GetHeight(&fh);
496 	return ((fh.ascent + fh.descent + fh.leading) * LINES_INTRODUCTION_TEXT)
497 		+ insetTop + insetBottom;
498 }
499