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( 345 responseEnvelopeMessage); 346 347 if (result == B_OK) { 348 // could be an error or could be a valid response envelope 349 // containing data. 350 switch (interface.ErrorCodeFromResponse(responseEnvelopeMessage)) { 351 case ERROR_CODE_NONE: 352 result = WebAppInterface::UnpackUserDetail( 353 responseEnvelopeMessage, userDetail); 354 break; 355 default: 356 ServerHelper::NotifyServerJsonRpcError(responseEnvelopeMessage); 357 result = B_ERROR; 358 // just any old error to stop 359 break; 360 } 361 } else { 362 HDERROR("an error has arisen communicating with the" 363 " server to obtain data for a user's user usage conditions" 364 " [%s]", strerror(result)); 365 ServerHelper::NotifyTransportError(result); 366 } 367 368 if (result == B_OK) { 369 BString userUsageConditionsCode = userDetail.Agreement().Code(); 370 HDDEBUG("the user [%s] has agreed to uuc [%s]", 371 interface.Nickname().String(), 372 userUsageConditionsCode.String()); 373 code.SetTo(userUsageConditionsCode); 374 } else { 375 HDDEBUG("unable to get details of the user [%s]", 376 interface.Nickname().String()); 377 } 378 379 return result; 380 } 381 382 383 void 384 UserUsageConditionsWindow::_NotifyFetchProblem() 385 { 386 AppUtils::NotifySimpleError( 387 B_TRANSLATE("Usage conditions download problem"), 388 B_TRANSLATE("An error has arisen downloading the usage " 389 "conditions. Check the log for details and try again. " 390 ALERT_MSG_LOGS_USER_GUIDE)); 391 } 392 393 394 void 395 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread) 396 { 397 if (!Lock()) 398 HDERROR("failed to lock window"); 399 else { 400 fWorkerThread = thread; 401 Unlock(); 402 } 403 } 404 405 406 void 407 UserUsageConditionsWindow::_DisplayData( 408 const UserDetail& userDetail, 409 const UserUsageConditions& userUsageConditions) 410 { 411 fCopyView->SetText(userUsageConditions.CopyMarkdown()); 412 fAgeNoteStringView->SetText(_MinimumAgeText( 413 userUsageConditions.MinimumAge())); 414 fVersionStringView->SetText(_VersionText(userUsageConditions.Code())); 415 if (fIntroductionTextView != NULL) { 416 fIntroductionTextView->SetText( 417 _IntroductionTextForMode(fMode, userDetail)); 418 } 419 } 420 421 422 /*static*/ const BString 423 UserUsageConditionsWindow::_VersionText(const BString& code) 424 { 425 BString versionText( 426 B_TRANSLATE("Version %Code%")); 427 versionText.ReplaceAll("%Code%", code); 428 return versionText; 429 } 430 431 432 /*static*/ const BString 433 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge) 434 { 435 BString ageNoteText; 436 static BStringFormat formatText(B_TRANSLATE("Users are required to be " 437 "{0, plural, one{# year of age} other{# years of age}} or older.")); 438 formatText.Format(ageNoteText, minimumAge); 439 return ageNoteText; 440 } 441 442 443 /*static*/ const BString 444 UserUsageConditionsWindow::_IntroductionTextForMode( 445 UserUsageConditionsSelectionMode mode, 446 const UserDetail& userDetail) 447 { 448 switch (mode) { 449 case LATEST: 450 return B_TRANSLATE(INTRODUCTION_TEXT_LATEST); 451 case USER: 452 { 453 BString nicknamePresentation = PLACEHOLDER_TEXT; 454 BString agreedToTimestampPresentation = PLACEHOLDER_TEXT; 455 456 if (!userDetail.Nickname().IsEmpty()) 457 nicknamePresentation = userDetail.Nickname(); 458 459 uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed(); 460 461 if (timestampAgreed > 0) { 462 agreedToTimestampPresentation = 463 LocaleUtils::TimestampToDateTimeString(timestampAgreed); 464 } 465 466 BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER); 467 text.ReplaceAll("%Nickname%", nicknamePresentation); 468 text.ReplaceAll("%AgreedToTimestamp%", 469 agreedToTimestampPresentation); 470 return text; 471 } 472 default: 473 return "???"; 474 } 475 } 476