1 /* 2 * Copyright 2020, Andrew Lindesay <apl@lindesay.co.nz>. 3 * All rights reserved. Distributed under the terms of the MIT License. 4 */ 5 6 #include "ToLatestUserUsageConditionsWindow.h" 7 8 #include <Alert.h> 9 #include <Autolock.h> 10 #include <AutoLocker.h> 11 #include <Button.h> 12 #include <Catalog.h> 13 #include <CheckBox.h> 14 #include <LayoutBuilder.h> 15 #include <Locker.h> 16 #include <SeparatorView.h> 17 18 #include "AppUtils.h" 19 #include "LinkView.h" 20 #include "LocaleUtils.h" 21 #include "Logger.h" 22 #include "Model.h" 23 #include "UserUsageConditionsWindow.h" 24 #include "ServerHelper.h" 25 #include "WebAppInterface.h" 26 #include "TextView.h" 27 28 #undef B_TRANSLATION_CONTEXT 29 #define B_TRANSLATION_CONTEXT "ToLatestUserUsageConditionsWindow" 30 31 #define PLACEHOLDER_TEXT B_UTF8_ELLIPSIS 32 33 #define KEY_USER_USAGE_CONDITIONS "userUsageConditions" 34 35 #define NO_PRIOR_MESSAGE_TEXT "The user [%Nickname%] has authenticated, but " \ 36 "before proceeding, you are required to agree to the most recent usage " \ 37 "conditions." 38 39 #define PRIOR_MESSAGE_TEXT "The user \"%Nickname%\" has previously agreed to " \ 40 "usage conditions, but the usage conditions have been updated since. " \ 41 "The updated usage conditions now need to be agreed to." 42 43 enum { 44 MSG_AGREE = 'agre', 45 MSG_AGREE_FAILED = 'agfa', 46 MSG_AGREE_MINIMUM_AGE_TOGGLE = 'amat', 47 MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE = 'auct' 48 }; 49 50 51 ToLatestUserUsageConditionsWindow::ToLatestUserUsageConditionsWindow( 52 BWindow* parent, 53 Model& model, const UserDetail& userDetail) 54 : 55 BWindow(BRect(), B_TRANSLATE("Update usage conditions"), 56 B_FLOATING_WINDOW_LOOK, B_MODAL_SUBSET_WINDOW_FEEL, 57 B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS 58 | B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_NOT_CLOSABLE), 59 fModel(model), 60 fUserDetail(userDetail), 61 fWorkerThread(-1), 62 fQuitRequestedDuringWorkerThread(false), 63 fMutableControlsEnabled(false) 64 { 65 AddToSubset(parent); 66 _InitUiControls(); 67 68 // some layout magic happening here. If the checkboxes are put directly 69 // into the main vertical-group then the window tries to shrink to the 70 // preferred size of the checkboxes. To avoid this, a grid is used to house 71 // the checkboxes and a fake extra grid column is added to the right of the 72 // checkboxes which prevents the window from reducing in size to meet the 73 // checkboxes. 74 75 BLayoutBuilder::Group<>(this, B_VERTICAL) 76 .AddGrid() 77 .Add(fMessageTextView, 0, 0, 2) 78 .Add(fConfirmMinimumAgeCheckBox, 0, 1, 1) 79 .Add(fConfirmUserUsageConditionsCheckBox, 0, 2, 1) 80 .Add(fUserUsageConditionsLink, 0, 3, 2) 81 .End() 82 .Add(new BSeparatorView(B_HORIZONTAL)) 83 // rule off 84 .AddGroup(B_HORIZONTAL, 1) 85 .AddGlue() 86 .Add(fLogoutButton) 87 .Add(fAgreeButton) 88 .End() 89 .Add(fWorkerIndicator, 1) 90 .SetInsets(B_USE_WINDOW_INSETS); 91 92 GetLayout()->SetExplicitMinSize(BSize(500, B_SIZE_UNSET)); 93 ResizeToPreferred(); 94 CenterOnScreen(); 95 96 _FetchData(); 97 // start a new thread to pull down the user usage conditions data. 98 } 99 100 101 ToLatestUserUsageConditionsWindow::~ToLatestUserUsageConditionsWindow() 102 { 103 BAutolock locker(&fLock); 104 105 if (fWorkerThread >= 0) 106 wait_for_thread(fWorkerThread, NULL); 107 } 108 109 110 void 111 ToLatestUserUsageConditionsWindow::_InitUiControls() 112 { 113 fMessageTextView = new TextView("message text view"); 114 BString message; 115 if (fUserDetail.Agreement().Code().IsEmpty()) 116 message = B_TRANSLATE(NO_PRIOR_MESSAGE_TEXT); 117 else 118 message = B_TRANSLATE(PRIOR_MESSAGE_TEXT); 119 message.ReplaceAll("%Nickname%", fUserDetail.Nickname()); 120 fMessageTextView->SetText(message); 121 122 fConfirmMinimumAgeCheckBox = new BCheckBox("confirm minimum age", 123 PLACEHOLDER_TEXT, 124 // is filled in when the user usage conditions data is available 125 new BMessage(MSG_AGREE_MINIMUM_AGE_TOGGLE)); 126 127 fConfirmUserUsageConditionsCheckBox = new BCheckBox( 128 "confirm usage conditions", 129 B_TRANSLATE("I agree to the usage conditions"), 130 new BMessage(MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE)); 131 132 fUserUsageConditionsLink = new LinkView("usage conditions view", 133 B_TRANSLATE("View the usage conditions"), 134 new BMessage(MSG_VIEW_LATEST_USER_USAGE_CONDITIONS)); 135 fUserUsageConditionsLink->SetTarget(this); 136 137 fLogoutButton = new BButton("logout", B_TRANSLATE("Logout"), 138 new BMessage(MSG_LOG_OUT)); 139 fAgreeButton = new BButton("agree", B_TRANSLATE("Agree"), 140 new BMessage(MSG_AGREE)); 141 142 fWorkerIndicator = new BarberPole("fetch data worker indicator"); 143 BSize workerIndicatorSize; 144 workerIndicatorSize.SetHeight(20); 145 fWorkerIndicator->SetExplicitSize(workerIndicatorSize); 146 147 fMutableControlsEnabled = false; 148 _EnableMutableControls(); 149 } 150 151 152 void 153 ToLatestUserUsageConditionsWindow::_EnableMutableControls() 154 { 155 bool ageChecked = fConfirmMinimumAgeCheckBox->Value() == 1; 156 bool conditionsChecked = fConfirmUserUsageConditionsCheckBox->Value() == 1; 157 fUserUsageConditionsLink->SetEnabled(fMutableControlsEnabled); 158 fAgreeButton->SetEnabled(fMutableControlsEnabled && ageChecked 159 && conditionsChecked); 160 fLogoutButton->SetEnabled(fMutableControlsEnabled); 161 fConfirmUserUsageConditionsCheckBox->SetEnabled(fMutableControlsEnabled); 162 fConfirmMinimumAgeCheckBox->SetEnabled(fMutableControlsEnabled); 163 } 164 165 166 void 167 ToLatestUserUsageConditionsWindow::MessageReceived(BMessage* message) 168 { 169 switch (message->what) { 170 case MSG_USER_USAGE_CONDITIONS_DATA: 171 { 172 BMessage userUsageConditionsMessage; 173 message->FindMessage(KEY_USER_USAGE_CONDITIONS, 174 &userUsageConditionsMessage); 175 UserUsageConditions userUsageConditions( 176 &userUsageConditionsMessage); 177 _DisplayData(userUsageConditions); 178 fWorkerIndicator->Stop(); 179 break; 180 } 181 case MSG_LOG_OUT: 182 _HandleLogout(); 183 break; 184 case MSG_AGREE: 185 _HandleAgree(); 186 break; 187 case MSG_AGREE_FAILED: 188 _HandleAgreeFailed(); 189 break; 190 case MSG_VIEW_LATEST_USER_USAGE_CONDITIONS: 191 _HandleViewUserUsageConditions(); 192 break; 193 case MSG_AGREE_MINIMUM_AGE_TOGGLE: 194 case MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE: 195 _EnableMutableControls(); 196 break; 197 default: 198 BWindow::MessageReceived(message); 199 break; 200 } 201 } 202 203 204 bool 205 ToLatestUserUsageConditionsWindow::QuitRequested() 206 { 207 BAutolock locker(&fLock); 208 209 if (fWorkerThread >= 0) { 210 if (Logger::IsDebugEnabled()) 211 HDINFO("quit requested while worker thread is operating -- will " 212 "try again once the worker thread has completed"); 213 fQuitRequestedDuringWorkerThread = true; 214 return false; 215 } 216 217 return true; 218 } 219 220 221 void 222 ToLatestUserUsageConditionsWindow::_SetWorkerThread(thread_id thread) 223 { 224 if (thread >= 0) { 225 fWorkerThread = thread; 226 resume_thread(fWorkerThread); 227 } else { 228 fWorkerThread = -1; 229 if (fQuitRequestedDuringWorkerThread) 230 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 231 fQuitRequestedDuringWorkerThread = false; 232 } 233 } 234 235 236 void 237 ToLatestUserUsageConditionsWindow::_SetWorkerThreadLocked(thread_id thread) 238 { 239 BAutolock locker(&fLock); 240 _SetWorkerThread(thread); 241 } 242 243 244 /*! This method is called on the main thread in order to initiate the background 245 processing to obtain the user usage conditions data. It will take 246 responsibility for coordinating the creation of the thread and starting the 247 thread etc... 248 */ 249 250 void 251 ToLatestUserUsageConditionsWindow::_FetchData() 252 { 253 { 254 BAutolock locker(&fLock); 255 if (-1 != fWorkerThread) { 256 debugger("illegal state - attempt to fetch, but thread in " 257 "progress"); 258 } 259 } 260 261 thread_id thread = spawn_thread(&_FetchDataThreadEntry, 262 "Fetch usage conditions data", B_NORMAL_PRIORITY, this); 263 if (thread >= 0) { 264 fWorkerIndicator->Start(); 265 _SetWorkerThreadLocked(thread); 266 } else { 267 debugger("unable to start a thread to fetch the user usage " 268 "conditions."); 269 } 270 } 271 272 273 /*! This method is called from the thread; it is 274 the entry-point for the background processing to obtain the user usage 275 conditions. 276 */ 277 278 /*static*/ int32 279 ToLatestUserUsageConditionsWindow::_FetchDataThreadEntry(void* data) 280 { 281 ToLatestUserUsageConditionsWindow* win 282 = reinterpret_cast<ToLatestUserUsageConditionsWindow*>(data); 283 win->_FetchDataPerform(); 284 return 0; 285 } 286 287 288 /*! This method will perform the task of obtaining data about the user usage 289 conditions. 290 */ 291 292 void 293 ToLatestUserUsageConditionsWindow::_FetchDataPerform() 294 { 295 UserUsageConditions conditions; 296 WebAppInterface* interface = fModel.GetWebAppInterface(); 297 298 if (interface->RetrieveUserUsageConditions("", conditions) == B_OK) { 299 BMessage userUsageConditionsMessage; 300 conditions.Archive(&userUsageConditionsMessage, true); 301 BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA); 302 dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS, 303 &userUsageConditionsMessage); 304 BMessenger(this).SendMessage(&dataMessage); 305 } else { 306 _NotifyFetchProblem(); 307 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 308 } 309 310 _SetWorkerThreadLocked(-1); 311 } 312 313 314 void 315 ToLatestUserUsageConditionsWindow::_NotifyFetchProblem() 316 { 317 AppUtils::NotifySimpleError( 318 B_TRANSLATE("Usage conditions download problem"), 319 B_TRANSLATE("An error has arisen downloading the usage " 320 "conditions. Check the log for details and try again. " 321 ALERT_MSG_LOGS_USER_GUIDE)); 322 } 323 324 325 void 326 ToLatestUserUsageConditionsWindow::_Agree() 327 { 328 { 329 BAutolock locker(&fLock); 330 if (-1 != fWorkerThread) { 331 debugger("illegal state - attempt to agree, but thread in " 332 "progress"); 333 } 334 } 335 336 fMutableControlsEnabled = false; 337 _EnableMutableControls(); 338 thread_id thread = spawn_thread(&_AgreeThreadEntry, 339 "Agree usage conditions", B_NORMAL_PRIORITY, this); 340 if (thread >= 0) { 341 fWorkerIndicator->Start(); 342 _SetWorkerThreadLocked(thread); 343 } else { 344 debugger("unable to start a thread to fetch the user usage " 345 "conditions."); 346 } 347 } 348 349 350 /*static*/ int32 351 ToLatestUserUsageConditionsWindow::_AgreeThreadEntry(void* data) 352 { 353 ToLatestUserUsageConditionsWindow* win 354 = reinterpret_cast<ToLatestUserUsageConditionsWindow*>(data); 355 win->_AgreePerform(); 356 return 0; 357 } 358 359 360 void 361 ToLatestUserUsageConditionsWindow::_AgreePerform() 362 { 363 BMessenger messenger(this); 364 BMessage responsePayload; 365 WebAppInterface* webApp = fModel.GetWebAppInterface(); 366 status_t result = webApp->AgreeUserUsageConditions( 367 fUserUsageConditions.Code(), responsePayload); 368 369 if (result != B_OK) { 370 ServerHelper::NotifyTransportError(result); 371 messenger.SendMessage(MSG_AGREE_FAILED); 372 } else { 373 int32 errorCode = WebAppInterface::ErrorCodeFromResponse(responsePayload); 374 if (errorCode == ERROR_CODE_NONE) { 375 AppUtils::NotifySimpleError( 376 B_TRANSLATE("Usage conditions agreed"), 377 B_TRANSLATE("The current usage conditions have been agreed " 378 "to.")); 379 messenger.SendMessage(B_QUIT_REQUESTED); 380 } 381 else { 382 AutoLocker<BLocker> locker(fModel.Lock()); 383 ServerHelper::NotifyServerJsonRpcError(responsePayload); 384 messenger.SendMessage(MSG_AGREE_FAILED); 385 } 386 } 387 388 _SetWorkerThreadLocked(-1); 389 } 390 391 392 void 393 ToLatestUserUsageConditionsWindow::_HandleAgreeFailed() 394 { 395 fWorkerIndicator->Stop(); 396 fMutableControlsEnabled = true; 397 _EnableMutableControls(); 398 } 399 400 401 void 402 ToLatestUserUsageConditionsWindow::_DisplayData( 403 const UserUsageConditions& userUsageConditions) 404 { 405 fUserUsageConditions = userUsageConditions; 406 fConfirmMinimumAgeCheckBox->SetLabel( 407 LocaleUtils::CreateTranslatedIAmMinimumAgeSlug( 408 fUserUsageConditions.MinimumAge())); 409 fMutableControlsEnabled = true; 410 _EnableMutableControls(); 411 } 412 413 414 void 415 ToLatestUserUsageConditionsWindow::_HandleViewUserUsageConditions() 416 { 417 if (!fUserUsageConditions.Code().IsEmpty()) { 418 UserUsageConditionsWindow* window = new UserUsageConditionsWindow( 419 fModel, fUserUsageConditions); 420 window->Show(); 421 } 422 } 423 424 void 425 ToLatestUserUsageConditionsWindow::_HandleLogout() 426 { 427 AutoLocker<BLocker> locker(fModel.Lock()); 428 fModel.SetNickname(""); 429 BMessenger(this).SendMessage(B_QUIT_REQUESTED); 430 } 431 432 433 void 434 ToLatestUserUsageConditionsWindow::_HandleAgree() 435 { 436 bool ageChecked = fConfirmMinimumAgeCheckBox->Value() == 1; 437 bool conditionsChecked = fConfirmUserUsageConditionsCheckBox->Value() == 1; 438 439 // precondition that the user has checked both of the checkboxes. 440 if (!ageChecked || !conditionsChecked) 441 debugger("the user has not agreed to the age and conditions"); 442 443 _Agree(); 444 } 445