From 10cd325cfafb80353704ef021205ff5e5523d3a3 Mon Sep 17 00:00:00 2001 From: Andrew Lindesay Date: Mon, 30 Mar 2020 22:31:34 +1300 Subject: [PATCH] HaikuDepot: Check User Auth on Start The user might have changed their authentication details on the server and the client won't detect this until they go to do something. Instead, if possible, check this as the client starts. Also check that the user has agreed to the current user usage conditions. As a side-effect this generalizes the logic for process coordination in the main window and also fixes some bugs in the main window's progress display as the application starts. Relates to #15209 Change-Id: I4c9620648819ecd14fb095e4cb2c66fe7b2a0920 Reviewed-on: https://review.haiku-os.org/c/haiku/+/2467 Reviewed-by: Adrien Destugues --- src/apps/haikudepot/HaikuDepotConstants.h | 15 +- src/apps/haikudepot/Jamfile | 2 + src/apps/haikudepot/model/Model.cpp | 83 +++- .../haikudepot/server/ProcessCoordinator.cpp | 25 +- .../haikudepot/server/ProcessCoordinator.h | 10 +- .../server/ProcessCoordinatorFactory.cpp | 21 +- .../server/ProcessCoordinatorFactory.h | 8 + .../server/UserDetailVerifierProcess.cpp | 141 ++++++ .../server/UserDetailVerifierProcess.h | 53 +++ .../haikudepot/server/WebAppInterface.cpp | 31 ++ src/apps/haikudepot/server/WebAppInterface.h | 3 + src/apps/haikudepot/ui/MainWindow.cpp | 340 ++++++++++--- src/apps/haikudepot/ui/MainWindow.h | 40 +- .../ui/ToLatestUserUsageConditionsWindow.cpp | 448 ++++++++++++++++++ .../ui/ToLatestUserUsageConditionsWindow.h | 77 +++ src/apps/haikudepot/ui/UserLoginWindow.cpp | 13 +- .../ui/UserUsageConditionsWindow.cpp | 2 +- src/apps/haikudepot/ui/WorkStatusView.cpp | 21 +- src/apps/haikudepot/util/LocaleUtils.cpp | 23 + src/apps/haikudepot/util/LocaleUtils.h | 2 + 20 files changed, 1242 insertions(+), 116 deletions(-) create mode 100644 src/apps/haikudepot/server/UserDetailVerifierProcess.cpp create mode 100644 src/apps/haikudepot/server/UserDetailVerifierProcess.h create mode 100644 src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.cpp create mode 100644 src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.h diff --git a/src/apps/haikudepot/HaikuDepotConstants.h b/src/apps/haikudepot/HaikuDepotConstants.h index 190f8cb825..21249cde3a 100644 --- a/src/apps/haikudepot/HaikuDepotConstants.h +++ b/src/apps/haikudepot/HaikuDepotConstants.h @@ -6,6 +6,7 @@ #define HAIKU_DEPOT_CONSTANTS_H enum { + MSG_BULK_LOAD_DONE = 'mmwd', MSG_MAIN_WINDOW_CLOSED = 'mwcl', MSG_PACKAGE_SELECTED = 'pkgs', MSG_PACKAGE_WORKER_BUSY = 'pkwb', @@ -23,7 +24,9 @@ enum { MSG_VIEW_LATEST_USER_USAGE_CONDITIONS = 'vluc', MSG_VIEW_USERS_USER_USAGE_CONDITIONS = 'vuuc', MSG_USER_USAGE_CONDITIONS_DATA = 'uucd', - MSG_USER_USAGE_CONDITIONS_ERROR = 'uuce' + MSG_USER_USAGE_CONDITIONS_ERROR = 'uuce', + MSG_USER_USAGE_CONDITIONS_NOT_LATEST = 'uucl', + MSG_LOG_OUT = 'lgot', }; @@ -34,11 +37,11 @@ enum { #define RGB_COLOR_WHITE (rgb_color) { 255, 255, 255, 255 } -#define HD_ERROR_BASE (B_ERRORS_END + 1) -#define HD_NETWORK_INACCESSIBLE (HD_ERROR_BASE + 1) -#define HD_CLIENT_TOO_OLD (HD_ERROR_BASE + 2) -#define HD_ERR_NOT_MODIFIED (HD_ERROR_BASE + 3) -#define HD_ERR_NO_DATA (HD_ERROR_BASE + 4) +#define HD_ERROR_BASE (B_ERRORS_END + 1) +#define HD_NETWORK_INACCESSIBLE (HD_ERROR_BASE + 1) +#define HD_CLIENT_TOO_OLD (HD_ERROR_BASE + 2) +#define HD_ERR_NOT_MODIFIED (HD_ERROR_BASE + 3) +#define HD_ERR_NO_DATA (HD_ERROR_BASE + 4) #define REPOSITORY_NAME_SYSTEM "system" diff --git a/src/apps/haikudepot/Jamfile b/src/apps/haikudepot/Jamfile index 27a1432e28..2d61ec59b5 100644 --- a/src/apps/haikudepot/Jamfile +++ b/src/apps/haikudepot/Jamfile @@ -146,6 +146,7 @@ local applicationSources = ScreenshotWindow.cpp ScrollableGroupView.cpp SharedBitmap.cpp + ToLatestUserUsageConditionsWindow.cpp UserCredentials.cpp UserDetail.cpp UserLoginWindow.cpp @@ -172,6 +173,7 @@ local applicationSources = ServerIconExportUpdateProcess.cpp StandardMetaDataJsonEventListener.cpp StandardMetaData.cpp + UserDetailVerifierProcess.cpp WebAppInterface.cpp # tar diff --git a/src/apps/haikudepot/model/Model.cpp b/src/apps/haikudepot/model/Model.cpp index ff8266aeb8..25d3d9749a 100644 --- a/src/apps/haikudepot/model/Model.cpp +++ b/src/apps/haikudepot/model/Model.cpp @@ -35,6 +35,10 @@ #define B_TRANSLATION_CONTEXT "Model" +#define KEY_STORE_IDENTIFIER_PREFIX "hds.password." + // this prefix is added before the nickname in the keystore + // so that HDS username/password pairs can be identified. + static const char* kHaikuDepotKeyring = "HaikuDepot"; @@ -804,20 +808,61 @@ Model::_PopulatePackageChangelog(const PackageInfoRef& package) } +static void +model_remove_key_for_user(const BString& nickname) +{ + if (nickname.IsEmpty()) + return; + BKeyStore keyStore; + BPasswordKey key; + BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) + << nickname; + status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, + passwordIdentifier, key); + + switch (result) { + case B_OK: + result = keyStore.RemoveKey(kHaikuDepotKeyring, key); + if (result != B_OK) { + printf("! error occurred when removing password for nickname " + "[%s] : %s\n", nickname.String(), strerror(result)); + } + break; + case B_ENTRY_NOT_FOUND: + return; + default: + printf("! error occurred when finding password for nickname " + "[%s] : %s\n", nickname.String(), strerror(result)); + break; + } +} + + void Model::SetNickname(BString nickname) { BString password; + BString existingNickname = Nickname(); + + // this happens when the user is logging out. Best to remove the password + // stored for the existing user since it is no longer required. + + if (!existingNickname.IsEmpty() && nickname.IsEmpty()) + model_remove_key_for_user(existingNickname); + if (nickname.Length() > 0) { BPasswordKey key; BKeyStore keyStore; - if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, nickname, - key) == B_OK) { + BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) + << nickname; + if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD, + passwordIdentifier, key) == B_OK) { password = key.Password(); - } else { - nickname = ""; } + if (password.IsEmpty()) + nickname = ""; } + SetAuthorization(nickname, password, false); } @@ -833,17 +878,35 @@ void Model::SetAuthorization(const BString& nickname, const BString& passwordClear, bool storePassword) { - if (storePassword && nickname.Length() > 0 && passwordClear.Length() > 0) { - BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, nickname); - BKeyStore keyStore; - keyStore.AddKeyring(kHaikuDepotKeyring); - keyStore.AddKey(kHaikuDepotKeyring, key); + BString existingNickname = Nickname(); + + if (storePassword) { + // no point continuing to store the password for the previous user. + + if (!existingNickname.IsEmpty()) + model_remove_key_for_user(existingNickname); + + // adding a key that is already there does not seem to override the + // existing key so the old key needs to be removed first. + + if (!nickname.IsEmpty()) + model_remove_key_for_user(nickname); + + if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) { + BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX) + << nickname; + BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier); + BKeyStore keyStore; + keyStore.AddKeyring(kHaikuDepotKeyring); + keyStore.AddKey(kHaikuDepotKeyring, key); + } } BAutolock locker(&fLock); fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear)); - _NotifyAuthorizationChanged(); + if (nickname != existingNickname) + _NotifyAuthorizationChanged(); } diff --git a/src/apps/haikudepot/server/ProcessCoordinator.cpp b/src/apps/haikudepot/server/ProcessCoordinator.cpp index 563c4e87fb..e1cccdb180 100644 --- a/src/apps/haikudepot/server/ProcessCoordinator.cpp +++ b/src/apps/haikudepot/server/ProcessCoordinator.cpp @@ -76,9 +76,13 @@ ProcessCoordinatorState::ErrorStatus() const // #pragma mark - ProcessCoordinator implementation -ProcessCoordinator::ProcessCoordinator(ProcessCoordinatorListener* listener) +ProcessCoordinator::ProcessCoordinator(const char* name, + ProcessCoordinatorListener* listener, + BMessage* message) : + fName(name), fListener(listener), + fMessage(message), fWasStopped(false) { } @@ -92,6 +96,7 @@ ProcessCoordinator::~ProcessCoordinator() node->Process()->SetListener(NULL); delete node; } + delete fMessage; } @@ -147,6 +152,10 @@ ProcessCoordinator::Stop() node->StopProcess(); } } + if (fListener != NULL) { + ProcessCoordinatorState state = _CreateStatus(); + fListener->CoordinatorChanged(state); + } } @@ -175,6 +184,20 @@ ProcessCoordinator::Progress() } +const BString& +ProcessCoordinator::Name() const +{ + return fName; +} + + +BMessage* +ProcessCoordinator::Message() const +{ + return fMessage; +} + + BString ProcessCoordinator::_CreateStatusMessage() { diff --git a/src/apps/haikudepot/server/ProcessCoordinator.h b/src/apps/haikudepot/server/ProcessCoordinator.h index c0a9d32fce..c7102f1075 100644 --- a/src/apps/haikudepot/server/ProcessCoordinator.h +++ b/src/apps/haikudepot/server/ProcessCoordinator.h @@ -74,7 +74,9 @@ public: class ProcessCoordinator : public AbstractProcessListener { public: ProcessCoordinator( - ProcessCoordinatorListener* listener); + const char* name, + ProcessCoordinatorListener* listener, + BMessage* message = NULL); virtual ~ProcessCoordinator(); void AddNode(ProcessNode* nodes); @@ -91,6 +93,9 @@ public: float Progress(); + const BString& Name() const; + BMessage* Message() const; + private: bool _IsRunning(ProcessNode* node); void _CoordinateAndCallListener(); @@ -103,11 +108,14 @@ private: void _StopSuccessorNodesToErroredOrStoppedNodes(); void _StopSuccessorNodes(ProcessNode* node); +private: + BString fName; BLocker fLock; List fNodes; ProcessCoordinatorListener* fListener; + BMessage* fMessage; bool fWasStopped; }; diff --git a/src/apps/haikudepot/server/ProcessCoordinatorFactory.cpp b/src/apps/haikudepot/server/ProcessCoordinatorFactory.cpp index 69ea8ee457..38b2e29be2 100644 --- a/src/apps/haikudepot/server/ProcessCoordinatorFactory.cpp +++ b/src/apps/haikudepot/server/ProcessCoordinatorFactory.cpp @@ -13,6 +13,7 @@ #include #include "AbstractServerProcess.h" +#include "HaikuDepotConstants.h" #include "LocalPkgDataLoadProcess.h" #include "LocalRepositoryUpdateProcess.h" #include "Model.h" @@ -26,11 +27,28 @@ #include "ServerRepositoryDataUpdateProcess.h" #include "ServerSettings.h" #include "StorageUtils.h" +#include "UserDetailVerifierProcess.h" using namespace BPackageKit; +/*static*/ ProcessCoordinator* +ProcessCoordinatorFactory::CreateUserDetailVerifierCoordinator( + UserDetailVerifierListener* userDetailVerifierListener, + ProcessCoordinatorListener* processCoordinatorListener, + Model* model) +{ + ProcessCoordinator* processCoordinator = new ProcessCoordinator( + "UserDetailVerifier", + processCoordinatorListener); + ProcessNode* userDetailVerifier = new ProcessNode( + new UserDetailVerifierProcess(model, userDetailVerifierListener)); + processCoordinator->AddNode(userDetailVerifier); + return processCoordinator; +} + + /* static */ ProcessCoordinator* ProcessCoordinatorFactory::CreateBulkLoadCoordinator( PackageInfoListener *packageInfoListener, @@ -41,7 +59,8 @@ ProcessCoordinatorFactory::CreateBulkLoadCoordinator( uint32 serverProcessOptions = _CalculateServerProcessOptions(); BAutolock locker(model->Lock()); ProcessCoordinator* processCoordinator = new ProcessCoordinator( - processCoordinatorListener); + "BulkLoad", + processCoordinatorListener, new BMessage(MSG_BULK_LOAD_DONE)); ProcessNode *localRepositoryUpdate = new ProcessNode(new LocalRepositoryUpdateProcess(model, diff --git a/src/apps/haikudepot/server/ProcessCoordinatorFactory.h b/src/apps/haikudepot/server/ProcessCoordinatorFactory.h index e3ba0b580c..41ca2710ab 100644 --- a/src/apps/haikudepot/server/ProcessCoordinatorFactory.h +++ b/src/apps/haikudepot/server/ProcessCoordinatorFactory.h @@ -13,6 +13,7 @@ class Model; class PackageInfoListener; class ProcessCoordinator; class ProcessCoordinatorListener; +class UserDetailVerifierListener; /*! This class is able to create ProcessCoordinators that are loaded-up with Processes that together complete some larger job. @@ -25,6 +26,13 @@ public: ProcessCoordinatorListener* processCoordinatorListener, Model* model, bool forceLocalUpdate); + + static ProcessCoordinator* CreateUserDetailVerifierCoordinator( + UserDetailVerifierListener* + userDetailVerifierListener, + ProcessCoordinatorListener* + processCoordinatorListener, + Model* model); private: static uint32 _CalculateServerProcessOptions(); diff --git a/src/apps/haikudepot/server/UserDetailVerifierProcess.cpp b/src/apps/haikudepot/server/UserDetailVerifierProcess.cpp new file mode 100644 index 0000000000..fad55c70b4 --- /dev/null +++ b/src/apps/haikudepot/server/UserDetailVerifierProcess.cpp @@ -0,0 +1,141 @@ +/* + * Copyright 2020, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + +#include "UserDetailVerifierProcess.h" + +#include +#include +#include + +#include "AppUtils.h" +#include "HaikuDepotConstants.h" +#include "Logger.h" +#include "ServerHelper.h" + +#undef B_TRANSLATION_CONTEXT +#define B_TRANSLATION_CONTEXT "UserDetailVerifierProcess" + + +UserDetailVerifierProcess::UserDetailVerifierProcess(Model* model, + UserDetailVerifierListener* listener) + : + fModel(model), + fListener(listener) +{ +} + + +UserDetailVerifierProcess::~UserDetailVerifierProcess() +{ +} + + +const char* +UserDetailVerifierProcess::Name() const +{ + return "UserDetailVerifierProcess"; +} + + +const char* +UserDetailVerifierProcess::Description() const +{ + return B_TRANSLATE("Checking user details"); +} + + +status_t +UserDetailVerifierProcess::RunInternal() +{ + status_t result = B_OK; + + if (_ShouldVerify()) { + UserDetail userDetail; + result = _TryFetchUserDetail(userDetail); + + switch (result) { + case B_PERMISSION_DENIED: + fListener->UserCredentialsFailed(); + result = B_OK; + break; + case B_OK: + if (!userDetail.Agreement().IsLatest()) { + printf("! the user has not agreed to the latest user usage" + " conditions.\n"); + fListener->UserUsageConditionsNotLatest(userDetail); + } + break; + default: + break; + } + } + + return result; +} + + +bool +UserDetailVerifierProcess::_ShouldVerify() +{ + if (!ServerHelper::IsNetworkAvailable()) { + printf("no network --> will not verify user\n"); + return false; + } + + { + AutoLocker locker(fModel->Lock()); + if (fModel->Nickname().IsEmpty()) { + printf("no nickname --> will not verify user\n"); + return false; + } + } + + return true; +} + + +status_t +UserDetailVerifierProcess::_TryFetchUserDetail(UserDetail& userDetail) +{ + WebAppInterface interface = fModel->GetWebAppInterface(); + BMessage userDetailResponse; + status_t result; + + result = interface.RetrieveCurrentUserDetail(userDetailResponse); + if (result != B_OK) { + printf("a problem has arisen retrieving the current user detail: %s\n", + strerror(result)); + } + + if (result == B_OK) { + int32 errorCode = interface.ErrorCodeFromResponse(userDetailResponse); + switch (errorCode) { + case ERROR_CODE_NONE: + break; + case ERROR_CODE_AUTHORIZATIONFAILURE: + result = B_PERMISSION_DENIED; + break; + default: + printf("! a problem has arisen retrieving the current user " + "detail for user [%s]: jrpc error code %" B_PRId32 "\n", + fModel->Nickname().String(), errorCode); + result = B_ERROR; + break; + } + } + + if (result == B_OK) { + + // now we have the user details by showing that an authentication has + // worked, it is now necessary to check to see that the user has agreed + // to the most recent user-usage conditions. + + result = interface.UnpackUserDetail(userDetailResponse, userDetail); + if (result != B_OK) + printf("! it was not possible to unpack the user details.\n"); + } + + return result; +} \ No newline at end of file diff --git a/src/apps/haikudepot/server/UserDetailVerifierProcess.h b/src/apps/haikudepot/server/UserDetailVerifierProcess.h new file mode 100644 index 0000000000..2e5ad39604 --- /dev/null +++ b/src/apps/haikudepot/server/UserDetailVerifierProcess.h @@ -0,0 +1,53 @@ +/* + * Copyright 2020, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + +#ifndef USER_DETAIL_VERIFIER_PROCESS_H +#define USER_DETAIL_VERIFIER_PROCESS_H + +#include + +#include "AbstractProcess.h" +#include "Model.h" + + +class UserDetailVerifierListener { +public: + virtual void UserUsageConditionsNotLatest( + const UserDetail& userDetail) = 0; + virtual void UserCredentialsFailed() = 0; +}; + + +/*! This service has the purpose of querying the application server (HDS) + for details of the authenticated user. This will check that the user + has the correct username / password and also that the user has agreed + to the current terms and conditions. + */ + +class UserDetailVerifierProcess : public AbstractProcess { +public: + UserDetailVerifierProcess( + Model* model, + UserDetailVerifierListener* listener); + virtual ~UserDetailVerifierProcess(); + + virtual const char* Name() const; + virtual const char* Description() const; + +protected: + virtual status_t RunInternal(); + +private: + status_t _TryFetchUserDetail(UserDetail& userDetail); + bool _ShouldVerify(); + +private: + Model* fModel; + UserDetailVerifierListener* + fListener; +}; + + +#endif // USER_DETAIL_VERIFIER_PROCESS_H diff --git a/src/apps/haikudepot/server/WebAppInterface.cpp b/src/apps/haikudepot/server/WebAppInterface.cpp index 3a57823b60..68059804a6 100644 --- a/src/apps/haikudepot/server/WebAppInterface.cpp +++ b/src/apps/haikudepot/server/WebAppInterface.cpp @@ -437,6 +437,37 @@ WebAppInterface::RetrieveUserUsageConditions(const BString& code, } +status_t +WebAppInterface::AgreeUserUsageConditions(const BString& code, + BMessage& responsePayload) +{ + BMallocIO* requestEnvelopeData = new BMallocIO(); + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); + + requestEnvelopeWriter.WriteObjectStart(); + _WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter, + "agreeUserUsageConditions"); + + requestEnvelopeWriter.WriteObjectName("params"); + requestEnvelopeWriter.WriteArrayStart(); + requestEnvelopeWriter.WriteObjectStart(); + requestEnvelopeWriter.WriteObjectName("userUsageConditionsCode"); + requestEnvelopeWriter.WriteString(code.String()); + requestEnvelopeWriter.WriteObjectName("nickname"); + requestEnvelopeWriter.WriteString(fCredentials.Nickname()); + requestEnvelopeWriter.WriteObjectEnd(); + requestEnvelopeWriter.WriteArrayEnd(); + + requestEnvelopeWriter.WriteObjectEnd(); + + // now fetch this information into an object. + + return _SendJsonRequest("user", requestEnvelopeData, + _LengthAndSeekToZero(requestEnvelopeData), NEEDS_AUTHORIZATION, + responsePayload); +} + + status_t WebAppInterface::_RetrieveUserUsageConditionsMeta(const BString& code, BMessage& message) diff --git a/src/apps/haikudepot/server/WebAppInterface.h b/src/apps/haikudepot/server/WebAppInterface.h index 449bff921f..38f10c35ac 100644 --- a/src/apps/haikudepot/server/WebAppInterface.h +++ b/src/apps/haikudepot/server/WebAppInterface.h @@ -100,6 +100,9 @@ public: const BString& code, UserUsageConditions& conditions); + status_t AgreeUserUsageConditions(const BString& code, + BMessage& responsePayload); + status_t RetrieveScreenshot( const BString& code, int32 width, int32 height, diff --git a/src/apps/haikudepot/ui/MainWindow.cpp b/src/apps/haikudepot/ui/MainWindow.cpp index 1882c393c2..12e6586f1a 100644 --- a/src/apps/haikudepot/ui/MainWindow.cpp +++ b/src/apps/haikudepot/ui/MainWindow.cpp @@ -3,7 +3,7 @@ * Copyright 2013-2014, Stephan Aßmus . * Copyright 2013, Rene Gollent, rene@gollent.com. * Copyright 2013, Ingo Weinhold, ingo_weinhold@gmx.de. - * Copyright 2016-2019, Andrew Lindesay . + * Copyright 2016-2020, Andrew Lindesay . * Copyright 2017, Julian Harnath . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -47,6 +47,7 @@ #include "RatePackageWindow.h" #include "support.h" #include "ScreenshotWindow.h" +#include "ToLatestUserUsageConditionsWindow.h" #include "UserLoginWindow.h" #include "UserUsageConditionsWindow.h" #include "WorkStatusView.h" @@ -57,12 +58,10 @@ enum { - MSG_BULK_LOAD_DONE = 'mmwd', MSG_REFRESH_REPOS = 'mrrp', MSG_MANAGE_REPOS = 'mmrp', MSG_SOFTWARE_UPDATER = 'mswu', MSG_LOG_IN = 'lgin', - MSG_LOG_OUT = 'lgot', MSG_AUTHORIZATION_CHANGED = 'athc', MSG_CATEGORIES_LIST_CHANGED = 'clic', MSG_PACKAGE_CHANGED = 'pchd', @@ -76,6 +75,7 @@ enum { MSG_SHOW_DEVELOP_PACKAGES = 'sdvl' }; +#define KEY_ERROR_STATUS "errorStatus" using namespace BPackageKit; using namespace BPackageKit::BManager::BPrivate; @@ -133,9 +133,12 @@ MainWindow::MainWindow(const BMessage& settings) fLogOutItem(NULL), fUsersUserUsageConditionsMenuItem(NULL), fModelListener(new MainWindowModelListener(BMessenger(this)), true), - fBulkLoadProcessCoordinator(NULL), + fCoordinator(NULL), fSinglePackageMode(false) { + if ((fCoordinatorRunningSem = create_sem(1, "ProcessCoordinatorSem")) < B_OK) + debugger("unable to create the process coordinator semaphore"); + BMenuBar* menuBar = new BMenuBar("Main Menu"); _BuildMenu(menuBar); @@ -144,7 +147,6 @@ MainWindow::MainWindow(const BMessage& settings) set_small_font(userMenuBar); userMenuBar->SetExplicitMaxSize(BSize(B_SIZE_UNSET, menuBar->MaxSize().height)); - _UpdateAuthorization(); fFilterView = new FilterView(); fFeaturedPackagesView = new FeaturedPackagesView(); @@ -207,6 +209,7 @@ MainWindow::MainWindow(const BMessage& settings) fListTabs->Select(1); _RestoreNickname(settings); + _UpdateAuthorization(); _RestoreWindowFrame(settings); atomic_set(&fPackagesToShowListID, 0); @@ -233,9 +236,12 @@ MainWindow::MainWindow(const BMessage& settings, const PackageInfoRef& package) fLogOutItem(NULL), fUsersUserUsageConditionsMenuItem(NULL), fModelListener(new MainWindowModelListener(BMessenger(this)), true), - fBulkLoadProcessCoordinator(NULL), + fCoordinator(NULL), fSinglePackageMode(true) { + if ((fCoordinatorRunningSem = create_sem(1, "ProcessCoordinatorSem")) < B_OK) + debugger("unable to create the process coordinator semaphore"); + fFilterView = new FilterView(); fPackageListView = new PackageListView(fModel.Lock()); fPackageInfoView = new PackageInfoView(fModel.Lock(), this); @@ -259,6 +265,10 @@ MainWindow::MainWindow(const BMessage& settings, const PackageInfoRef& package) MainWindow::~MainWindow() { + _SpinUntilProcessCoordinatorComplete(); + delete_sem(fCoordinatorRunningSem); + fCoordinatorRunningSem = 0; + BPackageRoster().StopWatching(this); delete_sem(fPendingActionsSem); @@ -290,7 +300,7 @@ MainWindow::QuitRequested() be_app->PostMessage(&message); - _StopBulkLoad(); + _StopProcessCoordinators(); return true; } @@ -301,8 +311,14 @@ MainWindow::MessageReceived(BMessage* message) { switch (message->what) { case MSG_BULK_LOAD_DONE: - _BulkLoadCompleteReceived(); + { + int64 errorStatus64; + if (message->FindInt64(KEY_ERROR_STATUS, &errorStatus64) == B_OK) + _BulkLoadCompleteReceived((status_t) errorStatus64); + else + printf("! expected [%s] value in message\n", KEY_ERROR_STATUS); break; + } case B_SIMPLE_DATA: case B_REFS_RECEIVED: // TODO: ? @@ -318,6 +334,10 @@ MainWindow::MessageReceived(BMessage* message) _StartBulkLoad(true); break; + case MSG_WORK_STATUS_CLEAR: + _HandleWorkStatusClear(); + break; + case MSG_WORK_STATUS_CHANGE: _HandleWorkStatusChangeMessageReceived(message); break; @@ -347,6 +367,7 @@ MainWindow::MessageReceived(BMessage* message) break; case MSG_AUTHORIZATION_CHANGED: + _StartUserVerify(); _UpdateAuthorization(); break; @@ -529,8 +550,10 @@ MainWindow::MessageReceived(BMessage* message) } } - if (!fSinglePackageMode && (changes & PKG_CHANGED_STATE) != 0) + if (!fSinglePackageMode && (changes & PKG_CHANGED_STATE) != 0 + && fCoordinator == NULL) { fWorkStatusView->PackageStatusChanged(ref); + } } break; } @@ -619,6 +642,18 @@ MainWindow::MessageReceived(BMessage* message) break; } + case MSG_USER_USAGE_CONDITIONS_NOT_LATEST: + { + BMessage userDetailMsg; + if (message->FindMessage("userDetail", &userDetailMsg) != B_OK) { + debugger("expected the [userDetail] data to be carried in the " + "message."); + } + UserDetail userDetail(&userDetailMsg); + _HandleUserUsageConditionsNotLatest(userDetail); + break; + } + default: BWindow::MessageReceived(message); break; @@ -904,68 +939,25 @@ MainWindow::_ClearPackage() } -void -MainWindow::_StopBulkLoad() -{ - AutoLocker lock(&fBulkLoadProcessCoordinatorLock); - - if (fBulkLoadProcessCoordinator != NULL) { - printf("will stop full update process coordinator\n"); - fBulkLoadProcessCoordinator->Stop(); - } -} - - void MainWindow::_StartBulkLoad(bool force) { - AutoLocker lock(&fBulkLoadProcessCoordinatorLock); - - if (fBulkLoadProcessCoordinator == NULL) { - fBulkLoadProcessCoordinator - = ProcessCoordinatorFactory::CreateBulkLoadCoordinator( - this, - // PackageInfoListener - this, - // ProcessCoordinatorListener - &fModel, force); - fBulkLoadProcessCoordinator->Start(); - fRefreshRepositoriesItem->SetEnabled(false); - } -} - - -/*! This method is called when there is some change in the bulk load process. - A change may mean that a new process has started / stopped etc... or it - may mean that the entire coordinator has finished. -*/ - -void -MainWindow::CoordinatorChanged(ProcessCoordinatorState& coordinatorState) -{ - AutoLocker lock(&fBulkLoadProcessCoordinatorLock); - - if (fBulkLoadProcessCoordinator == coordinatorState.Coordinator()) { - if (!coordinatorState.IsRunning()) - _BulkLoadProcessCoordinatorFinished(coordinatorState); - else { - _NotifyWorkStatusChange(coordinatorState.Message(), - coordinatorState.Progress()); - // show the progress to the user. - } - } else { - if (Logger::IsInfoEnabled()) { - printf("unknown process coordinator changed\n"); - } - } + fRefreshRepositoriesItem->SetEnabled(false); + ProcessCoordinator* bulkLoadCoordinator = + ProcessCoordinatorFactory::CreateBulkLoadCoordinator( + this, + // PackageInfoListener + this, + // ProcessCoordinatorListener + &fModel, force); + _AddProcessCoordinator(bulkLoadCoordinator); } void -MainWindow::_BulkLoadProcessCoordinatorFinished( - ProcessCoordinatorState& coordinatorState) +MainWindow::_BulkLoadCompleteReceived(status_t errorStatus) { - if (coordinatorState.ErrorStatus() != B_OK) { + if (errorStatus != B_OK) { AppUtils::NotifySimpleError( B_TRANSLATE("Package update error"), B_TRANSLATE("While updating package data, a problem has arisen " @@ -975,22 +967,25 @@ MainWindow::_BulkLoadProcessCoordinatorFinished( "logs." ALERT_MSG_LOGS_USER_GUIDE)); } - BMessenger messenger(this); - messenger.SendMessage(MSG_BULK_LOAD_DONE); - // it is safe to delete the coordinator here because it is already known - // that all of the processes have completed and their threads will have - // exited safely by this point. - delete fBulkLoadProcessCoordinator; - fBulkLoadProcessCoordinator = NULL; + fRefreshRepositoriesItem->SetEnabled(true); + _AdoptModel(); + _UpdateAvailableRepositories(); } void -MainWindow::_BulkLoadCompleteReceived() +MainWindow::_NotifyWorkStatusClear() { - _AdoptModel(); - _UpdateAvailableRepositories(); + BMessage message(MSG_WORK_STATUS_CLEAR); + this->PostMessage(&message, this); +} + + +void +MainWindow::_HandleWorkStatusClear() +{ + fWorkStatusView->SetText(""); fWorkStatusView->SetIdle(); } @@ -1204,6 +1199,21 @@ MainWindow::_OpenLoginWindow(const BMessage& onSuccessMessage) } +void +MainWindow::_StartUserVerify() +{ + if (!fModel.Nickname().IsEmpty()) { + _AddProcessCoordinator( + ProcessCoordinatorFactory::CreateUserDetailVerifierCoordinator( + this, + // UserDetailVerifierListener + this, + // ProcessCoordinatorListener + &fModel) ); + } +} + + void MainWindow::_UpdateAuthorization() { @@ -1368,4 +1378,188 @@ MainWindow::_ViewUserUsageConditions( UserUsageConditionsWindow* window = new UserUsageConditionsWindow( fModel, mode); window->Show(); +} + + +void +MainWindow::UserCredentialsFailed() +{ + BString message = B_TRANSLATE("The password previously " + "supplied for the user [%Nickname%] is not currently " + "valid. The user will be logged-out of this application " + "and you should login again with your updated password."); + message.ReplaceAll("%Nickname%", fModel.Nickname()); + + AppUtils::NotifySimpleError(B_TRANSLATE("Login issue"), + message); + + { + AutoLocker locker(fModel.Lock()); + fModel.SetNickname(""); + } +} + + +/*! \brief This method is invoked from the UserDetailVerifierProcess on a + background thread. For this reason it lodges a message into itself + which can then be handled on the main thread. +*/ + +void +MainWindow::UserUsageConditionsNotLatest(const UserDetail& userDetail) +{ + BMessage message(MSG_USER_USAGE_CONDITIONS_NOT_LATEST); + BMessage detailsMessage; + if (userDetail.Archive(&detailsMessage, true) != B_OK + || message.AddMessage("userDetail", &detailsMessage) != B_OK) { + printf("!! unable to archive the user detail into a message\n"); + } + else + BMessenger(this).SendMessage(&message); +} + + +void +MainWindow::_HandleUserUsageConditionsNotLatest( + const UserDetail& userDetail) +{ + ToLatestUserUsageConditionsWindow* window = + new ToLatestUserUsageConditionsWindow(this, fModel, userDetail); + window->Show(); +} + + +void +MainWindow::_AddProcessCoordinator(ProcessCoordinator* item) +{ + AutoLocker lock(&fCoordinatorLock); + + if (fCoordinator == NULL) { + if (acquire_sem(fCoordinatorRunningSem) != B_OK) + debugger("unable to acquire the process coordinator sem"); + if (Logger::IsInfoEnabled()) { + printf("adding and starting a process coordinator [%s]\n", + item->Name().String()); + } + fCoordinator = item; + fCoordinator->Start(); + } + else { + if (Logger::IsInfoEnabled()) { + printf("adding process coordinator [%s] to the queue\n", + item->Name().String()); + } + fCoordinatorQueue.push(item); + } +} + + +void +MainWindow::_SpinUntilProcessCoordinatorComplete() +{ + while (true) { + if (acquire_sem(fCoordinatorRunningSem) != B_OK) + debugger("unable to acquire the process coordinator sem"); + if (release_sem(fCoordinatorRunningSem) != B_OK) + debugger("unable to release the process coordinator sem"); + { + AutoLocker lock(&fCoordinatorLock); + if (fCoordinator == NULL) + return; + } + } +} + + +void +MainWindow::_StopProcessCoordinators() +{ + if (Logger::IsInfoEnabled()) + printf("will stop all process coordinators\n"); + + { + AutoLocker lock(&fCoordinatorLock); + + while (!fCoordinatorQueue.empty()) { + ProcessCoordinator *processCoordinator = fCoordinatorQueue.front(); + if (Logger::IsInfoEnabled()) { + printf("will drop queued process coordinator [%s]\n", + processCoordinator->Name().String()); + } + fCoordinatorQueue.pop(); + delete processCoordinator; + } + + if (fCoordinator != NULL) { + fCoordinator->Stop(); + } + } + + if (Logger::IsInfoEnabled()) + printf("will wait until the process coordinator has stopped\n"); + + _SpinUntilProcessCoordinatorComplete(); + + if (Logger::IsInfoEnabled()) + printf("did stop all process coordinators\n"); +} + + +/*! This method is called when there is some change in the bulk load process + or other process coordinator. + A change may mean that a new process has started / stopped etc... or it + may mean that the entire coordinator has finished. +*/ + +void +MainWindow::CoordinatorChanged(ProcessCoordinatorState& coordinatorState) +{ + AutoLocker lock(&fCoordinatorLock); + + if (fCoordinator == coordinatorState.Coordinator()) { + if (!coordinatorState.IsRunning()) { + if (release_sem(fCoordinatorRunningSem) != B_OK) + debugger("unable to release the process coordinator sem"); + if (Logger::IsInfoEnabled()) { + printf("process coordinator [%s] did complete\n", + fCoordinator->Name().String()); + } + // complete the last one that just finished + BMessage* message = fCoordinator->Message(); + + if (message != NULL) { + BMessenger messenger(this); + message->AddInt64(KEY_ERROR_STATUS, + (int64) fCoordinator->ErrorStatus()); + messenger.SendMessage(message); + } + + delete fCoordinator; + fCoordinator = NULL; + + // now schedule the next one. + if (!fCoordinatorQueue.empty()) { + if (acquire_sem(fCoordinatorRunningSem) != B_OK) + debugger("unable to acquire the process coordinator sem"); + fCoordinator = fCoordinatorQueue.front(); + if (Logger::IsInfoEnabled()) { + printf("starting next process coordinator [%s]\n", + fCoordinator->Name().String()); + } + fCoordinatorQueue.pop(); + fCoordinator->Start(); + } + else { + _NotifyWorkStatusClear(); + } + } + else { + _NotifyWorkStatusChange(coordinatorState.Message(), + coordinatorState.Progress()); + // show the progress to the user. + } + } else { + if (Logger::IsInfoEnabled()) + printf("! unknown process coordinator changed\n"); + } } \ No newline at end of file diff --git a/src/apps/haikudepot/ui/MainWindow.h b/src/apps/haikudepot/ui/MainWindow.h index 2c403d36ae..17cca4cfa1 100644 --- a/src/apps/haikudepot/ui/MainWindow.h +++ b/src/apps/haikudepot/ui/MainWindow.h @@ -2,7 +2,7 @@ * Copyright 2013-2014, Stephan Aßmus . * Copyright 2013, Rene Gollent . * Copyright 2017, Julian Harnath . - * Copyright 2017-2019, Andrew Lindesay . + * Copyright 2017-2020, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef MAIN_WINDOW_H @@ -10,6 +10,8 @@ #include +#include + #include "HaikuDepotConstants.h" #include "Model.h" #include "PackageAction.h" @@ -17,6 +19,8 @@ #include "ProcessCoordinator.h" #include "PackageInfoListener.h" #include "TabView.h" +#include "UserDetail.h" +#include "UserDetailVerifierProcess.h" class BCardLayout; @@ -33,7 +37,8 @@ class WorkStatusView; class MainWindow : public BWindow, private PackageInfoListener, - private PackageActionHandler, public ProcessCoordinatorListener { + private PackageActionHandler, public ProcessCoordinatorListener, + public UserDetailVerifierListener { public: MainWindow(const BMessage& settings); MainWindow(const BMessage& settings, @@ -49,6 +54,11 @@ public: // ProcessCoordinatorListener virtual void CoordinatorChanged( ProcessCoordinatorState& coordinatorState); + + // UserDetailVerifierProcessListener + virtual void UserCredentialsFailed(); + virtual void UserUsageConditionsNotLatest( + const UserDetail& userDetail); private: // PackageInfoListener virtual void PackageChanged( @@ -61,9 +71,11 @@ private: virtual Model* GetModel(); private: - void _BulkLoadProcessCoordinatorFinished( - ProcessCoordinatorState& - processCoordinatorState); + void _AddProcessCoordinator( + ProcessCoordinator* item); + void _StopProcessCoordinators(); + void _SpinUntilProcessCoordinatorComplete(); + bool _SelectedPackageHasWebAppRepositoryCode(); void _BuildMenu(BMenuBar* menuBar); @@ -80,9 +92,11 @@ private: void _ClearPackage(); void _PopulatePackageAsync(bool forcePopulate); - void _StopBulkLoad(); void _StartBulkLoad(bool force = false); - void _BulkLoadCompleteReceived(); + void _BulkLoadCompleteReceived(status_t errorStatus); + + void _NotifyWorkStatusClear(); + void _HandleWorkStatusClear(); void _NotifyWorkStatusChange(const BString& text, float progress); @@ -96,6 +110,7 @@ private: void _OpenLoginWindow( const BMessage& onSuccessMessage); + void _StartUserVerify(); void _UpdateAuthorization(); void _UpdateAvailableRepositories(); void _RatePackage(); @@ -104,6 +119,9 @@ private: void _ViewUserUsageConditions( UserUsageConditionsSelectionMode mode); + void _HandleUserUsageConditionsNotLatest( + const UserDetail& userDetail); + private: FilterView* fFilterView; TabView* fListTabs; @@ -131,8 +149,12 @@ private: Model fModel; ModelListenerRef fModelListener; PackageList fVisiblePackages; - ProcessCoordinator* fBulkLoadProcessCoordinator; - BLocker fBulkLoadProcessCoordinatorLock; + + std::queue + fCoordinatorQueue; + ProcessCoordinator* fCoordinator; + BLocker fCoordinatorLock; + sem_id fCoordinatorRunningSem; bool fSinglePackageMode; diff --git a/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.cpp b/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.cpp new file mode 100644 index 0000000000..430c7a280b --- /dev/null +++ b/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.cpp @@ -0,0 +1,448 @@ +/* + * Copyright 2020, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ + +#include "ToLatestUserUsageConditionsWindow.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AppUtils.h" +#include "LinkView.h" +#include "LocaleUtils.h" +#include "Logger.h" +#include "Model.h" +#include "UserUsageConditionsWindow.h" +#include "ServerHelper.h" +#include "WebAppInterface.h" + +#undef B_TRANSLATION_CONTEXT +#define B_TRANSLATION_CONTEXT "ToLatestUserUsageConditionsWindow" + +#define PLACEHOLDER_TEXT B_UTF8_ELLIPSIS + +#define WINDOW_FRAME BRect(0, 0, 500, 280) + +#define KEY_USER_USAGE_CONDITIONS "userUsageConditions" + +#define NO_PRIOR_MESSAGE_TEXT "The user [%Nickname%] has authenticated, but " \ + "before proceeding, you are required to agree to the most recent usage " \ + "conditions." + +#define PRIOR_MESSAGE_TEXT "The user \"%Nickname%\" has previously agreed to " \ + "usage conditions, but the usage conditions have been updated since. " \ + "The updated usage conditions now need to be agreed to." + +enum { + MSG_AGREE = 'agre', + MSG_AGREE_FAILED = 'agfa', + MSG_AGREE_MINIMUM_AGE_TOGGLE = 'amat', + MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE = 'auct' +}; + + +ToLatestUserUsageConditionsWindow::ToLatestUserUsageConditionsWindow( + BWindow* parent, + Model& model, const UserDetail& userDetail) + : + BWindow(WINDOW_FRAME, B_TRANSLATE("Update usage conditions"), + B_FLOATING_WINDOW_LOOK, B_MODAL_SUBSET_WINDOW_FEEL, + B_ASYNCHRONOUS_CONTROLS | B_AUTO_UPDATE_SIZE_LIMITS + | B_NOT_RESIZABLE | B_NOT_ZOOMABLE | B_NOT_CLOSABLE ), + fModel(model), + fUserDetail(userDetail), + fWorkerThread(-1), + fQuitRequestedDuringWorkerThread(false), + fMutableControlsEnabled(false) +{ + AddToSubset(parent); + _InitUiControls(); + + // some layout magic happening here. If the checkboxes are put directly + // into the main vertical-group then the window tries to shrink to the + // preferred size of the checkboxes. To avoid this, a grid is used to house + // the checkboxes and a fake extra grid column is added to the right of the + // checkboxes which prevents the window from reducing in size to meet the + // checkboxes. + + BLayoutBuilder::Group<>(this, B_VERTICAL) + .AddGrid() + .Add(fMessageTextView, 0, 0, 2) + .Add(fConfirmMinimumAgeCheckBox, 0, 1, 1) + .Add(fConfirmUserUsageConditionsCheckBox, 0, 2, 1) + .Add(fUserUsageConditionsLink, 0, 3, 2) + .End() + .Add(new BSeparatorView(B_HORIZONTAL)) + // rule off + .AddGroup(B_HORIZONTAL, 1) + .AddGlue() + .Add(fLogoutButton) + .Add(fAgreeButton) + .End() + .Add(fWorkerIndicator, 1) + .SetInsets(B_USE_WINDOW_INSETS); + + CenterOnScreen(); + + _FetchData(); + // start a new thread to pull down the user usage conditions data. +} + + +ToLatestUserUsageConditionsWindow::~ToLatestUserUsageConditionsWindow() +{ + BAutolock locker(&fLock); + + if (fWorkerThread >= 0) + wait_for_thread(fWorkerThread, NULL); +} + + +void +ToLatestUserUsageConditionsWindow::_InitUiControls() +{ + fMessageTextView = new BTextView("message text view"); + fMessageTextView->AdoptSystemColors(); + fMessageTextView->MakeEditable(false); + fMessageTextView->MakeSelectable(false); + BString message; + if (fUserDetail.Agreement().Code().IsEmpty()) + message = B_TRANSLATE(NO_PRIOR_MESSAGE_TEXT); + else + message = B_TRANSLATE(PRIOR_MESSAGE_TEXT); + message.ReplaceAll("%Nickname%", fUserDetail.Nickname()); + fMessageTextView->SetText(message); + + fConfirmMinimumAgeCheckBox = new BCheckBox("confirm minimum age", + PLACEHOLDER_TEXT, + // is filled in when the user usage conditions data is available + new BMessage(MSG_AGREE_MINIMUM_AGE_TOGGLE)); + + fConfirmUserUsageConditionsCheckBox = new BCheckBox( + "confirm usage conditions", + B_TRANSLATE("I agree to the usage conditions"), + new BMessage(MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE)); + + fUserUsageConditionsLink = new LinkView("usage conditions view", + B_TRANSLATE("View the usage conditions"), + new BMessage(MSG_VIEW_LATEST_USER_USAGE_CONDITIONS)); + fUserUsageConditionsLink->SetTarget(this); + + fLogoutButton = new BButton("logout", B_TRANSLATE("Logout"), + new BMessage(MSG_LOG_OUT)); + fAgreeButton = new BButton("agree", B_TRANSLATE("Agree"), + new BMessage(MSG_AGREE)); + + fWorkerIndicator = new BarberPole("fetch data worker indicator"); + BSize workerIndicatorSize; + workerIndicatorSize.SetHeight(20); + fWorkerIndicator->SetExplicitSize(workerIndicatorSize); + + fMutableControlsEnabled = false; + _EnableMutableControls(); +} + + +void +ToLatestUserUsageConditionsWindow::_EnableMutableControls() +{ + bool ageChecked = fConfirmMinimumAgeCheckBox->Value() == 1; + bool conditionsChecked = fConfirmUserUsageConditionsCheckBox->Value() == 1; + fUserUsageConditionsLink->SetEnabled(fMutableControlsEnabled); + fAgreeButton->SetEnabled(fMutableControlsEnabled && ageChecked + && conditionsChecked); + fLogoutButton->SetEnabled(fMutableControlsEnabled); + fConfirmUserUsageConditionsCheckBox->SetEnabled(fMutableControlsEnabled); + fConfirmMinimumAgeCheckBox->SetEnabled(fMutableControlsEnabled); +} + + +void +ToLatestUserUsageConditionsWindow::MessageReceived(BMessage* message) +{ + switch (message->what) { + case MSG_USER_USAGE_CONDITIONS_DATA: + { + BMessage userUsageConditionsMessage; + message->FindMessage(KEY_USER_USAGE_CONDITIONS, + &userUsageConditionsMessage); + UserUsageConditions userUsageConditions( + &userUsageConditionsMessage); + _DisplayData(userUsageConditions); + fWorkerIndicator->Stop(); + break; + } + case MSG_LOG_OUT: + _HandleLogout(); + break; + case MSG_AGREE: + _HandleAgree(); + break; + case MSG_AGREE_FAILED: + _HandleAgreeFailed(); + break; + case MSG_VIEW_LATEST_USER_USAGE_CONDITIONS: + _HandleViewUserUsageConditions(); + break; + case MSG_AGREE_MINIMUM_AGE_TOGGLE: + case MSG_AGREE_USER_USAGE_CONDITIONS_TOGGLE: + _EnableMutableControls(); + break; + default: + BWindow::MessageReceived(message); + break; + } +} + + +bool +ToLatestUserUsageConditionsWindow::QuitRequested() +{ + BAutolock locker(&fLock); + + if (fWorkerThread >= 0) { + if (Logger::IsDebugEnabled()) + printf("quit requested while worker thread is operating -- will " + "try again once the worker thread has completed\n"); + fQuitRequestedDuringWorkerThread = true; + return false; + } + + return true; +} + + +void +ToLatestUserUsageConditionsWindow::_SetWorkerThread(thread_id thread) +{ + if (thread >= 0) { + fWorkerThread = thread; + resume_thread(fWorkerThread); + } else { + fWorkerThread = -1; + if (fQuitRequestedDuringWorkerThread) + BMessenger(this).SendMessage(B_QUIT_REQUESTED); + fQuitRequestedDuringWorkerThread = false; + } +} + + +void +ToLatestUserUsageConditionsWindow::_SetWorkerThreadLocked(thread_id thread) +{ + BAutolock locker(&fLock); + _SetWorkerThread(thread); +} + + +/*! This method is called on the main thread in order to initiate the background + processing to obtain the user usage conditions data. It will take + responsibility for coordinating the creation of the thread and starting the + thread etc... +*/ + +void +ToLatestUserUsageConditionsWindow::_FetchData() +{ + { + BAutolock locker(&fLock); + if (-1 != fWorkerThread) { + debugger("illegal state - attempt to fetch, but thread in " + "progress"); + } + } + + thread_id thread = spawn_thread(&_FetchDataThreadEntry, + "Fetch usage conditions data", B_NORMAL_PRIORITY, this); + if (thread >= 0) { + fWorkerIndicator->Start(); + _SetWorkerThreadLocked(thread); + } else { + debugger("unable to start a thread to fetch the user usage " + "conditions."); + } +} + + +/*! This method is called from the thread; it is + the entry-point for the background processing to obtain the user usage + conditions. +*/ + +/*static*/ int32 +ToLatestUserUsageConditionsWindow::_FetchDataThreadEntry(void* data) +{ + ToLatestUserUsageConditionsWindow* win + = reinterpret_cast(data); + win->_FetchDataPerform(); + return 0; +} + + +/*! This method will perform the task of obtaining data about the user usage + conditions. +*/ + +void +ToLatestUserUsageConditionsWindow::_FetchDataPerform() +{ + UserUsageConditions conditions; + WebAppInterface interface = fModel.GetWebAppInterface(); + + if (interface.RetrieveUserUsageConditions("", conditions) == B_OK) { + BMessage userUsageConditionsMessage; + conditions.Archive(&userUsageConditionsMessage, true); + BMessage dataMessage(MSG_USER_USAGE_CONDITIONS_DATA); + dataMessage.AddMessage(KEY_USER_USAGE_CONDITIONS, + &userUsageConditionsMessage); + BMessenger(this).SendMessage(&dataMessage); + } else { + _NotifyFetchProblem(); + BMessenger(this).SendMessage(B_QUIT_REQUESTED); + } + + _SetWorkerThreadLocked(-1); +} + + +void +ToLatestUserUsageConditionsWindow::_NotifyFetchProblem() +{ + AppUtils::NotifySimpleError( + B_TRANSLATE("Usage conditions download problem"), + B_TRANSLATE("An error has arisen downloading the usage " + "conditions. Check the log for details and try again. " + ALERT_MSG_LOGS_USER_GUIDE)); +} + + +void +ToLatestUserUsageConditionsWindow::_Agree() +{ + { + BAutolock locker(&fLock); + if (-1 != fWorkerThread) { + debugger("illegal state - attempt to agree, but thread in " + "progress"); + } + } + + fMutableControlsEnabled = false; + _EnableMutableControls(); + thread_id thread = spawn_thread(&_AgreeThreadEntry, + "Agree usage conditions", B_NORMAL_PRIORITY, this); + if (thread >= 0) { + fWorkerIndicator->Start(); + _SetWorkerThreadLocked(thread); + } else { + debugger("unable to start a thread to fetch the user usage " + "conditions."); + } +} + + +/*static*/ int32 +ToLatestUserUsageConditionsWindow::_AgreeThreadEntry(void* data) +{ + ToLatestUserUsageConditionsWindow* win + = reinterpret_cast(data); + win->_AgreePerform(); + return 0; +} + + +void +ToLatestUserUsageConditionsWindow::_AgreePerform() +{ + BMessenger messenger(this); + BMessage responsePayload; + WebAppInterface webApp = fModel.GetWebAppInterface(); + status_t result = webApp.AgreeUserUsageConditions( + fUserUsageConditions.Code(), responsePayload); + + if (result != B_OK) { + ServerHelper::NotifyTransportError(result); + messenger.SendMessage(MSG_AGREE_FAILED); + } else { + int32 errorCode = WebAppInterface::ErrorCodeFromResponse( + responsePayload); + if (errorCode == ERROR_CODE_NONE) { + AppUtils::NotifySimpleError( + B_TRANSLATE("Usage conditions agreed"), + B_TRANSLATE("The current usage conditions have been agreed " + "to.")); + messenger.SendMessage(B_QUIT_REQUESTED); + } + else { + AutoLocker locker(fModel.Lock()); + ServerHelper::NotifyServerJsonRpcError(responsePayload); + messenger.SendMessage(MSG_AGREE_FAILED); + } + } + + _SetWorkerThreadLocked(-1); +} + + +void +ToLatestUserUsageConditionsWindow::_HandleAgreeFailed() +{ + fWorkerIndicator->Stop(); + fMutableControlsEnabled = true; + _EnableMutableControls(); +} + + +void +ToLatestUserUsageConditionsWindow::_DisplayData( + const UserUsageConditions& userUsageConditions) +{ + fUserUsageConditions = userUsageConditions; + fConfirmMinimumAgeCheckBox->SetLabel( + LocaleUtils::CreateTranslatedIAmMinimumAgeSlug( + fUserUsageConditions.MinimumAge())); + fMutableControlsEnabled = true; + _EnableMutableControls(); +} + + +void +ToLatestUserUsageConditionsWindow::_HandleViewUserUsageConditions() +{ + if (!fUserUsageConditions.Code().IsEmpty()) { + UserUsageConditionsWindow* window = new UserUsageConditionsWindow( + fModel, fUserUsageConditions); + window->Show(); + } +} + +void +ToLatestUserUsageConditionsWindow::_HandleLogout() +{ + AutoLocker locker(fModel.Lock()); + fModel.SetNickname(""); + BMessenger(this).SendMessage(B_QUIT_REQUESTED); +} + + +void +ToLatestUserUsageConditionsWindow::_HandleAgree() +{ + bool ageChecked = fConfirmMinimumAgeCheckBox->Value() == 1; + bool conditionsChecked = fConfirmUserUsageConditionsCheckBox->Value() == 1; + + // precondition that the user has checked both of the checkboxes. + if (!ageChecked || !conditionsChecked) + debugger("the user has not agreed to the age and conditions"); + + _Agree(); +} \ No newline at end of file diff --git a/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.h b/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.h new file mode 100644 index 0000000000..43dfdf47b4 --- /dev/null +++ b/src/apps/haikudepot/ui/ToLatestUserUsageConditionsWindow.h @@ -0,0 +1,77 @@ +/* + * Copyright 2019-2020, Andrew Lindesay . + * All rights reserved. Distributed under the terms of the MIT License. + */ +#ifndef TO_LATEST_USER_USAGE_CONDITIONS_WINDOW_H +#define TO_LATEST_USER_USAGE_CONDITIONS_WINDOW_H + +#include +#include +#include + +#include "BarberPole.h" +#include "HaikuDepotConstants.h" +#include "UserDetail.h" +#include "UserUsageConditions.h" + +class BButton; +class BCheckBox; +class BTextView; +class LinkView; +class Model; + + +class ToLatestUserUsageConditionsWindow : public BWindow { +public: + ToLatestUserUsageConditionsWindow( + BWindow* parent, + Model& model, const UserDetail& userDetail); + virtual ~ToLatestUserUsageConditionsWindow(); + + virtual void MessageReceived(BMessage* message); + virtual bool QuitRequested(); + +private: + void _EnableMutableControls(); + void _InitUiControls(); + + void _DisplayData(const UserUsageConditions& + userUsageConditions); + void _HandleViewUserUsageConditions(); + void _HandleLogout(); + void _HandleAgree(); + void _HandleAgreeFailed(); + + void _SetWorkerThread(thread_id thread); + void _SetWorkerThreadLocked(thread_id thread); + + void _FetchData(); + static int32 _FetchDataThreadEntry(void* data); + void _FetchDataPerform(); + void _NotifyFetchProblem(); + + void _Agree(); + static int32 _AgreeThreadEntry(void* data); + void _AgreePerform(); + +private: + UserUsageConditions fUserUsageConditions; + Model& fModel; + UserDetail fUserDetail; + + BTextView* fMessageTextView; + BButton* fLogoutButton; + BButton* fAgreeButton; + BCheckBox* fConfirmMinimumAgeCheckBox; + BCheckBox* fConfirmUserUsageConditionsCheckBox; + LinkView* fUserUsageConditionsLink; + BarberPole* fWorkerIndicator; + + BLocker fLock; + thread_id fWorkerThread; + bool fQuitRequestedDuringWorkerThread; + bool fMutableControlsEnabled; +}; + + +#endif // TO_LATEST_USER_USAGE_CONDITIONS_WINDOW_H diff --git a/src/apps/haikudepot/ui/UserLoginWindow.cpp b/src/apps/haikudepot/ui/UserLoginWindow.cpp index 23b90c9a1d..a6b4e9c32d 100644 --- a/src/apps/haikudepot/ui/UserLoginWindow.cpp +++ b/src/apps/haikudepot/ui/UserLoginWindow.cpp @@ -1,6 +1,6 @@ /* * Copyright 2014, Stephan Aßmus . - * Copyright 2019, Andrew Lindesay . + * Copyright 2019-2020, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -29,6 +29,7 @@ #include "HaikuDepotConstants.h" #include "LanguageMenuUtils.h" #include "LinkView.h" +#include "LocaleUtils.h" #include "Logger.h" #include "Model.h" #include "ServerHelper.h" @@ -942,13 +943,9 @@ UserLoginWindow::_SetUserUsageConditions( fUserUsageConditions = userUsageConditions; if (fUserUsageConditions != NULL) { - BString minimumAgeString; - minimumAgeString.SetToFormat("%" B_PRId8, - fUserUsageConditions->MinimumAge()); - BString label = B_TRANSLATE( - "I am %MinimumAgeYears% years of age or older"); - label.ReplaceAll("%MinimumAgeYears%", minimumAgeString); - fConfirmMinimumAgeCheckBox->SetLabel(label); + fConfirmMinimumAgeCheckBox->SetLabel( + LocaleUtils::CreateTranslatedIAmMinimumAgeSlug( + fUserUsageConditions->MinimumAge())); } else { fConfirmMinimumAgeCheckBox->SetLabel(PLACEHOLDER_TEXT); fConfirmMinimumAgeCheckBox->SetValue(0); diff --git a/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp b/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp index dc7bd042f4..a6c062b165 100644 --- a/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp +++ b/src/apps/haikudepot/ui/UserUsageConditionsWindow.cpp @@ -104,7 +104,7 @@ UserUsageConditionsWindow::UserUsageConditionsWindow( fWorkerIndicator = new BarberPole("fetch data worker indicator"); BSize workerIndicatorSize; workerIndicatorSize.SetHeight(20); - fWorkerIndicator->SetExplicitMinSize(workerIndicatorSize); + fWorkerIndicator->SetExplicitSize(workerIndicatorSize); fIntroductionTextView = new BTextView("introduction text view"); fIntroductionTextView->AdoptSystemColors(); diff --git a/src/apps/haikudepot/ui/WorkStatusView.cpp b/src/apps/haikudepot/ui/WorkStatusView.cpp index 7e5ade2039..d96bbc6c28 100644 --- a/src/apps/haikudepot/ui/WorkStatusView.cpp +++ b/src/apps/haikudepot/ui/WorkStatusView.cpp @@ -1,5 +1,6 @@ /* * Copyright 2017 Julian Harnath + * Copyright 2020 Andrew Lindesay * All rights reserved. Distributed under the terms of the MIT license. */ @@ -22,6 +23,12 @@ #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "WorkStatusView" +#define VIEW_INDEX_BARBER_POLE (int32) 0 +#define VIEW_INDEX_PROGRESS_BAR (int32) 1 + + +static const BSize kStatusBarSize = BSize(100,20); + WorkStatusView::WorkStatusView(const char* name) : @@ -36,8 +43,10 @@ WorkStatusView::WorkStatusView(const char* name) fProgressLayout->AddView(fBarberPole); fProgressLayout->AddView(fProgressBar); + fBarberPole->SetExplicitSize(kStatusBarSize); fProgressBar->SetMaxValue(1.0f); - fProgressBar->SetBarHeight(20); + fProgressBar->SetBarHeight(kStatusBarSize.Height()); + fProgressBar->SetExplicitSize(kStatusBarSize); fStatusText->SetFontSize(be_plain_font->Size() * 0.9f); @@ -70,8 +79,8 @@ void WorkStatusView::SetBusy() { fBarberPole->Start(); - if (fProgressLayout->VisibleIndex() != 0) - fProgressLayout->SetVisibleItem((int32)0); + if (fProgressLayout->VisibleIndex() != VIEW_INDEX_BARBER_POLE) + fProgressLayout->SetVisibleItem(VIEW_INDEX_BARBER_POLE); } @@ -79,7 +88,7 @@ void WorkStatusView::SetIdle() { fBarberPole->Stop(); - fProgressLayout->SetVisibleItem((int32)0); + fProgressLayout->SetVisibleItem(VIEW_INDEX_BARBER_POLE); SetText(NULL); } @@ -88,8 +97,8 @@ void WorkStatusView::SetProgress(float value) { fProgressBar->SetTo(value); - if (fProgressLayout->VisibleIndex() != 1) - fProgressLayout->SetVisibleItem(1); + if (fProgressLayout->VisibleIndex() != VIEW_INDEX_PROGRESS_BAR) + fProgressLayout->SetVisibleItem(VIEW_INDEX_PROGRESS_BAR); } diff --git a/src/apps/haikudepot/util/LocaleUtils.cpp b/src/apps/haikudepot/util/LocaleUtils.cpp index 3cb31963f5..46eb7bced4 100644 --- a/src/apps/haikudepot/util/LocaleUtils.cpp +++ b/src/apps/haikudepot/util/LocaleUtils.cpp @@ -9,10 +9,16 @@ #include #include +#include #include #include #include #include +#include + + +#undef B_TRANSLATION_CONTEXT +#define B_TRANSLATION_CONTEXT "LocaleUtils" BCollator* LocaleUtils::sSharedCollator = NULL; @@ -74,3 +80,20 @@ LocaleUtils::TimestampToDateTimeString(uint64 millis) icuResult.toUTF8(converter); return result; } + + +/*! This is used in situations where the user is required to confirm that they + are as old or older than some minimal age. This is associated with agreeing + to the user usage conditions. +*/ + +/*static*/ BString +LocaleUtils::CreateTranslatedIAmMinimumAgeSlug(int minimumAge) +{ + BString slug; + static BStringFormat format(B_TRANSLATE("{0, plural," + "one{I am at least one year old}" + "other{I am # years of age or older}}")); + format.Format(slug, minimumAge); + return slug; +} diff --git a/src/apps/haikudepot/util/LocaleUtils.h b/src/apps/haikudepot/util/LocaleUtils.h index caf718a066..bcf554f688 100644 --- a/src/apps/haikudepot/util/LocaleUtils.h +++ b/src/apps/haikudepot/util/LocaleUtils.h @@ -18,6 +18,8 @@ public: static BCollator* GetSharedCollator(); static BString TimestampToDateTimeString(uint64 millis); + static BString CreateTranslatedIAmMinimumAgeSlug(int minimumAge); + private: static void GetCollator(BCollator* collator);