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);