1/*
2 * Copyright 2020, Andrew Lindesay <apl@lindesay.co.nz>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5
6#include "ToLatestUserUsageConditionsWindow.h"
7
8#include <Alert.h>
9#include <Autolock.h>
10#include <AutoLocker.h>
11#include <Button.h>
12#include <Catalog.h>
13#include <CheckBox.h>
14#include <LayoutBuilder.h>
15#include <Locker.h>
16#include <SeparatorView.h>
17
18#include "AppUtils.h"
19#include "LinkView.h"
20#include "LocaleUtils.h"
21#include "Logger.h"
22#include "Model.h"
23#include "UserUsageConditionsWindow.h"
24#include "ServerHelper.h"
25#include "WebAppInterface.h"
26#include "TextView.h"
27
28#undef B_TRANSLATION_CONTEXT
29#define B_TRANSLATION_CONTEXT "ToLatestUserUsageConditionsWindow"
30
31#define PLACEHOLDER_TEXT B_UTF8_ELLIPSIS
32
33#define KEY_USER_USAGE_CONDITIONS	"userUsageConditions"
34
35#define NO_PRIOR_MESSAGE_TEXT "The user [%Nickname%] has authenticated, but " \
36	"before proceeding, you are required to agree to the most recent usage " \
37	"conditions."
38
39#define PRIOR_MESSAGE_TEXT "The user \"%Nickname%\" has previously agreed to " \
40	"usage conditions, but the usage conditions have been updated since. " \
41	"The updated usage conditions now need to be agreed to."
42
43enum {
44	MSG_AGREE =									'agre',
45	MSG_AGREE_FAILED =							'agfa',
46	MSG_AGREE_MINIMUM_AGE_TOGGLE =				'amat',
47	MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE =	'auct'
48};
49
50
51ToLatestUserUsageConditionsWindow::ToLatestUserUsageConditionsWindow(
52	BWindow* parent,
53	Model& model, const UserDetail& userDetail)
54	:
55	BWindow(BRect(), B_TRANSLATE("Update usage conditions"),
56		B_FLOATING_WINDOW_LOOK, B_MODAL_SUBSET_WINDOW_FEEL,
57		B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS
58			| B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_NOT_CLOSABLE),
59	fModel(model),
60	fUserDetail(userDetail),
61	fWorkerThread(-1),
62	fQuitRequestedDuringWorkerThread(false),
63	fMutableControlsEnabled(false)
64{
65	AddToSubset(parent);
66	_InitUiControls();
67
68	// some layout magic happening here.  If the checkboxes are put directly
69	// into the main vertical-group then the window tries to shrink to the
70	// preferred size of the checkboxes.  To avoid this, a grid is used to house
71	// the checkboxes and a fake extra grid column is added to the right of the
72	// checkboxes which prevents the window from reducing in size to meet the
73	// checkboxes.
74
75	BLayoutBuilder::Group<>(this, B_VERTICAL)
76		.AddGrid()
77			.Add(fMessageTextView, 0, 0, 2)
78			.Add(fConfirmMinimumAgeCheckBox, 0, 1, 1)
79			.Add(fConfirmUserUsageConditionsCheckBox, 0, 2, 1)
80			.Add(fUserUsageConditionsLink, 0, 3, 2)
81		.End()
82		.Add(new BSeparatorView(B_HORIZONTAL))
83			// rule off
84		.AddGroup(B_HORIZONTAL, 1)
85			.AddGlue()
86			.Add(fLogoutButton)
87			.Add(fAgreeButton)
88		.End()
89		.Add(fWorkerIndicator, 1)
90		.SetInsets(B_USE_WINDOW_INSETS);
91
92	GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET));
93	ResizeToPreferred();
94	CenterOnScreen();
95
96	_FetchData();
97		// start a new thread to pull down the user usage conditions data.
98}
99
100
101ToLatestUserUsageConditionsWindow::~ToLatestUserUsageConditionsWindow()
102{
103	BAutolock locker(&fLock);
104
105	if (fWorkerThread >= 0)
106		wait_for_thread(fWorkerThread, NULL);
107}
108
109
110void
111ToLatestUserUsageConditionsWindow::_InitUiControls()
112{
113	fMessageTextView = new TextView("message text view");
114	BString message;
115	if (fUserDetail.Agreement().Code().IsEmpty())
116		message = B_TRANSLATE(NO_PRIOR_MESSAGE_TEXT);
117	else
118		message = B_TRANSLATE(PRIOR_MESSAGE_TEXT);
119	message.ReplaceAll("%Nickname%", fUserDetail.Nickname());
120	fMessageTextView->SetText(message);
121
122	fConfirmMinimumAgeCheckBox = new BCheckBox("confirm minimum age",
123		PLACEHOLDER_TEXT,
124			// is filled in when the user usage conditions data is available
125		new BMessage(MSG_AGREE_MINIMUM_AGE_TOGGLE));
126
127	fConfirmUserUsageConditionsCheckBox = new BCheckBox(
128		"confirm usage conditions",
129		B_TRANSLATE("I agree to the usage conditions"),
130		new BMessage(MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE));
131
132	fUserUsageConditionsLink = new LinkView("usage conditions view",
133		B_TRANSLATE("View the usage conditions"),
134		new BMessage(MSG_VIEW_LATEST_USER_USAGE_CONDITIONS));
135	fUserUsageConditionsLink->SetTarget(this);
136
137	fLogoutButton = new BButton("logout", B_TRANSLATE("Logout"),
138		new BMessage(MSG_LOG_OUT));
139	fAgreeButton = new BButton("agree", B_TRANSLATE("Agree"),
140		new BMessage(MSG_AGREE));
141
142	fWorkerIndicator = new BarberPole("fetch data worker indicator");
143	BSize workerIndicatorSize;
144	workerIndicatorSize.SetHeight(20);
145	fWorkerIndicator->SetExplicitSize(workerIndicatorSize);
146
147	fMutableControlsEnabled = false;
148	_EnableMutableControls();
149}
150
151
152void
153ToLatestUserUsageConditionsWindow::_EnableMutableControls()
154{
155	bool ageChecked = fConfirmMinimumAgeCheckBox->Value() == 1;
156	bool conditionsChecked = fConfirmUserUsageConditionsCheckBox->Value() == 1;
157	fUserUsageConditionsLink->SetEnabled(fMutableControlsEnabled);
158	fAgreeButton->SetEnabled(fMutableControlsEnabled && ageChecked
159		&& conditionsChecked);
160	fLogoutButton->SetEnabled(fMutableControlsEnabled);
161	fConfirmUserUsageConditionsCheckBox->SetEnabled(fMutableControlsEnabled);
162	fConfirmMinimumAgeCheckBox->SetEnabled(fMutableControlsEnabled);
163}
164
165
166void
167ToLatestUserUsageConditionsWindow::MessageReceived(BMessage* message)
168{
169	switch (message->what) {
170		case MSG_USER_USAGE_CONDITIONS_DATA:
171		{
172			BMessage userUsageConditionsMessage;
173			message->FindMessage(KEY_USER_USAGE_CONDITIONS,
174				&userUsageConditionsMessage);
175			UserUsageConditions userUsageConditions(
176				&userUsageConditionsMessage);
177			_DisplayData(userUsageConditions);
178			fWorkerIndicator->Stop();
179			break;
180		}
181		case MSG_LOG_OUT:
182			_HandleLogout();
183			break;
184		case MSG_AGREE:
185			_HandleAgree();
186			break;
187		case MSG_AGREE_FAILED:
188			_HandleAgreeFailed();
189			break;
190		case MSG_VIEW_LATEST_USER_USAGE_CONDITIONS:
191			_HandleViewUserUsageConditions();
192			break;
193		case MSG_AGREE_MINIMUM_AGE_TOGGLE:
194		case MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE:
195			_EnableMutableControls();
196			break;
197		default:
198			BWindow::MessageReceived(message);
199			break;
200	}
201}
202
203
204bool
205ToLatestUserUsageConditionsWindow::QuitRequested()
206{
207	BAutolock locker(&fLock);
208
209	if (fWorkerThread >= 0) {
210		if (Logger::IsDebugEnabled())
211			HDINFO("quit requested while worker thread is operating -- will "
212				"try again once the worker thread has completed");
213		fQuitRequestedDuringWorkerThread = true;
214		return false;
215	}
216
217	return true;
218}
219
220
221void
222ToLatestUserUsageConditionsWindow::_SetWorkerThread(thread_id thread)
223{
224	if (thread >= 0) {
225		fWorkerThread = thread;
226		resume_thread(fWorkerThread);
227	} else {
228		fWorkerThread = -1;
229		if (fQuitRequestedDuringWorkerThread)
230			BMessenger(this).SendMessage(B_QUIT_REQUESTED);
231		fQuitRequestedDuringWorkerThread = false;
232	}
233}
234
235
236void
237ToLatestUserUsageConditionsWindow::_SetWorkerThreadLocked(thread_id thread)
238{
239	BAutolock locker(&fLock);
240	_SetWorkerThread(thread);
241}
242
243
244/*!	This method is called on the main thread in order to initiate the background
245	processing to obtain the user usage conditions data.  It will take
246	responsibility for coordinating the creation of the thread and starting the
247	thread etc...
248*/
249
250void
251ToLatestUserUsageConditionsWindow::_FetchData()
252{
253	{
254		BAutolock locker(&fLock);
255		if (-1 != fWorkerThread) {
256			debugger("illegal state - attempt to fetch, but thread in "
257				"progress");
258		}
259	}
260
261	thread_id thread = spawn_thread(&_FetchDataThreadEntry,
262		"Fetch usage conditions data", B_NORMAL_PRIORITY, this);
263	if (thread >= 0) {
264		fWorkerIndicator->Start();
265		_SetWorkerThreadLocked(thread);
266	} else {
267		debugger("unable to start a thread to fetch the user usage "
268			"conditions.");
269	}
270}
271
272
273/*!	This method is called from the thread; it is
274	the entry-point for the background processing to obtain the user usage
275	conditions.
276*/
277
278/*static*/ int32
279ToLatestUserUsageConditionsWindow::_FetchDataThreadEntry(void* data)
280{
281	ToLatestUserUsageConditionsWindow* win
282		= reinterpret_cast<ToLatestUserUsageConditionsWindow*>(data);
283	win->_FetchDataPerform();
284	return 0;
285}
286
287
288/*!	This method will perform the task of obtaining data about the user usage
289	conditions.
290*/
291
292void
293ToLatestUserUsageConditionsWindow::_FetchDataPerform()
294{
295	UserUsageConditions conditions;
296	WebAppInterface* interface = fModel.GetWebAppInterface();
297
298	if (interface->RetrieveUserUsageConditions("", conditions) == B_OK) {
299		BMessage userUsageConditionsMessage;
300		conditions.Archive(&userUsageConditionsMessage, true);
301		BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA);
302		dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS,
303			&userUsageConditionsMessage);
304		BMessenger(this).SendMessage(&dataMessage);
305	} else {
306		_NotifyFetchProblem();
307		BMessenger(this).SendMessage(B_QUIT_REQUESTED);
308	}
309
310	_SetWorkerThreadLocked(-1);
311}
312
313
314void
315ToLatestUserUsageConditionsWindow::_NotifyFetchProblem()
316{
317	AppUtils::NotifySimpleError(
318		B_TRANSLATE("Usage conditions download problem"),
319		B_TRANSLATE("An error has arisen downloading the usage "
320			"conditions. Check the log for details and try again. "
321			ALERT_MSG_LOGS_USER_GUIDE));
322}
323
324
325void
326ToLatestUserUsageConditionsWindow::_Agree()
327{
328	{
329		BAutolock locker(&fLock);
330		if (-1 != fWorkerThread) {
331			debugger("illegal state - attempt to agree, but thread in "
332				"progress");
333		}
334	}
335
336	fMutableControlsEnabled = false;
337	_EnableMutableControls();
338	thread_id thread = spawn_thread(&_AgreeThreadEntry,
339		"Agree usage conditions", B_NORMAL_PRIORITY, this);
340	if (thread >= 0) {
341		fWorkerIndicator->Start();
342		_SetWorkerThreadLocked(thread);
343	} else {
344		debugger("unable to start a thread to fetch the user usage "
345			"conditions.");
346	}
347}
348
349
350/*static*/ int32
351ToLatestUserUsageConditionsWindow::_AgreeThreadEntry(void* data)
352{
353	ToLatestUserUsageConditionsWindow* win
354		= reinterpret_cast<ToLatestUserUsageConditionsWindow*>(data);
355	win->_AgreePerform();
356	return 0;
357}
358
359
360void
361ToLatestUserUsageConditionsWindow::_AgreePerform()
362{
363	BMessenger messenger(this);
364	BMessage responsePayload;
365	WebAppInterface* webApp = fModel.GetWebAppInterface();
366	status_t result = webApp->AgreeUserUsageConditions(
367		fUserUsageConditions.Code(), responsePayload);
368
369	if (result != B_OK) {
370		ServerHelper::NotifyTransportError(result);
371		messenger.SendMessage(MSG_AGREE_FAILED);
372	} else {
373		int32 errorCode = WebAppInterface::ErrorCodeFromResponse(responsePayload);
374		if (errorCode == ERROR_CODE_NONE) {
375			AppUtils::NotifySimpleError(
376				B_TRANSLATE("Usage conditions agreed"),
377				B_TRANSLATE("The current usage conditions have been agreed "
378					"to."));
379			messenger.SendMessage(B_QUIT_REQUESTED);
380		}
381		else {
382			AutoLocker<BLocker> locker(fModel.Lock());
383			ServerHelper::NotifyServerJsonRpcError(responsePayload);
384			messenger.SendMessage(MSG_AGREE_FAILED);
385		}
386	}
387
388	_SetWorkerThreadLocked(-1);
389}
390
391
392void
393ToLatestUserUsageConditionsWindow::_HandleAgreeFailed()
394{
395	fWorkerIndicator->Stop();
396	fMutableControlsEnabled = true;
397	_EnableMutableControls();
398}
399
400
401void
402ToLatestUserUsageConditionsWindow::_DisplayData(
403	const UserUsageConditions& userUsageConditions)
404{
405	fUserUsageConditions = userUsageConditions;
406	fConfirmMinimumAgeCheckBox->SetLabel(
407		LocaleUtils::CreateTranslatedIAmMinimumAgeSlug(
408			fUserUsageConditions.MinimumAge()));
409	fMutableControlsEnabled = true;
410	_EnableMutableControls();
411}
412
413
414void
415ToLatestUserUsageConditionsWindow::_HandleViewUserUsageConditions()
416{
417	if (!fUserUsageConditions.Code().IsEmpty()) {
418		UserUsageConditionsWindow* window = new UserUsageConditionsWindow(
419			fModel, fUserUsageConditions);
420		window->Show();
421	}
422}
423
424void
425ToLatestUserUsageConditionsWindow::_HandleLogout()
426{
427	AutoLocker<BLocker> locker(fModel.Lock());
428	fModel.SetNickname("");
429	BMessenger(this).SendMessage(B_QUIT_REQUESTED);
430}
431
432
433void
434ToLatestUserUsageConditionsWindow::_HandleAgree()
435{
436	bool ageChecked = fConfirmMinimumAgeCheckBox->Value() == 1;
437	bool conditionsChecked = fConfirmUserUsageConditionsCheckBox->Value() == 1;
438
439	// precondition that the user has checked both of the checkboxes.
440	if (!ageChecked || !conditionsChecked)
441		debugger("the user has not agreed to the age and conditions");
442
443	_Agree();
444}
445