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 <StringView.h> 14 15 #include "AppUtils.h" 16 #include "BarberPole.h" 17 #include "HaikuDepotConstants.h" 18 #include "LocaleUtils.h" 19 #include "Logger.h" 20 #include "MarkupTextView.h" 21 #include "Model.h" 22 #include "UserUsageConditions.h" 23 #include "ServerHelper.h" 24 #include "TextView.h" 25 #include "WebAppInterface.h" 26 27 28 #undef B_TRANSLATION_CONTEXT 29 #define B_TRANSLATION_CONTEXT "UserUsageConditions" 30 31 #define PLACEHOLDER_TEXT "..." 32 33 #define INTRODUCTION_TEXT_LATEST "HaikuDepot communicates with a " \ 34 "server component called HaikuDepotServer. These are the latest " \ 35 "usage conditions for use of the HaikuDepotServer service." 36 37 #define INTRODUCTION_TEXT_USER "HaikuDepot communicates with a " \ 38 "server component called HaikuDepotServer. These are the usage " \ 39 "conditions that the user '%Nickname%' agreed to at %AgreedToTimestamp% "\ 40 "in relation to the use of the HaikuDepotServer service." 41 42 #define KEY_USER_USAGE_CONDITIONS "userUsageConditions" 43 #define KEY_USER_DETAIL "userDetail" 44 45 /*! This is the anticipated number of lines of test that appear in the 46 introduction. 47 */ 48 49 #define LINES_INTRODUCTION_TEXT 2 50 51 52 UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model, 53 UserUsageConditions& userUsageConditions) 54 : 55 BWindow(BRect(), B_TRANSLATE("Usage conditions"), 56 B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL, 57 B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS 58 | B_NOT_RESIZABLE | B_NOT_ZOOMABLE), 59 fMode(FIXED), 60 fModel(model), 61 fIntroductionTextView(NULL), 62 fWorkerThread(-1) 63 { 64 _InitUiControls(); 65 66 font_height fontHeight; 67 be_plain_font->GetHeight(&fontHeight); 68 const float lineHeight = fontHeight.ascent + fontHeight.descent; 69 70 BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView, 71 0, false, true, B_PLAIN_BORDER); 72 scrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight * 6)); 73 BButton* okButton = new BButton("ok", B_TRANSLATE("OK"), 74 new BMessage(B_QUIT_REQUESTED)); 75 76 BLayoutBuilder::Group<>(this, B_VERTICAL) 77 .SetInsets(B_USE_WINDOW_INSETS) 78 .Add(fVersionStringView, 1) 79 .Add(scrollView, 97) 80 .Add(fAgeNoteStringView, 1) 81 .AddGroup(B_HORIZONTAL, 1) 82 .AddGlue() 83 .Add(okButton) 84 .End() 85 .End(); 86 87 GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET)); 88 ResizeToPreferred(); 89 CenterOnScreen(); 90 91 UserDetail userDetail; 92 // invalid user detail 93 _DisplayData(userDetail, userUsageConditions); 94 } 95 96 UserUsageConditionsWindow::UserUsageConditionsWindow( 97 Model& model, UserUsageConditionsSelectionMode mode) 98 : 99 BWindow(BRect(), B_TRANSLATE("Usage conditions"), 100 B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL, 101 B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS 102 | B_NOT_RESIZABLE | B_NOT_ZOOMABLE), 103 fMode(mode), 104 fModel(model), 105 fWorkerThread(-1) 106 { 107 _InitUiControls(); 108 109 font_height fontHeight; 110 be_plain_font->GetHeight(&fontHeight); 111 const float lineHeight = fontHeight.ascent + fontHeight.descent; 112 113 fWorkerIndicator = new BarberPole("fetch data worker indicator"); 114 BSize workerIndicatorSize; 115 workerIndicatorSize.SetHeight(lineHeight); 116 fWorkerIndicator->SetExplicitSize(workerIndicatorSize); 117 118 fIntroductionTextView = new TextView("introduction text view"); 119 UserDetail userDetail; 120 fIntroductionTextView->SetText(_IntroductionTextForMode(mode, userDetail)); 121 122 BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView, 123 0, false, true, B_PLAIN_BORDER); 124 scrollView->SetExplicitMinSize(BSize(B_SIZE_UNSET, lineHeight * 6)); 125 BButton* okButton = new BButton("ok", B_TRANSLATE("OK"), 126 new BMessage(B_QUIT_REQUESTED)); 127 128 BLayoutBuilder::Group<>(this, B_VERTICAL) 129 .SetInsets(B_USE_WINDOW_INSETS) 130 .Add(fIntroductionTextView, 1) 131 .AddGlue() 132 .Add(fVersionStringView, 1) 133 .Add(scrollView, 95) 134 .Add(fAgeNoteStringView, 1) 135 .AddGroup(B_HORIZONTAL, 1) 136 .AddGlue() 137 .Add(okButton) 138 .End() 139 .Add(fWorkerIndicator, 1) 140 .End(); 141 142 GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET)); 143 ResizeToPreferred(); 144 CenterOnScreen(); 145 146 _FetchData(); 147 // start a new thread to pull down the user usage conditions data. 148 } 149 150 151 UserUsageConditionsWindow::~UserUsageConditionsWindow() 152 { 153 } 154 155 156 /*! This sets up the UI controls / interface elements that are not specific to 157 a given mode of viewing. 158 */ 159 160 void 161 UserUsageConditionsWindow::_InitUiControls() 162 { 163 fCopyView = new MarkupTextView("copy view"); 164 fCopyView->SetViewUIColor(B_NO_COLOR); 165 fCopyView->SetLowColor(RGB_COLOR_WHITE); 166 fCopyView->SetInsets(8.0f); 167 168 fAgeNoteStringView = new BStringView("age note string view", 169 PLACEHOLDER_TEXT); 170 fAgeNoteStringView->AdoptSystemColors(); 171 172 BFont versionFont(be_plain_font); 173 versionFont.SetSize(versionFont.Size() * 0.75f); 174 175 fVersionStringView = new BStringView("version string view", 176 PLACEHOLDER_TEXT); 177 fVersionStringView->AdoptSystemColors(); 178 fVersionStringView->SetFont(&versionFont); 179 fVersionStringView->SetAlignment(B_ALIGN_RIGHT); 180 fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT); 181 } 182 183 184 void 185 UserUsageConditionsWindow::MessageReceived(BMessage* message) 186 { 187 switch (message->what) { 188 case MSG_USER_USAGE_CONDITIONS_DATA: 189 { 190 BMessage userDetailMessage; 191 BMessage userUsageConditionsMessage; 192 message->FindMessage(KEY_USER_DETAIL, &userDetailMessage); 193 message->FindMessage(KEY_USER_USAGE_CONDITIONS, 194 &userUsageConditionsMessage); 195 UserDetail userDetail(&userDetailMessage); 196 UserUsageConditions userUsageConditions(&userUsageConditionsMessage); 197 _DisplayData(userDetail, userUsageConditions); 198 fWorkerIndicator->Stop(); 199 break; 200 } 201 default: 202 BWindow::MessageReceived(message); 203 break; 204 } 205 } 206 207 208 bool 209 UserUsageConditionsWindow::QuitRequested() 210 { 211 // for now we just don't allow the quit when the background thread 212 // is processing. In the future it would be good if the HTTP 213 // requests were re-organized such that cancellations were easier to 214 // implement. 215 216 if (fWorkerThread == -1) 217 return true; 218 HDINFO("unable to quit when the user usage " 219 "conditions window is still fetching data"); 220 return false; 221 } 222 223 224 /*! This method is called on the main thread in order to initiate the background 225 processing to obtain the user usage conditions data. It will take 226 responsibility for coordinating the creation of the thread and starting the 227 thread etc... 228 */ 229 230 void 231 UserUsageConditionsWindow::_FetchData() 232 { 233 if (-1 != fWorkerThread) 234 debugger("illegal state - attempt to fetch, but fetch in progress"); 235 thread_id thread = spawn_thread(&_FetchDataThreadEntry, 236 "Fetch usage conditions data", B_NORMAL_PRIORITY, this); 237 if (thread >= 0) { 238 fWorkerIndicator->Start(); 239 _SetWorkerThread(thread); 240 resume_thread(fWorkerThread); 241 } else { 242 debugger("unable to start a thread to fetch the user usage " 243 "conditions."); 244 } 245 } 246 247 248 /*! This method is called from the thread in order to start the thread; it is 249 the entry-point for the background processing to obtain the user usage 250 conditions. 251 */ 252 253 /*static*/ int32 254 UserUsageConditionsWindow::_FetchDataThreadEntry(void* data) 255 { 256 UserUsageConditionsWindow* win 257 = reinterpret_cast<UserUsageConditionsWindow*>(data); 258 win->_FetchDataPerform(); 259 return 0; 260 } 261 262 263 /*! This method will perform the task of obtaining data about the user usage 264 conditions. 265 */ 266 267 void 268 UserUsageConditionsWindow::_FetchDataPerform() 269 { 270 UserDetail userDetail; 271 UserUsageConditions conditions; 272 WebAppInterface interface = fModel.GetWebAppInterface(); 273 BString code; 274 status_t status = _FetchUserUsageConditionsCodePerform(userDetail, code); 275 276 if (status == B_OK) { 277 if (fMode == USER && code.IsEmpty()) { 278 BString message = B_TRANSLATE( 279 "The user '%Nickname%' has not agreed to any usage " 280 "conditions."); 281 message.ReplaceAll("%Nickname%", userDetail.Nickname()); 282 AppUtils::NotifySimpleError(B_TRANSLATE("No usage conditions"), 283 message); 284 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 285 status = B_BAD_DATA; 286 } 287 } else { 288 _NotifyFetchProblem(); 289 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 290 } 291 292 if (status == B_OK) { 293 if (interface.RetrieveUserUsageConditions(code, conditions) == B_OK) { 294 BMessage userUsageConditionsMessage; 295 BMessage userDetailMessage; 296 conditions.Archive(&userUsageConditionsMessage, true); 297 userDetail.Archive(&userDetailMessage, true); 298 BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA); 299 dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS, 300 &userUsageConditionsMessage); 301 dataMessage.AddMessage(KEY_USER_DETAIL, &userDetailMessage); 302 BMessenger(this).SendMessage(&dataMessage); 303 } else { 304 _NotifyFetchProblem(); 305 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 306 } 307 } 308 309 _SetWorkerThread(-1); 310 } 311 312 313 status_t 314 UserUsageConditionsWindow::_FetchUserUsageConditionsCodePerform( 315 UserDetail& userDetail, BString& code) 316 { 317 switch (fMode) { 318 case LATEST: 319 code.SetTo(""); 320 // no code in order to get the latest 321 return B_OK; 322 case USER: 323 return _FetchUserUsageConditionsCodeForUserPerform( 324 userDetail, code); 325 default: 326 debugger("unhanded mode"); 327 return B_ERROR; 328 } 329 } 330 331 332 status_t 333 UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform( 334 UserDetail& userDetail, BString& code) 335 { 336 WebAppInterface interface = fModel.GetWebAppInterface(); 337 338 if (interface.Nickname().IsEmpty()) 339 debugger("attempt to get user details for the current user, but" 340 " there is no current user"); 341 342 BMessage responseEnvelopeMessage; 343 status_t result = interface.RetrieveCurrentUserDetail( 344 responseEnvelopeMessage); 345 346 if (result == B_OK) { 347 // could be an error or could be a valid response envelope 348 // containing data. 349 switch (interface.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 minimumAgeString; 435 minimumAgeString.SetToFormat("%" B_PRId8, minimumAge); 436 BString ageNoteText( 437 B_TRANSLATE("Users are required to be %AgeYears% years of age or " 438 "older.")); 439 ageNoteText.ReplaceAll("%AgeYears%", minimumAgeString); 440 return ageNoteText; 441 } 442 443 444 /*static*/ const BString 445 UserUsageConditionsWindow::_IntroductionTextForMode( 446 UserUsageConditionsSelectionMode mode, 447 const UserDetail& userDetail) 448 { 449 switch (mode) { 450 case LATEST: 451 return B_TRANSLATE(INTRODUCTION_TEXT_LATEST); 452 case USER: 453 { 454 BString nicknamePresentation = PLACEHOLDER_TEXT; 455 BString agreedToTimestampPresentation = PLACEHOLDER_TEXT; 456 457 if (!userDetail.Nickname().IsEmpty()) 458 nicknamePresentation = userDetail.Nickname(); 459 460 uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed(); 461 462 if (timestampAgreed > 0) { 463 agreedToTimestampPresentation = 464 LocaleUtils::TimestampToDateTimeString(timestampAgreed); 465 } 466 467 BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER); 468 text.ReplaceAll("%Nickname%", nicknamePresentation); 469 text.ReplaceAll("%AgreedToTimestamp%", 470 agreedToTimestampPresentation); 471 return text; 472 } 473 default: 474 return "???"; 475 } 476 } 477