1 /* 2 * Copyright 2019, 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 "WebAppInterface.h" 25 26 27 #undef B_TRANSLATION_CONTEXT 28 #define B_TRANSLATION_CONTEXT "UserUsageConditions" 29 30 #define PLACEHOLDER_TEXT "..." 31 32 #define INTRODUCTION_TEXT_LATEST "HaikuDepot communicates with a " \ 33 "sever component called HaikuDepotServer. These are the latest " \ 34 "usage conditions for use of the HaikuDepotServer service." 35 36 #define INTRODUCTION_TEXT_USER "HaikuDepot communicates with a " \ 37 "sever component called HaikuDepotServer. These are the usage " \ 38 "conditions that the user '%Nickname%' agreed to at %AgreedToTimestamp% "\ 39 "in relation to the use of the HaikuDepotServer service." 40 41 #define KEY_USER_USAGE_CONDITIONS "userUsageConditions" 42 #define KEY_USER_DETAIL "userDetail" 43 44 /*! This is the anticipated number of lines of test that appear in the 45 introduction. 46 */ 47 48 #define LINES_INTRODUCTION_TEXT 2 49 50 #define WINDOW_FRAME BRect(0, 0, 500, 400) 51 52 53 UserUsageConditionsWindow::UserUsageConditionsWindow(Model& model, 54 UserUsageConditions& userUsageConditions) 55 : 56 BWindow(WINDOW_FRAME, 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 BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView, 68 0, false, true, B_PLAIN_BORDER); 69 BButton* okButton = new BButton("ok", B_TRANSLATE("OK"), 70 new BMessage(B_QUIT_REQUESTED)); 71 72 BLayoutBuilder::Group<>(this, B_VERTICAL) 73 .SetInsets(B_USE_WINDOW_INSETS) 74 .Add(fVersionStringView, 1) 75 .Add(scrollView, 97) 76 .Add(fAgeNoteStringView, 1) 77 .AddGroup(B_HORIZONTAL, 1) 78 .AddGlue() 79 .Add(okButton) 80 .End() 81 .End(); 82 83 CenterOnScreen(); 84 85 UserDetail userDetail; 86 // invalid user detail 87 _DisplayData(userDetail, userUsageConditions); 88 } 89 90 UserUsageConditionsWindow::UserUsageConditionsWindow( 91 Model& model, UserUsageConditionsSelectionMode mode) 92 : 93 BWindow(WINDOW_FRAME, B_TRANSLATE("Usage conditions"), 94 B_FLOATING_WINDOW_LOOK, B_NORMAL_WINDOW_FEEL, 95 B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS 96 | B_NOT_RESIZABLE | B_NOT_ZOOMABLE), 97 fMode(mode), 98 fModel(model), 99 fWorkerThread(-1) 100 { 101 _InitUiControls(); 102 103 fWorkerIndicator = new BarberPole("fetch data worker indicator"); 104 BSize workerIndicatorSize; 105 workerIndicatorSize.SetHeight(20); 106 fWorkerIndicator->SetExplicitMinSize(workerIndicatorSize); 107 108 fIntroductionTextView = new BTextView("introduction text view"); 109 fIntroductionTextView->AdoptSystemColors(); 110 fIntroductionTextView->MakeEditable(false); 111 fIntroductionTextView->MakeSelectable(false); 112 UserDetail userDetail; 113 fIntroductionTextView->SetText(_IntroductionTextForMode(mode, userDetail)); 114 115 BSize introductionSize; 116 introductionSize.SetHeight( 117 _ExpectedIntroductionTextHeight(fIntroductionTextView)); 118 fIntroductionTextView->SetExplicitPreferredSize(introductionSize); 119 120 BScrollView* scrollView = new BScrollView("copy scroll view", fCopyView, 121 0, false, true, B_PLAIN_BORDER); 122 BButton* okButton = new BButton("ok", B_TRANSLATE("OK"), 123 new BMessage(B_QUIT_REQUESTED)); 124 125 BLayoutBuilder::Group<>(this, B_VERTICAL) 126 .SetInsets(B_USE_WINDOW_INSETS) 127 .Add(fIntroductionTextView, 1) 128 .AddGlue() 129 .Add(fVersionStringView, 1) 130 .Add(scrollView, 95) 131 .Add(fAgeNoteStringView, 1) 132 .AddGroup(B_HORIZONTAL, 1) 133 .AddGlue() 134 .Add(okButton) 135 .End() 136 .Add(fWorkerIndicator, 1) 137 .End(); 138 139 CenterOnScreen(); 140 141 _FetchData(); 142 // start a new thread to pull down the user usage conditions data. 143 } 144 145 146 UserUsageConditionsWindow::~UserUsageConditionsWindow() 147 { 148 } 149 150 151 /*! This sets up the UI controls / interface elements that are not specific to 152 a given mode of viewing. 153 */ 154 155 void 156 UserUsageConditionsWindow::_InitUiControls() 157 { 158 fCopyView = new MarkupTextView("copy view"); 159 fCopyView->SetViewUIColor(B_NO_COLOR); 160 fCopyView->SetLowColor(RGB_COLOR_WHITE); 161 fCopyView->SetInsets(8.0f); 162 163 fAgeNoteStringView = new BStringView("age note string view", 164 PLACEHOLDER_TEXT); 165 fAgeNoteStringView->AdoptSystemColors(); 166 fAgeNoteStringView->SetExplicitMaxSize( 167 BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); 168 169 BFont versionFont(be_plain_font); 170 versionFont.SetSize(9.0); 171 172 fVersionStringView = new BStringView("version string view", 173 PLACEHOLDER_TEXT); 174 fVersionStringView->AdoptSystemColors(); 175 fVersionStringView->SetFont(&versionFont); 176 fVersionStringView->SetAlignment(B_ALIGN_RIGHT); 177 fVersionStringView->SetHighUIColor(B_PANEL_TEXT_COLOR, B_DARKEN_3_TINT); 178 fVersionStringView->SetExplicitMaxSize( 179 BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); 180 } 181 182 183 void 184 UserUsageConditionsWindow::MessageReceived(BMessage* message) 185 { 186 switch (message->what) { 187 case MSG_USER_USAGE_CONDITIONS_DATA: 188 { 189 BMessage userDetailMessage; 190 BMessage userUsageConditionsMessage; 191 message->FindMessage(KEY_USER_DETAIL, &userDetailMessage); 192 message->FindMessage(KEY_USER_USAGE_CONDITIONS, 193 &userUsageConditionsMessage); 194 UserDetail userDetail(&userDetailMessage); 195 UserUsageConditions userUsageConditions(&userUsageConditionsMessage); 196 _DisplayData(userDetail, userUsageConditions); 197 fWorkerIndicator->Stop(); 198 break; 199 } 200 default: 201 BWindow::MessageReceived(message); 202 break; 203 } 204 } 205 206 207 bool 208 UserUsageConditionsWindow::QuitRequested() 209 { 210 // for now we just don't allow the quit when the background thread 211 // is processing. In the future it would be good if the HTTP 212 // requests were re-organized such that cancellations were easier to 213 // implement. 214 215 if (fWorkerThread == -1) 216 return true; 217 if (Logger::IsInfoEnabled()) { 218 fprintf(stderr, "unable to quit when the user usage " 219 "conditions window is still fetching data\n"); 220 } 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 for the latest 322 return B_OK; 323 case USER: 324 { 325 WebAppInterface interface = fModel.GetWebAppInterface(); 326 327 if (interface.Nickname().IsEmpty()) 328 debugger("attempt to get user details for the current user, but" 329 " there is no current user"); 330 331 status_t result = interface.RetrieveCurrentUserDetail(userDetail); 332 333 if (result == B_OK) { 334 BString userUsageConditionsCode = userDetail.Agreement().Code(); 335 if (Logger::IsDebugEnabled()) { 336 printf("the user [%s] has agreed to uuc [%s]\n", 337 interface.Nickname().String(), 338 userUsageConditionsCode.String()); 339 } 340 code.SetTo(userUsageConditionsCode); 341 } else { 342 if (Logger::IsDebugEnabled()) { 343 printf("unable to get details of the user [%s]\n", 344 interface.Nickname().String()); 345 } 346 } 347 348 return result; 349 break; 350 } 351 default: 352 debugger("unhanded mode"); 353 return B_ERROR; 354 } 355 } 356 357 358 void 359 UserUsageConditionsWindow::_NotifyFetchProblem() 360 { 361 AppUtils::NotifySimpleError( 362 B_TRANSLATE("Usage conditions download problem"), 363 B_TRANSLATE("An error has arisen downloading the usage " 364 "conditions. Check the log for details and try again.")); 365 } 366 367 368 void 369 UserUsageConditionsWindow::_SetWorkerThread(thread_id thread) 370 { 371 if (!Lock()) { 372 if (Logger::IsInfoEnabled()) 373 fprintf(stderr, "failed to lock window\n"); 374 } else { 375 fWorkerThread = thread; 376 Unlock(); 377 } 378 } 379 380 381 void 382 UserUsageConditionsWindow::_DisplayData( 383 const UserDetail& userDetail, 384 const UserUsageConditions& userUsageConditions) 385 { 386 fCopyView->SetText(userUsageConditions.CopyMarkdown()); 387 fAgeNoteStringView->SetText(_MinimumAgeText( 388 userUsageConditions.MinimumAge())); 389 fVersionStringView->SetText(_VersionText(userUsageConditions.Code())); 390 if (fIntroductionTextView != NULL) { 391 fIntroductionTextView->SetText( 392 _IntroductionTextForMode(fMode, userDetail)); 393 } 394 } 395 396 397 /*static*/ const BString 398 UserUsageConditionsWindow::_VersionText(const BString& code) 399 { 400 BString versionText( 401 B_TRANSLATE("Version %Code%")); 402 versionText.ReplaceAll("%Code%", code); 403 return versionText; 404 } 405 406 407 /*static*/ const BString 408 UserUsageConditionsWindow::_MinimumAgeText(uint8 minimumAge) 409 { 410 BString minimumAgeString; 411 minimumAgeString.SetToFormat("%" B_PRId8, minimumAge); 412 BString ageNoteText( 413 B_TRANSLATE("Users are required to be %AgeYears% years of age or " 414 "older.")); 415 ageNoteText.ReplaceAll("%AgeYears%", minimumAgeString); 416 return ageNoteText; 417 } 418 419 420 /*static*/ const BString 421 UserUsageConditionsWindow::_IntroductionTextForMode( 422 UserUsageConditionsSelectionMode mode, 423 const UserDetail& userDetail) 424 { 425 switch (mode) { 426 case LATEST: 427 return B_TRANSLATE(INTRODUCTION_TEXT_LATEST); 428 case USER: 429 { 430 BString nicknamePresentation = PLACEHOLDER_TEXT; 431 BString agreedToTimestampPresentation = PLACEHOLDER_TEXT; 432 433 if (!userDetail.Nickname().IsEmpty()) 434 nicknamePresentation = userDetail.Nickname(); 435 436 uint64 timestampAgreed = userDetail.Agreement().TimestampAgreed(); 437 438 if (timestampAgreed > 0) { 439 agreedToTimestampPresentation = 440 LocaleUtils::TimestampToDateTimeString(timestampAgreed); 441 } 442 443 BString text = B_TRANSLATE(INTRODUCTION_TEXT_USER); 444 text.ReplaceAll("%Nickname%", nicknamePresentation); 445 text.ReplaceAll("%AgreedToTimestamp%", 446 agreedToTimestampPresentation); 447 return text; 448 } 449 default: 450 return "???"; 451 } 452 } 453 454 455 /*static*/ float 456 UserUsageConditionsWindow::_ExpectedIntroductionTextHeight( 457 BTextView* introductionTextView) 458 { 459 float insetTop; 460 float insetBottom; 461 introductionTextView->GetInsets(NULL, &insetTop, NULL, &insetBottom); 462 463 BSize introductionSize; 464 font_height fh; 465 be_plain_font->GetHeight(&fh); 466 return ((fh.ascent + fh.descent + fh.leading) * LINES_INTRODUCTION_TEXT) 467 + insetTop + insetBottom; 468 } 469