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 "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 #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->SetExplicitSize(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 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 478 479 /*static*/ float 480 UserUsageConditionsWindow::_ExpectedIntroductionTextHeight( 481 BTextView* introductionTextView) 482 { 483 float insetTop; 484 float insetBottom; 485 introductionTextView->GetInsets(NULL, &insetTop, NULL, &insetBottom); 486 487 BSize introductionSize; 488 font_height fh; 489 be_plain_font->GetHeight(&fh); 490 return ((fh.ascent + fh.descent + fh.leading) * LINES_INTRODUCTION_TEXT) 491 + insetTop + insetBottom; 492 } 493