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->SetLowColor(RGB_COLOR_WHITE); 167 fCopyView->SetInsets(8.0f); 168 169 fAgeNoteStringView = new BStringView("age note string view", 170 PLACEHOLDER_TEXT); 171 fAgeNoteStringView->AdoptSystemColors(); 172 173 BFont versionFont(be_plain_font); 174 versionFont.SetSize(versionFont.Size() * 0.75f); 175 176 fVersionStringView = new BStringView("version string view", 177 PLACEHOLDER_TEXT); 178 fVersionStringView->AdoptSystemColors(); 179 fVersionStringView->SetFont(&versionFont); 180 fVersionStringView->SetAlignment(B_ALIGN_RIGHT); 181 fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT); 182 } 183 184 185 void 186 UserUsageConditionsWindow::MessageReceived(BMessage* message) 187 { 188 switch (message->what) { 189 case MSG_USER_USAGE_CONDITIONS_DATA: 190 { 191 BMessage userDetailMessage; 192 BMessage userUsageConditionsMessage; 193 message->FindMessage(KEY_USER_DETAIL, &userDetailMessage); 194 message->FindMessage(KEY_USER_USAGE_CONDITIONS, 195 &userUsageConditionsMessage); 196 UserDetail userDetail(&userDetailMessage); 197 UserUsageConditions userUsageConditions(&userUsageConditionsMessage); 198 _DisplayData(userDetail, userUsageConditions); 199 fWorkerIndicator->Stop(); 200 break; 201 } 202 default: 203 BWindow::MessageReceived(message); 204 break; 205 } 206 } 207 208 209 bool 210 UserUsageConditionsWindow::QuitRequested() 211 { 212 // for now we just don't allow the quit when the background thread 213 // is processing. In the future it would be good if the HTTP 214 // requests were re-organized such that cancellations were easier to 215 // implement. 216 217 if (fWorkerThread == -1) 218 return true; 219 HDINFO("unable to quit when the user usage " 220 "conditions window is still fetching data"); 221 return false; 222 } 223 224 225 /*! This method is called on the main thread in order to initiate the background 226 processing to obtain the user usage conditions data. It will take 227 responsibility for coordinating the creation of the thread and starting the 228 thread etc... 229 */ 230 231 void 232 UserUsageConditionsWindow::_FetchData() 233 { 234 if (-1 != fWorkerThread) 235 debugger("illegal state - attempt to fetch, but fetch in progress"); 236 thread_id thread = spawn_thread(&_FetchDataThreadEntry, 237 "Fetch usage conditions data", B_NORMAL_PRIORITY, this); 238 if (thread >= 0) { 239 fWorkerIndicator->Start(); 240 _SetWorkerThread(thread); 241 resume_thread(fWorkerThread); 242 } else { 243 debugger("unable to start a thread to fetch the user usage " 244 "conditions."); 245 } 246 } 247 248 249 /*! This method is called from the thread in order to start the thread; it is 250 the entry-point for the background processing to obtain the user usage 251 conditions. 252 */ 253 254 /*static*/ int32 255 UserUsageConditionsWindow::_FetchDataThreadEntry(void* data) 256 { 257 UserUsageConditionsWindow* win 258 = reinterpret_cast<UserUsageConditionsWindow*>(data); 259 win->_FetchDataPerform(); 260 return 0; 261 } 262 263 264 /*! This method will perform the task of obtaining data about the user usage 265 conditions. 266 */ 267 268 void 269 UserUsageConditionsWindow::_FetchDataPerform() 270 { 271 UserDetail userDetail; 272 UserUsageConditions conditions; 273 WebAppInterface* interface = fModel.GetWebAppInterface(); 274 BString code; 275 status_t status = _FetchUserUsageConditionsCodePerform(userDetail, code); 276 277 if (status == B_OK) { 278 if (fMode == USER && code.IsEmpty()) { 279 BString message = B_TRANSLATE( 280 "The user '%Nickname%' has not agreed to any usage " 281 "conditions."); 282 message.ReplaceAll("%Nickname%", userDetail.Nickname()); 283 AppUtils::NotifySimpleError(B_TRANSLATE("No usage conditions"), 284 message); 285 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 286 status = B_BAD_DATA; 287 } 288 } else { 289 _NotifyFetchProblem(); 290 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 291 } 292 293 if (status == B_OK) { 294 if (interface->RetrieveUserUsageConditions(code, conditions) == B_OK) { 295 BMessage userUsageConditionsMessage; 296 BMessage userDetailMessage; 297 conditions.Archive(&userUsageConditionsMessage, true); 298 userDetail.Archive(&userDetailMessage, true); 299 BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA); 300 dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS, 301 &userUsageConditionsMessage); 302 dataMessage.AddMessage(KEY_USER_DETAIL, &userDetailMessage); 303 BMessenger(this).SendMessage(&dataMessage); 304 } else { 305 _NotifyFetchProblem(); 306 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 307 } 308 } 309 310 _SetWorkerThread(-1); 311 } 312 313 314 status_t 315 UserUsageConditionsWindow::_FetchUserUsageConditionsCodePerform( 316 UserDetail& userDetail, BString& code) 317 { 318 switch (fMode) { 319 case LATEST: 320 code.SetTo(""); 321 // no code in order to get the latest 322 return B_OK; 323 case USER: 324 return _FetchUserUsageConditionsCodeForUserPerform( 325 userDetail, code); 326 default: 327 debugger("unhanded mode"); 328 return B_ERROR; 329 } 330 } 331 332 333 status_t 334 UserUsageConditionsWindow::_FetchUserUsageConditionsCodeForUserPerform( 335 UserDetail& userDetail, BString& code) 336 { 337 WebAppInterface* interface = fModel.GetWebAppInterface(); 338 339 if (interface->Nickname().IsEmpty()) 340 debugger("attempt to get user details for the current user, but" 341 " there is no current user"); 342 343 BMessage responseEnvelopeMessage; 344 status_t result = interface->RetrieveCurrentUserDetail(responseEnvelopeMessage); 345 346 if (result == B_OK) { 347 // could be an error or could be a valid response envelope 348 // containing data. 349 switch (WebAppInterface::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 ageNoteText; 435 static BStringFormat formatText(B_TRANSLATE("Users are required to be " 436 "{0, plural, one{# year of age} other{# years of age}} or older.")); 437 formatText.Format(ageNoteText, minimumAge); 438 return ageNoteText; 439 } 440 441 442 /*static*/ const BString 443 UserUsageConditionsWindow::_IntroductionTextForMode( 444 UserUsageConditionsSelectionMode mode, 445 const UserDetail& userDetail) 446 { 447 switch (mode) { 448 case LATEST: 449 return B_TRANSLATE(INTRODUCTION_TEXT_LATEST); 450 case USER: 451 { 452 BString nicknamePresentation = PLACEHOLDER_TEXT; 453 BString agreedToTimestampPresentation = PLACEHOLDER_TEXT; 454 455 if (!userDetail.Nickname().IsEmpty()) 456 nicknamePresentation = userDetail.Nickname(); 457 458 uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed(); 459 460 if (timestampAgreed > 0) { 461 agreedToTimestampPresentation = 462 LocaleUtils::TimestampToDateTimeString(timestampAgreed); 463 } 464 465 BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER); 466 text.ReplaceAll("%Nickname%", nicknamePresentation); 467 text.ReplaceAll("%AgreedToTimestamp%", 468 agreedToTimestampPresentation); 469 return text; 470 } 471 default: 472 return "???"; 473 } 474 } 475