xref: /haiku/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp (revision 9d010ea47db677131e385b5e7855d38fd0c8103f)
1 /*
2  * Copyright 2019, 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 "WebAppInterface.h"
25 
26 
27 #undef B_TRANSLATION_CONTEXT
28 #define B_TRANSLATION_CONTEXT "UserUsageConditions"
29 
30 #define PLACEHOLDER_TEXT "..."
31 
32 #define INTRODUCTION_TEXT_LATEST "HaikuDepot communicates with a " \
33 	"sever component called HaikuDepotServer. These are the latest " \
34 	"usage conditions for use of the HaikuDepotServer service."
35 
36 #define INTRODUCTION_TEXT_USER "HaikuDepot communicates with a " \
37 	"sever component called HaikuDepotServer. These are the usage " \
38 	"conditions that the user '%Nickname%' agreed to at %AgreedToTimestamp% "\
39 	"in relation to the use of the HaikuDepotServer service."
40 
41 #define KEY_USER_USAGE_CONDITIONS	"userUsageConditions"
42 #define KEY_USER_DETAIL				"userDetail"
43 
44 /*!	This is the anticipated number of lines of test that appear in the
45 	introduction.
46 */
47 
48 #define LINES_INTRODUCTION_TEXT 2
49 
50 #define WINDOW_FRAME BRect(0, 0, 500, 400)
51 
52 
53 UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model,
54 	UserUsageConditions& userUsageConditions)
55 	:
56 	BWindow(WINDOW_FRAME, 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 	BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView,
68 		0, false, true, B_PLAIN_BORDER);
69 	BButton* okButton = new BButton("ok", B_TRANSLATE("OK"),
70 		new BMessage(B_QUIT_REQUESTED));
71 
72 	BLayoutBuilder::Group<>(this, B_VERTICAL)
73 		.SetInsets(B_USE_WINDOW_INSETS)
74 		.Add(fVersionStringView, 1)
75 		.Add(scrollView, 97)
76 		.Add(fAgeNoteStringView, 1)
77 		.AddGroup(B_HORIZONTAL, 1)
78 			.AddGlue()
79 			.Add(okButton)
80 			.End()
81 		.End();
82 
83 	CenterOnScreen();
84 
85 	UserDetail userDetail;
86 		// invalid user detail
87 	_DisplayData(userDetail, userUsageConditions);
88 }
89 
90 UserUsageConditionsWindow::UserUsageConditionsWindow(
91 	Model& model, UserUsageConditionsSelectionMode mode)
92 	:
93 	BWindow(WINDOW_FRAME, B_TRANSLATE("Usage conditions"),
94 			B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL,
95 			B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
96 				| B_NOT_RESIZABLE | B_NOT_ZOOMABLE),
97 	fMode(mode),
98 	fModel(model),
99 	fWorkerThread(-1)
100 {
101 	_InitUiControls();
102 
103 	fWorkerIndicator = new BarberPole("fetch data worker indicator");
104 	BSize workerIndicatorSize;
105 	workerIndicatorSize.SetHeight(20);
106 	fWorkerIndicator->SetExplicitMinSize(workerIndicatorSize);
107 
108 	fIntroductionTextView = new BTextView("introduction text view");
109 	fIntroductionTextView->AdoptSystemColors();
110 	fIntroductionTextView->MakeEditable(false);
111 	fIntroductionTextView->MakeSelectable(false);
112 	UserDetail userDetail;
113 	fIntroductionTextView->SetText(_IntroductionTextForMode(mode, userDetail));
114 
115 	BSize introductionSize;
116 	introductionSize.SetHeight(
117 		_ExpectedIntroductionTextHeight(fIntroductionTextView));
118 	fIntroductionTextView->SetExplicitPreferredSize(introductionSize);
119 
120 	BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView,
121 		0, false, true, B_PLAIN_BORDER);
122 	BButton* okButton = new BButton("ok", B_TRANSLATE("OK"),
123 		new BMessage(B_QUIT_REQUESTED));
124 
125 	BLayoutBuilder::Group<>(this, B_VERTICAL)
126 		.SetInsets(B_USE_WINDOW_INSETS)
127 		.Add(fIntroductionTextView, 1)
128 		.AddGlue()
129 		.Add(fVersionStringView, 1)
130 		.Add(scrollView, 95)
131 		.Add(fAgeNoteStringView, 1)
132 		.AddGroup(B_HORIZONTAL, 1)
133 			.AddGlue()
134 			.Add(okButton)
135 			.End()
136 		.Add(fWorkerIndicator, 1)
137 		.End();
138 
139 	CenterOnScreen();
140 
141 	_FetchData();
142 		// start a new thread to pull down the user usage conditions data.
143 }
144 
145 
146 UserUsageConditionsWindow::~UserUsageConditionsWindow()
147 {
148 }
149 
150 
151 /*! This sets up the UI controls / interface elements that are not specific to
152     a given mode of viewing.
153 */
154 
155 void
156 UserUsageConditionsWindow::_InitUiControls()
157 {
158 	fCopyView = new MarkupTextView("copy view");
159 	fCopyView->SetViewUIColor(B_NO_COLOR);
160 	fCopyView->SetLowColor(RGB_COLOR_WHITE);
161 	fCopyView->SetInsets(8.0f);
162 
163 	fAgeNoteStringView = new BStringView("age note string view",
164 		PLACEHOLDER_TEXT);
165 	fAgeNoteStringView->AdoptSystemColors();
166 	fAgeNoteStringView->SetExplicitMaxSize(
167 		BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
168 
169 	BFont versionFont(be_plain_font);
170 	versionFont.SetSize(9.0);
171 
172 	fVersionStringView = new BStringView("version string view",
173 		PLACEHOLDER_TEXT);
174 	fVersionStringView->AdoptSystemColors();
175 	fVersionStringView->SetFont(&versionFont);
176 	fVersionStringView->SetAlignment(B_ALIGN_RIGHT);
177 	fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT);
178 	fVersionStringView->SetExplicitMaxSize(
179 		BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET));
180 }
181 
182 
183 void
184 UserUsageConditionsWindow::MessageReceived(BMessage* message)
185 {
186 	switch (message->what) {
187 		case MSG_USER_USAGE_CONDITIONS_DATA:
188 		{
189 			BMessage userDetailMessage;
190 			BMessage userUsageConditionsMessage;
191 			message->FindMessage(KEY_USER_DETAIL, &userDetailMessage);
192 			message->FindMessage(KEY_USER_USAGE_CONDITIONS,
193 				&userUsageConditionsMessage);
194 			UserDetail userDetail(&userDetailMessage);
195 			UserUsageConditions userUsageConditions(&userUsageConditionsMessage);
196 			_DisplayData(userDetail, userUsageConditions);
197 			fWorkerIndicator->Stop();
198 			break;
199 		}
200 		default:
201 			BWindow::MessageReceived(message);
202 			break;
203 	}
204 }
205 
206 
207 bool
208 UserUsageConditionsWindow::QuitRequested()
209 {
210 	// for now we just don't allow the quit when the background thread
211 	// is processing.  In the future it would be good if the HTTP
212 	// requests were re-organized such that cancellations were easier to
213 	// implement.
214 
215 	if (fWorkerThread == -1)
216 		return true;
217 	if (Logger::IsInfoEnabled()) {
218 		fprintf(stderr, "unable to quit when the user usage "
219 			"conditions window is still fetching data\n");
220 	}
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 for the latest
322 			return B_OK;
323 		case USER:
324 		{
325 			WebAppInterface interface = fModel.GetWebAppInterface();
326 
327 			if (interface.Nickname().IsEmpty())
328 				debugger("attempt to get user details for the current user, but"
329 					" there is no current user");
330 
331 			status_t result = interface.RetrieveCurrentUserDetail(userDetail);
332 
333 			if (result == B_OK) {
334 				BString userUsageConditionsCode = userDetail.Agreement().Code();
335 				if (Logger::IsDebugEnabled()) {
336 					printf("the user [%s] has agreed to uuc [%s]\n",
337 						interface.Nickname().String(),
338 						userUsageConditionsCode.String());
339 				}
340 				code.SetTo(userUsageConditionsCode);
341 			} else {
342 				if (Logger::IsDebugEnabled()) {
343 					printf("unable to get details of the user [%s]\n",
344 						interface.Nickname().String());
345 				}
346 			}
347 
348 			return result;
349 			break;
350 		}
351 		default:
352 			debugger("unhanded mode");
353 			return B_ERROR;
354 	}
355 }
356 
357 
358 void
359 UserUsageConditionsWindow::_NotifyFetchProblem()
360 {
361 	AppUtils::NotifySimpleError(
362 		B_TRANSLATE("Usage conditions download problem"),
363 		B_TRANSLATE("An error has arisen downloading the usage "
364 			"conditions. Check the log for details and try again."));
365 }
366 
367 
368 void
369 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread)
370 {
371 	if (!Lock()) {
372 		if (Logger::IsInfoEnabled())
373 			fprintf(stderr, "failed to lock window\n");
374 	} else {
375 		fWorkerThread = thread;
376 		Unlock();
377 	}
378 }
379 
380 
381 void
382 UserUsageConditionsWindow::_DisplayData(
383 	const UserDetail& userDetail,
384 	const UserUsageConditions& userUsageConditions)
385 {
386 	fCopyView->SetText(userUsageConditions.CopyMarkdown());
387 	fAgeNoteStringView->SetText(_MinimumAgeText(
388 		userUsageConditions.MinimumAge()));
389 	fVersionStringView->SetText(_VersionText(userUsageConditions.Code()));
390 	if (fIntroductionTextView != NULL) {
391 		fIntroductionTextView->SetText(
392 			_IntroductionTextForMode(fMode, userDetail));
393 	}
394 }
395 
396 
397 /*static*/ const BString
398 UserUsageConditionsWindow::_VersionText(const BString& code)
399 {
400 	BString versionText(
401 		B_TRANSLATE("Version %Code%"));
402 	versionText.ReplaceAll("%Code%", code);
403 	return versionText;
404 }
405 
406 
407 /*static*/ const BString
408 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge)
409 {
410 	BString minimumAgeString;
411 	minimumAgeString.SetToFormat("%" B_PRId8, minimumAge);
412 	BString ageNoteText(
413 		B_TRANSLATE("Users are required to be %AgeYears% years of age or "
414 			"older."));
415 	ageNoteText.ReplaceAll("%AgeYears%", minimumAgeString);
416 	return ageNoteText;
417 }
418 
419 
420 /*static*/ const BString
421 UserUsageConditionsWindow::_IntroductionTextForMode(
422 	UserUsageConditionsSelectionMode mode,
423 	const UserDetail& userDetail)
424 {
425 	switch (mode) {
426 		case LATEST:
427 			return B_TRANSLATE(INTRODUCTION_TEXT_LATEST);
428 		case USER:
429 		{
430 			BString nicknamePresentation = PLACEHOLDER_TEXT;
431 			BString agreedToTimestampPresentation = PLACEHOLDER_TEXT;
432 
433 			if (!userDetail.Nickname().IsEmpty())
434 				nicknamePresentation = userDetail.Nickname();
435 
436 			uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed();
437 
438 			if (timestampAgreed > 0) {
439 				agreedToTimestampPresentation =
440 					LocaleUtils::TimestampToDateTimeString(timestampAgreed);
441 			}
442 
443 			BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER);
444 			text.ReplaceAll("%Nickname%", nicknamePresentation);
445 			text.ReplaceAll("%AgreedToTimestamp%",
446 				agreedToTimestampPresentation);
447 			return text;
448 		}
449 		default:
450 			return "???";
451 	}
452 }
453 
454 
455 /*static*/ float
456 UserUsageConditionsWindow::_ExpectedIntroductionTextHeight(
457 	BTextView* introductionTextView)
458 {
459 	float insetTop;
460 	float insetBottom;
461 	introductionTextView->GetInsets(NULL, &insetTop, NULL, &insetBottom);
462 
463 	BSize introductionSize;
464 	font_height fh;
465 	be_plain_font->GetHeight(&fh);
466 	return ((fh.ascent + fh.descent + fh.leading) * LINES_INTRODUCTION_TEXT)
467 		+ insetTop + insetBottom;
468 }
469