1 /* 2 * Copyright 2019-2020, 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 "ServerHelper.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 "sever 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 "sever 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 #define WINDOW_FRAME BRect(0, 0, 500, 400) 52 53 54 UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model, 55 UserUsageConditions& userUsageConditions) 56 : 57 BWindow(WINDOW_FRAME, B_TRANSLATE("Usage conditions"), 58 B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL, 59 B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS 60 | B_NOT_RESIZABLE | B_NOT_ZOOMABLE), 61 fMode(FIXED), 62 fModel(model), 63 fIntroductionTextView(NULL), 64 fWorkerThread(-1) 65 { 66 _InitUiControls(); 67 68 BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView, 69 0, false, true, B_PLAIN_BORDER); 70 BButton* okButton = new BButton("ok", B_TRANSLATE("OK"), 71 new BMessage(B_QUIT_REQUESTED)); 72 73 BLayoutBuilder::Group<>(this, B_VERTICAL) 74 .SetInsets(B_USE_WINDOW_INSETS) 75 .Add(fVersionStringView, 1) 76 .Add(scrollView, 97) 77 .Add(fAgeNoteStringView, 1) 78 .AddGroup(B_HORIZONTAL, 1) 79 .AddGlue() 80 .Add(okButton) 81 .End() 82 .End(); 83 84 CenterOnScreen(); 85 86 UserDetail userDetail; 87 // invalid user detail 88 _DisplayData(userDetail, userUsageConditions); 89 } 90 91 UserUsageConditionsWindow::UserUsageConditionsWindow( 92 Model& model, UserUsageConditionsSelectionMode mode) 93 : 94 BWindow(WINDOW_FRAME, B_TRANSLATE("Usage conditions"), 95 B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL, 96 B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS 97 | B_NOT_RESIZABLE | B_NOT_ZOOMABLE), 98 fMode(mode), 99 fModel(model), 100 fWorkerThread(-1) 101 { 102 _InitUiControls(); 103 104 fWorkerIndicator = new BarberPole("fetch data worker indicator"); 105 BSize workerIndicatorSize; 106 workerIndicatorSize.SetHeight(20); 107 fWorkerIndicator->SetExplicitMinSize(workerIndicatorSize); 108 109 fIntroductionTextView = new BTextView("introduction text view"); 110 fIntroductionTextView->AdoptSystemColors(); 111 fIntroductionTextView->MakeEditable(false); 112 fIntroductionTextView->MakeSelectable(false); 113 UserDetail userDetail; 114 fIntroductionTextView->SetText(_IntroductionTextForMode(mode, userDetail)); 115 116 BSize introductionSize; 117 introductionSize.SetHeight( 118 _ExpectedIntroductionTextHeight(fIntroductionTextView)); 119 fIntroductionTextView->SetExplicitPreferredSize(introductionSize); 120 121 BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView, 122 0, false, true, B_PLAIN_BORDER); 123 BButton* okButton = new BButton("ok", B_TRANSLATE("OK"), 124 new BMessage(B_QUIT_REQUESTED)); 125 126 BLayoutBuilder::Group<>(this, B_VERTICAL) 127 .SetInsets(B_USE_WINDOW_INSETS) 128 .Add(fIntroductionTextView, 1) 129 .AddGlue() 130 .Add(fVersionStringView, 1) 131 .Add(scrollView, 95) 132 .Add(fAgeNoteStringView, 1) 133 .AddGroup(B_HORIZONTAL, 1) 134 .AddGlue() 135 .Add(okButton) 136 .End() 137 .Add(fWorkerIndicator, 1) 138 .End(); 139 140 CenterOnScreen(); 141 142 _FetchData(); 143 // start a new thread to pull down the user usage conditions data. 144 } 145 146 147 UserUsageConditionsWindow::~UserUsageConditionsWindow() 148 { 149 } 150 151 152 /*! This sets up the UI controls / interface elements that are not specific to 153 a given mode of viewing. 154 */ 155 156 void 157 UserUsageConditionsWindow::_InitUiControls() 158 { 159 fCopyView = new MarkupTextView("copy view"); 160 fCopyView->SetViewUIColor(B_NO_COLOR); 161 fCopyView->SetLowColor(RGB_COLOR_WHITE); 162 fCopyView->SetInsets(8.0f); 163 164 fAgeNoteStringView = new BStringView("age note string view", 165 PLACEHOLDER_TEXT); 166 fAgeNoteStringView->AdoptSystemColors(); 167 fAgeNoteStringView->SetExplicitMaxSize( 168 BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); 169 170 BFont versionFont(be_plain_font); 171 versionFont.SetSize(9.0); 172 173 fVersionStringView = new BStringView("version string view", 174 PLACEHOLDER_TEXT); 175 fVersionStringView->AdoptSystemColors(); 176 fVersionStringView->SetFont(&versionFont); 177 fVersionStringView->SetAlignment(B_ALIGN_RIGHT); 178 fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT); 179 fVersionStringView->SetExplicitMaxSize( 180 BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); 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 if (Logger::IsInfoEnabled()) { 219 fprintf(stderr, "unable to quit when the user usage " 220 "conditions window is still fetching data\n"); 221 } 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( 346 responseEnvelopeMessage); 347 348 if (result == B_OK) { 349 // could be an error or could be a valid response envelope 350 // containing data. 351 switch (interface.ErrorCodeFromResponse(responseEnvelopeMessage)) { 352 case ERROR_CODE_NONE: 353 result = WebAppInterface::UnpackUserDetail( 354 responseEnvelopeMessage, userDetail); 355 break; 356 default: 357 ServerHelper::NotifyServerJsonRpcError(responseEnvelopeMessage); 358 result = B_ERROR; 359 // just any old error to stop 360 break; 361 } 362 } else { 363 fprintf(stderr, "an error has arisen communicating with the" 364 " server to obtain data for a user's user usage conditions" 365 " [%s]\n", strerror(result)); 366 ServerHelper::NotifyTransportError(result); 367 } 368 369 if (result == B_OK) { 370 BString userUsageConditionsCode = userDetail.Agreement().Code(); 371 if (Logger::IsDebugEnabled()) { 372 printf("the user [%s] has agreed to uuc [%s]\n", 373 interface.Nickname().String(), 374 userUsageConditionsCode.String()); 375 } 376 code.SetTo(userUsageConditionsCode); 377 } else { 378 if (Logger::IsDebugEnabled()) { 379 printf("unable to get details of the user [%s]\n", 380 interface.Nickname().String()); 381 } 382 } 383 384 return result; 385 } 386 387 388 void 389 UserUsageConditionsWindow::_NotifyFetchProblem() 390 { 391 AppUtils::NotifySimpleError( 392 B_TRANSLATE("Usage conditions download problem"), 393 B_TRANSLATE("An error has arisen downloading the usage " 394 "conditions. Check the log for details and try again. " 395 ALERT_MSG_LOGS_USER_GUIDE)); 396 } 397 398 399 void 400 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread) 401 { 402 if (!Lock()) { 403 if (Logger::IsInfoEnabled()) 404 fprintf(stderr, "failed to lock window\n"); 405 } else { 406 fWorkerThread = thread; 407 Unlock(); 408 } 409 } 410 411 412 void 413 UserUsageConditionsWindow::_DisplayData( 414 const UserDetail& userDetail, 415 const UserUsageConditions& userUsageConditions) 416 { 417 fCopyView->SetText(userUsageConditions.CopyMarkdown()); 418 fAgeNoteStringView->SetText(_MinimumAgeText( 419 userUsageConditions.MinimumAge())); 420 fVersionStringView->SetText(_VersionText(userUsageConditions.Code())); 421 if (fIntroductionTextView != NULL) { 422 fIntroductionTextView->SetText( 423 _IntroductionTextForMode(fMode, userDetail)); 424 } 425 } 426 427 428 /*static*/ const BString 429 UserUsageConditionsWindow::_VersionText(const BString& code) 430 { 431 BString versionText( 432 B_TRANSLATE("Version %Code%")); 433 versionText.ReplaceAll("%Code%", code); 434 return versionText; 435 } 436 437 438 /*static*/ const BString 439 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge) 440 { 441 BString minimumAgeString; 442 minimumAgeString.SetToFormat("%" B_PRId8, minimumAge); 443 BString ageNoteText( 444 B_TRANSLATE("Users are required to be %AgeYears% years of age or " 445 "older.")); 446 ageNoteText.ReplaceAll("%AgeYears%", minimumAgeString); 447 return ageNoteText; 448 } 449 450 451 /*static*/ const BString 452 UserUsageConditionsWindow::_IntroductionTextForMode( 453 UserUsageConditionsSelectionMode mode, 454 const UserDetail& userDetail) 455 { 456 switch (mode) { 457 case LATEST: 458 return B_TRANSLATE(INTRODUCTION_TEXT_LATEST); 459 case USER: 460 { 461 BString nicknamePresentation = PLACEHOLDER_TEXT; 462 BString agreedToTimestampPresentation = PLACEHOLDER_TEXT; 463 464 if (!userDetail.Nickname().IsEmpty()) 465 nicknamePresentation = userDetail.Nickname(); 466 467 uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed(); 468 469 if (timestampAgreed > 0) { 470 agreedToTimestampPresentation = 471 LocaleUtils::TimestampToDateTimeString(timestampAgreed); 472 } 473 474 BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER); 475 text.ReplaceAll("%Nickname%", nicknamePresentation); 476 text.ReplaceAll("%AgreedToTimestamp%", 477 agreedToTimestampPresentation); 478 return text; 479 } 480 default: 481 return "???"; 482 } 483 } 484 485 486 /*static*/ float 487 UserUsageConditionsWindow::_ExpectedIntroductionTextHeight( 488 BTextView* introductionTextView) 489 { 490 float insetTop; 491 float insetBottom; 492 introductionTextView->GetInsets(NULL, &insetTop, NULL, &insetBottom); 493 494 BSize introductionSize; 495 font_height fh; 496 be_plain_font->GetHeight(&fh); 497 return ((fh.ascent + fh.descent + fh.leading) * LINES_INTRODUCTION_TEXT) 498 + insetTop + insetBottom; 499 } 500