From aae8eafcf7d07846e06c84d1163f0676539b2e6e Mon Sep 17 00:00:00 2001 From: "r.kuznetsov" Date: Tue, 10 Oct 2017 17:21:27 +0300 Subject: [PATCH] Added UGC uploading --- android/jni/com/mapswithme/maps/Framework.cpp | 5 + android/jni/com/mapswithme/maps/Framework.hpp | 1 + android/jni/com/mapswithme/maps/ugc/UGC.cpp | 6 ++ .../com/mapswithme/maps/MwmApplication.java | 2 + .../maps/background/WorkerService.java | 21 ++++ android/src/com/mapswithme/maps/ugc/UGC.java | 21 ++++ iphone/Maps/Classes/MapsAppDelegate.h | 1 + iphone/Maps/Classes/MapsAppDelegate.mm | 35 ++++++- map/framework.cpp | 10 ++ map/framework.hpp | 4 + map/user.cpp | 95 ++++++++++++++++--- map/user.hpp | 18 +++- qt/mainwindow.cpp | 3 + 13 files changed, 207 insertions(+), 15 deletions(-) diff --git a/android/jni/com/mapswithme/maps/Framework.cpp b/android/jni/com/mapswithme/maps/Framework.cpp index 6fa086389e..fb1d22ce4b 100644 --- a/android/jni/com/mapswithme/maps/Framework.cpp +++ b/android/jni/com/mapswithme/maps/Framework.cpp @@ -593,6 +593,11 @@ void Framework::SetUGCUpdate(FeatureID const & fid, ugc::UGCUpdate const & ugc) m_work.GetUGCApi()->SetUGCUpdate(fid, ugc); } +void Framework::UploadUGC() +{ + m_work.UploadUGC(nullptr /* onCompleteUploading */); +} + uint64_t Framework::GetRentNearby(JNIEnv * env, jobject policy, ms::LatLon const & latlon, cian::Api::RentNearbyCallback const & onSuccess, cian::Api::ErrorCallback const & onError) diff --git a/android/jni/com/mapswithme/maps/Framework.hpp b/android/jni/com/mapswithme/maps/Framework.hpp index 952eed8d9e..96b1edd510 100644 --- a/android/jni/com/mapswithme/maps/Framework.hpp +++ b/android/jni/com/mapswithme/maps/Framework.hpp @@ -202,6 +202,7 @@ namespace android void RequestUGC(FeatureID const & fid, ugc::Api::UGCCallback const & ugcCallback); void SetUGCUpdate(FeatureID const & fid, ugc::UGCUpdate const & ugc); + void UploadUGC(); uint64_t GetRentNearby(JNIEnv * env, jobject policy, ms::LatLon const & latlon, cian::Api::RentNearbyCallback const & onSuccess, diff --git a/android/jni/com/mapswithme/maps/ugc/UGC.cpp b/android/jni/com/mapswithme/maps/ugc/UGC.cpp index aaabbbb60e..646c312dbb 100644 --- a/android/jni/com/mapswithme/maps/ugc/UGC.cpp +++ b/android/jni/com/mapswithme/maps/ugc/UGC.cpp @@ -237,4 +237,10 @@ void JNICALL Java_com_mapswithme_maps_ugc_UGC_setUGCUpdate(JNIEnv * env, jclass ugc::UGCUpdate update = g_bridge.ToNativeUGCUpdate(env, ugcUpdate); g_framework->SetUGCUpdate(fid, update); } + +JNIEXPORT +void JNICALL Java_com_mapswithme_maps_ugc_UGC_nativeUploadUGC(JNIEnv * env, jclass /* clazz */) +{ + g_framework->UploadUGC(); +} } diff --git a/android/src/com/mapswithme/maps/MwmApplication.java b/android/src/com/mapswithme/maps/MwmApplication.java index d0bd07b110..8ebf56526d 100644 --- a/android/src/com/mapswithme/maps/MwmApplication.java +++ b/android/src/com/mapswithme/maps/MwmApplication.java @@ -27,6 +27,7 @@ import com.mapswithme.maps.routing.RoutingController; import com.mapswithme.maps.settings.StoragePathManager; import com.mapswithme.maps.sound.TtsPlayer; import com.mapswithme.maps.traffic.TrafficManager; +import com.mapswithme.maps.ugc.UGC; import com.mapswithme.util.Config; import com.mapswithme.util.Constants; import com.mapswithme.util.Counters; @@ -206,6 +207,7 @@ public class MwmApplication extends Application mBackgroundTracker.addListener(mBackgroundListener); TrackRecorder.init(); Editor.init(); + UGC.init(); mPlatformInitialized = true; } diff --git a/android/src/com/mapswithme/maps/background/WorkerService.java b/android/src/com/mapswithme/maps/background/WorkerService.java index 02cd6128e0..756a0734cd 100644 --- a/android/src/com/mapswithme/maps/background/WorkerService.java +++ b/android/src/com/mapswithme/maps/background/WorkerService.java @@ -15,6 +15,7 @@ import com.mapswithme.maps.downloader.CountryItem; import com.mapswithme.maps.downloader.MapManager; import com.mapswithme.maps.editor.Editor; import com.mapswithme.maps.location.LocationHelper; +import com.mapswithme.maps.ugc.UGC; import com.mapswithme.util.CrashlyticsUtils; import com.mapswithme.util.PermissionsUtils; import com.mapswithme.util.concurrency.UiThread; @@ -29,6 +30,7 @@ public class WorkerService extends IntentService private static final String TAG = WorkerService.class.getSimpleName(); private static final String ACTION_CHECK_LOCATIION = "com.mapswithme.maps.action.check_location"; private static final String ACTION_UPLOAD_OSM_CHANGES = "com.mapswithme.maps.action.upload_osm_changes"; + private static final String ACTION_UPLOAD_UGC = "com.mapswithme.maps.action.upload_ugc"; private static final SharedPreferences PREFS = MwmApplication.prefs(); @@ -53,6 +55,16 @@ public class WorkerService extends IntentService MwmApplication.get().startService(intent); } + /** + * Starts this service to upload UGC to our servers. + */ + public static void startActionUploadUGC() + { + final Intent intent = new Intent(MwmApplication.get(), WorkerService.class); + intent.setAction(WorkerService.ACTION_UPLOAD_UGC); + MwmApplication.get().startService(intent); + } + public WorkerService() { super("WorkerService"); @@ -77,6 +89,10 @@ public class WorkerService extends IntentService case ACTION_UPLOAD_OSM_CHANGES: handleActionUploadOsmChanges(); break; + + case ACTION_UPLOAD_UGC: + handleUploadUGC(); + break; } } } @@ -124,6 +140,11 @@ public class WorkerService extends IntentService Editor.uploadChanges(); } + private static void handleUploadUGC() + { + UGC.nativeUploadUGC(); + } + @android.support.annotation.UiThread private static boolean processLocation() { diff --git a/android/src/com/mapswithme/maps/ugc/UGC.java b/android/src/com/mapswithme/maps/ugc/UGC.java index 3c30edbe9c..6b0c4d2879 100644 --- a/android/src/com/mapswithme/maps/ugc/UGC.java +++ b/android/src/com/mapswithme/maps/ugc/UGC.java @@ -6,6 +6,9 @@ import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.mapswithme.maps.MwmApplication; +import com.mapswithme.maps.background.AppBackgroundTracker; +import com.mapswithme.maps.background.WorkerService; import com.mapswithme.maps.bookmarks.data.FeatureId; import java.io.Serializable; @@ -34,6 +37,17 @@ public class UGC static final int RATING_EXCELLENT = 5; static final int RATING_COMING_SOON = 6; + private static final AppBackgroundTracker.OnTransitionListener UPLOADER = + new AppBackgroundTracker.OnTransitionListener() + { + @Override + public void onTransit(boolean foreground) + { + if (!foreground) + WorkerService.startActionUploadUGC(); + } + }; + @NonNull private final Rating[] mRatings; @Nullable @@ -43,6 +57,11 @@ public class UGC @Nullable private static UGCListener mListener; + public static void init() + { + MwmApplication.backgroundTracker().addListener(UPLOADER); + } + private UGC(@NonNull Rating[] ratings, float averageRating, @Nullable Review[] reviews, int basedOnCount) { @@ -95,6 +114,8 @@ public class UGC public static native void setUGCUpdate(@NonNull FeatureId fid, UGCUpdate update); + public static native void nativeUploadUGC(); + public static void onUGCReceived(@Nullable UGC ugc, @Nullable UGCUpdate ugcUpdate, @Impress int impress, @NonNull String rating) { diff --git a/iphone/Maps/Classes/MapsAppDelegate.h b/iphone/Maps/Classes/MapsAppDelegate.h index 6ef66afd12..dfccdab6cb 100644 --- a/iphone/Maps/Classes/MapsAppDelegate.h +++ b/iphone/Maps/Classes/MapsAppDelegate.h @@ -10,6 +10,7 @@ NSInteger m_activeDownloadsCounter; UIBackgroundTaskIdentifier m_backgroundTask; UIBackgroundTaskIdentifier m_editorUploadBackgroundTask; + UIBackgroundTaskIdentifier m_ugcUploadBackgroundTask; UIAlertView * m_loadingAlertView; } diff --git a/iphone/Maps/Classes/MapsAppDelegate.mm b/iphone/Maps/Classes/MapsAppDelegate.mm index d047189fe0..da2b5290bc 100644 --- a/iphone/Maps/Classes/MapsAppDelegate.mm +++ b/iphone/Maps/Classes/MapsAppDelegate.mm @@ -406,6 +406,14 @@ using namespace osm_auth_ios; lambda); } +// Starts async UGC uploading process. ++ (void)uploadUGC:(MWMVoidBlock)finishCallback +{ + GetFramework().UploadUGC([finishCallback](){ + finishCallback(); + }); +} + - (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { @@ -478,7 +486,12 @@ using namespace osm_auth_ios; with:AuthorizationGetCredentials()]; }); } - // 3. Check if map for current location is already downloaded, and if not - notify user to + // 3. Upload UGC. + runFetchTask(^{ + // Ignore completion callback for now. + [MapsAppDelegate uploadUGC:^(){}]; + }); + // 4. Check if map for current location is already downloaded, and if not - notify user to // download it. runFetchTask(^{ [[LocalNotificationManager sharedManager] showDownloadMapNotificationIfNeeded:callback]; @@ -543,6 +556,26 @@ using namespace osm_auth_ios; } with:AuthorizationGetCredentials()]; } + + // Upload UGC. All checks are inside the core part. + { + auto finishUGCUploadTaskBlock = ^{ + if (self->m_ugcUploadBackgroundTask != UIBackgroundTaskInvalid) + { + [application endBackgroundTask:self->m_ugcUploadBackgroundTask]; + self->m_ugcUploadBackgroundTask = UIBackgroundTaskInvalid; + } + }; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + static_cast(application.backgroundTimeRemaining)), + dispatch_get_main_queue(), finishUGCUploadTaskBlock); + m_ugcUploadBackgroundTask = + [application beginBackgroundTaskWithExpirationHandler:finishUGCUploadTaskBlock]; + [MapsAppDelegate uploadUGC:^() { + finishUGCUploadTaskBlock(); + }]; + } + [MWMRouter saveRouteIfNeeded]; LOG(LINFO, ("applicationDidEnterBackground - end")); } diff --git a/map/framework.cpp b/map/framework.cpp index 4d36efeac1..db5ade8b7c 100644 --- a/map/framework.cpp +++ b/map/framework.cpp @@ -3468,3 +3468,13 @@ void Framework::FillLocalExperts(FeatureType const & ft, place_page::Info & info info.SetLocalsStatus(place_page::LocalsStatus::Available); info.SetLocalsPageUrl(locals::Api::GetLocalsPageUrl()); } + +void Framework::UploadUGC(User::CompleteUploadingHandler const & onCompleteUploading) +{ + if (GetPlatform().ConnectionStatus() == Platform::EConnectionType::CONNECTION_NONE) + return; + + //TODO: extract UGC for uploading. + if (m_user.IsAuthenticated()) // && UGC not empty + m_user.UploadUserReviews("{}", onCompleteUploading); +} diff --git a/map/framework.hpp b/map/framework.hpp index 64be2fcf14..85d402b995 100644 --- a/map/framework.hpp +++ b/map/framework.hpp @@ -856,4 +856,8 @@ private: void InjectViator(place_page::Info & info); void FillLocalExperts(FeatureType const & ft, place_page::Info & info) const; + +public: + // UGC. + void UploadUGC(User::CompleteUploadingHandler const & onCompleteUploading); }; diff --git a/map/user.cpp b/map/user.cpp index fb31d3612f..801d756997 100644 --- a/map/user.cpp +++ b/map/user.cpp @@ -6,7 +6,9 @@ #include "coding/url_encode.hpp" #include "base/logging.hpp" +#include "base/string_utils.hpp" +#include "3party/Alohalytics/src/alohalytics.h" #include "3party/jansson/myjansson.hpp" #include @@ -24,6 +26,13 @@ std::string const kPassportServerUrl = PASSPORT_URL; std::string const kAppName = PASSPORT_APP_NAME; std::string const kUGCServerUrl = UGC_URL; +enum class ReviewReceiverProtocol : uint8_t +{ + v1 = 1, // October 2017. Initial version. + + LatestVersion = v1 +}; + std::string AuthenticationUrl(std::string const & socialToken, User::SocialTokenType socialTokenType) { @@ -52,8 +61,20 @@ std::string AuthenticationUrl(std::string const & socialToken, std::string UserDetailsUrl() { + if (kUGCServerUrl.empty()) + return {}; + + return kUGCServerUrl + "/user/reviews/"; +} + +std::string ReviewReceiverUrl() +{ + if (kUGCServerUrl.empty()) + return {}; + std::ostringstream ss; - ss << kUGCServerUrl << "/user/reviews"; + ss << kUGCServerUrl << "/receive/" + << static_cast(ReviewReceiverProtocol::LatestVersion) << "/"; return ss.str(); } @@ -183,7 +204,7 @@ void User::Authenticate(std::string const & socialToken, SocialTokenType socialT // a delayed task calls destructed object. m_workerThread.Push([this, url]() { - Request(url, {}, [this](std::string const & response) + Request(url, nullptr, [this](std::string const & response) { SetAccessToken(ParseAccessToken(response)); }); @@ -206,8 +227,11 @@ void User::RequestUserDetails() m_workerThread.Push([this, url]() { - auto const headers = std::map{{"Authorization", m_accessToken}}; - Request(url, headers, [this](std::string const & response) + Request(url, [this](platform::HttpClient & request) + { + request.SetRawHeader("Authorization", m_accessToken); + }, + [this](std::string const & response) { auto const reviewIds = DeserializeReviewIds(response); if (!reviewIds.empty()) @@ -220,9 +244,50 @@ void User::RequestUserDetails() }); } -void User::Request(std::string const & url, - std::map const & headers, - std::function const & onSuccess) +void User::UploadUserReviews(std::string const & dataStr, + CompleteUploadingHandler const & onCompleteUploading) +{ + std::string const url = ReviewReceiverUrl(); + if (url.empty()) + { + LOG(LWARNING, ("Reviews uploading is unavailable.")); + return; + } + + if (m_accessToken.empty()) + return; + + m_workerThread.Push([this, url, dataStr, onCompleteUploading]() + { + size_t const bytesCount = dataStr.size(); + Request(url, [this, dataStr](platform::HttpClient & request) + { + request.SetRawHeader("Authorization", m_accessToken); + request.SetBodyData(dataStr, "application/json"); + }, + [this, bytesCount, onCompleteUploading](std::string const &) + { + alohalytics::Stats::Instance().LogEvent("UGC_DataUpload_finished", + strings::to_string(bytesCount)); + LOG(LWARNING, ("Reviews have been uploaded.")); + + if (onCompleteUploading != nullptr) + onCompleteUploading(); + }, + [this, onCompleteUploading](int errorCode) + { + alohalytics::Stats::Instance().LogEvent("UGC_DataUpload_error", + strings::to_string(errorCode)); + LOG(LWARNING, ("Reviews have not been uploaded.")); + + if (onCompleteUploading != nullptr) + onCompleteUploading(); + }); + }); +} + +void User::Request(std::string const & url, BuildRequestHandler const & onBuildRequest, + SuccessHandler const & onSuccess, ErrorHandler const & onError) { ASSERT(onSuccess != nullptr, ()); @@ -231,25 +296,30 @@ void User::Request(std::string const & url, uint32_t constexpr kDegradationScalar = 2; uint32_t waitingTime = kWaitingInSeconds; + int resultCode = -1; + bool isSuccessfulCode = false; for (uint8_t i = 0; i < kAttemptsCount; ++i) { platform::HttpClient request(url); request.SetRawHeader("Accept", "application/json"); - for (auto const & header : headers) - request.SetRawHeader(header.first, header.second); + if (onBuildRequest != nullptr) + onBuildRequest(request); // TODO: Now passport service uses redirection. If it becomes false, uncomment checking. if (request.RunHttpRequest())// && !request.WasRedirected()) { - if (request.ErrorCode() == 200) // Ok. + resultCode = request.ErrorCode(); + isSuccessfulCode = (resultCode == 200 || resultCode == 201); + if (isSuccessfulCode) // Ok. { onSuccess(request.ServerResponse()); break; } - if (request.ErrorCode() == 403) // Forbidden. + if (resultCode == 403) // Forbidden. { ResetAccessToken(); + LOG(LWARNING, ("Access denied for", url)); break; } } @@ -262,4 +332,7 @@ void User::Request(std::string const & url, break; waitingTime *= kDegradationScalar; } + + if (!isSuccessfulCode && onError != nullptr) + onError(resultCode); } diff --git a/map/user.hpp b/map/user.hpp index 2a1808f4c0..68f053233e 100644 --- a/map/user.hpp +++ b/map/user.hpp @@ -9,6 +9,11 @@ #include #include +namespace platform +{ +class HttpClient; +} + // This class is thread-safe. class User { @@ -24,6 +29,11 @@ public: Google }; + using BuildRequestHandler = std::function; + using SuccessHandler = std::function; + using ErrorHandler = std::function; + using CompleteUploadingHandler = std::function; + User(); ~User(); void Authenticate(std::string const & socialToken, SocialTokenType socialTokenType); @@ -34,13 +44,15 @@ public: std::string GetAccessToken() const; Details GetDetails() const; + void UploadUserReviews(std::string const & dataStr, + CompleteUploadingHandler const & onCompleteUploading); + private: void Init(); void SetAccessToken(std::string const & accessToken); void RequestUserDetails(); - void Request(std::string const & url, - std::map const & headers, - std::function const & onSuccess); + void Request(std::string const & url, BuildRequestHandler const & onBuildRequest, + SuccessHandler const & onSuccess, ErrorHandler const & onError = nullptr); std::string m_accessToken; mutable std::mutex m_mutex; diff --git a/qt/mainwindow.cpp b/qt/mainwindow.cpp index 16b69cf607..bad2b9c906 100644 --- a/qt/mainwindow.cpp +++ b/qt/mainwindow.cpp @@ -154,6 +154,8 @@ MainWindow::MainWindow(Framework & framework, bool apiOpenGLES3, QString const & #endif // NO_DOWNLOADER m_pDrawWidget->UpdateAfterSettingsChanged(); + + m_pDrawWidget->GetFramework().UploadUGC(nullptr /* onCompleteUploading */); } #if defined(Q_WS_WIN) @@ -550,6 +552,7 @@ void MainWindow::OnSwitchSelectionMode() } void MainWindow::OnClearSelection() { m_pDrawWidget->GetFramework().GetDrapeApi().Clear(); } + void MainWindow::OnSearchButtonClicked() { if (m_pSearchAction->isChecked())