xref: /haiku/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp (revision e1c4049fed1047bdb957b0529e1921e97ef94770)
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 
53 UserUsageConditionsWindow::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 
97 UserUsageConditionsWindow::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 
152 UserUsageConditionsWindow::~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 
161 void
162 UserUsageConditionsWindow::_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 
185 void
186 UserUsageConditionsWindow::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 
209 bool
210 UserUsageConditionsWindow::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 
231 void
232 UserUsageConditionsWindow::_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
255 UserUsageConditionsWindow::_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 
268 void
269 UserUsageConditionsWindow::_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 
314 status_t
315 UserUsageConditionsWindow::_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 
333 status_t
334 UserUsageConditionsWindow::_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 
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 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
443 UserUsageConditionsWindow::_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