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
UserUsageConditionsWindow(Model & model,UserUsageConditions & userUsageConditions)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
UserUsageConditionsWindow(Model & model,UserUsageConditionsSelectionMode mode)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
~UserUsageConditionsWindow()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
_InitUiControls()162 UserUsageConditionsWindow::_InitUiControls()
163 {
164 fCopyView = new MarkupTextView("copy view");
165 fCopyView->SetViewUIColor(B_NO_COLOR);
166 fCopyView->SetLowUIColor(B_DOCUMENT_BACKGROUND_COLOR);
167 fCopyView->SetHighUIColor(B_DOCUMENT_TEXT_COLOR);
168 fCopyView->SetInsets(8.0f);
169
170 fAgeNoteStringView = new BStringView("age note string view",
171 PLACEHOLDER_TEXT);
172 fAgeNoteStringView->AdoptSystemColors();
173
174 BFont versionFont(be_plain_font);
175 versionFont.SetSize(versionFont.Size() * 0.75f);
176
177 fVersionStringView = new BStringView("version string view",
178 PLACEHOLDER_TEXT);
179 fVersionStringView->AdoptSystemColors();
180 fVersionStringView->SetFont(&versionFont);
181 fVersionStringView->SetAlignment(B_ALIGN_RIGHT);
182 fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT);
183 }
184
185
186 void
MessageReceived(BMessage * message)187 UserUsageConditionsWindow::MessageReceived(BMessage* message)
188 {
189 switch (message->what) {
190 case MSG_USER_USAGE_CONDITIONS_DATA:
191 {
192 BMessage userDetailMessage;
193 BMessage userUsageConditionsMessage;
194 message->FindMessage(KEY_USER_DETAIL, &userDetailMessage);
195 message->FindMessage(KEY_USER_USAGE_CONDITIONS,
196 &userUsageConditionsMessage);
197 UserDetail userDetail(&userDetailMessage);
198 UserUsageConditions userUsageConditions(&userUsageConditionsMessage);
199 _DisplayData(userDetail, userUsageConditions);
200 fWorkerIndicator->Stop();
201 break;
202 }
203 default:
204 BWindow::MessageReceived(message);
205 break;
206 }
207 }
208
209
210 bool
QuitRequested()211 UserUsageConditionsWindow::QuitRequested()
212 {
213 // for now we just don't allow the quit when the background thread
214 // is processing. In the future it would be good if the HTTP
215 // requests were re-organized such that cancellations were easier to
216 // implement.
217
218 if (fWorkerThread == -1)
219 return true;
220 HDINFO("unable to quit when the user usage "
221 "conditions window is still fetching data");
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
_FetchData()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
_FetchDataThreadEntry(void * data)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
_FetchDataPerform()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
_FetchUserUsageConditionsCodePerform(UserDetail & userDetail,BString & code)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
_FetchUserUsageConditionsCodeForUserPerform(UserDetail & userDetail,BString & code)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(responseEnvelopeMessage);
346
347 if (result == B_OK) {
348 // could be an error or could be a valid response envelope
349 // containing data.
350 switch (WebAppInterface::ErrorCodeFromResponse(responseEnvelopeMessage)) {
351 case ERROR_CODE_NONE:
352 result = WebAppInterface::UnpackUserDetail(
353 responseEnvelopeMessage, userDetail);
354 break;
355 default:
356 ServerHelper::NotifyServerJsonRpcError(responseEnvelopeMessage);
357 result = B_ERROR;
358 // just any old error to stop
359 break;
360 }
361 } else {
362 HDERROR("an error has arisen communicating with the"
363 " server to obtain data for a user's user usage conditions"
364 " [%s]", strerror(result));
365 ServerHelper::NotifyTransportError(result);
366 }
367
368 if (result == B_OK) {
369 BString userUsageConditionsCode = userDetail.Agreement().Code();
370 HDDEBUG("the user [%s] has agreed to uuc [%s]",
371 interface->Nickname().String(),
372 userUsageConditionsCode.String());
373 code.SetTo(userUsageConditionsCode);
374 } else {
375 HDDEBUG("unable to get details of the user [%s]",
376 interface->Nickname().String());
377 }
378
379 return result;
380 }
381
382
383 void
_NotifyFetchProblem()384 UserUsageConditionsWindow::_NotifyFetchProblem()
385 {
386 AppUtils::NotifySimpleError(
387 B_TRANSLATE("Usage conditions download problem"),
388 B_TRANSLATE("An error has arisen downloading the usage "
389 "conditions. Check the log for details and try again. "
390 ALERT_MSG_LOGS_USER_GUIDE));
391 }
392
393
394 void
_SetWorkerThread(thread_id thread)395 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread)
396 {
397 if (!Lock())
398 HDERROR("failed to lock window");
399 else {
400 fWorkerThread = thread;
401 Unlock();
402 }
403 }
404
405
406 void
_DisplayData(const UserDetail & userDetail,const UserUsageConditions & userUsageConditions)407 UserUsageConditionsWindow::_DisplayData(
408 const UserDetail& userDetail,
409 const UserUsageConditions& userUsageConditions)
410 {
411 fCopyView->SetText(userUsageConditions.CopyMarkdown());
412 fAgeNoteStringView->SetText(_MinimumAgeText(
413 userUsageConditions.MinimumAge()));
414 fVersionStringView->SetText(_VersionText(userUsageConditions.Code()));
415 if (fIntroductionTextView != NULL) {
416 fIntroductionTextView->SetText(
417 _IntroductionTextForMode(fMode, userDetail));
418 }
419 }
420
421
422 /*static*/ const BString
_VersionText(const BString & code)423 UserUsageConditionsWindow::_VersionText(const BString& code)
424 {
425 BString versionText(
426 B_TRANSLATE("Version %Code%"));
427 versionText.ReplaceAll("%Code%", code);
428 return versionText;
429 }
430
431
432 /*static*/ const BString
_MinimumAgeText(uint8 minimumAge)433 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge)
434 {
435 BString ageNoteText;
436 static BStringFormat formatText(B_TRANSLATE("Users are required to be "
437 "{0, plural, one{# year of age} other{# years of age}} or older."));
438 formatText.Format(ageNoteText, minimumAge);
439 return ageNoteText;
440 }
441
442
443 /*static*/ const BString
_IntroductionTextForMode(UserUsageConditionsSelectionMode mode,const UserDetail & userDetail)444 UserUsageConditionsWindow::_IntroductionTextForMode(
445 UserUsageConditionsSelectionMode mode,
446 const UserDetail& userDetail)
447 {
448 switch (mode) {
449 case LATEST:
450 return B_TRANSLATE(INTRODUCTION_TEXT_LATEST);
451 case USER:
452 {
453 BString nicknamePresentation = PLACEHOLDER_TEXT;
454 BString agreedToTimestampPresentation = PLACEHOLDER_TEXT;
455
456 if (!userDetail.Nickname().IsEmpty())
457 nicknamePresentation = userDetail.Nickname();
458
459 uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed();
460
461 if (timestampAgreed > 0) {
462 agreedToTimestampPresentation =
463 LocaleUtils::TimestampToDateTimeString(timestampAgreed);
464 }
465
466 BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER);
467 text.ReplaceAll("%Nickname%", nicknamePresentation);
468 text.ReplaceAll("%AgreedToTimestamp%",
469 agreedToTimestampPresentation);
470 return text;
471 }
472 default:
473 return "???";
474 }
475 }
476