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->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 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 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 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(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 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 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 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 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 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 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