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