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 } 396 397 398 void 399 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread) 400 { 401 if (!Lock()) { 402 if (Logger::IsInfoEnabled()) 403 fprintf(stderr, "failed to lock window\n"); 404 } else { 405 fWorkerThread = thread; 406 Unlock(); 407 } 408 } 409 410 411 void 412 UserUsageConditionsWindow::_DisplayData( 413 const UserDetail& userDetail, 414 const UserUsageConditions& userUsageConditions) 415 { 416 fCopyView->SetText(userUsageConditions.CopyMarkdown()); 417 fAgeNoteStringView->SetText(_MinimumAgeText( 418 userUsageConditions.MinimumAge())); 419 fVersionStringView->SetText(_VersionText(userUsageConditions.Code())); 420 if (fIntroductionTextView != NULL) { 421 fIntroductionTextView->SetText( 422 _IntroductionTextForMode(fMode, userDetail)); 423 } 424 } 425 426 427 /*static*/ const BString 428 UserUsageConditionsWindow::_VersionText(const BString& code) 429 { 430 BString versionText( 431 B_TRANSLATE("Version %Code%")); 432 versionText.ReplaceAll("%Code%", code); 433 return versionText; 434 } 435 436 437 /*static*/ const BString 438 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge) 439 { 440 BString minimumAgeString; 441 minimumAgeString.SetToFormat("%" B_PRId8, minimumAge); 442 BString ageNoteText( 443 B_TRANSLATE("Users are required to be %AgeYears% years of age or " 444 "older.")); 445 ageNoteText.ReplaceAll("%AgeYears%", minimumAgeString); 446 return ageNoteText; 447 } 448 449 450 /*static*/ const BString 451 UserUsageConditionsWindow::_IntroductionTextForMode( 452 UserUsageConditionsSelectionMode mode, 453 const UserDetail& userDetail) 454 { 455 switch (mode) { 456 case LATEST: 457 return B_TRANSLATE(INTRODUCTION_TEXT_LATEST); 458 case USER: 459 { 460 BString nicknamePresentation = PLACEHOLDER_TEXT; 461 BString agreedToTimestampPresentation = PLACEHOLDER_TEXT; 462 463 if (!userDetail.Nickname().IsEmpty()) 464 nicknamePresentation = userDetail.Nickname(); 465 466 uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed(); 467 468 if (timestampAgreed > 0) { 469 agreedToTimestampPresentation = 470 LocaleUtils::TimestampToDateTimeString(timestampAgreed); 471 } 472 473 BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER); 474 text.ReplaceAll("%Nickname%", nicknamePresentation); 475 text.ReplaceAll("%AgreedToTimestamp%", 476 agreedToTimestampPresentation); 477 return text; 478 } 479 default: 480 return "???"; 481 } 482 } 483 484 485 /*static*/ float 486 UserUsageConditionsWindow::_ExpectedIntroductionTextHeight( 487 BTextView* introductionTextView) 488 { 489 float insetTop; 490 float insetBottom; 491 introductionTextView->GetInsets(NULL, &insetTop, NULL, &insetBottom); 492 493 BSize introductionSize; 494 font_height fh; 495 be_plain_font->GetHeight(&fh); 496 return ((fh.ascent + fh.descent + fh.leading) * LINES_INTRODUCTION_TEXT) 497 + insetTop + insetBottom; 498 } 499