Repo created
This commit is contained in:
parent
4af19165ec
commit
68073add76
12458 changed files with 12350765 additions and 2 deletions
197
libs/platform/CMakeLists.txt
Normal file
197
libs/platform/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
project(platform)
|
||||
|
||||
set(SRC
|
||||
../../private.h # To properly detect its changes with CMake.
|
||||
battery_tracker.cpp
|
||||
battery_tracker.hpp
|
||||
chunks_download_strategy.cpp
|
||||
chunks_download_strategy.hpp
|
||||
constants.hpp
|
||||
country_defines.cpp
|
||||
country_defines.hpp
|
||||
country_file.cpp
|
||||
country_file.hpp
|
||||
distance.cpp
|
||||
distance.hpp
|
||||
duration.cpp
|
||||
duration.hpp
|
||||
downloader_defines.hpp
|
||||
downloader_utils.cpp
|
||||
downloader_utils.hpp
|
||||
get_text_by_id.cpp
|
||||
get_text_by_id.hpp
|
||||
gui_thread.hpp
|
||||
http_client.cpp
|
||||
http_client.hpp
|
||||
http_payload.cpp
|
||||
http_payload.hpp
|
||||
http_request.cpp
|
||||
http_request.hpp
|
||||
http_thread_callback.hpp
|
||||
http_uploader.hpp
|
||||
http_uploader_background.hpp
|
||||
local_country_file.cpp
|
||||
local_country_file.hpp
|
||||
local_country_file_utils.cpp
|
||||
local_country_file_utils.hpp
|
||||
locale.hpp
|
||||
localization.cpp
|
||||
localization.hpp
|
||||
location.hpp
|
||||
measurement_utils.cpp
|
||||
measurement_utils.hpp
|
||||
mwm_traits.cpp
|
||||
mwm_traits.hpp
|
||||
mwm_version.cpp
|
||||
mwm_version.hpp
|
||||
platform.cpp
|
||||
platform.hpp
|
||||
preferred_languages.cpp
|
||||
preferred_languages.hpp
|
||||
remote_file.cpp
|
||||
remote_file.hpp
|
||||
secure_storage.hpp
|
||||
servers_list.cpp
|
||||
servers_list.hpp
|
||||
settings.cpp
|
||||
settings.hpp
|
||||
socket.hpp
|
||||
trace.hpp
|
||||
string_storage_base.cpp
|
||||
string_storage_base.hpp
|
||||
utm_mgrs_utils.cpp
|
||||
utm_mgrs_utils.hpp
|
||||
)
|
||||
|
||||
if (PLATFORM_IPHONE)
|
||||
append(SRC
|
||||
background_downloader_ios.h
|
||||
background_downloader_ios.mm
|
||||
gui_thread_apple.mm
|
||||
http_thread_apple.h
|
||||
http_thread_apple.mm
|
||||
http_client_apple.mm
|
||||
http_uploader_apple.mm
|
||||
http_user_agent_ios.mm
|
||||
localization.mm
|
||||
locale.mm
|
||||
network_policy_ios.h
|
||||
network_policy_ios.mm
|
||||
platform_ios.mm
|
||||
platform_unix_impl.cpp
|
||||
platform_unix_impl.hpp
|
||||
secure_storage_ios.mm
|
||||
socket_apple.mm
|
||||
)
|
||||
elseif(${PLATFORM_ANDROID})
|
||||
append(SRC
|
||||
platform_android.cpp
|
||||
platform_unix_impl.cpp
|
||||
platform_unix_impl.hpp
|
||||
trace_android.cpp
|
||||
)
|
||||
else() # neither iPhone nor Android
|
||||
# Find bash first, on Windows it can be either in Git or in WSL
|
||||
find_program(BASH bash REQUIRED HINTS "$ENV{ProgramFiles}/Git/bin")
|
||||
# Generate version header file.
|
||||
execute_process(COMMAND "${BASH}" tools/unix/version.sh qt_version
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE OM_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
execute_process(COMMAND "${BASH}" tools/unix/version.sh qt_int_version
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE OM_INT_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/platform_qt_version.cpp.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/platform_qt_version.cpp"
|
||||
@ONLY)
|
||||
|
||||
append(SRC
|
||||
localization_dummy.cpp
|
||||
network_policy_dummy.cpp
|
||||
platform_qt.cpp
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/platform_qt_version.cpp"
|
||||
)
|
||||
|
||||
if (${PLATFORM_WIN})
|
||||
append(SRC
|
||||
gui_thread_qt.cpp
|
||||
http_client_curl.cpp
|
||||
http_thread_qt.cpp
|
||||
http_thread_qt.hpp
|
||||
http_uploader_background_dummy.cpp
|
||||
http_uploader_dummy.cpp
|
||||
locale_std.cpp
|
||||
platform_win.cpp
|
||||
secure_storage_dummy.cpp
|
||||
)
|
||||
elseif(${PLATFORM_MAC})
|
||||
append(SRC
|
||||
gui_thread_apple.mm
|
||||
http_client_apple.mm
|
||||
http_thread_apple.h
|
||||
http_thread_apple.mm
|
||||
http_uploader_apple.mm
|
||||
http_uploader_background_dummy.cpp
|
||||
locale.mm
|
||||
platform_mac.mm
|
||||
platform_unix_impl.cpp
|
||||
platform_unix_impl.hpp
|
||||
secure_storage_qt.cpp
|
||||
socket_apple.mm
|
||||
http_session_manager.mm
|
||||
)
|
||||
elseif(${PLATFORM_LINUX})
|
||||
append(SRC
|
||||
gui_thread_qt.cpp
|
||||
http_client_curl.cpp
|
||||
http_thread_qt.cpp
|
||||
http_thread_qt.hpp
|
||||
http_uploader_dummy.cpp
|
||||
http_uploader_background_dummy.cpp
|
||||
locale_std.cpp
|
||||
platform_linux.cpp
|
||||
platform_unix_impl.cpp
|
||||
platform_unix_impl.hpp
|
||||
secure_storage_qt.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
omim_add_library(${PROJECT_NAME} ${SRC})
|
||||
|
||||
if (APPLE)
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE -fobjc-arc -Wno-nullability-completeness)
|
||||
endif()
|
||||
|
||||
if (PLATFORM_LINUX OR PLATFORM_WIN)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES AUTOMOC ON)
|
||||
endif()
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
geometry # mercator::YToLat
|
||||
coding
|
||||
$<$<BOOL:${PLATFORM_DESKTOP}>:Qt6::Core>
|
||||
$<$<BOOL:${PLATFORM_LINUX}>:Qt6::Network>
|
||||
$<$<BOOL:${PLATFORM_WIN}>:Qt6::Network>
|
||||
$<$<BOOL:${PLATFORM_WIN}>:shlwapi> # PathIsDirectoryEmptyA
|
||||
$<$<BOOL:${PLATFORM_MAC}>:
|
||||
-framework\ Foundation
|
||||
-framework\ SystemConfiguration
|
||||
-framework\ CFNetwork
|
||||
-framework\ Security # SecPKCS12Import
|
||||
>
|
||||
)
|
||||
|
||||
omim_add_test_subdirectory(platform_tests_support)
|
||||
omim_add_test_subdirectory(platform_tests)
|
||||
if (NOT SKIP_QT_GUI)
|
||||
add_subdirectory(location_service)
|
||||
endif()
|
||||
|
||||
# strings::UniChar clashes with Apple's definition.
|
||||
set_property(SOURCE preferred_languages.cpp PROPERTY SKIP_UNITY_BUILD_INCLUSION ON)
|
||||
23
libs/platform/background_downloader_ios.h
Normal file
23
libs/platform/background_downloader_ios.h
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef void (^DownloadCompleteBlock)(NSError *_Nullable error);
|
||||
typedef void (^DownloadProgressBlock)(int64_t bytesWritten, int64_t bytesExpected);
|
||||
|
||||
/// Note: this class is NOT thread-safe and must be used from main thread only.
|
||||
@interface BackgroundDownloader : NSObject
|
||||
|
||||
@property(copy, nonatomic, nullable) void (^backgroundCompletionHandler)(void);
|
||||
|
||||
+ (BackgroundDownloader *)sharedBackgroundMapDownloader;
|
||||
|
||||
- (NSUInteger)downloadWithUrl:(NSURL *)url
|
||||
completion:(DownloadCompleteBlock)completion
|
||||
progress:(DownloadProgressBlock)progress;
|
||||
- (void)cancelTaskWithIdentifier:(NSUInteger)taskIdentifier;
|
||||
- (void)clear;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
252
libs/platform/background_downloader_ios.mm
Normal file
252
libs/platform/background_downloader_ios.mm
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
#import "platform/background_downloader_ios.h"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "platform/downloader_utils.hpp"
|
||||
|
||||
// How many seconds to wait before the request fails.
|
||||
static constexpr NSTimeInterval kTimeoutIntervalInSeconds = 10;
|
||||
|
||||
@interface TaskInfo : NSObject
|
||||
|
||||
@property(nonatomic, strong) NSURLSessionTask *task;
|
||||
@property(nonatomic, copy) DownloadCompleteBlock completion;
|
||||
@property(nonatomic, copy) DownloadProgressBlock progress;
|
||||
|
||||
- (instancetype)initWithTask:(NSURLSessionTask *)task
|
||||
completion:(DownloadCompleteBlock)completion
|
||||
progress:(DownloadProgressBlock)progress;
|
||||
|
||||
@end
|
||||
|
||||
@implementation TaskInfo
|
||||
|
||||
- (instancetype)initWithTask:(NSURLSessionTask *)task
|
||||
completion:(DownloadCompleteBlock)completion
|
||||
progress:(DownloadProgressBlock)progress {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_task = task;
|
||||
_completion = completion;
|
||||
_progress = progress;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MapFileSaveStrategy : NSObject
|
||||
|
||||
- (NSURL *)getLocationForWebUrl:(NSURL *)webUrl;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MapFileSaveStrategy
|
||||
|
||||
- (NSURL *)getLocationForWebUrl:(NSURL *)webUrl {
|
||||
NSString *path = @(downloader::GetFilePathByUrl(webUrl.path.UTF8String).c_str());
|
||||
return [NSURL fileURLWithPath:path];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface BackgroundDownloader () <NSURLSessionDownloadDelegate>
|
||||
|
||||
@property(nonatomic, strong) NSURLSession *session;
|
||||
@property(nonatomic, strong) NSMutableDictionary *tasks;
|
||||
@property(nonatomic, strong) NSMutableDictionary *restoredTasks;
|
||||
/// Stores a map of URL.path => NSData to resume failed downloads.
|
||||
@property(nonatomic, strong) NSMutableDictionary *resumeData;
|
||||
@property(nonatomic, strong) MapFileSaveStrategy *saveStrategy;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BackgroundDownloader
|
||||
|
||||
+ (BackgroundDownloader *)sharedBackgroundMapDownloader {
|
||||
static dispatch_once_t dispatchOnce;
|
||||
static BackgroundDownloader *backgroundDownloader;
|
||||
dispatch_once(&dispatchOnce, ^{
|
||||
MapFileSaveStrategy *mapFileSaveStrategy = [[MapFileSaveStrategy alloc] init];
|
||||
backgroundDownloader = [[BackgroundDownloader alloc] initWithSessionName:@"background_map_downloader"
|
||||
saveStrategy:mapFileSaveStrategy];
|
||||
});
|
||||
|
||||
return backgroundDownloader;
|
||||
}
|
||||
|
||||
- (instancetype)initWithSessionName:(NSString *)name saveStrategy:(MapFileSaveStrategy *)saveStrategy {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
NSURLSessionConfiguration *configuration =
|
||||
[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:name];
|
||||
configuration.sessionSendsLaunchEvents = YES;
|
||||
configuration.timeoutIntervalForRequest = kTimeoutIntervalInSeconds;
|
||||
_session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];
|
||||
_tasks = [NSMutableDictionary dictionary];
|
||||
_saveStrategy = saveStrategy;
|
||||
_restoredTasks = [NSMutableDictionary dictionary];
|
||||
_resumeData = [NSMutableDictionary dictionary];
|
||||
|
||||
[_session getAllTasksWithCompletionHandler:^(NSArray<__kindof NSURLSessionTask *> *_Nonnull downloadTasks) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
for (NSURLSessionTask *downloadTask in downloadTasks) {
|
||||
TaskInfo *info = [self.tasks objectForKey:@(downloadTask.taskIdentifier)];
|
||||
if (info)
|
||||
continue;
|
||||
|
||||
NSString *urlString = downloadTask.currentRequest.URL.path;
|
||||
|
||||
BOOL isTaskReplaced = NO;
|
||||
// Replacing task with another one which was added into queue earlier (on previous application session).
|
||||
for (id key in self.tasks)
|
||||
{
|
||||
TaskInfo * info = [self.tasks objectForKey:key];
|
||||
if (![info.task.currentRequest.URL.path isEqualToString:urlString])
|
||||
continue;
|
||||
|
||||
TaskInfo *newInfo = [[TaskInfo alloc] initWithTask:downloadTask
|
||||
completion:info.completion
|
||||
progress:info.progress];
|
||||
[self.tasks setObject:newInfo forKey:@(downloadTask.taskIdentifier)];
|
||||
|
||||
[info.task cancel];
|
||||
[self.tasks removeObjectForKey:@(info.task.taskIdentifier)];
|
||||
isTaskReplaced = YES;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isTaskReplaced)
|
||||
[self.restoredTasks setObject:downloadTask forKey:urlString];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSUInteger)downloadWithUrl:(NSURL *)url
|
||||
completion:(DownloadCompleteBlock)completion
|
||||
progress:(DownloadProgressBlock)progress {
|
||||
NSUInteger taskIdentifier;
|
||||
NSURLSessionTask *restoredTask = [self.restoredTasks objectForKey:url.path];
|
||||
if (restoredTask) {
|
||||
TaskInfo *info = [[TaskInfo alloc] initWithTask:restoredTask completion:completion progress:progress];
|
||||
[self.tasks setObject:info forKey:@(restoredTask.taskIdentifier)];
|
||||
[self.restoredTasks removeObjectForKey:url.path];
|
||||
taskIdentifier = restoredTask.taskIdentifier;
|
||||
} else {
|
||||
NSData *resumeData = self.resumeData[url.path];
|
||||
NSURLSessionTask *task = resumeData ? [self.session downloadTaskWithResumeData:resumeData]
|
||||
: [self.session downloadTaskWithURL:url];
|
||||
TaskInfo *info = [[TaskInfo alloc] initWithTask:task completion:completion progress:progress];
|
||||
[self.tasks setObject:info forKey:@(task.taskIdentifier)];
|
||||
[task resume];
|
||||
taskIdentifier = task.taskIdentifier;
|
||||
}
|
||||
|
||||
return taskIdentifier;
|
||||
}
|
||||
|
||||
- (void)cancelTaskWithIdentifier:(NSUInteger)taskIdentifier {
|
||||
TaskInfo *info = self.tasks[@(taskIdentifier)];
|
||||
if (info) {
|
||||
[info.task cancel];
|
||||
[self.resumeData removeObjectForKey:info.task.currentRequest.URL.path];
|
||||
[self.tasks removeObjectForKey:@(taskIdentifier)];
|
||||
} else {
|
||||
for (NSString *key in self.restoredTasks) {
|
||||
NSURLSessionTask *restoredTask = self.restoredTasks[key];
|
||||
if (restoredTask.taskIdentifier == taskIdentifier) {
|
||||
[restoredTask cancel];
|
||||
[self.resumeData removeObjectForKey:restoredTask.currentRequest.URL.path];
|
||||
[self.restoredTasks removeObjectForKey:key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)clear {
|
||||
for (TaskInfo *info in self.tasks) {
|
||||
[info.task cancel];
|
||||
}
|
||||
|
||||
for (NSURLSessionTask *restoredTask in self.restoredTasks) {
|
||||
[restoredTask cancel];
|
||||
}
|
||||
|
||||
[self.tasks removeAllObjects];
|
||||
[self.restoredTasks removeAllObjects];
|
||||
[self.resumeData removeAllObjects];
|
||||
}
|
||||
|
||||
#pragma mark - NSURLSessionDownloadDelegate implementation
|
||||
|
||||
- (void)finishDownloading:(NSURLSessionTask *)downloadTask error:(nullable NSError *)error {
|
||||
NSString *urlPath = downloadTask.currentRequest.URL.path;
|
||||
[self.restoredTasks removeObjectForKey:urlPath];
|
||||
if (error && error.userInfo && error.userInfo[NSURLSessionDownloadTaskResumeData])
|
||||
self.resumeData[urlPath] = error.userInfo[NSURLSessionDownloadTaskResumeData];
|
||||
else
|
||||
[self.resumeData removeObjectForKey:urlPath];
|
||||
|
||||
TaskInfo *info = [self.tasks objectForKey:@(downloadTask.taskIdentifier)];
|
||||
if (!info)
|
||||
return;
|
||||
|
||||
info.completion(error);
|
||||
|
||||
[self.tasks removeObjectForKey:@(downloadTask.taskIdentifier)];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
downloadTask:(NSURLSessionDownloadTask *)downloadTask
|
||||
didFinishDownloadingToURL:(NSURL *)location {
|
||||
NSError *error;
|
||||
// Check for HTTP errors.
|
||||
// TODO: Check and prevent redirects.
|
||||
NSInteger statusCode = ((NSHTTPURLResponse *)downloadTask.response).statusCode;
|
||||
// 206 for resumed downloads.
|
||||
if (statusCode != 200 && statusCode != 206) {
|
||||
LOG(LWARNING, ("Failed to download", downloadTask.originalRequest.URL.absoluteString, "HTTP statusCode:", statusCode));
|
||||
error = [[NSError alloc] initWithDomain:@"app.omaps.http" code:statusCode userInfo:nil];
|
||||
[[NSFileManager defaultManager] removeItemAtURL:location.filePathURL error:nil];
|
||||
} else {
|
||||
NSURL *destinationUrl = [self.saveStrategy getLocationForWebUrl:downloadTask.currentRequest.URL];
|
||||
[[NSFileManager defaultManager] moveItemAtURL:location.filePathURL toURL:destinationUrl error:&error];
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self finishDownloading:downloadTask error:error];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
downloadTask:(NSURLSessionDownloadTask *)downloadTask
|
||||
didWriteData:(int64_t)bytesWritten
|
||||
totalBytesWritten:(int64_t)totalBytesWritten
|
||||
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
TaskInfo *info = [self.tasks objectForKey:@(downloadTask.taskIdentifier)];
|
||||
if (!info)
|
||||
return;
|
||||
info.progress(totalBytesWritten, totalBytesExpectedToWrite);
|
||||
});
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)downloadTask didCompleteWithError:(NSError *)error {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (error && error.code != NSURLErrorCancelled)
|
||||
[self finishDownloading:downloadTask error:error];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.backgroundCompletionHandler != nil)
|
||||
self.backgroundCompletionHandler();
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
61
libs/platform/battery_tracker.cpp
Normal file
61
libs/platform/battery_tracker.cpp
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#include "platform/battery_tracker.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
auto const kBatteryTrackingInterval = std::chrono::minutes(10);
|
||||
|
||||
bool IsLevelExpired(std::chrono::system_clock::time_point lastRequestTime)
|
||||
{
|
||||
return std::chrono::system_clock::now() - lastRequestTime > kBatteryTrackingInterval;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace platform
|
||||
{
|
||||
void BatteryLevelTracker::Subscribe(Subscriber * subscriber)
|
||||
{
|
||||
m_subscribers.push_back(subscriber);
|
||||
|
||||
if (!IsLevelExpired(m_lastRequestTime))
|
||||
subscriber->OnBatteryLevelReceived(m_lastReceivedLevel);
|
||||
|
||||
if (!m_isTrackingInProgress)
|
||||
{
|
||||
m_isTrackingInProgress = true;
|
||||
RequestBatteryLevel();
|
||||
}
|
||||
}
|
||||
|
||||
void BatteryLevelTracker::Unsubscribe(Subscriber * subscriber)
|
||||
{
|
||||
m_subscribers.erase(std::remove(m_subscribers.begin(), m_subscribers.end(), subscriber), m_subscribers.end());
|
||||
}
|
||||
|
||||
void BatteryLevelTracker::UnsubscribeAll()
|
||||
{
|
||||
m_subscribers.clear();
|
||||
}
|
||||
|
||||
void BatteryLevelTracker::RequestBatteryLevel()
|
||||
{
|
||||
if (m_subscribers.empty())
|
||||
{
|
||||
m_isTrackingInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsLevelExpired(m_lastRequestTime))
|
||||
{
|
||||
m_lastReceivedLevel = GetPlatform().GetBatteryLevel();
|
||||
m_lastRequestTime = std::chrono::system_clock::now();
|
||||
}
|
||||
|
||||
for (auto s : m_subscribers)
|
||||
s->OnBatteryLevelReceived(m_lastReceivedLevel);
|
||||
|
||||
GetPlatform().RunDelayedTask(Platform::Thread::Background, kBatteryTrackingInterval, [this]
|
||||
{ GetPlatform().RunTask(Platform::Thread::Gui, [this] { RequestBatteryLevel(); }); });
|
||||
}
|
||||
} // namespace platform
|
||||
34
libs/platform/battery_tracker.hpp
Normal file
34
libs/platform/battery_tracker.hpp
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
// Note: this class is NOT thread-safe.
|
||||
class BatteryLevelTracker
|
||||
{
|
||||
public:
|
||||
class Subscriber
|
||||
{
|
||||
public:
|
||||
virtual void OnBatteryLevelReceived(uint8_t level) = 0;
|
||||
|
||||
protected:
|
||||
virtual ~Subscriber() = default;
|
||||
};
|
||||
|
||||
void Subscribe(Subscriber * subscriber);
|
||||
void Unsubscribe(Subscriber * subscriber);
|
||||
void UnsubscribeAll();
|
||||
|
||||
private:
|
||||
void RequestBatteryLevel();
|
||||
|
||||
std::vector<Subscriber *> m_subscribers;
|
||||
std::chrono::system_clock::time_point m_lastRequestTime;
|
||||
uint8_t m_lastReceivedLevel = 0;
|
||||
bool m_isTrackingInProgress = false;
|
||||
};
|
||||
} // namespace platform
|
||||
238
libs/platform/chunks_download_strategy.cpp
Normal file
238
libs/platform/chunks_download_strategy.cpp
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
#include "platform/chunks_download_strategy.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/file_reader.hpp"
|
||||
#include "coding/file_writer.hpp"
|
||||
#include "coding/varint.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/macros.hpp"
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
ChunksDownloadStrategy::ChunksDownloadStrategy(std::vector<std::string> const & urls)
|
||||
{
|
||||
// init servers list
|
||||
for (size_t i = 0; i < urls.size(); ++i)
|
||||
m_servers.push_back(ServerT(urls[i], SERVER_READY));
|
||||
}
|
||||
|
||||
std::pair<ChunksDownloadStrategy::ChunkT *, int> ChunksDownloadStrategy::GetChunk(RangeT const & range)
|
||||
{
|
||||
std::vector<ChunkT>::iterator i = lower_bound(m_chunks.begin(), m_chunks.end(), range.first, LessChunks());
|
||||
|
||||
if (i != m_chunks.end() && i->m_pos == range.first)
|
||||
{
|
||||
ASSERT_EQUAL((i + 1)->m_pos, range.second + 1, ());
|
||||
return std::pair<ChunkT *, int>(&(*i), std::distance(m_chunks.begin(), i));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LERROR, ("Downloader error. Invalid chunk range: ", range));
|
||||
return std::pair<ChunkT *, int>(static_cast<ChunkT *>(0), -1);
|
||||
}
|
||||
}
|
||||
|
||||
void ChunksDownloadStrategy::InitChunks(int64_t fileSize, int64_t chunkSize, ChunkStatusT status)
|
||||
{
|
||||
if (chunkSize == 0)
|
||||
{
|
||||
int64_t constexpr kMb = 1024 * 1024;
|
||||
size_t const sizeMb = std::max(fileSize / kMb, static_cast<int64_t>(1));
|
||||
|
||||
size_t constexpr kTargetCount = 40;
|
||||
size_t constexpr kMinMb = 1;
|
||||
size_t constexpr kMaxMb = 16;
|
||||
size_t const chunkMb = std::min(std::max(sizeMb / kTargetCount, kMinMb), kMaxMb);
|
||||
|
||||
size_t chunksCount = sizeMb / chunkMb;
|
||||
if (static_cast<int64_t>(kMb * chunkMb * chunksCount) < fileSize)
|
||||
++chunksCount;
|
||||
ASSERT_GREATER_OR_EQUAL(static_cast<int64_t>(kMb * chunkMb * chunksCount), fileSize, ());
|
||||
|
||||
LOG(LINFO, ("File size", sizeMb, "MB; chunk size", chunkMb, "MB; chunks count", chunksCount));
|
||||
|
||||
m_chunks.reserve(chunksCount + 1);
|
||||
chunkSize = chunkMb * kMb;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_chunks.reserve(static_cast<size_t>(fileSize / chunkSize + 2));
|
||||
}
|
||||
|
||||
for (int64_t i = 0; i < fileSize; i += chunkSize)
|
||||
m_chunks.push_back(ChunkT(i, status));
|
||||
// The last AUX chunk is just used to hold end of the range (eof) for the previous chunk.
|
||||
m_chunks.push_back(ChunkT(fileSize, CHUNK_AUX));
|
||||
}
|
||||
|
||||
void ChunksDownloadStrategy::AddChunk(RangeT const & range, ChunkStatusT status)
|
||||
{
|
||||
ASSERT_LESS_OR_EQUAL(range.first, range.second, ());
|
||||
if (m_chunks.empty())
|
||||
{
|
||||
ASSERT_EQUAL(range.first, 0, ());
|
||||
m_chunks.push_back(ChunkT(range.first, status));
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT_EQUAL(m_chunks.back().m_pos, range.first, ());
|
||||
m_chunks.back().m_status = status;
|
||||
}
|
||||
|
||||
m_chunks.push_back(ChunkT(range.second + 1, CHUNK_AUX));
|
||||
}
|
||||
|
||||
void ChunksDownloadStrategy::SaveChunks(int64_t fileSize, std::string const & fName)
|
||||
{
|
||||
if (!m_chunks.empty())
|
||||
{
|
||||
try
|
||||
{
|
||||
FileWriter w(fName);
|
||||
WriteVarInt(w, fileSize);
|
||||
|
||||
w.Write(&m_chunks[0], sizeof(ChunkT) * m_chunks.size());
|
||||
return;
|
||||
}
|
||||
catch (FileWriter::Exception const & e)
|
||||
{
|
||||
LOG(LWARNING, ("Can't save chunks statuses to file", e.Msg()));
|
||||
}
|
||||
}
|
||||
|
||||
// Delete if no chunks or some error occured.
|
||||
UNUSED_VALUE(Platform::RemoveFileIfExists(fName));
|
||||
}
|
||||
|
||||
int64_t ChunksDownloadStrategy::LoadOrInitChunks(std::string const & fName, int64_t fileSize, int64_t chunkSize)
|
||||
{
|
||||
ASSERT(fileSize > 0, ());
|
||||
|
||||
if (Platform::IsFileExistsByFullPath(fName))
|
||||
{
|
||||
try
|
||||
{
|
||||
FileReader r(fName);
|
||||
ReaderSource<FileReader> src(r);
|
||||
|
||||
int64_t const readSize = ReadVarInt<int64_t>(src);
|
||||
if (readSize == fileSize)
|
||||
{
|
||||
// Load chunks.
|
||||
uint64_t const size = src.Size();
|
||||
int const stSize = sizeof(ChunkT);
|
||||
auto const count = static_cast<size_t>(size / stSize);
|
||||
ASSERT_EQUAL(size, stSize * count, ());
|
||||
|
||||
m_chunks.resize(count);
|
||||
src.Read(&m_chunks[0], stSize * count);
|
||||
|
||||
// Reset status "downloading" to "free".
|
||||
int64_t downloadedSize = 0;
|
||||
size_t completed = 0;
|
||||
for (size_t i = 0; i < count - 1; ++i)
|
||||
{
|
||||
if (m_chunks[i].m_status != CHUNK_COMPLETE)
|
||||
m_chunks[i].m_status = CHUNK_FREE;
|
||||
else
|
||||
{
|
||||
downloadedSize += (m_chunks[i + 1].m_pos - m_chunks[i].m_pos);
|
||||
++completed;
|
||||
}
|
||||
}
|
||||
LOG(LINFO, ("Resumed file: downloaded", downloadedSize / 1024 / 1024, "out of", fileSize / 1024 / 1024,
|
||||
"MB; completed chunks", completed, "out of", count - 1));
|
||||
|
||||
return downloadedSize;
|
||||
}
|
||||
}
|
||||
catch (FileReader::Exception const & e)
|
||||
{
|
||||
LOG(LDEBUG, (e.Msg()));
|
||||
}
|
||||
}
|
||||
|
||||
InitChunks(fileSize, chunkSize);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string ChunksDownloadStrategy::ChunkFinished(bool success, RangeT const & range)
|
||||
{
|
||||
std::pair<ChunkT *, int> res = GetChunk(range);
|
||||
std::string url;
|
||||
// find server which was downloading this chunk
|
||||
if (res.first)
|
||||
{
|
||||
for (size_t s = 0; s < m_servers.size(); ++s)
|
||||
{
|
||||
if (m_servers[s].m_chunkIndex == res.second)
|
||||
{
|
||||
url = m_servers[s].m_url;
|
||||
if (success)
|
||||
{
|
||||
LOG(LDEBUG, ("Completed chunk", m_servers[s].m_chunkIndex, "via", m_servers[s].m_url));
|
||||
// mark server as free and chunk as ready
|
||||
m_servers[s].m_chunkIndex = SERVER_READY;
|
||||
res.first->m_status = CHUNK_COMPLETE;
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LWARNING, ("Failed to dl chunk", m_servers[s].m_chunkIndex, "via", m_servers[s].m_url));
|
||||
// remove failed server and mark chunk as free
|
||||
m_servers.erase(m_servers.begin() + s);
|
||||
res.first->m_status = CHUNK_FREE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
ChunksDownloadStrategy::ResultT ChunksDownloadStrategy::NextChunk(std::string & outUrl, RangeT & range)
|
||||
{
|
||||
// If no servers at all.
|
||||
if (m_servers.empty())
|
||||
return EDownloadFailed;
|
||||
|
||||
// Find first free server.
|
||||
ServerT * server = 0;
|
||||
for (size_t i = 0; i < m_servers.size(); ++i)
|
||||
{
|
||||
if (m_servers[i].m_chunkIndex == SERVER_READY)
|
||||
{
|
||||
server = &m_servers[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (server == 0)
|
||||
return ENoFreeServers;
|
||||
|
||||
bool allChunksDownloaded = true;
|
||||
|
||||
// Find first free chunk.
|
||||
for (size_t i = 0; i < m_chunks.size() - 1; ++i)
|
||||
{
|
||||
switch (m_chunks[i].m_status)
|
||||
{
|
||||
case CHUNK_FREE:
|
||||
server->m_chunkIndex = static_cast<int>(i);
|
||||
outUrl = server->m_url;
|
||||
|
||||
range.first = m_chunks[i].m_pos;
|
||||
range.second = m_chunks[i + 1].m_pos - 1;
|
||||
|
||||
m_chunks[i].m_status = CHUNK_DOWNLOADING;
|
||||
LOG(LDEBUG, ("Download chunk", server->m_chunkIndex, "via", outUrl));
|
||||
return ENextChunk;
|
||||
|
||||
case CHUNK_DOWNLOADING: allChunksDownloaded = false; break;
|
||||
}
|
||||
}
|
||||
|
||||
return (allChunksDownloaded ? EDownloadSucceeded : ENoFreeServers);
|
||||
}
|
||||
} // namespace downloader
|
||||
94
libs/platform/chunks_download_strategy.hpp
Normal file
94
libs/platform/chunks_download_strategy.hpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
/// Single-threaded code
|
||||
class ChunksDownloadStrategy
|
||||
{
|
||||
public:
|
||||
enum ChunkStatusT
|
||||
{
|
||||
CHUNK_FREE = 0,
|
||||
CHUNK_DOWNLOADING = 1,
|
||||
CHUNK_COMPLETE = 2,
|
||||
CHUNK_AUX = -1
|
||||
};
|
||||
|
||||
private:
|
||||
#pragma pack(push, 1)
|
||||
struct ChunkT
|
||||
{
|
||||
/// position of chunk in file
|
||||
int64_t m_pos;
|
||||
/// @see ChunkStatusT
|
||||
int8_t m_status;
|
||||
|
||||
ChunkT() : m_pos(-1), m_status(-1)
|
||||
{
|
||||
static_assert(sizeof(ChunkT) == 9, "Be sure to avoid overhead in writing to file.");
|
||||
}
|
||||
ChunkT(int64_t pos, int8_t st) : m_pos(pos), m_status(st) {}
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct LessChunks
|
||||
{
|
||||
bool operator()(ChunkT const & r1, ChunkT const & r2) const { return r1.m_pos < r2.m_pos; }
|
||||
bool operator()(ChunkT const & r1, int64_t const & r2) const { return r1.m_pos < r2; }
|
||||
bool operator()(int64_t const & r1, ChunkT const & r2) const { return r1 < r2.m_pos; }
|
||||
};
|
||||
|
||||
using RangeT = std::pair<int64_t, int64_t>;
|
||||
|
||||
static int const SERVER_READY = -1;
|
||||
struct ServerT
|
||||
{
|
||||
std::string m_url;
|
||||
int m_chunkIndex;
|
||||
|
||||
ServerT(std::string const & url, int ind) : m_url(url), m_chunkIndex(ind) {}
|
||||
};
|
||||
|
||||
std::vector<ChunkT> m_chunks;
|
||||
|
||||
std::vector<ServerT> m_servers;
|
||||
|
||||
/// @return Chunk pointer and it's index for given file offsets range.
|
||||
std::pair<ChunkT *, int> GetChunk(RangeT const & range);
|
||||
|
||||
public:
|
||||
ChunksDownloadStrategy(std::vector<std::string> const & urls);
|
||||
|
||||
/// Init chunks vector for fileSize.
|
||||
void InitChunks(int64_t fileSize, int64_t chunkSize, ChunkStatusT status = CHUNK_FREE);
|
||||
|
||||
/// Used in unit tests only!
|
||||
void AddChunk(RangeT const & range, ChunkStatusT status);
|
||||
|
||||
void SaveChunks(int64_t fileSize, std::string const & fName);
|
||||
/// Inits the chunks list or loads from the resume file if there is an unfinished download.
|
||||
/// @return Already downloaded size.
|
||||
int64_t LoadOrInitChunks(std::string const & fName, int64_t fileSize, int64_t chunkSize);
|
||||
|
||||
/// Should be called for every completed chunk (no matter successful or not).
|
||||
/// @returns url of the chunk
|
||||
std::string ChunkFinished(bool success, RangeT const & range);
|
||||
|
||||
size_t ActiveServersCount() const { return m_servers.size(); }
|
||||
|
||||
enum ResultT
|
||||
{
|
||||
ENextChunk,
|
||||
ENoFreeServers,
|
||||
EDownloadFailed,
|
||||
EDownloadSucceeded
|
||||
};
|
||||
/// Get next chunk url ready to download. Should be called until returns ENextChunk.
|
||||
ResultT NextChunk(std::string & outUrl, RangeT & range);
|
||||
};
|
||||
} // namespace downloader
|
||||
7
libs/platform/constants.hpp
Normal file
7
libs/platform/constants.hpp
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
constexpr uint32_t READER_CHUNK_LOG_SIZE = 10; // 1024 bytes pages
|
||||
constexpr uint32_t READER_CHUNK_LOG_COUNT =
|
||||
12; // 2^12 = 4096 pages count, hence 4M size cache overall (for data files only)
|
||||
14
libs/platform/country_defines.cpp
Normal file
14
libs/platform/country_defines.cpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#include "platform/country_defines.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
std::string DebugPrint(MapFileType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MapFileType::Map: return "Map";
|
||||
case MapFileType::Diff: return "Diff";
|
||||
case MapFileType::Count: return "Count";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
20
libs/platform/country_defines.hpp
Normal file
20
libs/platform/country_defines.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
// Note: new values must be added before MapFileType::Count.
|
||||
enum class MapFileType : uint8_t
|
||||
{
|
||||
Map,
|
||||
Diff,
|
||||
|
||||
Count
|
||||
};
|
||||
|
||||
using MwmCounter = uint32_t;
|
||||
using MwmSize = uint64_t;
|
||||
using LocalAndRemoteSize = std::pair<MwmSize, MwmSize>;
|
||||
|
||||
std::string DebugPrint(MapFileType type);
|
||||
41
libs/platform/country_file.cpp
Normal file
41
libs/platform/country_file.cpp
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#include "platform/country_file.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
std::string GetFileName(std::string const & countryName, MapFileType type)
|
||||
{
|
||||
ASSERT(!countryName.empty(), ());
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case MapFileType::Map: return countryName + DATA_FILE_EXTENSION;
|
||||
case MapFileType::Diff: return countryName + DIFF_FILE_EXTENSION;
|
||||
case MapFileType::Count: break;
|
||||
}
|
||||
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
CountryFile::CountryFile() : m_mapSize(0) {}
|
||||
|
||||
CountryFile::CountryFile(std::string name) : m_name(std::move(name)), m_mapSize(0) {}
|
||||
|
||||
CountryFile::CountryFile(std::string name, MwmSize size, std::string sha1)
|
||||
: m_name(std::move(name))
|
||||
, m_mapSize(size)
|
||||
, m_sha1(std::move(sha1))
|
||||
{}
|
||||
|
||||
std::string DebugPrint(CountryFile const & file)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << "CountryFile [" << file.m_name << "]";
|
||||
return os.str();
|
||||
}
|
||||
} // namespace platform
|
||||
50
libs/platform/country_file.hpp
Normal file
50
libs/platform/country_file.hpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/country_defines.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
/// \param countryName Country's name without any extensions. For example "Abkhazia".
|
||||
/// \returns File name with extension (for download url and save on disk) for \a type. For example "Abkhazia.mwm".
|
||||
std::string GetFileName(std::string const & countryName, MapFileType type);
|
||||
|
||||
/// This class represents a country file name and sizes of
|
||||
/// corresponding map files on a server, which should correspond to an
|
||||
/// entry in countries.txt file. Also, this class can be used to
|
||||
/// represent a hand-made-country name. Instances of this class don't
|
||||
/// represent paths to disk files.
|
||||
class CountryFile
|
||||
{
|
||||
public:
|
||||
CountryFile();
|
||||
explicit CountryFile(std::string name);
|
||||
CountryFile(std::string name, MwmSize size, std::string sha1);
|
||||
|
||||
std::string GetFileName(MapFileType type) const { return platform::GetFileName(m_name, type); }
|
||||
|
||||
/// \returns Empty (invalid) CountryFile.
|
||||
bool IsEmpty() const { return m_name.empty(); }
|
||||
|
||||
std::string const & GetName() const { return m_name; }
|
||||
MwmSize GetRemoteSize() const { return m_mapSize; }
|
||||
std::string const & GetSha1() const { return m_sha1; }
|
||||
|
||||
inline bool operator<(CountryFile const & rhs) const { return m_name < rhs.m_name; }
|
||||
inline bool operator==(CountryFile const & rhs) const { return m_name == rhs.m_name; }
|
||||
inline bool operator!=(CountryFile const & rhs) const { return !(*this == rhs); }
|
||||
|
||||
private:
|
||||
friend std::string DebugPrint(CountryFile const & file);
|
||||
|
||||
/// Base name (without any extensions) of the file. Same as id of country/region.
|
||||
std::string m_name;
|
||||
MwmSize m_mapSize = 0;
|
||||
/// \note SHA1 is encoded to base64.
|
||||
std::string m_sha1;
|
||||
};
|
||||
|
||||
std::string DebugPrint(CountryFile const & file);
|
||||
} // namespace platform
|
||||
217
libs/platform/distance.cpp
Normal file
217
libs/platform/distance.cpp
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#include "distance.hpp"
|
||||
|
||||
#include "platform/locale.hpp"
|
||||
#include "platform/localization.hpp"
|
||||
#include "platform/measurement_utils.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
using namespace measurement_utils;
|
||||
|
||||
namespace
|
||||
{
|
||||
Distance MetersTo(double distance, Distance::Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case Distance::Units::Meters: return Distance(distance);
|
||||
case Distance::Units::Kilometers: return {distance / 1000, Distance::Units::Kilometers};
|
||||
case Distance::Units::Feet: return {MetersToFeet(distance), Distance::Units::Feet};
|
||||
case Distance::Units::Miles: return {MetersToMiles(distance), Distance::Units::Miles};
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
Distance KilometersTo(double distance, Distance::Units units)
|
||||
{
|
||||
return MetersTo(distance * 1000, units);
|
||||
}
|
||||
|
||||
Distance FeetTo(double distance, Distance::Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case Distance::Units::Meters: return {FeetToMeters(distance), Distance::Units::Meters};
|
||||
case Distance::Units::Kilometers: return {FeetToMeters(distance) / 1000, Distance::Units::Kilometers};
|
||||
case Distance::Units::Miles: return {FeetToMiles(distance), Distance::Units::Miles};
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
Distance MilesTo(double distance, Distance::Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case Distance::Units::Meters: return {MilesToMeters(distance), Distance::Units::Meters};
|
||||
case Distance::Units::Kilometers: return {MilesToMeters(distance) / 1000, Distance::Units::Kilometers};
|
||||
case Distance::Units::Feet: return {MilesToFeet(distance), Distance::Units::Feet};
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
double WithPrecision(double value, uint8_t precision)
|
||||
{
|
||||
if (precision == 0)
|
||||
return std::round(value);
|
||||
|
||||
double const factor = math::PowUint(10.0, precision);
|
||||
return std::round(value * factor) / factor;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Distance::Distance() : Distance(-1.0) {}
|
||||
|
||||
Distance::Distance(double distanceInMeters) : Distance(distanceInMeters, Units::Meters) {}
|
||||
|
||||
Distance::Distance(double distance, platform::Distance::Units units) : m_distance(distance), m_units(units) {}
|
||||
|
||||
Distance Distance::CreateFormatted(double distanceInMeters)
|
||||
{
|
||||
return Distance(distanceInMeters).ToPlatformUnitsFormatted();
|
||||
}
|
||||
|
||||
std::string Distance::FormatAltitude(double meters)
|
||||
{
|
||||
Distance elevation = Distance(fabs(meters))
|
||||
.To(GetMeasurementUnits() == measurement_utils::Units::Metric ? Units::Meters : Units::Feet);
|
||||
|
||||
ASSERT(elevation.IsLowUnits(), ());
|
||||
elevation.m_distance = WithPrecision(elevation.m_distance, 0);
|
||||
|
||||
auto res = elevation.ToString();
|
||||
return meters < 0 ? "-" + res : res;
|
||||
}
|
||||
|
||||
bool Distance::IsValid() const
|
||||
{
|
||||
return m_distance >= 0.0;
|
||||
}
|
||||
|
||||
bool Distance::IsLowUnits() const
|
||||
{
|
||||
return m_units == Units::Meters || m_units == Units::Feet;
|
||||
}
|
||||
|
||||
bool Distance::IsHighUnits() const
|
||||
{
|
||||
return !IsLowUnits();
|
||||
}
|
||||
|
||||
Distance Distance::To(Units units) const
|
||||
{
|
||||
if (m_units == units)
|
||||
return *this;
|
||||
|
||||
/// @todo These double switches can be replaced with 4x4 factors matrix.
|
||||
switch (m_units)
|
||||
{
|
||||
case Units::Meters: return MetersTo(m_distance, units);
|
||||
case Units::Kilometers: return KilometersTo(m_distance, units);
|
||||
case Units::Feet: return FeetTo(m_distance, units);
|
||||
case Units::Miles: return MilesTo(m_distance, units);
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
Distance Distance::ToPlatformUnitsFormatted() const
|
||||
{
|
||||
return To(GetMeasurementUnits() == measurement_utils::Units::Metric ? Units::Meters : Units::Feet)
|
||||
.GetFormattedDistance();
|
||||
}
|
||||
|
||||
double Distance::GetDistance() const
|
||||
{
|
||||
return m_distance;
|
||||
}
|
||||
|
||||
Distance::Units Distance::GetUnits() const
|
||||
{
|
||||
return m_units;
|
||||
}
|
||||
|
||||
std::string Distance::GetDistanceString() const
|
||||
{
|
||||
if (!IsValid())
|
||||
return "";
|
||||
|
||||
// Default precision is 0 (no decimals).
|
||||
int precision = 0;
|
||||
|
||||
// Set 1 decimal precision for high distances (km, miles) lower than 10.0 (9.5, 7.0,...).
|
||||
if (m_distance < 10.0 && IsHighUnits())
|
||||
precision = 1;
|
||||
|
||||
return ToStringPrecision(m_distance, precision);
|
||||
}
|
||||
|
||||
std::string Distance::GetUnitsString() const
|
||||
{
|
||||
switch (m_units)
|
||||
{
|
||||
case Units::Meters: return GetLocalizedString("m");
|
||||
case Units::Kilometers: return GetLocalizedString("km");
|
||||
case Units::Feet: return GetLocalizedString("ft");
|
||||
case Units::Miles: return GetLocalizedString("mi");
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
Distance Distance::GetFormattedDistance() const
|
||||
{
|
||||
ASSERT(IsValid(), ());
|
||||
|
||||
// To low units.
|
||||
Distance res;
|
||||
if (m_units == Units::Kilometers)
|
||||
res = To(Units::Meters);
|
||||
else if (m_units == Units::Miles)
|
||||
res = To(Units::Feet);
|
||||
else
|
||||
res = *this;
|
||||
|
||||
double lowRound = std::round(res.m_distance);
|
||||
// Round distances over 100 units to 10 units, e.g. 112 -> 110, 998 -> 1000
|
||||
if (lowRound > 100)
|
||||
lowRound = std::round(lowRound / 10) * 10;
|
||||
|
||||
// Use high units for distances of 1000 units and over,
|
||||
// e.g. 1000m -> 1.0km, 1290m -> 1.3km, 1000ft -> 0.2mi
|
||||
if (lowRound >= 1000.0)
|
||||
{
|
||||
// To high units.
|
||||
res = res.To(res.m_units == Units::Meters ? Units::Kilometers : Units::Miles);
|
||||
|
||||
// For distances of 10.0 high units and over round to a whole number, e.g. 9.98 -> 10, 10.9 -> 11
|
||||
uint8_t const precision = (std::round(res.m_distance * 10) / 10 >= 10.0) ? 0 : 1;
|
||||
return {WithPrecision(res.m_distance, precision), res.m_units};
|
||||
}
|
||||
|
||||
res.m_distance = lowRound;
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string Distance::ToString() const
|
||||
{
|
||||
if (!IsValid())
|
||||
return "";
|
||||
|
||||
return GetDistanceString() + kNarrowNonBreakingSpace + GetUnitsString();
|
||||
}
|
||||
|
||||
std::string DebugPrint(Distance::Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case Distance::Units::Meters: return "m";
|
||||
case Distance::Units::Kilometers: return "km";
|
||||
case Distance::Units::Feet: return "ft";
|
||||
case Distance::Units::Miles: return "mi";
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
67
libs/platform/distance.hpp
Normal file
67
libs/platform/distance.hpp
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/measurement_utils.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
class Distance
|
||||
{
|
||||
public:
|
||||
/*!
|
||||
* \warning The order of values below shall not be changed.
|
||||
* \warning The values of Units shall be synchronized with values of Distance.Units enum in
|
||||
* java (see app.organicmaps.util.Distance for details).
|
||||
* \warning The values of Units shall be synchronized with values of unitLength func in
|
||||
* swift (see iphone/Maps/Classes/CarPlay/Templates Data/RouteInfo.swift for details).
|
||||
*/
|
||||
enum class Units
|
||||
{
|
||||
Meters = 0,
|
||||
Kilometers = 1,
|
||||
Feet = 2,
|
||||
Miles = 3
|
||||
};
|
||||
|
||||
Distance();
|
||||
|
||||
explicit Distance(double distanceInMeters);
|
||||
|
||||
Distance(double distance, Units units);
|
||||
|
||||
static Distance CreateFormatted(double distanceInMeters);
|
||||
static std::string FormatAltitude(double meters);
|
||||
|
||||
bool IsValid() const;
|
||||
|
||||
bool IsLowUnits() const;
|
||||
bool IsHighUnits() const;
|
||||
|
||||
Distance To(Units units) const;
|
||||
Distance ToPlatformUnitsFormatted() const;
|
||||
|
||||
double GetDistance() const;
|
||||
Units GetUnits() const;
|
||||
|
||||
std::string GetDistanceString() const;
|
||||
std::string GetUnitsString() const;
|
||||
|
||||
/// Formats distance in the following way:
|
||||
/// * rounds distances over 100 units to 10 units, e.g. 112 -> 110, 998 -> 1000
|
||||
/// * for distances of 10.0 high units and over rounds to a whole number, e.g. 9.98 -> 10, 10.9 -> 11
|
||||
/// * use high units for distances of 1000 units and over, e.g. 1000m -> 1.0km, 1290m -> 1.3km, 1000ft -> 0.2mi
|
||||
Distance GetFormattedDistance() const;
|
||||
|
||||
std::string ToString() const;
|
||||
|
||||
friend std::string DebugPrint(Distance const & d) { return d.ToString(); }
|
||||
|
||||
private:
|
||||
double m_distance;
|
||||
Units m_units;
|
||||
};
|
||||
|
||||
std::string DebugPrint(Distance::Units units);
|
||||
|
||||
} // namespace platform
|
||||
52
libs/platform/downloader_defines.hpp
Normal file
52
libs/platform/downloader_defines.hpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#pragma once
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
enum class DownloadStatus
|
||||
{
|
||||
InProgress,
|
||||
Completed,
|
||||
Failed,
|
||||
FileNotFound,
|
||||
FailedSHA,
|
||||
};
|
||||
|
||||
inline std::string DebugPrint(DownloadStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case DownloadStatus::InProgress: return "In progress";
|
||||
case DownloadStatus::Completed: return "Completed";
|
||||
case DownloadStatus::Failed: return "Failed";
|
||||
case DownloadStatus::FileNotFound: return "File not found";
|
||||
case DownloadStatus::FailedSHA: return "Failed SHA check";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
struct Progress
|
||||
{
|
||||
static int64_t constexpr kUnknownTotalSize = -1;
|
||||
|
||||
static Progress constexpr Unknown() { return {0, kUnknownTotalSize}; }
|
||||
|
||||
bool IsUnknown() const { return m_bytesTotal == kUnknownTotalSize; }
|
||||
|
||||
int64_t m_bytesDownloaded = 0;
|
||||
/// Total can be kUnknownTotalSize if size is unknown.
|
||||
int64_t m_bytesTotal = 0;
|
||||
};
|
||||
|
||||
inline std::string DebugPrint(Progress const & progress)
|
||||
{
|
||||
std::ostringstream out;
|
||||
out << "(downloaded " << progress.m_bytesDownloaded << " bytes out of " << progress.m_bytesTotal << " bytes)";
|
||||
return out.str();
|
||||
}
|
||||
} // namespace downloader
|
||||
79
libs/platform/downloader_utils.cpp
Normal file
79
libs/platform/downloader_utils.cpp
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
#include "platform/downloader_utils.hpp"
|
||||
|
||||
#include "platform/country_defines.hpp"
|
||||
#include "platform/country_file.hpp"
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
|
||||
#include "coding/url.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include "std/target_os.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string const kMapsPath = "maps";
|
||||
std::string const kDiffsPath = "diffs";
|
||||
} // namespace
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
|
||||
std::string GetFileDownloadUrl(std::string const & fileName, int64_t dataVersion, uint64_t diffVersion /* = 0 */)
|
||||
{
|
||||
if (diffVersion == 0)
|
||||
return url::Join(kMapsPath, strings::to_string(dataVersion), url::UrlEncode(fileName));
|
||||
|
||||
return url::Join(kDiffsPath, strings::to_string(dataVersion), strings::to_string(diffVersion),
|
||||
url::UrlEncode(fileName));
|
||||
}
|
||||
|
||||
bool IsUrlSupported(std::string const & url)
|
||||
{
|
||||
auto const urlComponents = strings::Tokenize(url, "/");
|
||||
if (urlComponents.empty())
|
||||
return false;
|
||||
|
||||
if (urlComponents[0] != kMapsPath && urlComponents[0] != kDiffsPath)
|
||||
return false;
|
||||
|
||||
if (urlComponents[0] == kMapsPath && urlComponents.size() != 3)
|
||||
return false;
|
||||
|
||||
if (urlComponents[0] == kDiffsPath && urlComponents.size() != 4)
|
||||
return false;
|
||||
|
||||
uint64_t dataVersion = 0;
|
||||
if (!strings::to_uint(urlComponents[1], dataVersion))
|
||||
return false;
|
||||
|
||||
if (urlComponents[0] == kDiffsPath)
|
||||
{
|
||||
uint64_t diffVersion = 0;
|
||||
if (!strings::to_uint(urlComponents[2], diffVersion))
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t count = 0;
|
||||
strings::Tokenize(url::UrlDecode(urlComponents.back()), ".", [&count](std::string_view) { ++count; });
|
||||
return count == 2;
|
||||
}
|
||||
|
||||
std::string GetFilePathByUrl(std::string const & url)
|
||||
{
|
||||
auto const urlComponents = strings::Tokenize(url, "/");
|
||||
CHECK_GREATER(urlComponents.size(), 2, (urlComponents));
|
||||
CHECK_LESS(urlComponents.size(), 5, (urlComponents));
|
||||
|
||||
uint64_t dataVersion = 0;
|
||||
CHECK(strings::to_uint(urlComponents[1], dataVersion), ());
|
||||
|
||||
std::string mwmFile = url::UrlDecode(urlComponents.back());
|
||||
// remove extension
|
||||
mwmFile = mwmFile.substr(0, mwmFile.find('.'));
|
||||
|
||||
auto const fileType = urlComponents[0] == kDiffsPath ? MapFileType::Diff : MapFileType::Map;
|
||||
return platform::GetFileDownloadPath(dataVersion, mwmFile, fileType);
|
||||
}
|
||||
|
||||
} // namespace downloader
|
||||
11
libs/platform/downloader_utils.hpp
Normal file
11
libs/platform/downloader_utils.hpp
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
std::string GetFileDownloadUrl(std::string const & fileName, int64_t dataVersion, uint64_t diffVersion = 0);
|
||||
bool IsUrlSupported(std::string const & url);
|
||||
std::string GetFilePathByUrl(std::string const & url);
|
||||
} // namespace downloader
|
||||
123
libs/platform/duration.cpp
Normal file
123
libs/platform/duration.cpp
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
#include "duration.hpp"
|
||||
|
||||
#include "base/stl_helpers.hpp"
|
||||
|
||||
/// @todo(KK): move the formatting code from the platform namespace
|
||||
namespace platform
|
||||
{
|
||||
namespace
|
||||
{
|
||||
using namespace std::chrono;
|
||||
|
||||
static constexpr std::string_view kNoSpace = "";
|
||||
|
||||
unsigned long SecondsToUnits(seconds duration, Duration::Units unit)
|
||||
{
|
||||
switch (unit)
|
||||
{
|
||||
case Duration::Units::Days: return duration_cast<days>(duration).count();
|
||||
case Duration::Units::Hours: return duration_cast<hours>(duration).count();
|
||||
case Duration::Units::Minutes: return duration_cast<minutes>(duration).count();
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
seconds UnitsToSeconds(long value, Duration::Units unit)
|
||||
{
|
||||
switch (unit)
|
||||
{
|
||||
case Duration::Units::Days: return days(value);
|
||||
case Duration::Units::Hours: return hours(value);
|
||||
case Duration::Units::Minutes: return minutes(value);
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view GetUnitSeparator(Locale const & locale)
|
||||
{
|
||||
static constexpr auto kEmptyNumberUnitSeparatorLocales =
|
||||
std::array{"en", "de", "fr", "he", "fa", "ja", "ko", "mr", "th", "tr", "vi", "zh"};
|
||||
bool const isEmptySeparator = base::IsExist(kEmptyNumberUnitSeparatorLocales, locale.m_language);
|
||||
return isEmptySeparator ? kNoSpace : kNarrowNonBreakingSpace;
|
||||
}
|
||||
|
||||
std::string_view GetUnitsGroupingSeparator(Locale const & locale)
|
||||
{
|
||||
static constexpr auto kEmptyGroupingSeparatorLocales = std::array{"ja", "zh"};
|
||||
bool const isEmptySeparator = base::IsExist(kEmptyGroupingSeparatorLocales, locale.m_language);
|
||||
return isEmptySeparator ? kNoSpace : kNonBreakingSpace;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Duration::Duration(unsigned long seconds) : m_seconds(seconds) {}
|
||||
|
||||
std::string Duration::GetLocalizedString(std::initializer_list<Units> units, Locale const & locale) const
|
||||
{
|
||||
return GetString(std::move(units), GetUnitSeparator(locale), GetUnitsGroupingSeparator(locale));
|
||||
}
|
||||
|
||||
std::string Duration::GetPlatformLocalizedString() const
|
||||
{
|
||||
struct InitSeparators
|
||||
{
|
||||
std::string_view m_unitSep, m_groupingSep;
|
||||
InitSeparators()
|
||||
{
|
||||
auto const loc = GetCurrentLocale();
|
||||
m_unitSep = GetUnitSeparator(loc);
|
||||
m_groupingSep = GetUnitsGroupingSeparator(loc);
|
||||
}
|
||||
};
|
||||
static InitSeparators seps;
|
||||
|
||||
return GetString({Units::Days, Units::Hours, Units::Minutes}, seps.m_unitSep, seps.m_groupingSep);
|
||||
}
|
||||
|
||||
std::string Duration::GetString(std::initializer_list<Units> units, std::string_view unitSeparator,
|
||||
std::string_view groupingSeparator) const
|
||||
{
|
||||
ASSERT(units.size(), ());
|
||||
ASSERT(base::IsSortedAndUnique(units), ());
|
||||
|
||||
if (SecondsToUnits(m_seconds, Units::Minutes) == 0)
|
||||
return std::to_string(0U).append(unitSeparator).append(GetUnitsString(Units::Minutes));
|
||||
|
||||
std::string formattedTime;
|
||||
seconds remainingSeconds = m_seconds;
|
||||
|
||||
for (auto const unit : units)
|
||||
{
|
||||
unsigned long const unitsCount = SecondsToUnits(remainingSeconds, unit);
|
||||
if (unitsCount > 0)
|
||||
{
|
||||
if (!formattedTime.empty())
|
||||
formattedTime.append(groupingSeparator);
|
||||
formattedTime.append(std::to_string(unitsCount).append(unitSeparator).append(GetUnitsString(unit)));
|
||||
remainingSeconds -= UnitsToSeconds(unitsCount, unit);
|
||||
}
|
||||
}
|
||||
return formattedTime;
|
||||
}
|
||||
|
||||
std::string Duration::GetUnitsString(Units unit)
|
||||
{
|
||||
switch (unit)
|
||||
{
|
||||
case Units::Minutes: return platform::GetLocalizedString("minute");
|
||||
case Units::Hours: return platform::GetLocalizedString("hour");
|
||||
case Units::Days: return platform::GetLocalizedString("day");
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
std::string DebugPrint(Duration::Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case Duration::Units::Days: return "d";
|
||||
case Duration::Units::Hours: return "h";
|
||||
case Duration::Units::Minutes: return "m";
|
||||
default: UNREACHABLE();
|
||||
}
|
||||
}
|
||||
} // namespace platform
|
||||
38
libs/platform/duration.hpp
Normal file
38
libs/platform/duration.hpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/localization.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
|
||||
class Duration
|
||||
{
|
||||
public:
|
||||
enum class Units
|
||||
{
|
||||
Days = 0,
|
||||
Hours = 1,
|
||||
Minutes = 2,
|
||||
};
|
||||
|
||||
explicit Duration(unsigned long seconds);
|
||||
|
||||
static std::string GetUnitsString(Units unit);
|
||||
|
||||
std::string GetLocalizedString(std::initializer_list<Units> units, Locale const & locale) const;
|
||||
std::string GetPlatformLocalizedString() const;
|
||||
|
||||
private:
|
||||
std::chrono::seconds const m_seconds;
|
||||
|
||||
std::string GetString(std::initializer_list<Units> units, std::string_view unitSeparator,
|
||||
std::string_view groupingSeparator) const;
|
||||
};
|
||||
|
||||
std::string DebugPrint(Duration::Units units);
|
||||
|
||||
} // namespace platform
|
||||
124
libs/platform/get_text_by_id.cpp
Normal file
124
libs/platform/get_text_by_id.cpp
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
#include "platform/get_text_by_id.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include "cppjansson/cppjansson.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
using std::string;
|
||||
|
||||
namespace
|
||||
{
|
||||
string const kDefaultLanguage = "en";
|
||||
|
||||
string GetTextSourceString(platform::TextSource textSource)
|
||||
{
|
||||
switch (textSource)
|
||||
{
|
||||
case platform::TextSource::TtsSound: return string("sound-strings");
|
||||
case platform::TextSource::Countries: return string("countries-strings");
|
||||
}
|
||||
ASSERT(false, ());
|
||||
return string();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool GetJsonBuffer(platform::TextSource textSource, string const & localeName, string & jsonBuffer)
|
||||
{
|
||||
string const pathToJson = base::JoinPath(GetTextSourceString(textSource), localeName + ".json", "localize.json");
|
||||
|
||||
try
|
||||
{
|
||||
jsonBuffer.clear();
|
||||
GetPlatform().GetReader(pathToJson)->ReadAsString(jsonBuffer);
|
||||
}
|
||||
catch (RootException const & ex)
|
||||
{
|
||||
LOG(LWARNING, ("Can't open", localeName, "localization file:", pathToJson, ex.what()));
|
||||
return false; // No json file for localeName
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
TGetTextByIdPtr GetTextById::Create(string const & jsonBuffer, string const & localeName)
|
||||
{
|
||||
TGetTextByIdPtr result(new GetTextById(jsonBuffer, localeName));
|
||||
if (!result->IsValid())
|
||||
{
|
||||
ASSERT(false, ("Can't create a GetTextById instance from a json file. localeName=", localeName));
|
||||
return nullptr;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
TGetTextByIdPtr GetTextByIdFactory(TextSource textSource, string const & localeName)
|
||||
{
|
||||
string jsonBuffer;
|
||||
if (GetJsonBuffer(textSource, localeName, jsonBuffer))
|
||||
return GetTextById::Create(jsonBuffer, localeName);
|
||||
|
||||
if (GetJsonBuffer(textSource, kDefaultLanguage, jsonBuffer))
|
||||
return GetTextById::Create(jsonBuffer, kDefaultLanguage);
|
||||
|
||||
ASSERT(false, ("Can't find translate for default language. (Lang:", localeName, ")"));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TGetTextByIdPtr ForTestingGetTextByIdFactory(string const & jsonBuffer, string const & localeName)
|
||||
{
|
||||
return GetTextById::Create(jsonBuffer, localeName);
|
||||
}
|
||||
|
||||
GetTextById::GetTextById(string const & jsonBuffer, string const & localeName) : m_locale(localeName)
|
||||
{
|
||||
if (jsonBuffer.empty())
|
||||
{
|
||||
ASSERT(false, ("No json files found."));
|
||||
return;
|
||||
}
|
||||
|
||||
base::Json root(jsonBuffer.c_str());
|
||||
if (root.get() == nullptr)
|
||||
{
|
||||
ASSERT(false, ("Cannot parse the json file."));
|
||||
return;
|
||||
}
|
||||
|
||||
char const * key = nullptr;
|
||||
json_t * value = nullptr;
|
||||
json_object_foreach(root.get(), key, value)
|
||||
{
|
||||
ASSERT(key, ());
|
||||
ASSERT(value, ());
|
||||
char const * const valueStr = json_string_value(value);
|
||||
ASSERT(valueStr, ());
|
||||
m_localeTexts[key] = valueStr;
|
||||
}
|
||||
ASSERT_EQUAL(m_localeTexts.size(), json_object_size(root.get()), ());
|
||||
}
|
||||
|
||||
string GetTextById::operator()(string const & textId) const
|
||||
{
|
||||
auto const textIt = m_localeTexts.find(textId);
|
||||
if (textIt == m_localeTexts.end())
|
||||
return string();
|
||||
return textIt->second;
|
||||
}
|
||||
|
||||
TTranslations GetTextById::GetAllSortedTranslations() const
|
||||
{
|
||||
TTranslations all;
|
||||
all.reserve(m_localeTexts.size());
|
||||
for (auto const & tr : m_localeTexts)
|
||||
all.emplace_back(tr.first, tr.second);
|
||||
using TValue = TTranslations::value_type;
|
||||
sort(all.begin(), all.end(), [](TValue const & v1, TValue const & v2) { return v1.second < v2.second; });
|
||||
return all;
|
||||
}
|
||||
} // namespace platform
|
||||
57
libs/platform/get_text_by_id.hpp
Normal file
57
libs/platform/get_text_by_id.hpp
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
// class GetTextById is ready to work with different sources of text strings.
|
||||
// For the time being it's only strings for TTS.
|
||||
enum class TextSource
|
||||
{
|
||||
TtsSound = 0, //!< Maneuvers text to speech strings.
|
||||
Countries, //!< Countries names strings.
|
||||
};
|
||||
|
||||
class GetTextById;
|
||||
using TGetTextByIdPtr = std::unique_ptr<GetTextById>;
|
||||
using TTranslations = std::vector<std::pair<std::string, std::string>>;
|
||||
|
||||
/// GetTextById represents text messages which are saved in textsDir
|
||||
/// in a specified locale.
|
||||
class GetTextById
|
||||
{
|
||||
public:
|
||||
/// @return a pair of a text string in a specified locale for textId and a boolean flag.
|
||||
/// If textId is found in m_localeTexts then the boolean flag is set to true.
|
||||
/// The boolean flag is set to false otherwise.
|
||||
std::string operator()(std::string const & textId) const;
|
||||
std::string GetLocale() const { return m_locale; }
|
||||
TTranslations GetAllSortedTranslations() const;
|
||||
|
||||
static TGetTextByIdPtr Create(std::string const & jsonBuffer, std::string const & localeName);
|
||||
|
||||
private:
|
||||
GetTextById(std::string const & jsonBuffer, std::string const & localeName);
|
||||
|
||||
/// \note IsValid is used only in factories and shall be private.
|
||||
bool IsValid() const { return !m_localeTexts.empty(); }
|
||||
|
||||
std::string m_locale;
|
||||
std::unordered_map<std::string, std::string> m_localeTexts;
|
||||
};
|
||||
|
||||
/// Factories to create GetTextById instances.
|
||||
/// If TGetTextByIdPtr is created by GetTextByIdFactory or ForTestingGetTextByIdFactory
|
||||
/// there are only two possibities:
|
||||
/// * a factory returns a valid instance
|
||||
/// * a factory returns nullptr
|
||||
TGetTextByIdPtr GetTextByIdFactory(TextSource textSource, std::string const & localeName);
|
||||
TGetTextByIdPtr ForTestingGetTextByIdFactory(std::string const & jsonBuffer, std::string const & localeName);
|
||||
|
||||
/// \bried fills jsonBuffer with json file in twine format with strings in a language of localeName.
|
||||
/// @return true if no error was happened and false otherwise.
|
||||
bool GetJsonBuffer(platform::TextSource textSource, std::string const & localeName, std::string & jsonBuffer);
|
||||
} // namespace platform
|
||||
13
libs/platform/gui_thread.hpp
Normal file
13
libs/platform/gui_thread.hpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#include "base/task_loop.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
class GuiThread : public base::TaskLoop
|
||||
{
|
||||
public:
|
||||
PushResult Push(Task && task) override;
|
||||
PushResult Push(Task const & task) override;
|
||||
};
|
||||
} // namespace platform
|
||||
31
libs/platform/gui_thread_apple.mm
Normal file
31
libs/platform/gui_thread_apple.mm
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#include "platform/gui_thread.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
void PerformImpl(void * task)
|
||||
{
|
||||
using base::TaskLoop;
|
||||
std::unique_ptr<TaskLoop::Task> t(reinterpret_cast<TaskLoop::Task *>(task));
|
||||
(*t)();
|
||||
}
|
||||
}
|
||||
|
||||
namespace platform
|
||||
{
|
||||
base::TaskLoop::PushResult GuiThread::Push(Task && task)
|
||||
{
|
||||
dispatch_async_f(dispatch_get_main_queue(), new Task(std::move(task)), &PerformImpl);
|
||||
return {true, TaskLoop::kNoId};
|
||||
}
|
||||
|
||||
base::TaskLoop::PushResult GuiThread::Push(Task const & task)
|
||||
{
|
||||
dispatch_async_f(dispatch_get_main_queue(), new Task(task), &PerformImpl);
|
||||
return {true, TaskLoop::kNoId};
|
||||
}
|
||||
} // namespace platform
|
||||
28
libs/platform/gui_thread_qt.cpp
Normal file
28
libs/platform/gui_thread_qt.cpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
#include "platform/gui_thread.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
base::TaskLoop::PushResult GuiThread::Push(Task && task)
|
||||
{
|
||||
// Following hack is used to post on main message loop |fn| when
|
||||
// |source| is destroyed (at the exit of the code block).
|
||||
QObject source;
|
||||
QObject::connect(&source, &QObject::destroyed, QCoreApplication::instance(), std::move(task));
|
||||
|
||||
return {true, base::TaskLoop::kNoId};
|
||||
}
|
||||
|
||||
base::TaskLoop::PushResult GuiThread::Push(Task const & task)
|
||||
{
|
||||
// Following hack is used to post on main message loop |fn| when
|
||||
// |source| is destroyed (at the exit of the code block).
|
||||
QObject source;
|
||||
QObject::connect(&source, &QObject::destroyed, QCoreApplication::instance(), task);
|
||||
|
||||
return {true, base::TaskLoop::kNoId};
|
||||
}
|
||||
} // namespace platform
|
||||
206
libs/platform/http_client.cpp
Normal file
206
libs/platform/http_client.cpp
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
#include "platform/http_client.hpp"
|
||||
|
||||
#include "coding/base64.hpp"
|
||||
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
using std::string;
|
||||
|
||||
HttpClient::HttpClient(string const & url) : m_urlRequested(url) {}
|
||||
|
||||
bool HttpClient::RunHttpRequest(string & response, SuccessChecker checker /* = nullptr */)
|
||||
{
|
||||
static auto const simpleChecker = [](HttpClient const & request) { return request.ErrorCode() == 200; };
|
||||
|
||||
if (checker == nullptr)
|
||||
checker = simpleChecker;
|
||||
|
||||
if (RunHttpRequest() && checker(*this))
|
||||
{
|
||||
response = ServerResponse();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetUrlRequested(string const & url)
|
||||
{
|
||||
m_urlRequested = url;
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetHttpMethod(string const & method)
|
||||
{
|
||||
m_httpMethod = method;
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetBodyFile(string const & body_file, string const & content_type,
|
||||
string const & http_method /* = "POST" */,
|
||||
string const & content_encoding /* = "" */)
|
||||
{
|
||||
m_inputFile = body_file;
|
||||
m_bodyData.clear();
|
||||
m_headers.emplace("Content-Type", content_type);
|
||||
m_httpMethod = http_method;
|
||||
m_headers.emplace("Content-Encoding", content_encoding);
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetReceivedFile(string const & received_file)
|
||||
{
|
||||
m_outputFile = received_file;
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetUserAndPassword(string const & user, string const & password)
|
||||
{
|
||||
m_headers.emplace("Authorization", "Basic " + base64::Encode(user + ":" + password));
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetCookies(string const & cookies)
|
||||
{
|
||||
m_cookies = cookies;
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetFollowRedirects(bool followRedirects)
|
||||
{
|
||||
m_followRedirects = followRedirects;
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetRawHeader(string const & key, string const & value)
|
||||
{
|
||||
m_headers.emplace(key, value);
|
||||
return *this;
|
||||
}
|
||||
|
||||
HttpClient & HttpClient::SetRawHeaders(Headers const & headers)
|
||||
{
|
||||
m_headers.insert(headers.begin(), headers.end());
|
||||
return *this;
|
||||
}
|
||||
|
||||
void HttpClient::SetTimeout(double timeoutSec)
|
||||
{
|
||||
m_timeoutSec = timeoutSec;
|
||||
}
|
||||
|
||||
string const & HttpClient::UrlRequested() const
|
||||
{
|
||||
return m_urlRequested;
|
||||
}
|
||||
|
||||
string const & HttpClient::UrlReceived() const
|
||||
{
|
||||
return m_urlReceived;
|
||||
}
|
||||
|
||||
bool HttpClient::WasRedirected() const
|
||||
{
|
||||
return m_urlRequested != m_urlReceived;
|
||||
}
|
||||
|
||||
int HttpClient::ErrorCode() const
|
||||
{
|
||||
return m_errorCode;
|
||||
}
|
||||
|
||||
string const & HttpClient::ServerResponse() const
|
||||
{
|
||||
return m_serverResponse;
|
||||
}
|
||||
|
||||
string const & HttpClient::HttpMethod() const
|
||||
{
|
||||
return m_httpMethod;
|
||||
}
|
||||
|
||||
string HttpClient::CombinedCookies() const
|
||||
{
|
||||
string serverCookies;
|
||||
auto const it = m_headers.find("Set-Cookie");
|
||||
if (it != m_headers.end())
|
||||
serverCookies = it->second;
|
||||
|
||||
if (serverCookies.empty())
|
||||
return m_cookies;
|
||||
|
||||
if (m_cookies.empty())
|
||||
return serverCookies;
|
||||
|
||||
return serverCookies + "; " + m_cookies;
|
||||
}
|
||||
|
||||
string HttpClient::CookieByName(string name) const
|
||||
{
|
||||
string const str = CombinedCookies();
|
||||
name += "=";
|
||||
auto const cookie = str.find(name);
|
||||
auto const eq = cookie + name.size();
|
||||
if (cookie != string::npos && str.size() > eq)
|
||||
return str.substr(eq, str.find(';', eq) - eq);
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void HttpClient::LoadHeaders(bool loadHeaders)
|
||||
{
|
||||
m_loadHeaders = loadHeaders;
|
||||
}
|
||||
|
||||
HttpClient::Headers const & HttpClient::GetHeaders() const
|
||||
{
|
||||
return m_headers;
|
||||
}
|
||||
|
||||
// static
|
||||
string HttpClient::NormalizeServerCookies(string && cookies)
|
||||
{
|
||||
std::istringstream is(cookies);
|
||||
string str, result;
|
||||
|
||||
// Split by ", ". Can have invalid tokens here, expires= can also contain a comma.
|
||||
while (getline(is, str, ','))
|
||||
{
|
||||
size_t const leading = str.find_first_not_of(' ');
|
||||
if (leading != string::npos)
|
||||
str.substr(leading).swap(str);
|
||||
|
||||
// In the good case, we have '=' and it goes before any ' '.
|
||||
auto const eq = str.find('=');
|
||||
if (eq == string::npos)
|
||||
continue; // It's not a cookie: no valid key value pair.
|
||||
|
||||
auto const sp = str.find(' ');
|
||||
if (sp != string::npos && eq > sp)
|
||||
continue; // It's not a cookie: comma in expires date.
|
||||
|
||||
// Insert delimiter.
|
||||
if (!result.empty())
|
||||
result.append("; ");
|
||||
|
||||
// Read cookie itself.
|
||||
result.append(str, 0, str.find(';'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
string DebugPrint(HttpClient const & request)
|
||||
{
|
||||
std::ostringstream ostr;
|
||||
ostr << "HTTP " << request.ErrorCode() << " url [" << request.UrlRequested() << "]";
|
||||
if (request.WasRedirected())
|
||||
ostr << " was redirected to [" << request.UrlReceived() << "]";
|
||||
if (!request.ServerResponse().empty())
|
||||
ostr << " response: " << request.ServerResponse();
|
||||
return ostr.str();
|
||||
}
|
||||
} // namespace platform
|
||||
139
libs/platform/http_client.hpp
Normal file
139
libs/platform/http_client.hpp
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
/*******************************************************************************
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Alexander Borsuk <me@alex.bio> from Minsk, Belarus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*******************************************************************************/
|
||||
#pragma once
|
||||
|
||||
#include "base/macros.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
class HttpClient
|
||||
{
|
||||
public:
|
||||
static auto constexpr kNoError = -1;
|
||||
|
||||
struct Header
|
||||
{
|
||||
std::string m_name;
|
||||
std::string m_value;
|
||||
};
|
||||
|
||||
using Headers = std::unordered_map<std::string, std::string>;
|
||||
|
||||
HttpClient() = default;
|
||||
explicit HttpClient(std::string const & url);
|
||||
|
||||
// Synchronous (blocking) call, should be implemented for each platform
|
||||
// @returns true if connection was made and server returned something (200, 404, etc.).
|
||||
// @note Implementations should transparently support all needed HTTP redirects.
|
||||
// Implemented for each platform.
|
||||
bool RunHttpRequest();
|
||||
using SuccessChecker = std::function<bool(HttpClient const & request)>;
|
||||
// Returns true and copy of server response into [response] in case when RunHttpRequest() and
|
||||
// [checker] return true. When [checker] is equal to nullptr then default checker will be used.
|
||||
// Check by default: ErrorCode() == 200
|
||||
bool RunHttpRequest(std::string & response, SuccessChecker checker = nullptr);
|
||||
|
||||
HttpClient & SetUrlRequested(std::string const & url);
|
||||
HttpClient & SetHttpMethod(std::string const & method);
|
||||
// This method is mutually exclusive with set_body_data().
|
||||
HttpClient & SetBodyFile(std::string const & body_file, std::string const & content_type,
|
||||
std::string const & http_method = "POST", std::string const & content_encoding = "");
|
||||
// If set, stores server reply in file specified.
|
||||
HttpClient & SetReceivedFile(std::string const & received_file);
|
||||
// This method is mutually exclusive with set_body_file().
|
||||
template <typename StringT>
|
||||
HttpClient & SetBodyData(StringT && body_data, std::string const & content_type,
|
||||
std::string const & http_method = "POST", std::string const & content_encoding = {})
|
||||
{
|
||||
m_bodyData = std::forward<StringT>(body_data);
|
||||
m_inputFile.clear();
|
||||
m_headers.emplace("Content-Type", content_type);
|
||||
m_httpMethod = http_method;
|
||||
if (!content_encoding.empty())
|
||||
m_headers.emplace("Content-Encoding", content_encoding);
|
||||
return *this;
|
||||
}
|
||||
// HTTP Basic Auth.
|
||||
HttpClient & SetUserAndPassword(std::string const & user, std::string const & password);
|
||||
// Set HTTP Cookie header.
|
||||
HttpClient & SetCookies(std::string const & cookies);
|
||||
// When set to false (default), clients never get 3XX codes from servers, redirects are handled automatically.
|
||||
HttpClient & SetFollowRedirects(bool follow_redirects);
|
||||
HttpClient & SetRawHeader(std::string const & key, std::string const & value);
|
||||
HttpClient & SetRawHeaders(Headers const & headers);
|
||||
void SetTimeout(double timeoutSec);
|
||||
|
||||
std::string const & UrlRequested() const;
|
||||
// @returns empty string in the case of error
|
||||
std::string const & UrlReceived() const;
|
||||
bool WasRedirected() const;
|
||||
// Mix of HTTP errors (in case of successful connection) and system-dependent error codes,
|
||||
// in the simplest success case use 'if (200 == client.error_code())' // 200 means OK in HTTP
|
||||
int ErrorCode() const;
|
||||
std::string const & ServerResponse() const;
|
||||
std::string const & HttpMethod() const;
|
||||
// Pass this getter's value to the set_cookies() method for easier cookies support in the next request.
|
||||
std::string CombinedCookies() const;
|
||||
// Returns cookie value or empty string if it's not present.
|
||||
std::string CookieByName(std::string name) const;
|
||||
void LoadHeaders(bool loadHeaders);
|
||||
Headers const & GetHeaders() const;
|
||||
|
||||
private:
|
||||
// Internal helper to convert cookies like this:
|
||||
// "first=value1; expires=Mon, 26-Dec-2016 12:12:32 GMT; path=/, second=value2; path=/, third=value3; "
|
||||
// into this:
|
||||
// "first=value1; second=value2; third=value3"
|
||||
static std::string NormalizeServerCookies(std::string && cookies);
|
||||
|
||||
std::string m_urlRequested;
|
||||
// Contains final content's url taking redirects (if any) into an account.
|
||||
std::string m_urlReceived;
|
||||
int m_errorCode = kNoError;
|
||||
std::string m_inputFile;
|
||||
// Used instead of server_reply_ if set.
|
||||
std::string m_outputFile;
|
||||
// Data we received from the server if output_file_ wasn't initialized.
|
||||
std::string m_serverResponse;
|
||||
std::string m_bodyData;
|
||||
std::string m_httpMethod = "GET";
|
||||
// Cookies set by the client before request is run.
|
||||
std::string m_cookies;
|
||||
Headers m_headers;
|
||||
bool m_followRedirects =
|
||||
false; // If true then in case of HTTP response 3XX make another request to follow redirected URL
|
||||
bool m_loadHeaders = false;
|
||||
// Use 30 seconds timeout by default.
|
||||
double m_timeoutSec = 30.0;
|
||||
|
||||
DISALLOW_COPY_AND_MOVE(HttpClient);
|
||||
};
|
||||
|
||||
std::string DebugPrint(HttpClient const & request);
|
||||
} // namespace platform
|
||||
222
libs/platform/http_client_apple.mm
Normal file
222
libs/platform/http_client_apple.mm
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
/*******************************************************************************
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Alexander Borsuk <me@alex.bio> from Minsk, Belarus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*******************************************************************************/
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
#error This file must be compiled with ARC. Either turn on ARC for the project or use -fobjc-arc flag
|
||||
#endif
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "platform/http_client.hpp"
|
||||
#import "platform/http_session_manager.h"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
|
||||
@interface RedirectDelegate : NSObject<NSURLSessionDataDelegate>
|
||||
|
||||
// If YES - redirect response triggeres new request automatically
|
||||
// If NO - redirect response is returned to result handler
|
||||
@property(nonatomic) BOOL followRedirects;
|
||||
|
||||
- (instancetype)init:(BOOL)followRedirects;
|
||||
|
||||
- (void) URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
||||
newRequest:(NSURLRequest *)newRequest
|
||||
completionHandler:(void (^)(NSURLRequest *))completionHandler;
|
||||
@end
|
||||
|
||||
@implementation RedirectDelegate
|
||||
- (instancetype)init:(BOOL)followRedirects
|
||||
{
|
||||
if (self = [super init])
|
||||
_followRedirects = followRedirects;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
||||
newRequest:(NSURLRequest *)newRequest
|
||||
completionHandler:(void (^)(NSURLRequest *))completionHandler
|
||||
{
|
||||
if (!_followRedirects && response.statusCode >= 300 && response.statusCode < 400)
|
||||
completionHandler(nil);
|
||||
else
|
||||
completionHandler(newRequest);
|
||||
}
|
||||
@end
|
||||
|
||||
|
||||
@interface Connection : NSObject
|
||||
+ (nullable NSData *)sendSynchronousRequest:(NSURLRequest *)request
|
||||
followRedirects:(BOOL)followRedirects
|
||||
returningResponse:(NSURLResponse **)response
|
||||
error:(NSError **)error;
|
||||
@end
|
||||
|
||||
@implementation Connection
|
||||
|
||||
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request
|
||||
followRedirects:(BOOL)followRedirects
|
||||
returningResponse:(NSURLResponse * __autoreleasing *)response
|
||||
error:(NSError * __autoreleasing *)error
|
||||
{
|
||||
Connection * connection = [[Connection alloc] init];
|
||||
return [connection sendSynchronousRequest:request followRedirects: followRedirects returningResponse:response error:error];
|
||||
}
|
||||
|
||||
- (NSData *)sendSynchronousRequest:(NSURLRequest *)request
|
||||
followRedirects:(BOOL)followRedirects
|
||||
returningResponse:(NSURLResponse * __autoreleasing *)response
|
||||
error:(NSError * __autoreleasing *)error {
|
||||
__block NSData * resultData = nil;
|
||||
__block NSURLResponse * resultResponse = nil;
|
||||
__block NSError * resultError = nil;
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
dispatch_group_enter(group);
|
||||
|
||||
RedirectDelegate * delegate = [[RedirectDelegate alloc] init: followRedirects];
|
||||
|
||||
[[[HttpSessionManager sharedManager]
|
||||
dataTaskWithRequest:request
|
||||
delegate:delegate
|
||||
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response,
|
||||
NSError * _Nullable error) {
|
||||
resultData = data;
|
||||
resultResponse = response;
|
||||
resultError = error;
|
||||
dispatch_group_leave(group);
|
||||
}] resume];
|
||||
|
||||
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
|
||||
*response = resultResponse;
|
||||
*error = resultError;
|
||||
return resultData;
|
||||
}
|
||||
@end
|
||||
|
||||
namespace platform
|
||||
{
|
||||
bool HttpClient::RunHttpRequest()
|
||||
{
|
||||
NSURL * url = static_cast<NSURL *>([NSURL URLWithString:@(m_urlRequested.c_str())]);
|
||||
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL: url
|
||||
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:m_timeoutSec];
|
||||
// We handle cookies manually.
|
||||
request.HTTPShouldHandleCookies = NO;
|
||||
|
||||
request.HTTPMethod = @(m_httpMethod.c_str());
|
||||
for (auto const & header : m_headers)
|
||||
{
|
||||
NSString * field = @(header.first.c_str());
|
||||
[request setValue:@(header.second.c_str()) forHTTPHeaderField:field];
|
||||
}
|
||||
|
||||
if (!m_cookies.empty())
|
||||
[request setValue:[NSString stringWithUTF8String:m_cookies.c_str()] forHTTPHeaderField:@"Cookie"];
|
||||
|
||||
if (!m_bodyData.empty())
|
||||
{
|
||||
request.HTTPBody = [NSData dataWithBytes:m_bodyData.data() length:m_bodyData.size()];
|
||||
LOG(LDEBUG, ("Uploading buffer of size", m_bodyData.size(), "bytes"));
|
||||
}
|
||||
else if (!m_inputFile.empty())
|
||||
{
|
||||
NSError * err = nil;
|
||||
NSString * path = [NSString stringWithUTF8String:m_inputFile.c_str()];
|
||||
const unsigned long long file_size = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&err].fileSize;
|
||||
if (err)
|
||||
{
|
||||
m_errorCode = static_cast<int>(err.code);
|
||||
LOG(LDEBUG, ("Error: ", m_errorCode, err.localizedDescription.UTF8String));
|
||||
|
||||
return false;
|
||||
}
|
||||
request.HTTPBodyStream = [NSInputStream inputStreamWithFileAtPath:path];
|
||||
[request setValue:[NSString stringWithFormat:@"%llu", file_size] forHTTPHeaderField:@"Content-Length"];
|
||||
LOG(LDEBUG, ("Uploading file", m_inputFile, file_size, "bytes"));
|
||||
}
|
||||
|
||||
NSHTTPURLResponse * response = nil;
|
||||
NSError * err = nil;
|
||||
NSData * url_data = [Connection sendSynchronousRequest:request followRedirects:m_followRedirects returningResponse:&response error:&err];
|
||||
|
||||
m_headers.clear();
|
||||
|
||||
if (response)
|
||||
{
|
||||
m_errorCode = static_cast<int>(response.statusCode);
|
||||
|
||||
NSString * redirectUri = [response.allHeaderFields objectForKey:@"Location"];
|
||||
if (redirectUri)
|
||||
m_urlReceived = redirectUri.UTF8String;
|
||||
else
|
||||
m_urlReceived = response.URL.absoluteString.UTF8String;
|
||||
|
||||
if (m_loadHeaders)
|
||||
{
|
||||
[response.allHeaderFields enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * obj, BOOL * stop)
|
||||
{
|
||||
m_headers.emplace(key.lowercaseString.UTF8String, obj.UTF8String);
|
||||
}];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSString * cookies = [response.allHeaderFields objectForKey:@"Set-Cookie"];
|
||||
if (cookies)
|
||||
m_headers.emplace("Set-Cookie", NormalizeServerCookies(cookies.UTF8String));
|
||||
}
|
||||
|
||||
if (url_data)
|
||||
{
|
||||
if (m_outputFile.empty())
|
||||
m_serverResponse.assign(reinterpret_cast<char const *>(url_data.bytes), url_data.length);
|
||||
else
|
||||
[url_data writeToFile:@(m_outputFile.c_str()) atomically:YES];
|
||||
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// Request has failed if we are here.
|
||||
// MacOSX/iOS-specific workaround for HTTP 401 error bug.
|
||||
// @see bit.ly/1TrHlcS for more details.
|
||||
if (err.code == NSURLErrorUserCancelledAuthentication)
|
||||
{
|
||||
m_errorCode = 401;
|
||||
return true;
|
||||
}
|
||||
|
||||
m_errorCode = static_cast<int>(err.code);
|
||||
LOG(LDEBUG, ("Error: ", m_errorCode, ':', err.localizedDescription.UTF8String,
|
||||
"while connecting to", m_urlRequested));
|
||||
|
||||
return false;
|
||||
}
|
||||
} // namespace platform
|
||||
309
libs/platform/http_client_curl.cpp
Normal file
309
libs/platform/http_client_curl.cpp
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
/*******************************************************************************
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Alexander Borsuk <me@alex.bio> from Minsk, Belarus
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*******************************************************************************/
|
||||
#include "platform/http_client.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/zlib.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/exception.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <cstdio> // popen, tmpnam
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define popen _popen
|
||||
#define pclose _pclose
|
||||
#else
|
||||
#include <unistd.h> // close
|
||||
#endif
|
||||
|
||||
using namespace coding;
|
||||
|
||||
namespace
|
||||
{
|
||||
DECLARE_EXCEPTION(PipeCallError, RootException);
|
||||
|
||||
struct ScopedRemoveFile
|
||||
{
|
||||
ScopedRemoveFile() = default;
|
||||
explicit ScopedRemoveFile(std::string const & fileName) : m_fileName(fileName) {}
|
||||
|
||||
~ScopedRemoveFile()
|
||||
{
|
||||
if (!m_fileName.empty())
|
||||
std::remove(m_fileName.c_str());
|
||||
}
|
||||
|
||||
std::string m_fileName;
|
||||
};
|
||||
|
||||
static std::string ReadFileAsString(std::string const & filePath)
|
||||
{
|
||||
std::ifstream ifs(filePath, std::ifstream::in);
|
||||
if (!ifs.is_open())
|
||||
return {};
|
||||
|
||||
return {std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
std::string RunCurl(std::string const & cmd)
|
||||
{
|
||||
FILE * pipe = ::popen(cmd.c_str(), "r");
|
||||
ASSERT(pipe, ());
|
||||
std::array<char, 8 * 1024> arr;
|
||||
std::string result;
|
||||
size_t read;
|
||||
do
|
||||
{
|
||||
read = ::fread(arr.data(), 1, arr.size(), pipe);
|
||||
if (read > 0)
|
||||
result.append(arr.data(), read);
|
||||
}
|
||||
while (read == arr.size());
|
||||
|
||||
auto const err = ::pclose(pipe);
|
||||
// Exception will be cought in RunHTTPRequest
|
||||
if (err)
|
||||
throw PipeCallError("", "Error " + strings::to_string(err) + " while calling " + cmd);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string GetTmpFileName()
|
||||
{
|
||||
boost::uuids::random_generator gen;
|
||||
boost::uuids::uuid u = gen();
|
||||
|
||||
std::stringstream ss;
|
||||
ss << u;
|
||||
|
||||
ASSERT(!ss.str().empty(), ());
|
||||
|
||||
return GetPlatform().TmpPathForFile(ss.str());
|
||||
}
|
||||
|
||||
using HeadersVector = std::vector<std::pair<std::string, std::string>>;
|
||||
|
||||
HeadersVector ParseHeaders(std::string const & raw)
|
||||
{
|
||||
std::istringstream stream(raw);
|
||||
HeadersVector headers;
|
||||
std::string line;
|
||||
while (getline(stream, line))
|
||||
{
|
||||
auto const cr = line.rfind('\r');
|
||||
if (cr != std::string::npos)
|
||||
line.erase(cr);
|
||||
|
||||
auto const delims = line.find(": ");
|
||||
if (delims != std::string::npos)
|
||||
headers.emplace_back(line.substr(0, delims), line.substr(delims + 2));
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
bool WriteToFile(std::string const & fileName, std::string const & data)
|
||||
{
|
||||
std::ofstream ofs(fileName);
|
||||
if (!ofs.is_open())
|
||||
{
|
||||
LOG(LERROR, ("Failed to write into a temporary file."));
|
||||
return false;
|
||||
}
|
||||
|
||||
ofs << data;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Decompress(std::string const & compressed, std::string const & encoding)
|
||||
{
|
||||
std::string decompressed;
|
||||
|
||||
if (encoding == "deflate")
|
||||
{
|
||||
ZLib::Inflate inflate(ZLib::Inflate::Format::ZLib);
|
||||
|
||||
// We do not check return value of inflate here.
|
||||
// It may return false if compressed data is broken or if there is some unconsumed data
|
||||
// at the end of buffer. The second case considered as ok by some http clients.
|
||||
// For example, server we use for AsyncGuiThread_GetHotelInfo test adds '\n' to the end of the buffer
|
||||
// and MacOS client and some versions of curl return no error.
|
||||
UNUSED_VALUE(inflate(compressed, back_inserter(decompressed)));
|
||||
}
|
||||
else
|
||||
{
|
||||
ASSERT(false, ("Unsupported Content-Encoding:", encoding));
|
||||
}
|
||||
|
||||
return decompressed;
|
||||
}
|
||||
} // namespace
|
||||
// Used as a test stub for basic HTTP client implementation.
|
||||
// Make sure that you have curl installed in the PATH.
|
||||
// TODO(AlexZ): Not a production-ready implementation.
|
||||
namespace platform
|
||||
{
|
||||
// Extract HTTP headers via temporary file with -D switch.
|
||||
// HTTP status code is extracted from curl output (-w switches).
|
||||
// Redirects are handled recursively. TODO(AlexZ): avoid infinite redirects loop.
|
||||
bool HttpClient::RunHttpRequest()
|
||||
{
|
||||
ScopedRemoveFile headers_deleter(GetTmpFileName());
|
||||
ScopedRemoveFile body_deleter;
|
||||
ScopedRemoveFile received_file_deleter;
|
||||
|
||||
std::string cmd = "curl -s -w \"%{http_code}\" -D \"" + headers_deleter.m_fileName + "\" ";
|
||||
// From curl manual:
|
||||
// This option [-X] only changes the actual word used in the HTTP request, it does not alter
|
||||
// the way curl behaves. So for example if you want to make a proper
|
||||
// HEAD request, using -X HEAD will not suffice. You need to use the -I, --head option.
|
||||
if (m_httpMethod == "HEAD")
|
||||
cmd += "-I ";
|
||||
else
|
||||
cmd += "-X " + m_httpMethod + " ";
|
||||
|
||||
for (auto const & header : m_headers)
|
||||
cmd += "-H \"" + header.first + ": " + header.second + "\" ";
|
||||
|
||||
if (!m_cookies.empty())
|
||||
cmd += "-b \"" + m_cookies + "\" ";
|
||||
|
||||
cmd += "-m \"" + strings::to_string(m_timeoutSec) + "\" ";
|
||||
|
||||
if (!m_bodyData.empty())
|
||||
{
|
||||
body_deleter.m_fileName = GetTmpFileName();
|
||||
// POST body through tmp file to avoid breaking command line.
|
||||
if (!WriteToFile(body_deleter.m_fileName, m_bodyData))
|
||||
return false;
|
||||
|
||||
// TODO(AlexZ): Correctly clean up this internal var to avoid client confusion.
|
||||
m_inputFile = body_deleter.m_fileName;
|
||||
}
|
||||
// Content-Length is added automatically by curl.
|
||||
if (!m_inputFile.empty())
|
||||
cmd += "--data-binary \"@" + m_inputFile + "\" ";
|
||||
|
||||
// Use temporary file to receive data from server.
|
||||
// If user has specified file name to save data, it is not temporary and is not deleted automatically.
|
||||
std::string rfile = m_outputFile;
|
||||
if (rfile.empty())
|
||||
{
|
||||
rfile = GetTmpFileName();
|
||||
received_file_deleter.m_fileName = rfile;
|
||||
}
|
||||
|
||||
cmd += "-o " + rfile + strings::to_string(" ") + "\"" + m_urlRequested + "\"";
|
||||
|
||||
LOG(LDEBUG, ("Executing", cmd));
|
||||
|
||||
try
|
||||
{
|
||||
m_errorCode = stoi(RunCurl(cmd));
|
||||
}
|
||||
catch (RootException const & ex)
|
||||
{
|
||||
LOG(LERROR, (ex.Msg()));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_headers.clear();
|
||||
auto const headers = ParseHeaders(ReadFileAsString(headers_deleter.m_fileName));
|
||||
std::string serverCookies;
|
||||
std::string headerKey;
|
||||
for (auto const & header : headers)
|
||||
{
|
||||
if (strings::EqualNoCase(header.first, "Set-Cookie"))
|
||||
{
|
||||
serverCookies += header.second + ", ";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (strings::EqualNoCase(header.first, "Location"))
|
||||
m_urlReceived = header.second;
|
||||
|
||||
if (m_loadHeaders)
|
||||
{
|
||||
headerKey = header.first;
|
||||
strings::AsciiToLower(headerKey);
|
||||
m_headers.emplace(headerKey, header.second);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_headers.emplace("Set-Cookie", NormalizeServerCookies(std::move(serverCookies)));
|
||||
|
||||
if (m_urlReceived.empty())
|
||||
{
|
||||
m_urlReceived = m_urlRequested;
|
||||
// Load body contents in final request only (skip redirects).
|
||||
// Sometimes server can reply with empty body, and it's ok.
|
||||
if (m_outputFile.empty())
|
||||
m_serverResponse = ReadFileAsString(rfile);
|
||||
}
|
||||
else if (m_followRedirects)
|
||||
{
|
||||
// Follow HTTP redirect.
|
||||
// TODO(AlexZ): Should we check HTTP redirect code here?
|
||||
LOG(LDEBUG, ("HTTP redirect", m_errorCode, "to", m_urlReceived));
|
||||
|
||||
HttpClient redirect(m_urlReceived);
|
||||
redirect.SetCookies(CombinedCookies());
|
||||
|
||||
if (!redirect.RunHttpRequest())
|
||||
{
|
||||
m_errorCode = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_errorCode = redirect.ErrorCode();
|
||||
m_urlReceived = redirect.UrlReceived();
|
||||
m_headers = std::move(redirect.m_headers);
|
||||
m_serverResponse = std::move(redirect.m_serverResponse);
|
||||
}
|
||||
|
||||
for (auto const & header : headers)
|
||||
{
|
||||
if (strings::EqualNoCase(header.first, "content-encoding") && !strings::EqualNoCase(header.second, "identity"))
|
||||
{
|
||||
m_serverResponse = Decompress(m_serverResponse, header.second);
|
||||
LOG(LDEBUG, ("Response with", header.second, "is decompressed."));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace platform
|
||||
6
libs/platform/http_payload.cpp
Normal file
6
libs/platform/http_payload.cpp
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include "platform/http_payload.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
HttpPayload::HttpPayload() : m_method("POST"), m_fileKey("file"), m_needClientAuth(false) {}
|
||||
} // namespace platform
|
||||
20
libs/platform/http_payload.hpp
Normal file
20
libs/platform/http_payload.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
struct HttpPayload
|
||||
{
|
||||
HttpPayload();
|
||||
|
||||
std::string m_method;
|
||||
std::string m_url;
|
||||
std::map<std::string, std::string> m_params;
|
||||
std::map<std::string, std::string> m_headers;
|
||||
std::string m_fileKey;
|
||||
std::string m_filePath;
|
||||
bool m_needClientAuth;
|
||||
};
|
||||
} // namespace platform
|
||||
378
libs/platform/http_request.cpp
Normal file
378
libs/platform/http_request.cpp
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
#include "platform/http_request.hpp"
|
||||
|
||||
#include "platform/chunks_download_strategy.hpp"
|
||||
#include "platform/http_thread_callback.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#ifdef DEBUG
|
||||
#include "base/thread.hpp"
|
||||
#endif
|
||||
|
||||
#include "coding/file_writer.hpp"
|
||||
#include "coding/internal/file_data.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
class HttpThread;
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
using std::string;
|
||||
|
||||
namespace non_http_error_code
|
||||
{
|
||||
string DebugPrint(long errorCode)
|
||||
{
|
||||
switch (errorCode)
|
||||
{
|
||||
case kIOException: return "IO exception";
|
||||
case kWriteException: return "Write exception";
|
||||
case kInconsistentFileSize: return "Inconsistent file size";
|
||||
case kNonHttpResponse: return "Non-http response";
|
||||
case kInvalidURL: return "Invalid URL";
|
||||
case kCancelled: return "Cancelled";
|
||||
default: return std::to_string(errorCode);
|
||||
}
|
||||
}
|
||||
} // namespace non_http_error_code
|
||||
|
||||
/// @return 0 if creation failed
|
||||
HttpThread * CreateNativeHttpThread(string const & url, IHttpThreadCallback & callback, int64_t begRange = 0,
|
||||
int64_t endRange = -1, int64_t expectedSize = -1,
|
||||
string const & postBody = string());
|
||||
void DeleteNativeHttpThread(HttpThread * thread);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////
|
||||
/// Stores server response into the memory
|
||||
class MemoryHttpRequest
|
||||
: public HttpRequest
|
||||
, public IHttpThreadCallback
|
||||
{
|
||||
HttpThread * m_thread;
|
||||
|
||||
string m_requestUrl;
|
||||
string m_downloadedData;
|
||||
MemWriter<string> m_writer;
|
||||
|
||||
virtual bool OnWrite(int64_t, void const * buffer, size_t size)
|
||||
{
|
||||
m_writer.Write(buffer, size);
|
||||
m_progress.m_bytesDownloaded += size;
|
||||
if (m_onProgress)
|
||||
m_onProgress(*this);
|
||||
return true;
|
||||
}
|
||||
|
||||
virtual void OnFinish(long httpOrErrorCode, int64_t, int64_t)
|
||||
{
|
||||
if (httpOrErrorCode == 200)
|
||||
{
|
||||
m_status = DownloadStatus::Completed;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const message = non_http_error_code::DebugPrint(httpOrErrorCode);
|
||||
LOG(LWARNING, ("HttpRequest error:", message));
|
||||
if (httpOrErrorCode == 404)
|
||||
m_status = DownloadStatus::FileNotFound;
|
||||
else
|
||||
m_status = DownloadStatus::Failed;
|
||||
}
|
||||
|
||||
m_onFinish(*this);
|
||||
}
|
||||
|
||||
public:
|
||||
MemoryHttpRequest(string const & url, Callback && onFinish, Callback && onProgress)
|
||||
: HttpRequest(std::move(onFinish), std::move(onProgress))
|
||||
, m_requestUrl(url)
|
||||
, m_writer(m_downloadedData)
|
||||
{
|
||||
m_thread = CreateNativeHttpThread(url, *this);
|
||||
ASSERT(m_thread, ());
|
||||
}
|
||||
|
||||
MemoryHttpRequest(string const & url, string const & postData, Callback && onFinish, Callback && onProgress)
|
||||
: HttpRequest(std::move(onFinish), std::move(onProgress))
|
||||
, m_writer(m_downloadedData)
|
||||
{
|
||||
m_thread = CreateNativeHttpThread(url, *this, 0, -1, -1, postData);
|
||||
ASSERT(m_thread, ());
|
||||
}
|
||||
|
||||
virtual ~MemoryHttpRequest() { DeleteNativeHttpThread(m_thread); }
|
||||
|
||||
virtual string const & GetData() const { return m_downloadedData; }
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
class FileHttpRequest
|
||||
: public HttpRequest
|
||||
, public IHttpThreadCallback
|
||||
{
|
||||
ChunksDownloadStrategy m_strategy;
|
||||
typedef std::pair<HttpThread *, int64_t> ThreadHandleT;
|
||||
typedef std::list<ThreadHandleT> ThreadsContainerT;
|
||||
ThreadsContainerT m_threads;
|
||||
|
||||
std::string m_filePath;
|
||||
std::unique_ptr<FileWriter> m_writer;
|
||||
|
||||
bool m_doCleanProgressFiles;
|
||||
|
||||
// Starts a thread per each free/available server.
|
||||
ChunksDownloadStrategy::ResultT StartThreads()
|
||||
{
|
||||
string url;
|
||||
std::pair<int64_t, int64_t> range;
|
||||
ChunksDownloadStrategy::ResultT result;
|
||||
while ((result = m_strategy.NextChunk(url, range)) == ChunksDownloadStrategy::ENextChunk)
|
||||
{
|
||||
HttpThread * p = CreateNativeHttpThread(url, *this, range.first, range.second, m_progress.m_bytesTotal);
|
||||
ASSERT(p, ());
|
||||
m_threads.push_back(std::make_pair(p, range.first));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
class ThreadByPos
|
||||
{
|
||||
int64_t m_pos;
|
||||
|
||||
public:
|
||||
explicit ThreadByPos(int64_t pos) : m_pos(pos) {}
|
||||
inline bool operator()(ThreadHandleT const & p) const { return (p.second == m_pos); }
|
||||
};
|
||||
|
||||
void RemoveHttpThreadByKey(int64_t begRange)
|
||||
{
|
||||
ThreadsContainerT::iterator it = find_if(m_threads.begin(), m_threads.end(), ThreadByPos(begRange));
|
||||
if (it != m_threads.end())
|
||||
{
|
||||
HttpThread * p = it->first;
|
||||
m_threads.erase(it);
|
||||
DeleteNativeHttpThread(p);
|
||||
}
|
||||
else
|
||||
LOG(LERROR, ("Tried to remove invalid thread for position", begRange));
|
||||
}
|
||||
|
||||
virtual bool OnWrite(int64_t offset, void const * buffer, size_t size)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
static threads::ThreadID const id = threads::GetCurrentThreadID();
|
||||
ASSERT_EQUAL(id, threads::GetCurrentThreadID(), ("OnWrite called from different threads"));
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
m_writer->Seek(offset);
|
||||
m_writer->Write(buffer, size);
|
||||
return true;
|
||||
}
|
||||
catch (Writer::Exception const & e)
|
||||
{
|
||||
LOG(LWARNING, ("Can't write buffer for size", size, e.Msg()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Saves current chunks' statuses into a resume file.
|
||||
void SaveResumeChunks()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Flush writer before saving downloaded chunks.
|
||||
m_writer->Flush();
|
||||
|
||||
m_strategy.SaveChunks(m_progress.m_bytesTotal, m_filePath + RESUME_FILE_EXTENSION);
|
||||
}
|
||||
catch (Writer::Exception const & e)
|
||||
{
|
||||
LOG(LWARNING, ("Can't flush writer", e.Msg()));
|
||||
}
|
||||
}
|
||||
|
||||
/// Called for each chunk by one main (GUI) thread.
|
||||
virtual void OnFinish(long httpOrErrorCode, int64_t begRange, int64_t endRange)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
static threads::ThreadID const id = threads::GetCurrentThreadID();
|
||||
ASSERT_EQUAL(id, threads::GetCurrentThreadID(), ("OnFinish called from different threads"));
|
||||
#endif
|
||||
|
||||
bool const isChunkOk = (httpOrErrorCode == 200);
|
||||
string const urlError = m_strategy.ChunkFinished(isChunkOk, std::make_pair(begRange, endRange));
|
||||
|
||||
// remove completed chunk from the list, beg is the key
|
||||
RemoveHttpThreadByKey(begRange);
|
||||
|
||||
// report progress
|
||||
if (isChunkOk)
|
||||
{
|
||||
m_progress.m_bytesDownloaded += (endRange - begRange) + 1;
|
||||
if (m_onProgress)
|
||||
m_onProgress(*this);
|
||||
}
|
||||
else
|
||||
{
|
||||
auto const message = non_http_error_code::DebugPrint(httpOrErrorCode);
|
||||
LOG(LWARNING, (m_filePath, "HttpRequest error:", message));
|
||||
}
|
||||
|
||||
ChunksDownloadStrategy::ResultT const result = StartThreads();
|
||||
if (result == ChunksDownloadStrategy::EDownloadFailed)
|
||||
m_status = httpOrErrorCode == 404 ? DownloadStatus::FileNotFound : DownloadStatus::Failed;
|
||||
else if (result == ChunksDownloadStrategy::EDownloadSucceeded)
|
||||
m_status = DownloadStatus::Completed;
|
||||
|
||||
// Save chunks statuses into the resume file.
|
||||
if (isChunkOk && m_status != DownloadStatus::Completed)
|
||||
SaveResumeChunks();
|
||||
|
||||
if (m_status == DownloadStatus::InProgress)
|
||||
return;
|
||||
|
||||
// 1. Save downloaded chunks if some error occured.
|
||||
if (m_status == DownloadStatus::Failed || m_status == DownloadStatus::FileNotFound)
|
||||
SaveResumeChunks();
|
||||
|
||||
// 2. Free file handle.
|
||||
CloseWriter();
|
||||
|
||||
// 3. Clean up resume file with chunks range on success
|
||||
if (m_status == DownloadStatus::Completed)
|
||||
{
|
||||
Platform::RemoveFileIfExists(m_filePath + RESUME_FILE_EXTENSION);
|
||||
|
||||
// Rename finished file to it's original name.
|
||||
Platform::RemoveFileIfExists(m_filePath);
|
||||
base::RenameFileX(m_filePath + DOWNLOADING_FILE_EXTENSION, m_filePath);
|
||||
}
|
||||
|
||||
// 4. Finish downloading.
|
||||
m_onFinish(*this);
|
||||
}
|
||||
|
||||
void CloseWriter()
|
||||
{
|
||||
try
|
||||
{
|
||||
m_writer.reset();
|
||||
}
|
||||
catch (Writer::Exception const & e)
|
||||
{
|
||||
LOG(LWARNING, ("Can't close file correctly", e.Msg()));
|
||||
|
||||
m_status = DownloadStatus::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
FileHttpRequest(std::vector<std::string> const & urls, std::string const & filePath, int64_t fileSize,
|
||||
Callback && onFinish, Callback && onProgress, int64_t chunkSize, bool doCleanProgressFiles)
|
||||
: HttpRequest(std::move(onFinish), std::move(onProgress))
|
||||
, m_strategy(urls)
|
||||
, m_filePath(filePath)
|
||||
, m_doCleanProgressFiles(doCleanProgressFiles)
|
||||
{
|
||||
ASSERT(!urls.empty(), ());
|
||||
|
||||
// Load resume downloading information.
|
||||
m_progress.m_bytesDownloaded = m_strategy.LoadOrInitChunks(m_filePath + RESUME_FILE_EXTENSION, fileSize, chunkSize);
|
||||
m_progress.m_bytesTotal = fileSize;
|
||||
|
||||
FileWriter::Op openMode = FileWriter::OP_WRITE_TRUNCATE;
|
||||
if (m_progress.m_bytesDownloaded != 0)
|
||||
{
|
||||
// Check that resume information is correct with existing file.
|
||||
uint64_t size;
|
||||
if (base::GetFileSize(filePath + DOWNLOADING_FILE_EXTENSION, size) && size <= static_cast<uint64_t>(fileSize))
|
||||
openMode = FileWriter::OP_WRITE_EXISTING;
|
||||
else
|
||||
{
|
||||
LOG(LWARNING, ("Incomplete file size is bigger than expected, re-downloading."));
|
||||
m_strategy.InitChunks(fileSize, chunkSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Create file and reserve needed size.
|
||||
std::unique_ptr<FileWriter> writer(new FileWriter(filePath + DOWNLOADING_FILE_EXTENSION, openMode));
|
||||
|
||||
// Assign here, because previous functions can throw an exception.
|
||||
m_writer.swap(writer);
|
||||
Platform::DisableBackupForFile(filePath + DOWNLOADING_FILE_EXTENSION);
|
||||
StartThreads();
|
||||
}
|
||||
|
||||
virtual ~FileHttpRequest()
|
||||
{
|
||||
// Do safe delete with removing from list in case if DeleteNativeHttpThread
|
||||
// can produce final notifications to this->OnFinish().
|
||||
while (!m_threads.empty())
|
||||
{
|
||||
HttpThread * p = m_threads.back().first;
|
||||
m_threads.pop_back();
|
||||
DeleteNativeHttpThread(p);
|
||||
}
|
||||
|
||||
if (m_status == DownloadStatus::InProgress)
|
||||
{
|
||||
// means that client canceled download process, so delete all temporary files
|
||||
CloseWriter();
|
||||
|
||||
if (m_doCleanProgressFiles)
|
||||
{
|
||||
Platform::RemoveFileIfExists(m_filePath + DOWNLOADING_FILE_EXTENSION);
|
||||
Platform::RemoveFileIfExists(m_filePath + RESUME_FILE_EXTENSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual string const & GetData() const { return m_filePath; }
|
||||
};
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
HttpRequest::HttpRequest(Callback && onFinish, Callback && onProgress)
|
||||
: m_status(DownloadStatus::InProgress)
|
||||
, m_progress(Progress::Unknown())
|
||||
, m_onFinish(std::move(onFinish))
|
||||
, m_onProgress(std::move(onProgress))
|
||||
{}
|
||||
|
||||
HttpRequest::~HttpRequest() {}
|
||||
|
||||
HttpRequest * HttpRequest::Get(string const & url, Callback && onFinish, Callback && onProgress)
|
||||
{
|
||||
return new MemoryHttpRequest(url, std::move(onFinish), std::move(onProgress));
|
||||
}
|
||||
|
||||
HttpRequest * HttpRequest::PostJson(string const & url, string const & postData, Callback && onFinish,
|
||||
Callback && onProgress)
|
||||
{
|
||||
return new MemoryHttpRequest(url, postData, std::move(onFinish), std::move(onProgress));
|
||||
}
|
||||
|
||||
HttpRequest * HttpRequest::GetFile(std::vector<string> const & urls, string const & filePath, int64_t fileSize,
|
||||
Callback && onFinish, Callback && onProgress, int64_t chunkSize,
|
||||
bool doCleanOnCancel)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new FileHttpRequest(urls, filePath, fileSize, std::move(onFinish), std::move(onProgress), chunkSize,
|
||||
doCleanOnCancel);
|
||||
}
|
||||
catch (FileWriter::Exception const & e)
|
||||
{
|
||||
// Can't create or open file for writing.
|
||||
LOG(LWARNING, ("Can't create file", filePath, "with size", fileSize, e.Msg()));
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
} // namespace downloader
|
||||
60
libs/platform/http_request.hpp
Normal file
60
libs/platform/http_request.hpp
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/downloader_defines.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
namespace non_http_error_code
|
||||
{
|
||||
auto constexpr kIOException = -1;
|
||||
auto constexpr kWriteException = -2;
|
||||
auto constexpr kInconsistentFileSize = -3;
|
||||
auto constexpr kNonHttpResponse = -4;
|
||||
auto constexpr kInvalidURL = -5;
|
||||
auto constexpr kCancelled = -6;
|
||||
} // namespace non_http_error_code
|
||||
|
||||
/// Request in progress will be canceled on delete
|
||||
class HttpRequest
|
||||
{
|
||||
public:
|
||||
using Callback = std::function<void(HttpRequest & request)>;
|
||||
|
||||
protected:
|
||||
DownloadStatus m_status;
|
||||
Progress m_progress;
|
||||
Callback m_onFinish;
|
||||
Callback m_onProgress;
|
||||
|
||||
HttpRequest(Callback && onFinish, Callback && onProgress);
|
||||
|
||||
public:
|
||||
virtual ~HttpRequest() = 0;
|
||||
|
||||
DownloadStatus GetStatus() const { return m_status; }
|
||||
Progress const & GetProgress() const { return m_progress; }
|
||||
/// Either file path (for chunks) or downloaded data
|
||||
virtual std::string const & GetData() const = 0;
|
||||
|
||||
/// Response saved to memory buffer and retrieved with Data()
|
||||
static HttpRequest * Get(std::string const & url, Callback && onFinish, Callback && onProgress = Callback());
|
||||
|
||||
/// Content-type for request is always "application/json"
|
||||
static HttpRequest * PostJson(std::string const & url, std::string const & postData, Callback && onFinish,
|
||||
Callback && onProgress = Callback());
|
||||
|
||||
/// Download file to filePath.
|
||||
/// Pulls chunks simultaneously from all available servers, 1 thread per server.
|
||||
/// @param[in] fileSize Correct file size (needed for resuming and reserving).
|
||||
static HttpRequest * GetFile(std::vector<std::string> const & urls, std::string const & filePath, int64_t fileSize,
|
||||
Callback && onFinish, Callback && onProgress = Callback(),
|
||||
int64_t chunkSize = 0, // 0 for auto
|
||||
bool doCleanOnCancel = true);
|
||||
};
|
||||
} // namespace downloader
|
||||
12
libs/platform/http_session_manager.h
Normal file
12
libs/platform/http_session_manager.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface HttpSessionManager : NSObject
|
||||
|
||||
+ (HttpSessionManager *)sharedManager;
|
||||
|
||||
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
|
||||
delegate:(id<NSURLSessionDataDelegate>)delegate
|
||||
completionHandler:(void (^)(NSData * data, NSURLResponse * response,
|
||||
NSError * error))completionHandler;
|
||||
|
||||
@end
|
||||
198
libs/platform/http_session_manager.mm
Normal file
198
libs/platform/http_session_manager.mm
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
#import "platform/http_session_manager.h"
|
||||
|
||||
@interface DataTaskInfo : NSObject
|
||||
|
||||
@property(nonatomic, weak) id<NSURLSessionDataDelegate> delegate;
|
||||
@property(nonatomic) NSURLSessionDataTask * task;
|
||||
|
||||
- (instancetype)initWithTask:(NSURLSessionDataTask *)task
|
||||
delegate:(id<NSURLSessionDataDelegate>)delegate;
|
||||
|
||||
@end
|
||||
|
||||
@implementation DataTaskInfo
|
||||
|
||||
- (instancetype)initWithTask:(NSURLSessionDataTask *)task
|
||||
delegate:(id<NSURLSessionDataDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_task = task;
|
||||
_delegate = delegate;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface HttpSessionManager ()<NSURLSessionDataDelegate>
|
||||
|
||||
@property(nonatomic) NSURLSession * session;
|
||||
@property(nonatomic) NSMutableDictionary * taskInfoByTaskID;
|
||||
@property(nonatomic) dispatch_queue_t taskInfoQueue;
|
||||
@property(nonatomic) dispatch_queue_t delegateQueue;
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpSessionManager
|
||||
|
||||
+ (HttpSessionManager *)sharedManager
|
||||
{
|
||||
static dispatch_once_t sOnceToken;
|
||||
static HttpSessionManager * sManager;
|
||||
dispatch_once(&sOnceToken, ^{
|
||||
sManager = [[HttpSessionManager alloc] init];
|
||||
});
|
||||
|
||||
return sManager;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]
|
||||
delegate:self
|
||||
delegateQueue:nil];
|
||||
_taskInfoByTaskID = [NSMutableDictionary dictionary];
|
||||
_taskInfoQueue = dispatch_queue_create("http_session_manager.queue", DISPATCH_QUEUE_CONCURRENT);
|
||||
// TODO(AB): As the main thread in tests that are using synchronous HTTP calls is blocked
|
||||
// by dispatch_group_wait(group, DISPATCH_TIME_FOREVER) in http_client_apple.mm,
|
||||
// and delegate should not strictly use only one (main) thread and can be run on
|
||||
// any thread, this workaround is needed.
|
||||
// Refactor out the whole sync HTTP implementation to use async + lambdas/callbacks.
|
||||
BOOL const isSyncHttpTest = [NSBundle.mainBundle.executableURL.lastPathComponent hasSuffix:@"osm_auth_tests"];
|
||||
_delegateQueue = isSyncHttpTest ? _taskInfoQueue : dispatch_get_main_queue();
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
|
||||
delegate:(id<NSURLSessionDataDelegate>)delegate
|
||||
completionHandler:(void (^)(NSData * data, NSURLResponse * response,
|
||||
NSError * error))completionHandler
|
||||
{
|
||||
NSURLSessionDataTask * task = [self.session dataTaskWithRequest:request
|
||||
completionHandler:completionHandler];
|
||||
|
||||
DataTaskInfo * taskInfo = [[DataTaskInfo alloc] initWithTask:task delegate:delegate];
|
||||
[self setDataTaskInfo:taskInfo forTask:task];
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
- (void)setDataTaskInfo:(DataTaskInfo *)taskInfo forTask:(NSURLSessionTask *)task
|
||||
{
|
||||
dispatch_barrier_sync(self.taskInfoQueue, ^{
|
||||
self.taskInfoByTaskID[@(task.taskIdentifier)] = taskInfo;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)removeTaskInfoForTask:(NSURLSessionTask *)task
|
||||
{
|
||||
dispatch_barrier_sync(self.taskInfoQueue, ^{
|
||||
[self.taskInfoByTaskID removeObjectForKey:@(task.taskIdentifier)];
|
||||
});
|
||||
}
|
||||
|
||||
- (DataTaskInfo *)taskInfoForTask:(NSURLSessionTask *)task
|
||||
{
|
||||
__block DataTaskInfo * taskInfo = nil;
|
||||
dispatch_sync(self.taskInfoQueue, ^{
|
||||
taskInfo = self.taskInfoByTaskID[@(task.taskIdentifier)];
|
||||
});
|
||||
|
||||
return taskInfo;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
||||
newRequest:(NSURLRequest *)newRequest
|
||||
completionHandler:(void (^)(NSURLRequest *))completionHandler
|
||||
{
|
||||
DataTaskInfo * taskInfo = [self taskInfoForTask:task];
|
||||
if ([taskInfo.delegate
|
||||
respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)])
|
||||
{
|
||||
dispatch_async(self.delegateQueue, ^{
|
||||
[taskInfo.delegate URLSession:session
|
||||
task:task
|
||||
willPerformHTTPRedirection:response
|
||||
newRequest:newRequest
|
||||
completionHandler:completionHandler];
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(newRequest);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
didCompleteWithError:(NSError *)error
|
||||
{
|
||||
DataTaskInfo * taskInfo = [self taskInfoForTask:task];
|
||||
[self removeTaskInfoForTask:task];
|
||||
|
||||
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:task:didCompleteWithError:)])
|
||||
{
|
||||
dispatch_async(self.delegateQueue, ^{
|
||||
[taskInfo.delegate URLSession:session task:task didCompleteWithError:error];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveResponse:(NSURLResponse *)response
|
||||
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
|
||||
{
|
||||
DataTaskInfo * taskInfo = [self taskInfoForTask:dataTask];
|
||||
if ([taskInfo.delegate
|
||||
respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)])
|
||||
{
|
||||
dispatch_async(self.delegateQueue, ^{
|
||||
[taskInfo.delegate URLSession:session
|
||||
dataTask:dataTask
|
||||
didReceiveResponse:response
|
||||
completionHandler:completionHandler];
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
completionHandler(NSURLSessionResponseAllow);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveData:(NSData *)data
|
||||
{
|
||||
DataTaskInfo * taskInfo = [self taskInfoForTask:dataTask];
|
||||
if ([taskInfo.delegate respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)])
|
||||
{
|
||||
dispatch_async(self.delegateQueue, ^{
|
||||
[taskInfo.delegate URLSession:session dataTask:dataTask didReceiveData:data];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
||||
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
|
||||
NSURLCredential * _Nullable credential))completionHandler
|
||||
{
|
||||
NSURLCredential * credential =
|
||||
[[NSURLCredential alloc] initWithTrust:[challenge protectionSpace].serverTrust];
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
30
libs/platform/http_thread_apple.h
Normal file
30
libs/platform/http_thread_apple.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "std/target_os.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace downloader { class IHttpThreadCallback; }
|
||||
|
||||
#ifdef OMIM_OS_IPHONE
|
||||
#import "../iphone/Maps/Classes/DownloadIndicatorProtocol.h"
|
||||
#endif
|
||||
|
||||
@interface HttpThreadImpl : NSObject
|
||||
|
||||
- (instancetype)initWithURL:(std::string const &)url
|
||||
callback:(downloader::IHttpThreadCallback &)cb
|
||||
begRange:(int64_t)beg
|
||||
endRange:(int64_t)end
|
||||
expectedSize:(int64_t)size
|
||||
postBody:(std::string const &)pb;
|
||||
|
||||
- (void)cancel;
|
||||
|
||||
#ifdef OMIM_OS_IPHONE
|
||||
+ (void)setDownloadIndicatorProtocol:(id<DownloadIndicatorProtocol>)indicator;
|
||||
#endif
|
||||
|
||||
@end
|
||||
259
libs/platform/http_thread_apple.mm
Normal file
259
libs/platform/http_thread_apple.mm
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
#import "platform/http_thread_apple.h"
|
||||
|
||||
#include "platform/http_request.hpp"
|
||||
#import "platform/http_session_manager.h"
|
||||
#include "platform/http_thread_callback.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/macros.hpp"
|
||||
|
||||
static const NSTimeInterval kTimeoutInterval = 10.0;
|
||||
|
||||
@interface HttpThreadImpl ()<NSURLSessionDataDelegate>
|
||||
{
|
||||
downloader::IHttpThreadCallback * m_callback;
|
||||
NSURLSessionDataTask * m_dataTask;
|
||||
int64_t m_begRange;
|
||||
int64_t m_endRange;
|
||||
int64_t m_downloadedBytes;
|
||||
int64_t m_expectedSize;
|
||||
BOOL m_cancelRequested;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation HttpThreadImpl
|
||||
|
||||
#ifdef OMIM_OS_IPHONE
|
||||
static id<DownloadIndicatorProtocol> downloadIndicator = nil;
|
||||
|
||||
+ (void)setDownloadIndicatorProtocol:(id<DownloadIndicatorProtocol>)indicator
|
||||
{
|
||||
downloadIndicator = indicator;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
LOG(LDEBUG, ("ID:", [self hash], "Connection is destroyed"));
|
||||
[m_dataTask cancel];
|
||||
#ifdef OMIM_OS_IPHONE
|
||||
[downloadIndicator enableStandby];
|
||||
[downloadIndicator disableDownloadIndicator];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
[m_dataTask cancel];
|
||||
m_cancelRequested = true;
|
||||
}
|
||||
|
||||
- (instancetype)initWithURL:(std::string const &)url
|
||||
callback:(downloader::IHttpThreadCallback &)cb
|
||||
begRange:(int64_t)beg
|
||||
endRange:(int64_t)end
|
||||
expectedSize:(int64_t)size
|
||||
postBody:(std::string const &)pb
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
m_callback = &cb;
|
||||
m_begRange = beg;
|
||||
m_endRange = end;
|
||||
m_downloadedBytes = 0;
|
||||
m_expectedSize = size;
|
||||
|
||||
NSMutableURLRequest * request =
|
||||
[NSMutableURLRequest requestWithURL:[NSURL URLWithString:@(url.c_str())]
|
||||
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
|
||||
timeoutInterval:kTimeoutInterval];
|
||||
|
||||
// use Range header only if we don't download whole file from start
|
||||
if (!(beg == 0 && end < 0))
|
||||
{
|
||||
NSString * val;
|
||||
if (end > 0)
|
||||
{
|
||||
LOG(LDEBUG, (url, "downloading range [", beg, ",", end, "]"));
|
||||
val = [[NSString alloc] initWithFormat: @"bytes=%qi-%qi", beg, end];
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LDEBUG, (url, "resuming download from position", beg));
|
||||
val = [[NSString alloc] initWithFormat: @"bytes=%qi-", beg];
|
||||
}
|
||||
[request addValue:val forHTTPHeaderField:@"Range"];
|
||||
}
|
||||
|
||||
if (!pb.empty())
|
||||
{
|
||||
NSData * postData = [NSData dataWithBytes:pb.data() length:pb.size()];
|
||||
[request setHTTPBody:postData];
|
||||
[request setHTTPMethod:@"POST"];
|
||||
[request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
}
|
||||
|
||||
#ifdef OMIM_OS_IPHONE
|
||||
[downloadIndicator disableStandby];
|
||||
[downloadIndicator enableDownloadIndicator];
|
||||
#endif
|
||||
|
||||
// create the task with the request and start loading the data
|
||||
m_dataTask = [[HttpSessionManager sharedManager] dataTaskWithRequest:request
|
||||
delegate:self
|
||||
completionHandler:nil];
|
||||
|
||||
if (m_dataTask)
|
||||
{
|
||||
[m_dataTask resume];
|
||||
LOG(LDEBUG, ("ID:", [self hash], "Starting data task for", url));
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LERROR, ("Can't create data task for", url));
|
||||
return nil;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/// We cancel and don't support any redirects to avoid data corruption
|
||||
/// @TODO Display content to user - router is redirecting us somewhere
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
|
||||
newRequest:(NSURLRequest *)request
|
||||
completionHandler:(void (^)(NSURLRequest *))completionHandler
|
||||
{
|
||||
LOG(LWARNING, ("Canceling because of redirect from", response.URL.absoluteString.UTF8String, "to",
|
||||
request.URL.absoluteString.UTF8String));
|
||||
completionHandler(nil);
|
||||
m_callback->OnFinish(static_cast<NSHTTPURLResponse *>(response).statusCode, m_begRange,
|
||||
m_endRange);
|
||||
}
|
||||
|
||||
/// @return -1 if can't decode
|
||||
+ (int64_t)getContentRange:(NSDictionary *)httpHeader
|
||||
{
|
||||
if (NSString * cr = [httpHeader valueForKey:@"Content-Range"])
|
||||
{
|
||||
NSArray * arr = [cr componentsSeparatedByString:@"/"];
|
||||
if ([arr count])
|
||||
return [(NSString *)[arr objectAtIndex:[arr count] - 1] longLongValue];
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveResponse:(NSURLResponse *)response
|
||||
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
|
||||
{
|
||||
// This method is called when the server has determined that it
|
||||
// has enough information to create the NSURLResponse.
|
||||
|
||||
// check if this is OK (not a 404 or the like)
|
||||
if ([response isKindOfClass:[NSHTTPURLResponse class]])
|
||||
{
|
||||
NSInteger const statusCode = [(NSHTTPURLResponse *)response statusCode];
|
||||
LOG(LDEBUG, ("Got response with status code", statusCode));
|
||||
// When we didn't ask for chunks, code should be 200
|
||||
// When we asked for a chunk, code should be 206
|
||||
bool const isChunk = !(m_begRange == 0 && m_endRange < 0);
|
||||
if ((isChunk && statusCode != 206) || (!isChunk && statusCode != 200))
|
||||
{
|
||||
LOG(LWARNING, ("Received invalid HTTP status code, canceling download", statusCode));
|
||||
completionHandler(NSURLSessionResponseCancel);
|
||||
m_callback->OnFinish(statusCode, m_begRange, m_endRange);
|
||||
return;
|
||||
}
|
||||
else if (m_expectedSize > 0)
|
||||
{
|
||||
// get full file expected size from Content-Range header
|
||||
int64_t sizeOnServer = [HttpThreadImpl getContentRange:[(NSHTTPURLResponse *)response allHeaderFields]];
|
||||
// if it's absent, use Content-Length instead
|
||||
if (sizeOnServer < 0)
|
||||
sizeOnServer = [response expectedContentLength];
|
||||
// We should always check returned size, even if it's invalid (-1)
|
||||
if (m_expectedSize != sizeOnServer)
|
||||
{
|
||||
LOG(LWARNING, ("Canceling download - server replied with invalid size", sizeOnServer,
|
||||
"!=", m_expectedSize));
|
||||
completionHandler(NSURLSessionResponseCancel);
|
||||
m_callback->OnFinish(downloader::non_http_error_code::kInconsistentFileSize, m_begRange, m_endRange);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
completionHandler(NSURLSessionResponseAllow);
|
||||
}
|
||||
else
|
||||
{
|
||||
// In theory, we should never be here.
|
||||
ASSERT(false, ("Invalid non-http response, aborting request"));
|
||||
completionHandler(NSURLSessionResponseCancel);
|
||||
m_callback->OnFinish(downloader::non_http_error_code::kNonHttpResponse, m_begRange, m_endRange);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
dataTask:(NSURLSessionDataTask *)dataTask
|
||||
didReceiveData:(NSData *)data
|
||||
{
|
||||
int64_t const length = [data length];
|
||||
m_downloadedBytes += length;
|
||||
if(!m_callback->OnWrite(m_begRange + m_downloadedBytes - length, [data bytes], length))
|
||||
{
|
||||
[m_dataTask cancel];
|
||||
m_callback->OnFinish(downloader::non_http_error_code::kWriteException, m_begRange, m_endRange);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
didCompleteWithError:(NSError *)error
|
||||
{
|
||||
if (error.code == NSURLErrorCancelled || m_cancelRequested)
|
||||
return;
|
||||
|
||||
if (error)
|
||||
m_callback->OnFinish([error code], m_begRange, m_endRange);
|
||||
else
|
||||
m_callback->OnFinish(200, m_begRange, m_endRange);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
class HttpThread
|
||||
{
|
||||
public:
|
||||
HttpThread(HttpThreadImpl * request)
|
||||
: m_request(request)
|
||||
{}
|
||||
|
||||
HttpThreadImpl * m_request;
|
||||
};
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////
|
||||
namespace downloader
|
||||
{
|
||||
HttpThread * CreateNativeHttpThread(std::string const & url,
|
||||
downloader::IHttpThreadCallback & cb,
|
||||
int64_t beg,
|
||||
int64_t end,
|
||||
int64_t size,
|
||||
std::string const & pb)
|
||||
{
|
||||
HttpThreadImpl * request = [[HttpThreadImpl alloc] initWithURL:url callback:cb begRange:beg endRange:end expectedSize:size postBody:pb];
|
||||
return new HttpThread(request);
|
||||
}
|
||||
|
||||
void DeleteNativeHttpThread(HttpThread * request)
|
||||
{
|
||||
[request->m_request cancel];
|
||||
delete request;
|
||||
}
|
||||
} // namespace downloader
|
||||
16
libs/platform/http_thread_callback.hpp
Normal file
16
libs/platform/http_thread_callback.hpp
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
class IHttpThreadCallback
|
||||
{
|
||||
public:
|
||||
virtual bool OnWrite(int64_t offset, void const * buffer, size_t size) = 0;
|
||||
virtual void OnFinish(long httpOrErrorCode, int64_t begRange, int64_t endRange) = 0;
|
||||
|
||||
protected:
|
||||
virtual ~IHttpThreadCallback() = default;
|
||||
};
|
||||
} // namespace downloader
|
||||
156
libs/platform/http_thread_qt.cpp
Normal file
156
libs/platform/http_thread_qt.cpp
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
#include "platform/http_thread_qt.hpp"
|
||||
|
||||
#include "platform/http_thread_callback.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSslError>
|
||||
#include <QUrl>
|
||||
|
||||
HttpThread::HttpThread(std::string const & url, downloader::IHttpThreadCallback & cb, int64_t beg, int64_t end,
|
||||
int64_t size, std::string const & pb)
|
||||
: m_callback(cb)
|
||||
, m_begRange(beg)
|
||||
, m_endRange(end)
|
||||
, m_downloadedBytes(0)
|
||||
, m_expectedSize(size)
|
||||
{
|
||||
QUrl const qUrl(url.c_str());
|
||||
QNetworkRequest request(qUrl);
|
||||
|
||||
// use Range header only if we don't download whole file from start
|
||||
if (!(beg == 0 && end < 0))
|
||||
{
|
||||
if (end > 0)
|
||||
{
|
||||
LOG(LDEBUG, (url, "downloading range [", beg, ",", end, "]"));
|
||||
QString const range = QString("bytes=") + QString::number(beg) + '-' + QString::number(end);
|
||||
request.setRawHeader("Range", range.toUtf8());
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LDEBUG, (url, "resuming download from position", beg));
|
||||
QString const range = QString("bytes=") + QString::number(beg) + '-';
|
||||
request.setRawHeader("Range", range.toUtf8());
|
||||
}
|
||||
}
|
||||
|
||||
/// Use single instance for whole app
|
||||
static QNetworkAccessManager netManager;
|
||||
|
||||
if (pb.empty())
|
||||
m_reply = netManager.get(request);
|
||||
else
|
||||
{
|
||||
request.setRawHeader("Content-Type", "application/json");
|
||||
request.setRawHeader("Content-Length", QString::number(pb.size()).toLocal8Bit());
|
||||
m_reply = netManager.post(request, pb.c_str());
|
||||
}
|
||||
|
||||
connect(m_reply, SIGNAL(metaDataChanged()), this, SLOT(OnHeadersReceived()));
|
||||
connect(m_reply, SIGNAL(readyRead()), this, SLOT(OnChunkDownloaded()));
|
||||
connect(m_reply, SIGNAL(finished()), this, SLOT(OnDownloadFinished()));
|
||||
LOG(LDEBUG, ("Connecting to", url, "[", beg, ",", end, "]", "size=", size));
|
||||
}
|
||||
|
||||
HttpThread::~HttpThread()
|
||||
{
|
||||
LOG(LDEBUG, ("Destroying HttpThread"));
|
||||
disconnect(m_reply, SIGNAL(metaDataChanged()), this, SLOT(OnHeadersReceived()));
|
||||
disconnect(m_reply, SIGNAL(readyRead()), this, SLOT(OnChunkDownloaded()));
|
||||
disconnect(m_reply, SIGNAL(finished()), this, SLOT(OnDownloadFinished()));
|
||||
|
||||
m_reply->deleteLater();
|
||||
}
|
||||
|
||||
void HttpThread::OnHeadersReceived()
|
||||
{
|
||||
// We don't notify our callback here, because OnDownloadFinished() will always be called
|
||||
// Note: after calling reply->abort() all other reply's members calls crash the app
|
||||
if (m_reply->error())
|
||||
return;
|
||||
|
||||
int const httpStatusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
// When we didn't ask for chunks, code should be 200
|
||||
// When we asked for a chunk, code should be 206
|
||||
bool const isChunk = !(m_begRange == 0 && m_endRange < 0);
|
||||
if ((isChunk && httpStatusCode != 206) || (!isChunk && httpStatusCode != 200))
|
||||
{
|
||||
LOG(LWARNING,
|
||||
("Http request to", m_reply->url().toEncoded().constData(), "aborted with HTTP code", httpStatusCode));
|
||||
m_reply->abort();
|
||||
}
|
||||
else if (m_expectedSize > 0)
|
||||
{
|
||||
// try to get content length from Content-Range header first
|
||||
if (m_reply->hasRawHeader("Content-Range"))
|
||||
{
|
||||
QList<QByteArray> const contentRange = m_reply->rawHeader("Content-Range").split('/');
|
||||
int const numElements = contentRange.size();
|
||||
if (numElements && contentRange.at(numElements - 1).toLongLong() != m_expectedSize)
|
||||
{
|
||||
LOG(LWARNING, ("Http request to", m_reply->url().toEncoded().constData(),
|
||||
"aborted - invalid Content-Range:", contentRange.at(numElements - 1).toLongLong()));
|
||||
m_reply->abort();
|
||||
}
|
||||
}
|
||||
else if (m_reply->hasRawHeader("Content-Length"))
|
||||
{
|
||||
QByteArray const header = m_reply->rawHeader("Content-Length");
|
||||
int64_t const expSize = header.toLongLong();
|
||||
if (expSize != m_expectedSize)
|
||||
{
|
||||
LOG(LWARNING, ("Http request to", m_reply->url().toEncoded().constData(),
|
||||
"aborted - invalid Content-Length:", m_reply->rawHeader("Content-Length").toLongLong()));
|
||||
m_reply->abort();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(LWARNING, ("Http request to", m_reply->url().toEncoded().constData(),
|
||||
"aborted, server didn't send any valid file size"));
|
||||
m_reply->abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HttpThread::OnChunkDownloaded()
|
||||
{
|
||||
QByteArray const data = m_reply->readAll();
|
||||
int const chunkSize = data.size();
|
||||
m_downloadedBytes += chunkSize;
|
||||
m_callback.OnWrite(m_begRange + m_downloadedBytes - chunkSize, data.constData(), chunkSize);
|
||||
}
|
||||
|
||||
void HttpThread::OnDownloadFinished()
|
||||
{
|
||||
if (m_reply->error() != QNetworkReply::NetworkError::NoError)
|
||||
{
|
||||
auto const httpStatusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
LOG(LWARNING, ("Download has finished with code:", httpStatusCode, "error:", m_reply->errorString().toStdString()));
|
||||
m_callback.OnFinish(httpStatusCode, m_begRange, m_endRange);
|
||||
}
|
||||
else
|
||||
{
|
||||
m_callback.OnFinish(200, m_begRange, m_endRange);
|
||||
}
|
||||
}
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
|
||||
HttpThread * CreateNativeHttpThread(std::string const & url, downloader::IHttpThreadCallback & cb, int64_t beg,
|
||||
int64_t end, int64_t size, std::string const & pb)
|
||||
{
|
||||
return new HttpThread(url, cb, beg, end, size, pb);
|
||||
}
|
||||
|
||||
void DeleteNativeHttpThread(HttpThread * request)
|
||||
{
|
||||
delete request;
|
||||
}
|
||||
} // namespace downloader
|
||||
35
libs/platform/http_thread_qt.hpp
Normal file
35
libs/platform/http_thread_qt.hpp
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
|
||||
namespace downloader
|
||||
{
|
||||
class IHttpThreadCallback;
|
||||
}
|
||||
|
||||
class HttpThread : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
HttpThread(std::string const & url, downloader::IHttpThreadCallback & cb, int64_t beg, int64_t end, int64_t size,
|
||||
std::string const & pb);
|
||||
virtual ~HttpThread();
|
||||
|
||||
private slots:
|
||||
void OnHeadersReceived();
|
||||
void OnChunkDownloaded();
|
||||
void OnDownloadFinished();
|
||||
|
||||
private:
|
||||
downloader::IHttpThreadCallback & m_callback;
|
||||
QNetworkReply * m_reply;
|
||||
int64_t m_begRange;
|
||||
int64_t m_endRange;
|
||||
int64_t m_downloadedBytes;
|
||||
int64_t m_expectedSize;
|
||||
};
|
||||
29
libs/platform/http_uploader.hpp
Normal file
29
libs/platform/http_uploader.hpp
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/http_payload.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
class HttpUploader
|
||||
{
|
||||
public:
|
||||
struct Result
|
||||
{
|
||||
int32_t m_httpCode = 0;
|
||||
std::string m_description;
|
||||
};
|
||||
|
||||
HttpUploader() = delete;
|
||||
explicit HttpUploader(HttpPayload const & payload) : m_payload(payload) {}
|
||||
HttpPayload const & GetPayload() const { return m_payload; }
|
||||
Result Upload() const;
|
||||
|
||||
private:
|
||||
HttpPayload const m_payload;
|
||||
};
|
||||
} // namespace platform
|
||||
218
libs/platform/http_uploader_apple.mm
Normal file
218
libs/platform/http_uploader_apple.mm
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "platform/http_uploader.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/waiter.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "private.h"
|
||||
|
||||
@interface IdentityAndTrust : NSObject
|
||||
|
||||
@property(nonatomic) SecIdentityRef identityRef;
|
||||
@property(nonatomic) NSArray * certArray;
|
||||
|
||||
@end
|
||||
|
||||
@implementation IdentityAndTrust
|
||||
|
||||
- (instancetype)initWithIdentity:(CFTypeRef)identity certChain:(CFTypeRef)certs
|
||||
{
|
||||
self = [super init];
|
||||
if (self)
|
||||
{
|
||||
_identityRef = (SecIdentityRef)CFRetain(identity);
|
||||
_certArray = (__bridge_transfer NSArray *)CFRetain(certs);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
CFRelease(_identityRef);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface MultipartUploadTask : NSObject<NSURLSessionDelegate>
|
||||
|
||||
@property(copy, nonatomic) NSString * method;
|
||||
@property(copy, nonatomic) NSString * urlString;
|
||||
@property(copy, nonatomic) NSString * fileKey;
|
||||
@property(copy, nonatomic) NSString * filePath;
|
||||
@property(copy, nonatomic) NSDictionary<NSString *, NSString *> * params;
|
||||
@property(copy, nonatomic) NSDictionary<NSString *, NSString *> * headers;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MultipartUploadTask
|
||||
|
||||
- (NSData *)requestDataWithBoundary:(NSString *)boundary {
|
||||
NSMutableData * data = [NSMutableData data];
|
||||
|
||||
[self.params enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * value, BOOL * stop) {
|
||||
[data appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary]
|
||||
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"\r\n\r\n",
|
||||
key] dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[data appendData:[[NSString stringWithFormat:@"%@\r\n", value]
|
||||
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
}];
|
||||
|
||||
NSString * fileName = self.filePath.lastPathComponent;
|
||||
NSData * fileData = [NSData dataWithContentsOfFile:self.filePath];
|
||||
NSString * mimeType = @"application/octet-stream";
|
||||
|
||||
[data appendData:[[NSString stringWithFormat:@"--%@\r\n", boundary]
|
||||
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[data appendData:[[NSString stringWithFormat:@"Content-Disposition: form-data; name=\"%@\"; filename=\"%@\"\r\n",
|
||||
self.fileKey,
|
||||
fileName]
|
||||
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[data appendData:[[NSString stringWithFormat:@"Content-Type: %@\r\n\r\n", mimeType]
|
||||
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[data appendData:fileData];
|
||||
[data appendData:[@"\r\n" dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
[data appendData:[[NSString stringWithFormat:@"--%@--", boundary]
|
||||
dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
- (void)uploadWithCompletion:(void (^)(NSInteger httpCode, NSString *description))completion {
|
||||
NSURL * url = [NSURL URLWithString:self.urlString];
|
||||
NSMutableURLRequest * uploadRequest = [NSMutableURLRequest requestWithURL:url];
|
||||
uploadRequest.timeoutInterval = 15;
|
||||
uploadRequest.HTTPMethod = self.method;
|
||||
|
||||
NSString * boundary = [NSString stringWithFormat:@"Boundary-%@", [[NSUUID UUID] UUIDString]];
|
||||
NSString * contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@", boundary];
|
||||
[uploadRequest setValue:contentType forHTTPHeaderField:@"Content-Type"];
|
||||
|
||||
[self.headers enumerateKeysAndObjectsUsingBlock:^(NSString * key, NSString * value, BOOL * stop) {
|
||||
[uploadRequest setValue:value forHTTPHeaderField:key];
|
||||
}];
|
||||
|
||||
NSData * postData = [self requestDataWithBoundary:boundary];
|
||||
|
||||
NSURLSession * session =
|
||||
[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
|
||||
delegate:self
|
||||
delegateQueue:nil];
|
||||
NSURLSessionUploadTask * uploadTask = [session
|
||||
uploadTaskWithRequest:uploadRequest
|
||||
fromData:postData
|
||||
completionHandler:^(NSData * data, NSURLResponse * response, NSError * error) {
|
||||
NSHTTPURLResponse * httpResponse = (NSHTTPURLResponse *)response;
|
||||
if (error == nil)
|
||||
{
|
||||
NSString * description = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
completion(httpResponse.statusCode, description);
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(-1, error.localizedDescription);
|
||||
}
|
||||
}];
|
||||
[uploadTask resume];
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
||||
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
|
||||
NSURLCredential * _Nullable credential))completionHandler
|
||||
{
|
||||
NSString * authenticationMethod = challenge.protectionSpace.authenticationMethod;
|
||||
if (authenticationMethod == NSURLAuthenticationMethodClientCertificate)
|
||||
{
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, [self getClientUrlCredential]);
|
||||
}
|
||||
else if (authenticationMethod == NSURLAuthenticationMethodServerTrust)
|
||||
{
|
||||
#if DEBUG
|
||||
NSURLCredential * credential =
|
||||
[[NSURLCredential alloc] initWithTrust:[challenge protectionSpace].serverTrust];
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
||||
#else
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (NSURLCredential *)getClientUrlCredential
|
||||
{
|
||||
NSData * certData = [[NSData alloc] initWithBase64EncodedString:@USER_BINDING_PKCS12 options:0];
|
||||
IdentityAndTrust * identity = [self extractIdentityWithCertData:certData
|
||||
certPassword:@USER_BINDING_PKCS12_PASSWORD];
|
||||
|
||||
NSURLCredential * urlCredential =
|
||||
[[NSURLCredential alloc] initWithIdentity:identity.identityRef
|
||||
certificates:identity.certArray
|
||||
persistence:NSURLCredentialPersistenceForSession];
|
||||
|
||||
return urlCredential;
|
||||
}
|
||||
|
||||
- (IdentityAndTrust *)extractIdentityWithCertData:(NSData *)certData
|
||||
certPassword:(NSString *)certPassword
|
||||
{
|
||||
IdentityAndTrust * identityAndTrust;
|
||||
|
||||
NSDictionary * certOptions = @{(NSString *)kSecImportExportPassphrase : certPassword};
|
||||
|
||||
CFArrayRef items = nullptr;
|
||||
OSStatus status = SecPKCS12Import((CFDataRef)certData, (CFDictionaryRef)certOptions, &items);
|
||||
|
||||
if (status == errSecSuccess && CFArrayGetCount(items) > 0)
|
||||
{
|
||||
CFDictionaryRef firstItem = (CFDictionaryRef)CFArrayGetValueAtIndex(items, 0);
|
||||
|
||||
CFTypeRef identityRef = CFDictionaryGetValue(firstItem, kSecImportItemIdentity);
|
||||
CFTypeRef certChainRef = CFDictionaryGetValue(firstItem, kSecImportItemCertChain);
|
||||
|
||||
identityAndTrust = [[IdentityAndTrust alloc] initWithIdentity:identityRef
|
||||
certChain:certChainRef];
|
||||
|
||||
CFRelease(items);
|
||||
}
|
||||
|
||||
return identityAndTrust;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
namespace platform
|
||||
{
|
||||
HttpUploader::Result HttpUploader::Upload() const
|
||||
{
|
||||
std::shared_ptr<Result> resultPtr = std::make_shared<Result>();
|
||||
std::shared_ptr<base::Waiter> waiterPtr = std::make_shared<base::Waiter>();
|
||||
|
||||
auto mapTransform =
|
||||
^NSDictionary<NSString *, NSString *> * (std::map<std::string, std::string> keyValues)
|
||||
{
|
||||
NSMutableDictionary<NSString *, NSString *> * params = [NSMutableDictionary dictionary];
|
||||
for (auto const & keyValue : keyValues)
|
||||
params[@(keyValue.first.c_str())] = @(keyValue.second.c_str());
|
||||
return [params copy];
|
||||
};
|
||||
|
||||
auto uploadTask = [[MultipartUploadTask alloc] init];
|
||||
uploadTask.method = @(m_payload.m_method.c_str());
|
||||
uploadTask.urlString = @(m_payload.m_url.c_str());
|
||||
uploadTask.fileKey = @(m_payload.m_fileKey.c_str());
|
||||
uploadTask.filePath = @(m_payload.m_filePath.c_str());
|
||||
uploadTask.params = mapTransform(m_payload.m_params);
|
||||
uploadTask.headers = mapTransform(m_payload.m_headers);
|
||||
[uploadTask uploadWithCompletion:[resultPtr, waiterPtr](NSInteger httpCode, NSString * description) {
|
||||
resultPtr->m_httpCode = static_cast<int32_t>(httpCode);
|
||||
resultPtr->m_description = description.UTF8String;
|
||||
waiterPtr->Notify();
|
||||
}];
|
||||
waiterPtr->Wait();
|
||||
return *resultPtr;
|
||||
}
|
||||
} // namespace platform
|
||||
20
libs/platform/http_uploader_background.hpp
Normal file
20
libs/platform/http_uploader_background.hpp
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/http_payload.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
class HttpUploaderBackground
|
||||
{
|
||||
public:
|
||||
HttpUploaderBackground() = delete;
|
||||
explicit HttpUploaderBackground(HttpPayload const & payload) : m_payload(payload) {}
|
||||
HttpPayload const & GetPayload() const { return m_payload; }
|
||||
|
||||
// TODO add platform-specific implementation
|
||||
void Upload() const;
|
||||
|
||||
private:
|
||||
HttpPayload m_payload;
|
||||
};
|
||||
} // namespace platform
|
||||
94
libs/platform/http_uploader_background.mm
Normal file
94
libs/platform/http_uploader_background.mm
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
|
||||
#include "http_uploader_background.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/logging.hpp"
|
||||
|
||||
static NSString *const kSessionId = @"MWMBackgroundUploader_sessionId";
|
||||
|
||||
@interface MWMBackgroundUploader : NSObject <NSURLSessionDelegate>
|
||||
|
||||
@property(nonatomic, strong) NSURLSession *session;
|
||||
|
||||
+ (MWMBackgroundUploader *)sharedUploader;
|
||||
- (void)upload:(platform::HttpPayload const &)payload;
|
||||
|
||||
@end
|
||||
|
||||
@implementation MWMBackgroundUploader
|
||||
|
||||
+ (MWMBackgroundUploader *)sharedUploader {
|
||||
static MWMBackgroundUploader *uploader;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
uploader = [[MWMBackgroundUploader alloc] init];
|
||||
});
|
||||
return uploader;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
NSURLSessionConfiguration *config =
|
||||
[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:kSessionId];
|
||||
config.allowsCellularAccess = NO;
|
||||
config.sessionSendsLaunchEvents = NO;
|
||||
config.discretionary = YES;
|
||||
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)upload:(platform::HttpPayload const &)payload {
|
||||
NSURLComponents *components = [[NSURLComponents alloc] initWithString:@(payload.m_url.c_str())];
|
||||
NSMutableArray *newQueryItems = [NSMutableArray arrayWithArray:components.queryItems];
|
||||
std::for_each(payload.m_params.begin(), payload.m_params.end(), [newQueryItems](auto const &pair) {
|
||||
[newQueryItems addObject:[NSURLQueryItem queryItemWithName:@(pair.first.c_str()) value:@(pair.second.c_str())]];
|
||||
});
|
||||
[components setQueryItems:newQueryItems];
|
||||
NSURL *url = components.URL;
|
||||
CHECK(url, ());
|
||||
|
||||
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
|
||||
request.HTTPMethod = @(payload.m_method.c_str());
|
||||
std::for_each(payload.m_headers.begin(), payload.m_headers.end(), [request](auto const &pair) {
|
||||
[request addValue:@(pair.second.c_str()) forHTTPHeaderField:@(pair.first.c_str())];
|
||||
});
|
||||
|
||||
NSURL *fileUrl = [NSURL fileURLWithPath:@(payload.m_filePath.c_str())];
|
||||
CHECK(fileUrl, ());
|
||||
|
||||
[[self.session uploadTaskWithRequest:request fromFile:fileUrl] resume];
|
||||
NSError *error;
|
||||
[[NSFileManager defaultManager] removeItemAtURL:fileUrl error:&error];
|
||||
if (error) {
|
||||
LOG(LDEBUG, ([error description].UTF8String));
|
||||
}
|
||||
}
|
||||
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
task:(NSURLSessionTask *)task
|
||||
didCompleteWithError:(nullable NSError *)error {
|
||||
if (error) {
|
||||
LOG(LDEBUG, ("Upload failed:", [error description].UTF8String));
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
- (void)URLSession:(NSURLSession *)session
|
||||
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
|
||||
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
|
||||
NSURLCredential *_Nullable credential))completionHandler {
|
||||
NSURLCredential *credential = [[NSURLCredential alloc] initWithTrust:[challenge protectionSpace].serverTrust];
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
namespace platform {
|
||||
void HttpUploaderBackground::Upload() const {
|
||||
[[MWMBackgroundUploader sharedUploader] upload:GetPayload()];
|
||||
}
|
||||
}
|
||||
6
libs/platform/http_uploader_background_dummy.cpp
Normal file
6
libs/platform/http_uploader_background_dummy.cpp
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#include "platform/http_uploader_background.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
void HttpUploaderBackground::Upload() const {}
|
||||
} // namespace platform
|
||||
12
libs/platform/http_uploader_dummy.cpp
Normal file
12
libs/platform/http_uploader_dummy.cpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
#include "platform/http_uploader.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
HttpUploader::Result HttpUploader::Upload() const
|
||||
{
|
||||
// Dummy implementation.
|
||||
return {};
|
||||
}
|
||||
} // namespace platform
|
||||
84
libs/platform/languages.hpp
Normal file
84
libs/platform/languages.hpp
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "std/target_os.hpp"
|
||||
|
||||
namespace routing::turns::sound
|
||||
{
|
||||
/**
|
||||
* @brief The list of languages which can be used by TTS (Text-To-Speech).
|
||||
*
|
||||
* Supported language identifiers follow the format:
|
||||
* @code
|
||||
* language[-COUNTRY][:internal_code]
|
||||
* @endcode
|
||||
*
|
||||
* Where:
|
||||
* - `language`: a two-letter ISO 639-1 language code (e.g., "en", "fr", "zh").
|
||||
* - `COUNTRY`: optional two-letter ISO 3166-1 alpha-2 country code (e.g., "US", "CN", "TW").
|
||||
* - `internal_code`: optional internal language code used by the TTS core.
|
||||
* If not specified, `language` is used as the default.
|
||||
*
|
||||
* @note Special handling for Chinese:
|
||||
* - `zh_TW`, `zh_MO`, and `zh_HK` are treated as `zh-Hant` (Traditional Chinese).
|
||||
* - All other variants default to `zh-Hans` (Simplified Chinese).
|
||||
*
|
||||
*/
|
||||
std::array<std::pair<std::string_view, std::string_view>, 40> constexpr kLanguageList = {{
|
||||
{"en", "English"},
|
||||
{"id", "Bahasa Indonesia"},
|
||||
{"ca", "Català"},
|
||||
{"da", "Dansk"},
|
||||
{"de", "Deutsch"},
|
||||
#ifdef OMIM_OS_ANDROID
|
||||
{"es-ES:es", "Español"},
|
||||
{"es-MX:es-MX", "Español (México)"},
|
||||
#else
|
||||
{"es", "Español"}, {"es-MX", "Español (México)"},
|
||||
#endif
|
||||
{"eu", "Euskara"},
|
||||
{"fr", "Français"},
|
||||
{"hr", "Hrvatski"},
|
||||
{"it", "Italiano"},
|
||||
{"sw", "Kiswahili"},
|
||||
{"hu", "Magyar"},
|
||||
{"nl", "Nederlands"},
|
||||
{"nb", "Norsk Bokmål"},
|
||||
{"pl", "Polski"},
|
||||
#ifdef OMIM_OS_ANDROID
|
||||
{"pt-PT:pt", "Português"},
|
||||
{"pt-BR:pt-BR", "Português (Brasil)"},
|
||||
#else
|
||||
{"pt", "Português"}, {"pt-BR", "Português (Brasil)"},
|
||||
#endif
|
||||
{"ro", "Română"},
|
||||
{"sk", "Slovenčina"},
|
||||
{"fi", "Suomi"},
|
||||
{"sv", "Svenska"},
|
||||
{"vi", "Tiếng Việt"},
|
||||
{"tr", "Türkçe"},
|
||||
{"cs", "Čeština"},
|
||||
{"el", "Ελληνικά"},
|
||||
{"be", "Беларуская"},
|
||||
{"bg", "Български"},
|
||||
{"ru", "Русский"},
|
||||
{"sr", "Српски"},
|
||||
{"uk", "Українська"},
|
||||
{"ar", "العربية"},
|
||||
{"fa", "فارسی"},
|
||||
{"mr", "मराठी"},
|
||||
{"hi", "हिंदी"},
|
||||
{"th", "ไทย"},
|
||||
#ifdef OMIM_OS_ANDROID
|
||||
{"zh-CN:zh-Hans", "中文简体"},
|
||||
{"zh-TW:zh-Hant", "中文繁體"},
|
||||
#else
|
||||
{"zh-Hans", "中文简体"}, {"zh-Hant", "中文繁體"},
|
||||
#endif
|
||||
{"ja", "日本語"},
|
||||
{"ko", "한국어"},
|
||||
}};
|
||||
} // namespace routing::turns::sound
|
||||
152
libs/platform/local_country_file.cpp
Normal file
152
libs/platform/local_country_file.cpp
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#include "platform/local_country_file.hpp"
|
||||
|
||||
#include "platform/mwm_version.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/internal/file_data.hpp"
|
||||
#include "coding/sha1.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/stl_helpers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
|
||||
LocalCountryFile::LocalCountryFile() : m_version(0) {}
|
||||
|
||||
LocalCountryFile::LocalCountryFile(std::string directory, CountryFile countryFile, int64_t version)
|
||||
: m_directory(std::move(directory))
|
||||
, m_countryFile(std::move(countryFile))
|
||||
, m_version(version)
|
||||
{}
|
||||
|
||||
void LocalCountryFile::SyncWithDisk()
|
||||
{
|
||||
// World files from resources have an empty directory. See todo in the header.
|
||||
if (m_directory.empty())
|
||||
return;
|
||||
|
||||
m_files = {};
|
||||
uint64_t size = 0;
|
||||
|
||||
// Now we are not working with several files at the same time and diffs have greater priority.
|
||||
Platform & platform = GetPlatform();
|
||||
for (MapFileType type : {MapFileType::Diff, MapFileType::Map})
|
||||
{
|
||||
auto const ut = base::Underlying(type);
|
||||
ASSERT_LESS(ut, m_files.size(), ());
|
||||
|
||||
if (platform.GetFileSizeByFullPath(GetPath(type), size))
|
||||
{
|
||||
m_files[ut] = size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LocalCountryFile::DeleteFromDisk(MapFileType type) const
|
||||
{
|
||||
ASSERT_LESS(base::Underlying(type), m_files.size(), ());
|
||||
|
||||
if (OnDisk(type) && !base::DeleteFileX(GetPath(type)))
|
||||
LOG(LERROR, (type, "from", *this, "wasn't deleted from disk."));
|
||||
}
|
||||
|
||||
std::string LocalCountryFile::GetPath(MapFileType type) const
|
||||
{
|
||||
return base::JoinPath(m_directory, GetFileName(type));
|
||||
}
|
||||
|
||||
std::string LocalCountryFile::GetFileName(MapFileType type) const
|
||||
{
|
||||
return m_countryFile.GetFileName(type);
|
||||
}
|
||||
|
||||
uint64_t LocalCountryFile::GetSize(MapFileType type) const
|
||||
{
|
||||
auto const ut = base::Underlying(type);
|
||||
ASSERT_LESS(ut, m_files.size(), ());
|
||||
return m_files[ut].value_or(0);
|
||||
}
|
||||
|
||||
bool LocalCountryFile::HasFiles() const
|
||||
{
|
||||
return std::any_of(m_files.cbegin(), m_files.cend(), [](auto const & value) { return value.has_value(); });
|
||||
}
|
||||
|
||||
bool LocalCountryFile::OnDisk(MapFileType type) const
|
||||
{
|
||||
auto const ut = base::Underlying(type);
|
||||
ASSERT_LESS(ut, m_files.size(), ());
|
||||
return m_files[ut].has_value();
|
||||
}
|
||||
|
||||
bool LocalCountryFile::operator<(LocalCountryFile const & rhs) const
|
||||
{
|
||||
if (m_countryFile != rhs.m_countryFile)
|
||||
return m_countryFile < rhs.m_countryFile;
|
||||
if (m_version != rhs.m_version)
|
||||
return m_version < rhs.m_version;
|
||||
if (m_directory != rhs.m_directory)
|
||||
return m_directory < rhs.m_directory;
|
||||
if (m_files != rhs.m_files)
|
||||
return m_files < rhs.m_files;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LocalCountryFile::operator==(LocalCountryFile const & rhs) const
|
||||
{
|
||||
return m_directory == rhs.m_directory && m_countryFile == rhs.m_countryFile && m_version == rhs.m_version &&
|
||||
m_files == rhs.m_files;
|
||||
}
|
||||
|
||||
bool LocalCountryFile::ValidateIntegrity() const
|
||||
{
|
||||
auto calculatedSha1 = coding::SHA1::CalculateBase64(GetPath(MapFileType::Map));
|
||||
ASSERT_EQUAL(calculatedSha1, m_countryFile.GetSha1(), ("Integrity failure"));
|
||||
return calculatedSha1 == m_countryFile.GetSha1();
|
||||
}
|
||||
|
||||
// static
|
||||
LocalCountryFile LocalCountryFile::MakeForTesting(std::string countryFileName, int64_t version)
|
||||
{
|
||||
LocalCountryFile localFile(GetPlatform().WritableDir(), CountryFile(std::move(countryFileName)), version);
|
||||
localFile.SyncWithDisk();
|
||||
return localFile;
|
||||
}
|
||||
|
||||
// static
|
||||
LocalCountryFile LocalCountryFile::MakeTemporary(std::string const & fullPath)
|
||||
{
|
||||
auto name = fullPath;
|
||||
base::GetNameFromFullPath(name);
|
||||
base::GetNameWithoutExt(name);
|
||||
|
||||
return LocalCountryFile(base::GetDirectory(fullPath), CountryFile(std::move(name)), 0 /* version */);
|
||||
}
|
||||
|
||||
std::string DebugPrint(LocalCountryFile const & file)
|
||||
{
|
||||
std::ostringstream os;
|
||||
os << "LocalCountryFile [" << file.m_directory << ", " << DebugPrint(file.m_countryFile) << ", " << file.m_version
|
||||
<< ", [";
|
||||
|
||||
bool fileAdded = false;
|
||||
for (auto const & mapFile : file.m_files)
|
||||
{
|
||||
if (mapFile)
|
||||
{
|
||||
os << (fileAdded ? ", " : "") << *mapFile;
|
||||
fileAdded = true;
|
||||
}
|
||||
}
|
||||
|
||||
os << "]]";
|
||||
return os.str();
|
||||
}
|
||||
} // namespace platform
|
||||
99
libs/platform/local_country_file.hpp
Normal file
99
libs/platform/local_country_file.hpp
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/country_defines.hpp"
|
||||
#include "platform/country_file.hpp"
|
||||
|
||||
#include "base/stl_helpers.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
// This class represents a path to disk files corresponding to some
|
||||
// country region.
|
||||
//
|
||||
// This class also wraps World.mwm and WorldCoasts.mwm
|
||||
// files from resource bundle, when they can't be found in a data
|
||||
// directory. In this exceptional case, directory will be empty and
|
||||
// SyncWithDisk()/DeleteFromDisk()/GetPath()/GetSize() will return
|
||||
// incorrect results.
|
||||
//
|
||||
// In any case, when you're going to read a file LocalCountryFile points to,
|
||||
// use platform::GetCountryReader().
|
||||
class LocalCountryFile
|
||||
{
|
||||
public:
|
||||
LocalCountryFile();
|
||||
|
||||
// Creates an instance holding a path to countryFile's in a
|
||||
// directory. Note that no disk operations are not performed until
|
||||
// SyncWithDisk() is called.
|
||||
// The directory must contain a full path to the country file.
|
||||
LocalCountryFile(std::string directory, CountryFile countryFile, int64_t version);
|
||||
|
||||
// Syncs internal state like availability of files, their sizes etc. with disk.
|
||||
// Generality speaking it's not always true. To know it for sure it's necessary to read a mwm in
|
||||
// this method but it's not implemented by performance reasons. This check is done on
|
||||
// building routes stage.
|
||||
void SyncWithDisk();
|
||||
|
||||
// Removes specified file from disk if it is known for LocalCountryFile, i.e.
|
||||
// it was found by a previous SyncWithDisk() call.
|
||||
void DeleteFromDisk(MapFileType type) const;
|
||||
|
||||
// Returns path to a file.
|
||||
// Return value may be empty until SyncWithDisk() is called.
|
||||
std::string GetPath(MapFileType type) const;
|
||||
std::string GetFileName(MapFileType type) const;
|
||||
|
||||
// Returns size of a file.
|
||||
// Return value may be zero until SyncWithDisk() is called.
|
||||
uint64_t GetSize(MapFileType type) const;
|
||||
|
||||
// Returns true when some files are found during SyncWithDisk.
|
||||
// Return value is false until SyncWithDisk() is called.
|
||||
bool HasFiles() const;
|
||||
|
||||
// Checks whether files specified in filesMask are on disk.
|
||||
// Return value will be false until SyncWithDisk() is called.
|
||||
bool OnDisk(MapFileType type) const;
|
||||
|
||||
bool IsInBundle() const { return m_directory.empty(); }
|
||||
std::string const & GetDirectory() const { return m_directory; }
|
||||
std::string const & GetCountryName() const { return m_countryFile.GetName(); }
|
||||
int64_t GetVersion() const { return m_version; }
|
||||
CountryFile const & GetCountryFile() const { return m_countryFile; }
|
||||
|
||||
bool operator<(LocalCountryFile const & rhs) const;
|
||||
bool operator==(LocalCountryFile const & rhs) const;
|
||||
bool operator!=(LocalCountryFile const & rhs) const { return !(*this == rhs); }
|
||||
|
||||
bool ValidateIntegrity() const;
|
||||
|
||||
// Creates LocalCountryFile for test purposes, for a country region
|
||||
// with countryFileName (without any extensions). Automatically performs sync with disk.
|
||||
static LocalCountryFile MakeForTesting(std::string countryFileName, int64_t version = 0);
|
||||
|
||||
// Used in generator only to simplify getting instance from path.
|
||||
static LocalCountryFile MakeTemporary(std::string const & fullPath);
|
||||
|
||||
private:
|
||||
friend std::string DebugPrint(LocalCountryFile const &);
|
||||
friend void FindAllLocalMapsAndCleanup(int64_t latestVersion, std::string const & dataDir,
|
||||
std::vector<LocalCountryFile> & localFiles);
|
||||
|
||||
/// Can be bundled (empty directory) or path to the file.
|
||||
std::string m_directory;
|
||||
CountryFile m_countryFile;
|
||||
int64_t m_version;
|
||||
|
||||
using File = std::optional<uint64_t>;
|
||||
std::array<File, base::Underlying(MapFileType::Count)> m_files = {};
|
||||
};
|
||||
|
||||
std::string DebugPrint(LocalCountryFile const & file);
|
||||
} // namespace platform
|
||||
426
libs/platform/local_country_file_utils.cpp
Normal file
426
libs/platform/local_country_file_utils.cpp
Normal file
|
|
@ -0,0 +1,426 @@
|
|||
#include "platform/local_country_file_utils.hpp"
|
||||
|
||||
#include "platform/country_file.hpp"
|
||||
#include "platform/mwm_version.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include "coding/internal/file_data.hpp"
|
||||
#include "coding/reader.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/stl_helpers.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
using std::string;
|
||||
|
||||
namespace
|
||||
{
|
||||
char constexpr kBitsExt[] = ".bftsegbits";
|
||||
char constexpr kNodesExt[] = ".bftsegnodes";
|
||||
char constexpr kOffsetsExt[] = ".offsets";
|
||||
|
||||
string GetAdditionalWorldScope()
|
||||
{
|
||||
return "r";
|
||||
}
|
||||
/*
|
||||
bool IsSpecialName(string const & name) { return name == "." || name == ".."; }
|
||||
*/
|
||||
bool IsDownloaderFile(string const & name)
|
||||
{
|
||||
static boost::regex const filter(".*\\.(downloading|resume|ready)[0-9]?$");
|
||||
return boost::regex_match(name.begin(), name.end(), filter);
|
||||
}
|
||||
|
||||
bool IsDiffFile(string const & name)
|
||||
{
|
||||
return name.ends_with(DIFF_FILE_EXTENSION) || name.ends_with(DIFF_APPLYING_FILE_EXTENSION);
|
||||
}
|
||||
|
||||
/*
|
||||
bool DirectoryHasIndexesOnly(string const & directory)
|
||||
{
|
||||
Platform::TFilesWithType fwts;
|
||||
Platform::GetFilesByType(directory, Platform::EFileType::Regular | Platform::EFileType::Directory, fwts);
|
||||
|
||||
for (auto const & fwt : fwts)
|
||||
{
|
||||
auto const & name = fwt.first;
|
||||
auto const & type = fwt.second;
|
||||
if (type == Platform::EFileType::Directory)
|
||||
{
|
||||
if (!IsSpecialName(name))
|
||||
return false;
|
||||
}
|
||||
else if (!CountryIndexes::IsIndexFile(name))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
|
||||
inline string GetDataDirFullPath(string const & dataDir)
|
||||
{
|
||||
Platform & platform = GetPlatform();
|
||||
return dataDir.empty() ? platform.WritableDir() : base::JoinPath(platform.WritableDir(), dataDir);
|
||||
}
|
||||
|
||||
void FindAllDiffsInDirectory(string const & dir, std::vector<LocalCountryFile> & diffs)
|
||||
{
|
||||
Platform & platform = GetPlatform();
|
||||
|
||||
Platform::TFilesWithType files;
|
||||
platform.GetFilesByType(dir, Platform::EFileType::Regular, files);
|
||||
|
||||
for (auto const & fileWithType : files)
|
||||
{
|
||||
string name = fileWithType.first;
|
||||
|
||||
auto const isDiffReady = name.ends_with(DIFF_FILE_EXTENSION READY_FILE_EXTENSION);
|
||||
auto const isDiff = name.ends_with(DIFF_FILE_EXTENSION);
|
||||
|
||||
if (!isDiff && !isDiffReady)
|
||||
continue;
|
||||
|
||||
base::GetNameWithoutExt(name);
|
||||
if (isDiffReady)
|
||||
base::GetNameWithoutExt(name);
|
||||
|
||||
diffs.emplace_back(dir, CountryFile(std::move(name)), 0 /* version */);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
string GetFilePath(int64_t version, string const & dataDir, string const & countryName, MapFileType type)
|
||||
{
|
||||
string const filename = GetFileName(countryName, type);
|
||||
string const dir = GetDataDirFullPath(dataDir);
|
||||
if (version == 0)
|
||||
return base::JoinPath(dir, filename);
|
||||
return base::JoinPath(dir, strings::to_string(version), filename);
|
||||
}
|
||||
|
||||
void DeleteDownloaderFilesForCountry(int64_t version, CountryFile const & countryFile)
|
||||
{
|
||||
DeleteDownloaderFilesForCountry(version, string(), countryFile);
|
||||
}
|
||||
|
||||
void DeleteDownloaderFilesForCountry(int64_t version, string const & dataDir, CountryFile const & countryFile)
|
||||
{
|
||||
for (size_t type = 0; type < base::Underlying(MapFileType::Count); ++type)
|
||||
{
|
||||
string const path = GetFileDownloadPath(version, dataDir, countryFile, static_cast<MapFileType>(type));
|
||||
ASSERT(path.ends_with(READY_FILE_EXTENSION), ());
|
||||
Platform::RemoveFileIfExists(path);
|
||||
Platform::RemoveFileIfExists(path + RESUME_FILE_EXTENSION);
|
||||
Platform::RemoveFileIfExists(path + DOWNLOADING_FILE_EXTENSION);
|
||||
}
|
||||
|
||||
// Delete the diff that was downloaded but wasn't applied.
|
||||
{
|
||||
string const path = GetFilePath(version, dataDir, countryFile.GetName(), MapFileType::Diff);
|
||||
Platform::RemoveFileIfExists(path);
|
||||
}
|
||||
}
|
||||
|
||||
size_t FindAllLocalMapsInDirectoryAndCleanup(string const & directory, int64_t version, int64_t latestVersion,
|
||||
std::vector<LocalCountryFile> & localFiles)
|
||||
{
|
||||
Platform & platform = GetPlatform();
|
||||
size_t const szBefore = localFiles.size();
|
||||
|
||||
Platform::TFilesWithType fwts;
|
||||
platform.GetFilesByType(directory, Platform::EFileType::Regular | Platform::EFileType::Directory, fwts);
|
||||
|
||||
for (auto const & fwt : fwts)
|
||||
{
|
||||
if (fwt.second != Platform::EFileType::Regular)
|
||||
continue;
|
||||
|
||||
string name = fwt.first;
|
||||
|
||||
// Remove downloader and diff files for old version directories.
|
||||
if (version < latestVersion && (IsDownloaderFile(name) || IsDiffFile(name)))
|
||||
{
|
||||
base::DeleteFileX(base::JoinPath(directory, name));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!name.ends_with(DATA_FILE_EXTENSION))
|
||||
continue;
|
||||
|
||||
// Remove DATA_FILE_EXTENSION and use base name as a country file name.
|
||||
base::GetNameWithoutExt(name);
|
||||
localFiles.emplace_back(directory, CountryFile(std::move(name)), version);
|
||||
}
|
||||
|
||||
return localFiles.size() - szBefore;
|
||||
|
||||
// Probably, indices will appear in future.
|
||||
/*
|
||||
for (auto const & fwt : fwts)
|
||||
{
|
||||
if (fwt.second != Platform::EFileType::Directory)
|
||||
continue;
|
||||
|
||||
string const & name = fwt.first;
|
||||
if (IsSpecialName(name))
|
||||
continue;
|
||||
|
||||
if (names.count(name) == 0 && DirectoryHasIndexesOnly(base::JoinPath(directory, name)))
|
||||
{
|
||||
// Directory which looks like a directory with indexes for absent country. It's OK to remove it.
|
||||
LocalCountryFile absentCountry(directory, CountryFile(name), version);
|
||||
CountryIndexes::DeleteFromDisk(absentCountry);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
void FindAllDiffs(std::string const & dataDir, std::vector<LocalCountryFile> & diffs)
|
||||
{
|
||||
string const dir = GetDataDirFullPath(dataDir);
|
||||
FindAllDiffsInDirectory(dir, diffs);
|
||||
|
||||
Platform::TFilesWithType fwts;
|
||||
Platform::GetFilesByType(dir, Platform::EFileType::Directory, fwts);
|
||||
|
||||
for (auto const & fwt : fwts)
|
||||
FindAllDiffsInDirectory(base::JoinPath(dir, fwt.first /* subdir */), diffs);
|
||||
}
|
||||
|
||||
void FindAllLocalMapsAndCleanup(int64_t latestVersion, std::vector<LocalCountryFile> & localFiles)
|
||||
{
|
||||
FindAllLocalMapsAndCleanup(latestVersion, string(), localFiles);
|
||||
}
|
||||
|
||||
void FindAllLocalMapsAndCleanup(int64_t latestVersion, string const & dataDir,
|
||||
std::vector<LocalCountryFile> & localFiles)
|
||||
{
|
||||
string const dir = GetDataDirFullPath(dataDir);
|
||||
|
||||
// Do not search root folder! We have separate World processing in Storage::GetForceDownloadWorlds.
|
||||
// FindAllLocalMapsInDirectoryAndCleanup(dir, 0 /* version */, latestVersion, localFiles);
|
||||
|
||||
Platform::TFilesWithType fwts;
|
||||
Platform::GetFilesByType(dir, Platform::EFileType::Directory, fwts);
|
||||
for (auto const & fwt : fwts)
|
||||
{
|
||||
string const & subdir = fwt.first;
|
||||
int64_t version;
|
||||
if (!ParseVersion(subdir, version) || version > latestVersion)
|
||||
continue;
|
||||
|
||||
string const fullPath = base::JoinPath(dir, subdir);
|
||||
if (0 == FindAllLocalMapsInDirectoryAndCleanup(fullPath, version, latestVersion, localFiles))
|
||||
{
|
||||
Platform::EError err = Platform::RmDir(fullPath);
|
||||
if (err != Platform::ERR_OK)
|
||||
LOG(LWARNING, ("Can't remove directory:", fullPath, err));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for World and WorldCoasts in app bundle or in resources.
|
||||
Platform & platform = GetPlatform();
|
||||
string const world(WORLD_FILE_NAME);
|
||||
string const worldCoasts(WORLD_COASTS_FILE_NAME);
|
||||
for (string const & file : {world, worldCoasts})
|
||||
{
|
||||
auto i = localFiles.begin();
|
||||
for (; i != localFiles.end(); ++i)
|
||||
if (i->GetCountryName() == file)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
ModelReaderPtr reader(platform.GetReader(file + DATA_FILE_EXTENSION, GetAdditionalWorldScope()));
|
||||
|
||||
// Empty path means the resource file.
|
||||
LocalCountryFile worldFile(string(), CountryFile(file), version::ReadVersionDate(reader));
|
||||
worldFile.m_files[base::Underlying(MapFileType::Map)] = reader.Size();
|
||||
|
||||
// Replace if newer only.
|
||||
if (i != localFiles.end())
|
||||
{
|
||||
if (worldFile.GetVersion() > i->GetVersion())
|
||||
*i = std::move(worldFile);
|
||||
}
|
||||
else
|
||||
localFiles.push_back(std::move(worldFile));
|
||||
}
|
||||
catch (RootException const & ex)
|
||||
{
|
||||
if (i == localFiles.end())
|
||||
{
|
||||
// This warning is possible on android devices without bundled Worlds.
|
||||
LOG(LWARNING, ("Can't find any:", file, "Reason:", ex.Msg()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CleanupMapsDirectory(int64_t latestVersion)
|
||||
{
|
||||
std::vector<LocalCountryFile> localFiles;
|
||||
FindAllLocalMapsAndCleanup(latestVersion, localFiles);
|
||||
}
|
||||
|
||||
bool ParseVersion(string const & s, int64_t & version)
|
||||
{
|
||||
// Folder version format is 211122. Unit tests use simple "1", "2" versions.
|
||||
if (s.empty() || s.size() > 6)
|
||||
return false;
|
||||
|
||||
int64_t v = 0;
|
||||
for (char const c : s)
|
||||
{
|
||||
if (!isdigit(c))
|
||||
return false;
|
||||
v = v * 10 + c - '0';
|
||||
}
|
||||
version = v;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::shared_ptr<LocalCountryFile> PreparePlaceForCountryFiles(int64_t version, CountryFile const & countryFile)
|
||||
{
|
||||
return PreparePlaceForCountryFiles(version, string(), countryFile);
|
||||
}
|
||||
|
||||
std::shared_ptr<LocalCountryFile> PreparePlaceForCountryFiles(int64_t version, string const & dataDir,
|
||||
CountryFile const & countryFile)
|
||||
{
|
||||
string const dir = PrepareDirToDownloadCountry(version, dataDir);
|
||||
return (!dir.empty() ? make_shared<LocalCountryFile>(dir, countryFile, version) : nullptr);
|
||||
}
|
||||
|
||||
std::string PrepareDirToDownloadCountry(int64_t version, std::string const & dataDir)
|
||||
{
|
||||
string dir = GetDataDirFullPath(dataDir);
|
||||
if (version == 0)
|
||||
return dir;
|
||||
dir = base::JoinPath(dir, strings::to_string(version));
|
||||
return (Platform::MkDirChecked(dir) ? dir : std::string());
|
||||
}
|
||||
|
||||
string GetFileDownloadPath(int64_t version, string const & dataDir, string const & countryName, MapFileType type)
|
||||
{
|
||||
return GetFilePath(version, dataDir, countryName, type) + READY_FILE_EXTENSION;
|
||||
}
|
||||
|
||||
std::unique_ptr<ModelReader> GetCountryReader(LocalCountryFile const & file, MapFileType type)
|
||||
{
|
||||
Platform & platform = GetPlatform();
|
||||
if (file.IsInBundle())
|
||||
return platform.GetReader(file.GetFileName(type), GetAdditionalWorldScope());
|
||||
else
|
||||
return platform.GetReader(file.GetPath(type), "f");
|
||||
}
|
||||
|
||||
// static
|
||||
void CountryIndexes::PreparePlaceOnDisk(LocalCountryFile const & localFile)
|
||||
{
|
||||
string const dir = IndexesDir(localFile);
|
||||
if (!Platform::MkDirChecked(dir))
|
||||
MYTHROW(FileSystemException, ("Can't create directory", dir));
|
||||
}
|
||||
|
||||
// static
|
||||
bool CountryIndexes::DeleteFromDisk(LocalCountryFile const & localFile)
|
||||
{
|
||||
string const directory = IndexesDir(localFile);
|
||||
bool ok = true;
|
||||
|
||||
for (auto index : {Index::Bits, Index::Nodes, Index::Offsets})
|
||||
{
|
||||
string const path = GetPath(localFile, index);
|
||||
if (Platform::IsFileExistsByFullPath(path) && !base::DeleteFileX(path))
|
||||
{
|
||||
LOG(LWARNING, ("Can't remove country index:", path));
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
Platform::EError const ret = Platform::RmDir(directory);
|
||||
if (ret != Platform::ERR_OK && ret != Platform::ERR_FILE_DOES_NOT_EXIST)
|
||||
{
|
||||
LOG(LWARNING, ("Can't remove indexes directory:", directory, ret));
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
// static
|
||||
string CountryIndexes::GetPath(LocalCountryFile const & localFile, Index index)
|
||||
{
|
||||
char const * ext = nullptr;
|
||||
switch (index)
|
||||
{
|
||||
case Index::Bits: ext = kBitsExt; break;
|
||||
case Index::Nodes: ext = kNodesExt; break;
|
||||
case Index::Offsets: ext = kOffsetsExt; break;
|
||||
}
|
||||
return base::JoinPath(IndexesDir(localFile), localFile.GetCountryName() + ext);
|
||||
}
|
||||
|
||||
// static
|
||||
void CountryIndexes::GetIndexesExts(std::vector<string> & exts)
|
||||
{
|
||||
exts.push_back(kBitsExt);
|
||||
exts.push_back(kNodesExt);
|
||||
exts.push_back(kOffsetsExt);
|
||||
}
|
||||
|
||||
// static
|
||||
bool CountryIndexes::IsIndexFile(string const & file)
|
||||
{
|
||||
return file.ends_with(kBitsExt) || file.ends_with(kNodesExt) || file.ends_with(kOffsetsExt);
|
||||
}
|
||||
|
||||
// static
|
||||
string CountryIndexes::IndexesDir(LocalCountryFile const & localFile)
|
||||
{
|
||||
string dir = localFile.GetDirectory();
|
||||
CountryFile const & file = localFile.GetCountryFile();
|
||||
|
||||
if (localFile.IsInBundle())
|
||||
{
|
||||
// Local file is stored in bundle. Need to prepare index folder in the writable directory.
|
||||
int64_t const version = localFile.GetVersion();
|
||||
ASSERT_GREATER(version, 0, ());
|
||||
|
||||
dir = base::JoinPath(GetPlatform().WritableDir(), strings::to_string(version));
|
||||
if (!Platform::MkDirChecked(dir))
|
||||
MYTHROW(FileSystemException, ("Can't create directory", dir));
|
||||
}
|
||||
|
||||
return base::JoinPath(dir, file.GetName());
|
||||
}
|
||||
|
||||
string DebugPrint(CountryIndexes::Index index)
|
||||
{
|
||||
switch (index)
|
||||
{
|
||||
case CountryIndexes::Index::Bits: return "Bits";
|
||||
case CountryIndexes::Index::Nodes: return "Nodes";
|
||||
case CountryIndexes::Index::Offsets: return "Offsets";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
} // namespace platform
|
||||
127
libs/platform/local_country_file_utils.hpp
Normal file
127
libs/platform/local_country_file_utils.hpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/country_defines.hpp"
|
||||
#include "platform/local_country_file.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
class ModelReader;
|
||||
|
||||
namespace platform
|
||||
{
|
||||
// Removes all files downloader creates during downloading of a country.
|
||||
// Note. The the maps are deleted from writable dir/|dataDir|/|version| directory.
|
||||
// If |dataDir| is empty (or is not set) the function deletes maps from writable dir.
|
||||
void DeleteDownloaderFilesForCountry(int64_t version, CountryFile const & countryFile);
|
||||
void DeleteDownloaderFilesForCountry(int64_t version, std::string const & dataDir, CountryFile const & countryFile);
|
||||
|
||||
// Finds all local map files in |directory|. Version of these files is
|
||||
// passed as an argument. Also, performs cleanup described in comment
|
||||
// for FindAllLocalMapsAndCleanup().
|
||||
size_t FindAllLocalMapsInDirectoryAndCleanup(std::string const & directory, int64_t version, int64_t latestVersion,
|
||||
std::vector<LocalCountryFile> & localFiles);
|
||||
|
||||
// Finds all local map files in resources and writable directory. For
|
||||
// Android, checks /Android/obb directory. Subdirectories in the
|
||||
// writable directory should have the following structure:
|
||||
//
|
||||
// dir/*.mwm -- map files, base name should correspond to countries.txt,
|
||||
// -- version is assumed to be zero (unknown).
|
||||
// dir/*.mwm.routing -- routing files for corresponding map files,
|
||||
// -- version is assumed to be zero (unknown).
|
||||
// dir/[0-9]+/*.mwm -- map files, base name should correspond to countries.txt,
|
||||
// -- version is assumed to be the name of a directory.
|
||||
// dir/[0-9]{1,18}/*.mwm.routing -- routing file for corresponding map files,
|
||||
// -- version is assumed to be the name of a directory.
|
||||
//
|
||||
// Also, this method performs cleanup described in a comment for
|
||||
// CleanupMapsDirectory().
|
||||
// Note. The the maps are looked for writable dir/|dataDir|/|version| directory.
|
||||
// If |dataDir| is empty (or is not set) the function looks for maps in writable dir.
|
||||
void FindAllLocalMapsAndCleanup(int64_t latestVersion, std::vector<LocalCountryFile> & localFiles);
|
||||
void FindAllLocalMapsAndCleanup(int64_t latestVersion, std::string const & dataDir,
|
||||
std::vector<LocalCountryFile> & localFiles);
|
||||
|
||||
void FindAllDiffs(std::string const & dataDir, std::vector<LocalCountryFile> & diffs);
|
||||
|
||||
// This method removes:
|
||||
// * partially downloaded non-latest maps (with version less than |latestVersion|)
|
||||
// * empty directories
|
||||
// * old (format v1) maps
|
||||
// * old (split) Japan and Brazil maps
|
||||
// * indexes for absent countries
|
||||
void CleanupMapsDirectory(int64_t latestVersion);
|
||||
|
||||
// Tries to parse a version from a string of size not longer than 18
|
||||
// symbols and representing an unsigned decimal number. Leading zeroes
|
||||
// are allowed.
|
||||
bool ParseVersion(std::string const & s, int64_t & version);
|
||||
|
||||
// When version is zero, uses writable directory, otherwise, creates
|
||||
// directory with name equal to decimal representation of version.
|
||||
// Note. The function assumes the maps are located in writable dir/|dataDir|/|version| directory.
|
||||
// If |dataDir| is empty (or is not set) the function assumes that maps are in writable dir.
|
||||
std::shared_ptr<LocalCountryFile> PreparePlaceForCountryFiles(int64_t version, CountryFile const & countryFile);
|
||||
std::shared_ptr<LocalCountryFile> PreparePlaceForCountryFiles(int64_t version, std::string const & dataDir,
|
||||
CountryFile const & countryFile);
|
||||
std::string PrepareDirToDownloadCountry(int64_t version, std::string const & dataDir);
|
||||
|
||||
/// @note The function assumes the maps are located in writable dir/|dataDir|/|version| directory.
|
||||
/// If |dataDir| is empty (or is not set) the function assumes that maps are in writable dir.
|
||||
/// @{
|
||||
/// @param[in] countryName Actually, same as storage::CountryId, like "Abkhazia".
|
||||
std::string GetFilePath(int64_t version, std::string const & dataDir, std::string const & countryName,
|
||||
MapFileType type);
|
||||
std::string GetFileDownloadPath(int64_t version, std::string const & dataDir, std::string const & countryName,
|
||||
MapFileType type);
|
||||
|
||||
inline std::string GetFileDownloadPath(int64_t version, std::string const & dataDir, CountryFile const & countryFile,
|
||||
MapFileType type)
|
||||
{
|
||||
return GetFileDownloadPath(version, dataDir, countryFile.GetName(), type);
|
||||
}
|
||||
inline std::string GetFileDownloadPath(int64_t version, std::string const & mwmName, MapFileType type)
|
||||
{
|
||||
return GetFileDownloadPath(version, {}, mwmName, type);
|
||||
}
|
||||
/// @}
|
||||
|
||||
std::unique_ptr<ModelReader> GetCountryReader(LocalCountryFile const & file, MapFileType type);
|
||||
|
||||
/// An API for managing country indexes.
|
||||
/// Not used now (except tests), but will be usefull for the Terrain index in future.
|
||||
class CountryIndexes
|
||||
{
|
||||
public:
|
||||
enum class Index
|
||||
{
|
||||
Bits,
|
||||
Nodes,
|
||||
Offsets
|
||||
};
|
||||
|
||||
/// Prepares (if necessary) directory for country indexes.
|
||||
/// @throw FileSystemException if any file system error occured.
|
||||
static void PreparePlaceOnDisk(LocalCountryFile const & localFile);
|
||||
|
||||
// Removes country indexes from disk including containing directory.
|
||||
static bool DeleteFromDisk(LocalCountryFile const & localFile);
|
||||
|
||||
// Returns full path to country index. Note that this method does
|
||||
// not create a file on disk - it just returns a path where the
|
||||
// index should be created/accessed/removed.
|
||||
static std::string GetPath(LocalCountryFile const & localFile, Index index);
|
||||
|
||||
// Pushes to the exts's end possible index files extensions.
|
||||
static void GetIndexesExts(std::vector<std::string> & exts);
|
||||
|
||||
// Returns true if |file| corresponds to an index file.
|
||||
static bool IsIndexFile(std::string const & file);
|
||||
|
||||
static std::string IndexesDir(LocalCountryFile const & localFile);
|
||||
};
|
||||
|
||||
std::string DebugPrint(CountryIndexes::Index index);
|
||||
} // namespace platform
|
||||
22
libs/platform/locale.hpp
Normal file
22
libs/platform/locale.hpp
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
std::string const kNonBreakingSpace = "\u00A0";
|
||||
std::string const kNarrowNonBreakingSpace = "\u202F";
|
||||
|
||||
struct Locale
|
||||
{
|
||||
public:
|
||||
std::string m_language;
|
||||
std::string m_country;
|
||||
std::string m_currency;
|
||||
std::string m_decimalSeparator;
|
||||
std::string m_groupingSeparator;
|
||||
};
|
||||
|
||||
Locale GetCurrentLocale();
|
||||
bool GetLocale(std::string localeName, Locale & result);
|
||||
} // namespace platform
|
||||
31
libs/platform/locale.mm
Normal file
31
libs/platform/locale.mm
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#include "platform/locale.hpp"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
Locale NSLocale2Locale(NSLocale *locale)
|
||||
{
|
||||
return {locale.languageCode ? [locale.languageCode UTF8String] : "",
|
||||
locale.countryCode ? [locale.countryCode UTF8String] : "",
|
||||
locale.currencyCode ? [locale.currencyCode UTF8String] : "",
|
||||
locale.decimalSeparator ? [locale.decimalSeparator UTF8String] : ".",
|
||||
locale.groupingSeparator ? [locale.groupingSeparator UTF8String] : kNarrowNonBreakingSpace};
|
||||
}
|
||||
|
||||
Locale GetCurrentLocale()
|
||||
{
|
||||
return NSLocale2Locale([NSLocale currentLocale]);
|
||||
}
|
||||
|
||||
bool GetLocale(std::string localeName, Locale& result)
|
||||
{
|
||||
NSLocale *loc = [[NSLocale alloc] initWithLocaleIdentifier: @(localeName.c_str())];
|
||||
|
||||
if (!loc)
|
||||
return false;
|
||||
|
||||
result = NSLocale2Locale(loc);
|
||||
return true;
|
||||
}
|
||||
} // namespace platform
|
||||
38
libs/platform/locale_std.cpp
Normal file
38
libs/platform/locale_std.cpp
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#include "platform/locale.hpp"
|
||||
|
||||
#include <locale>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
Locale StdLocale2Locale(std::locale loc)
|
||||
{
|
||||
return {"", "", std::use_facet<std::moneypunct<char, true>>(loc).curr_symbol(),
|
||||
std::string(1, std::use_facet<std::numpunct<char>>(loc).decimal_point()),
|
||||
std::string(1, std::use_facet<std::numpunct<char>>(loc).thousands_sep())};
|
||||
}
|
||||
|
||||
Locale GetCurrentLocale()
|
||||
{
|
||||
// Environment's default locale.
|
||||
std::locale loc;
|
||||
|
||||
return StdLocale2Locale(loc);
|
||||
}
|
||||
|
||||
bool GetLocale(std::string const localeName, Locale & result)
|
||||
{
|
||||
try
|
||||
{
|
||||
std::locale loc(localeName);
|
||||
|
||||
result = StdLocale2Locale(loc);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
66
libs/platform/localization.cpp
Normal file
66
libs/platform/localization.cpp
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#include "platform/localization.hpp"
|
||||
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
namespace
|
||||
{
|
||||
enum class MeasurementType
|
||||
{
|
||||
Distance,
|
||||
Speed,
|
||||
Altitude
|
||||
};
|
||||
|
||||
LocalizedUnits const & GetLocalizedUnits(measurement_utils::Units units, MeasurementType measurementType)
|
||||
{
|
||||
static LocalizedUnits const lengthImperial = {GetLocalizedString("ft"), GetLocalizedString("mi")};
|
||||
static LocalizedUnits const lengthMetric = {GetLocalizedString("m"), GetLocalizedString("km")};
|
||||
|
||||
static LocalizedUnits const speedImperial = {GetLocalizedString("ft"), GetLocalizedString("miles_per_hour")};
|
||||
static LocalizedUnits const speedMetric = {GetLocalizedString("m"), GetLocalizedString("kilometers_per_hour")};
|
||||
|
||||
switch (measurementType)
|
||||
{
|
||||
case MeasurementType::Distance:
|
||||
case MeasurementType::Altitude:
|
||||
switch (units)
|
||||
{
|
||||
case measurement_utils::Units::Imperial: return lengthImperial;
|
||||
case measurement_utils::Units::Metric: return lengthMetric;
|
||||
}
|
||||
break;
|
||||
case MeasurementType::Speed:
|
||||
switch (units)
|
||||
{
|
||||
case measurement_utils::Units::Imperial: return speedImperial;
|
||||
case measurement_utils::Units::Metric: return speedMetric;
|
||||
}
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
LocalizedUnits const & GetLocalizedDistanceUnits()
|
||||
{
|
||||
return GetLocalizedUnits(measurement_utils::GetMeasurementUnits(), MeasurementType::Distance);
|
||||
}
|
||||
|
||||
LocalizedUnits const & GetLocalizedAltitudeUnits()
|
||||
{
|
||||
return GetLocalizedUnits(measurement_utils::GetMeasurementUnits(), MeasurementType::Altitude);
|
||||
}
|
||||
|
||||
std::string const & GetLocalizedSpeedUnits(measurement_utils::Units units)
|
||||
{
|
||||
return GetLocalizedUnits(units, MeasurementType::Speed).m_high;
|
||||
}
|
||||
|
||||
std::string const & GetLocalizedSpeedUnits()
|
||||
{
|
||||
return GetLocalizedSpeedUnits(measurement_utils::GetMeasurementUnits());
|
||||
}
|
||||
} // namespace platform
|
||||
26
libs/platform/localization.hpp
Normal file
26
libs/platform/localization.hpp
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "platform/measurement_utils.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
struct LocalizedUnits
|
||||
{
|
||||
std::string m_low;
|
||||
std::string m_high;
|
||||
};
|
||||
|
||||
extern std::string GetLocalizedTypeName(std::string const & type);
|
||||
extern std::string GetLocalizedBrandName(std::string const & brand);
|
||||
extern std::string GetLocalizedString(std::string const & key);
|
||||
extern std::string GetCurrencySymbol(std::string const & currencyCode);
|
||||
extern std::string GetLocalizedMyPositionBookmarkName();
|
||||
|
||||
extern LocalizedUnits const & GetLocalizedDistanceUnits();
|
||||
extern LocalizedUnits const & GetLocalizedAltitudeUnits();
|
||||
|
||||
extern std::string const & GetLocalizedSpeedUnits(measurement_utils::Units units);
|
||||
extern std::string const & GetLocalizedSpeedUnits();
|
||||
} // namespace platform
|
||||
47
libs/platform/localization.mm
Normal file
47
libs/platform/localization.mm
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#include "platform/localization.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
std::string GetLocalizedTypeName(std::string const & type)
|
||||
{
|
||||
auto key = "type." + type;
|
||||
std::replace(key.begin(), key.end(), '-', '.');
|
||||
std::replace(key.begin(), key.end(), ':', '_');
|
||||
|
||||
return [NSLocalizedStringFromTableInBundle(@(key.c_str()), @"LocalizableTypes", NSBundle.mainBundle, @"") UTF8String];
|
||||
}
|
||||
|
||||
std::string GetLocalizedBrandName(std::string const & brand)
|
||||
{
|
||||
auto const key = "brand." + brand;
|
||||
return [NSLocalizedStringWithDefaultValue(@(key.c_str()), nil, NSBundle.mainBundle, @(brand.c_str()), @"") UTF8String];
|
||||
}
|
||||
|
||||
std::string GetLocalizedString(std::string const & key)
|
||||
{
|
||||
return [NSLocalizedString(@(key.c_str()), @"") UTF8String];
|
||||
}
|
||||
|
||||
std::string GetCurrencySymbol(std::string const & currencyCode)
|
||||
{
|
||||
NSLocale * locale = [NSLocale currentLocale];
|
||||
NSString * symbol = [locale displayNameForKey:NSLocaleCurrencySymbol
|
||||
value:@(currencyCode.c_str())];
|
||||
if (!symbol)
|
||||
return currencyCode;
|
||||
|
||||
return [symbol UTF8String];
|
||||
}
|
||||
|
||||
std::string GetLocalizedMyPositionBookmarkName()
|
||||
{
|
||||
NSDate * now = [NSDate date];
|
||||
return [NSDateFormatter localizedStringFromDate:now
|
||||
dateStyle:NSDateFormatterLongStyle
|
||||
timeStyle:NSDateFormatterShortStyle].UTF8String;
|
||||
}
|
||||
} // namespace platform
|
||||
33
libs/platform/localization_dummy.cpp
Normal file
33
libs/platform/localization_dummy.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#include <ctime>
|
||||
#include "platform/localization.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
std::string GetLocalizedTypeName(std::string const & type)
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
std::string GetLocalizedBrandName(std::string const & brand)
|
||||
{
|
||||
return brand;
|
||||
}
|
||||
|
||||
std::string GetLocalizedString(std::string const & key)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string GetCurrencySymbol(std::string const & currencyCode)
|
||||
{
|
||||
return currencyCode;
|
||||
}
|
||||
|
||||
std::string GetLocalizedMyPositionBookmarkName()
|
||||
{
|
||||
std::time_t t = std::time(nullptr);
|
||||
char buf[100] = {0};
|
||||
(void)std::strftime(buf, sizeof(buf), "%Ec", std::localtime(&t));
|
||||
return buf;
|
||||
}
|
||||
} // namespace platform
|
||||
147
libs/platform/location.hpp
Normal file
147
libs/platform/location.hpp
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
#pragma once
|
||||
|
||||
#include "geometry/latlon.hpp"
|
||||
#include "geometry/point2d.hpp"
|
||||
|
||||
#include "base/base.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace location
|
||||
{
|
||||
/// @note Do not change values of this constants.
|
||||
enum TLocationError
|
||||
{
|
||||
ENoError = 0,
|
||||
ENotSupported,
|
||||
EDenied,
|
||||
EGPSIsOff,
|
||||
ETimeout, // Only used on Qt https://doc.qt.io/qt-6/qgeopositioninfosource.html#Error-enum
|
||||
EUnknown
|
||||
};
|
||||
|
||||
enum TLocationSource
|
||||
{
|
||||
EUndefined,
|
||||
EAppleNative,
|
||||
EWindowsNative,
|
||||
EAndroidNative,
|
||||
EGoogle,
|
||||
ETizen, // Deprecated but left here for backward compatibility.
|
||||
EGeoClue2,
|
||||
EPredictor,
|
||||
EUser
|
||||
};
|
||||
|
||||
/// Our structure ALWAYS has valid lat, lon and horizontal accuracy.
|
||||
/// We filter out location events without lat/lon/acc in native code as we don't need them.
|
||||
class GpsInfo
|
||||
{
|
||||
public:
|
||||
TLocationSource m_source = EUndefined;
|
||||
/// @TODO(bykoianko) |m_timestamp| is calculated based on platform methods which don't
|
||||
/// guarantee that |m_timestamp| is monotonic. |m_monotonicTimeMs| should be added to
|
||||
/// class |GpsInfo|. This time should be calculated based on Location::getElapsedRealtimeNanos()
|
||||
/// method in case of Android. How to calculate such time in case of iOS should be
|
||||
/// investigated.
|
||||
/// \note For most cases |m_timestamp| is monotonic.
|
||||
double m_timestamp = 0.0; //!< seconds from 1st Jan 1970
|
||||
double m_latitude = 0.0; //!< degrees
|
||||
double m_longitude = 0.0; //!< degrees
|
||||
double m_horizontalAccuracy = 100.0; //!< metres
|
||||
double m_altitude = 0.0; //!< metres
|
||||
double m_verticalAccuracy = -1.0; //!< metres
|
||||
double m_bearing = -1.0; //!< positive degrees from the true North
|
||||
double m_speed = -1.0; //!< metres per second
|
||||
|
||||
bool IsValid() const { return m_source != EUndefined; }
|
||||
bool HasBearing() const { return m_bearing >= 0.0; }
|
||||
bool HasSpeed() const { return m_speed >= 0.0; }
|
||||
bool HasVerticalAccuracy() const { return m_verticalAccuracy >= 0.0; }
|
||||
ms::LatLon GetLatLon() const { return {m_latitude, m_longitude}; }
|
||||
};
|
||||
|
||||
class CompassInfo
|
||||
{
|
||||
public:
|
||||
// double m_timestamp; //!< seconds from 1st Jan 1970
|
||||
// double m_magneticHeading; //!< positive radians from the magnetic North
|
||||
// double m_trueHeading; //!< positive radians from the true North
|
||||
// double m_accuracy; //!< offset from the magnetic to the true North in radians
|
||||
double m_bearing; //!< positive radians from the true North
|
||||
};
|
||||
|
||||
static inline bool IsLatValid(double lat)
|
||||
{
|
||||
return lat != 0. && lat < 90. && lat > -90.;
|
||||
}
|
||||
static inline bool IsLonValid(double lon)
|
||||
{
|
||||
return lon != 0. && lon < 180. && lon > -180.;
|
||||
}
|
||||
|
||||
// Convert angle (in degrees counterclockwise from X) to bearing ([0, 360) clockwise from the north)
|
||||
static inline double AngleToBearing(double a)
|
||||
{
|
||||
double reverseAng = fmod(-a + 90, 360.);
|
||||
if (reverseAng < 0)
|
||||
reverseAng += 360.;
|
||||
return reverseAng;
|
||||
}
|
||||
|
||||
// Convert bearing (in degrees clockwise from the north) to angle ([0, 360) counterclockwise from X)
|
||||
static inline double BearingToAngle(double a)
|
||||
{
|
||||
return AngleToBearing(a);
|
||||
}
|
||||
|
||||
class RouteMatchingInfo
|
||||
{
|
||||
m2::PointD m_matchedPosition;
|
||||
size_t m_indexInRoute;
|
||||
bool m_isPositionMatched;
|
||||
bool m_hasDistanceFromBegin;
|
||||
double m_distanceFromBegin;
|
||||
|
||||
public:
|
||||
RouteMatchingInfo()
|
||||
: m_matchedPosition(0., 0.)
|
||||
, m_indexInRoute(0)
|
||||
, m_isPositionMatched(false)
|
||||
, m_hasDistanceFromBegin(false)
|
||||
, m_distanceFromBegin(0.0)
|
||||
{}
|
||||
|
||||
void Set(m2::PointD const & matchedPosition, size_t indexInRoute, double distanceFromBegin)
|
||||
{
|
||||
m_matchedPosition = matchedPosition;
|
||||
m_indexInRoute = indexInRoute;
|
||||
m_isPositionMatched = true;
|
||||
|
||||
m_distanceFromBegin = distanceFromBegin;
|
||||
m_hasDistanceFromBegin = true;
|
||||
}
|
||||
|
||||
void Reset() { m_isPositionMatched = false; }
|
||||
bool IsMatched() const { return m_isPositionMatched; }
|
||||
size_t GetIndexInRoute() const { return m_indexInRoute; }
|
||||
m2::PointD GetPosition() const { return m_matchedPosition; }
|
||||
bool HasDistanceFromBegin() const { return m_hasDistanceFromBegin; }
|
||||
double GetDistanceFromBegin() const { return m_distanceFromBegin; }
|
||||
};
|
||||
|
||||
enum EMyPositionMode
|
||||
{
|
||||
PendingPosition = 0,
|
||||
NotFollowNoPosition,
|
||||
NotFollow,
|
||||
Follow,
|
||||
FollowAndRotate
|
||||
};
|
||||
|
||||
using TMyPositionModeChanged = std::function<void(location::EMyPositionMode, bool)>;
|
||||
|
||||
} // namespace location
|
||||
28
libs/platform/location_service/CMakeLists.txt
Normal file
28
libs/platform/location_service/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
project(location_service)
|
||||
|
||||
if (NOT SKIP_QT_GUI AND PLATFORM_DESKTOP AND PLATFORM_LINUX)
|
||||
message("Building with Qt Positioning")
|
||||
find_package(Qt6 REQUIRED COMPONENTS Positioning)
|
||||
set(QT_LOCATION_SERVICE true)
|
||||
elseif(NOT SKIP_QT_GUI AND PLATFORM_DESKTOP AND PLATFORM_MAC)
|
||||
set(APPLE_LOCATION_SERVICE true)
|
||||
endif()
|
||||
|
||||
set(SRC
|
||||
location_service.cpp
|
||||
location_service.hpp
|
||||
$<$<BOOL:${APPLE_LOCATION_SERVICE}>:apple_location_service.mm>
|
||||
$<$<BOOL:${QT_LOCATION_SERVICE}>:qt_location_service.hpp>
|
||||
$<$<BOOL:${QT_LOCATION_SERVICE}>:qt_location_service.cpp>
|
||||
)
|
||||
|
||||
omim_add_library(${PROJECT_NAME} ${SRC})
|
||||
|
||||
if (QT_LOCATION_SERVICE)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE "QT_LOCATION_SERVICE")
|
||||
target_link_libraries(${PROJECT_NAME} Qt6::Positioning)
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES AUTOMOC ON)
|
||||
elseif(APPLE_LOCATION_SERVICE)
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE "APPLE_LOCATION_SERVICE")
|
||||
target_link_libraries(${PROJECT_NAME} -framework\ CoreLocation)
|
||||
endif()
|
||||
112
libs/platform/location_service/apple_location_service.mm
Normal file
112
libs/platform/location_service/apple_location_service.mm
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
#include "platform/location_service/location_service.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/macros.hpp"
|
||||
|
||||
#include "std/target_os.hpp"
|
||||
|
||||
#import <CoreLocation/CoreLocation.h>
|
||||
|
||||
class AppleLocationService;
|
||||
|
||||
@interface LocationManagerWrapper : NSObject <CLLocationManagerDelegate> {
|
||||
@private
|
||||
AppleLocationService * m_service;
|
||||
}
|
||||
- (id)initWithService:(AppleLocationService *) service;
|
||||
@end
|
||||
|
||||
using namespace location;
|
||||
|
||||
#define ROUGH_ACCURACY kCLLocationAccuracyNearestTenMeters
|
||||
|
||||
class AppleLocationService : public LocationService
|
||||
{
|
||||
LocationManagerWrapper * m_objCppWrapper;
|
||||
CLLocationManager * m_locationManager;
|
||||
|
||||
public:
|
||||
AppleLocationService(LocationObserver & observer) : LocationService(observer)
|
||||
{
|
||||
m_objCppWrapper = [[LocationManagerWrapper alloc] initWithService:this];
|
||||
m_locationManager = [[CLLocationManager alloc] init];
|
||||
m_locationManager.delegate = m_objCppWrapper;
|
||||
m_locationManager.desiredAccuracy = kCLLocationAccuracyBest;
|
||||
}
|
||||
|
||||
void OnLocationUpdate(GpsInfo const & info)
|
||||
{
|
||||
m_observer.OnLocationUpdated(info);
|
||||
}
|
||||
|
||||
void OnDeniedError()
|
||||
{
|
||||
m_observer.OnLocationError(location::EDenied);
|
||||
}
|
||||
|
||||
virtual void Start()
|
||||
{
|
||||
if (![CLLocationManager locationServicesEnabled])
|
||||
{
|
||||
// @TODO correctly handle situation in GUI when wifi is working and native is disabled
|
||||
//m_observer.OnLocationStatusChanged(location::ENotSupported);
|
||||
}
|
||||
else
|
||||
{
|
||||
[m_locationManager startUpdatingLocation];
|
||||
}
|
||||
}
|
||||
|
||||
virtual void Stop()
|
||||
{
|
||||
[m_locationManager stopUpdatingLocation];
|
||||
}
|
||||
};
|
||||
|
||||
@implementation LocationManagerWrapper
|
||||
|
||||
- (id)initWithService:(AppleLocationService *) service
|
||||
{
|
||||
if ((self = [super init]))
|
||||
m_service = service;
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (void)location:(CLLocation *)location toGpsInfo:(GpsInfo &) info
|
||||
{
|
||||
info.m_horizontalAccuracy = location.horizontalAccuracy;
|
||||
info.m_latitude = location.coordinate.latitude;
|
||||
info.m_longitude = location.coordinate.longitude;
|
||||
info.m_timestamp = [location.timestamp timeIntervalSince1970];
|
||||
info.m_source = location::EAppleNative;
|
||||
//info.m_verticalAccuracy = location.verticalAccuracy;
|
||||
//info.m_altitude = location.altitude;
|
||||
//info.m_course = location.course;
|
||||
//info.m_speed = location.speed;
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager
|
||||
didUpdateLocations:(NSArray<CLLocation *> *)locations
|
||||
{
|
||||
UNUSED_VALUE(manager);
|
||||
GpsInfo newInfo;
|
||||
[LocationManagerWrapper location:locations.firstObject toGpsInfo:newInfo];
|
||||
m_service->OnLocationUpdate(newInfo);
|
||||
}
|
||||
|
||||
- (void)locationManager:(CLLocationManager *)manager
|
||||
didFailWithError:(NSError *)error
|
||||
{
|
||||
UNUSED_VALUE(manager);
|
||||
LOG(LWARNING, ("locationManager failed with error", error.code, error.description.UTF8String));
|
||||
|
||||
if (error.code == kCLErrorDenied)
|
||||
m_service->OnDeniedError();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
std::unique_ptr<location::LocationService> CreateAppleLocationService(LocationObserver & observer)
|
||||
{
|
||||
return std::make_unique<AppleLocationService>(observer);
|
||||
}
|
||||
101
libs/platform/location_service/location_service.cpp
Normal file
101
libs/platform/location_service/location_service.cpp
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
#include "platform/location_service/location_service.hpp"
|
||||
|
||||
#include "std/target_os.hpp"
|
||||
|
||||
#include <ctime>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#if defined(OMIM_OS_MAC)
|
||||
std::unique_ptr<location::LocationService> CreateAppleLocationService(location::LocationObserver &);
|
||||
#elif defined(QT_LOCATION_SERVICE)
|
||||
std::unique_ptr<location::LocationService> CreateQtLocationService(location::LocationObserver &,
|
||||
std::string const & sourceName);
|
||||
#endif
|
||||
|
||||
namespace location
|
||||
{
|
||||
static double ApproxDistanceSquareInMeters(double lat1, double lon1, double lat2, double lon2)
|
||||
{
|
||||
double const m1 = (lat1 - lat2) / 111111.;
|
||||
double const m2 = (lon1 - lon2) / 111111.;
|
||||
return m1 * m1 + m2 * m2;
|
||||
}
|
||||
|
||||
/// Chooses most accurate data from different position services
|
||||
class PositionFilter
|
||||
{
|
||||
std::optional<location::GpsInfo> m_prevLocation;
|
||||
|
||||
public:
|
||||
/// @return true if location should be sent to observers
|
||||
bool Passes(location::GpsInfo const & newLocation)
|
||||
{
|
||||
if (std::time(nullptr) - newLocation.m_timestamp > 300.0)
|
||||
return false;
|
||||
|
||||
bool passes = true;
|
||||
if (m_prevLocation)
|
||||
{
|
||||
if (newLocation.m_timestamp < m_prevLocation->m_timestamp)
|
||||
passes = false;
|
||||
else if (newLocation.m_source != m_prevLocation->m_source &&
|
||||
newLocation.m_horizontalAccuracy > m_prevLocation->m_horizontalAccuracy &&
|
||||
ApproxDistanceSquareInMeters(newLocation.m_latitude, newLocation.m_longitude, m_prevLocation->m_latitude,
|
||||
m_prevLocation->m_longitude) >
|
||||
newLocation.m_horizontalAccuracy * newLocation.m_horizontalAccuracy)
|
||||
passes = false;
|
||||
}
|
||||
else
|
||||
m_prevLocation = newLocation;
|
||||
return passes;
|
||||
}
|
||||
};
|
||||
|
||||
class DesktopLocationService
|
||||
: public LocationService
|
||||
, public LocationObserver
|
||||
{
|
||||
std::vector<std::unique_ptr<LocationService>> m_services;
|
||||
PositionFilter m_filter;
|
||||
bool m_reportFirstEvent;
|
||||
|
||||
virtual void OnLocationError(location::TLocationError errorCode) { m_observer.OnLocationError(errorCode); }
|
||||
|
||||
virtual void OnLocationUpdated(GpsInfo const & info)
|
||||
{
|
||||
if (m_filter.Passes(info))
|
||||
m_observer.OnLocationUpdated(info);
|
||||
}
|
||||
|
||||
public:
|
||||
explicit DesktopLocationService(LocationObserver & observer) : LocationService(observer), m_reportFirstEvent(true)
|
||||
{
|
||||
#if defined(QT_LOCATION_SERVICE)
|
||||
#if defined(OMIM_OS_LINUX)
|
||||
m_services.push_back(CreateQtLocationService(*this, "geoclue2"));
|
||||
#endif // OMIM_OS_LINUX
|
||||
#elif defined(APPLE_LOCATION_SERVICE) // No QT_LOCATION_SERVICE
|
||||
m_services.push_back(CreateAppleLocationService(*this));
|
||||
#endif // QT_LOCATION_SERVICE
|
||||
}
|
||||
|
||||
virtual void Start()
|
||||
{
|
||||
for (auto & service : m_services)
|
||||
service->Start();
|
||||
}
|
||||
|
||||
virtual void Stop()
|
||||
{
|
||||
for (auto & service : m_services)
|
||||
service->Stop();
|
||||
m_reportFirstEvent = true;
|
||||
}
|
||||
};
|
||||
} // namespace location
|
||||
|
||||
std::unique_ptr<location::LocationService> CreateDesktopLocationService(location::LocationObserver & observer)
|
||||
{
|
||||
return std::make_unique<location::DesktopLocationService>(observer);
|
||||
}
|
||||
33
libs/platform/location_service/location_service.hpp
Normal file
33
libs/platform/location_service/location_service.hpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/location.hpp"
|
||||
|
||||
namespace location
|
||||
{
|
||||
|
||||
class LocationObserver
|
||||
{
|
||||
public:
|
||||
virtual void OnLocationError(TLocationError errorCode) = 0;
|
||||
virtual void OnLocationUpdated(GpsInfo const & info) = 0;
|
||||
|
||||
protected:
|
||||
virtual ~LocationObserver() = default;
|
||||
};
|
||||
|
||||
class LocationService
|
||||
{
|
||||
protected:
|
||||
LocationObserver & m_observer;
|
||||
|
||||
public:
|
||||
LocationService(LocationObserver & observer) : m_observer(observer) {}
|
||||
virtual ~LocationService() = default;
|
||||
|
||||
virtual void Start() = 0;
|
||||
virtual void Stop() = 0;
|
||||
};
|
||||
|
||||
} // namespace location
|
||||
|
||||
std::unique_ptr<location::LocationService> CreateDesktopLocationService(location::LocationObserver & observer);
|
||||
162
libs/platform/location_service/qt_location_service.cpp
Normal file
162
libs/platform/location_service/qt_location_service.cpp
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
#include "platform/location_service/qt_location_service.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/macros.hpp"
|
||||
|
||||
#include "std/target_os.hpp"
|
||||
|
||||
#include <QGeoPositionInfoSource>
|
||||
|
||||
namespace
|
||||
{
|
||||
static location::GpsInfo gpsInfoFromQGeoPositionInfo(QGeoPositionInfo const & i, location::TLocationSource source)
|
||||
{
|
||||
location::GpsInfo info;
|
||||
info.m_source = source;
|
||||
|
||||
info.m_latitude = i.coordinate().latitude();
|
||||
info.m_longitude = i.coordinate().longitude();
|
||||
info.m_timestamp = static_cast<double>(i.timestamp().toSecsSinceEpoch());
|
||||
|
||||
if (i.hasAttribute(QGeoPositionInfo::HorizontalAccuracy))
|
||||
info.m_horizontalAccuracy = static_cast<double>(i.attribute(QGeoPositionInfo::HorizontalAccuracy));
|
||||
|
||||
if (i.hasAttribute(QGeoPositionInfo::VerticalAccuracy))
|
||||
info.m_verticalAccuracy = static_cast<double>(i.attribute(QGeoPositionInfo::VerticalAccuracy));
|
||||
|
||||
if (i.hasAttribute(QGeoPositionInfo::Direction))
|
||||
info.m_bearing = static_cast<double>(i.attribute(QGeoPositionInfo::Direction));
|
||||
|
||||
if (i.hasAttribute(QGeoPositionInfo::GroundSpeed))
|
||||
info.m_speed = static_cast<double>(i.attribute(QGeoPositionInfo::GroundSpeed));
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
static location::TLocationError tLocationErrorFromQGeoPositionInfoError(QGeoPositionInfoSource::Error error)
|
||||
{
|
||||
location::TLocationError result = location::TLocationError::ENotSupported;
|
||||
switch (error)
|
||||
{
|
||||
case QGeoPositionInfoSource::AccessError: result = location::TLocationError::EDenied; break;
|
||||
case QGeoPositionInfoSource::ClosedError: result = location::TLocationError::EGPSIsOff; break;
|
||||
case QGeoPositionInfoSource::NoError: result = location::TLocationError::ENoError; break;
|
||||
case QGeoPositionInfoSource::UpdateTimeoutError: result = location::TLocationError::ETimeout; break;
|
||||
case QGeoPositionInfoSource::UnknownSourceError: result = location::TLocationError::EUnknown; break;
|
||||
default: break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static location::TLocationSource qStringToTLocationSource(QString const & sourceName)
|
||||
{
|
||||
if ("geoclue2" == sourceName)
|
||||
return location::TLocationSource::EGeoClue2;
|
||||
|
||||
return location::TLocationSource::EUndefined;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
QtLocationService::QtLocationService(location::LocationObserver & observer, std::string const & sourceName)
|
||||
: LocationService(observer)
|
||||
{
|
||||
QVariantMap params;
|
||||
params["desktopId"] = "app.comaps.comaps";
|
||||
m_positionSource = QGeoPositionInfoSource::createSource(QString::fromStdString(sourceName), params, this);
|
||||
|
||||
if (!m_positionSource)
|
||||
{
|
||||
LOG(LWARNING, ("Failed to acquire QGeoPositionInfoSource from ", sourceName));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!connect(m_positionSource, &QGeoPositionInfoSource::positionUpdated, this, &QtLocationService::OnLocationUpdate))
|
||||
{
|
||||
LOG(LERROR, ("Failed to connect the signal:", "positionUpdated"));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG(LDEBUG, ("Signal successfully connected:", "positionUpdated"));
|
||||
|
||||
if (!connect(m_positionSource, &QGeoPositionInfoSource::errorOccurred, this, &QtLocationService::OnErrorOccurred))
|
||||
{
|
||||
LOG(LERROR, ("Failed to connect the signal:", "errorOccurred"));
|
||||
return;
|
||||
}
|
||||
LOG(LDEBUG, ("Signal successfully connected:", "errorOccurred"));
|
||||
|
||||
if (!connect(m_positionSource, &QGeoPositionInfoSource::supportedPositioningMethodsChanged, this,
|
||||
&QtLocationService::OnSupportedPositioningMethodsChanged))
|
||||
{
|
||||
LOG(LERROR, ("Failed to connect the signal:", "supportedPositioningMethodsChanged"));
|
||||
return;
|
||||
}
|
||||
LOG(LDEBUG, ("Signal successfully connected:", "supportedPositioningMethodsChanged"));
|
||||
|
||||
m_positionSource->setUpdateInterval(1000);
|
||||
m_positionSource->setPreferredPositioningMethods(QGeoPositionInfoSource::AllPositioningMethods);
|
||||
}
|
||||
|
||||
void QtLocationService::OnLocationUpdate(QGeoPositionInfo const & info)
|
||||
{
|
||||
if (!info.isValid())
|
||||
{
|
||||
LOG(LWARNING,
|
||||
("Location update with Invalid timestamp or coordinates from:", m_positionSource->sourceName().toStdString()));
|
||||
return;
|
||||
}
|
||||
auto const & coordinate = info.coordinate();
|
||||
LOG(LDEBUG, ("Location updated with valid coordinates:", coordinate.longitude(), coordinate.latitude()));
|
||||
m_observer.OnLocationUpdated(
|
||||
gpsInfoFromQGeoPositionInfo(info, qStringToTLocationSource(m_positionSource->sourceName())));
|
||||
if (!m_clientIsActive)
|
||||
{
|
||||
m_clientIsActive = true;
|
||||
m_positionSource->startUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
void QtLocationService::OnErrorOccurred(QGeoPositionInfoSource::Error positioningError)
|
||||
{
|
||||
LOG(LWARNING, ("Location error occured QGeoPositionInfoSource::Error code:", positioningError));
|
||||
m_clientIsActive = false;
|
||||
m_observer.OnLocationError(tLocationErrorFromQGeoPositionInfoError(positioningError));
|
||||
}
|
||||
|
||||
void QtLocationService::OnSupportedPositioningMethodsChanged()
|
||||
{
|
||||
auto positioningMethods = m_positionSource->supportedPositioningMethods();
|
||||
LOG(LDEBUG, ("Supported Positioning Method changed for:", m_positionSource->sourceName().toStdString(),
|
||||
"to:", positioningMethods));
|
||||
if (positioningMethods == QGeoPositionInfoSource::NoPositioningMethods)
|
||||
{
|
||||
m_clientIsActive = false;
|
||||
m_observer.OnLocationError(location::TLocationError::EGPSIsOff);
|
||||
}
|
||||
}
|
||||
|
||||
void QtLocationService::Start()
|
||||
{
|
||||
if (m_positionSource)
|
||||
{
|
||||
LOG(LDEBUG, ("Starting Updates from:", m_positionSource->sourceName().toStdString()));
|
||||
// Request the first update with a timeout to 30 minutes which is needed on devices that don't make use of `A-GNSS`
|
||||
// and can't get a lock within Qt's default `UPDATE_TIMEOUT_COLDSTART` (currently 2 minutes).
|
||||
m_positionSource->requestUpdate(1800000);
|
||||
}
|
||||
}
|
||||
|
||||
void QtLocationService::Stop()
|
||||
{
|
||||
if (m_positionSource && m_clientIsActive)
|
||||
{
|
||||
LOG(LDEBUG, ("Stopping Updates from:", m_positionSource->sourceName().toStdString()));
|
||||
m_positionSource->stopUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<location::LocationService> CreateQtLocationService(location::LocationObserver & observer,
|
||||
std::string const & sourceName)
|
||||
{
|
||||
return std::make_unique<QtLocationService>(observer, sourceName);
|
||||
}
|
||||
30
libs/platform/location_service/qt_location_service.hpp
Normal file
30
libs/platform/location_service/qt_location_service.hpp
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/location_service/location_service.hpp"
|
||||
|
||||
#include <QGeoPositionInfoSource>
|
||||
|
||||
class QtLocationService
|
||||
: public QObject
|
||||
, public location::LocationService
|
||||
{
|
||||
Q_OBJECT
|
||||
QGeoPositionInfoSource * m_positionSource;
|
||||
// Unfortunately when the source is `geoclue2`
|
||||
// we would need access to the `Active` D-Bus property
|
||||
// https://www.freedesktop.org/software/geoclue/docs
|
||||
// /gdbus-org.freedesktop.GeoClue2.Client.html#gdbus-property-org-freedesktop-GeoClue2-Client.Active
|
||||
// But `QGeoPositionInfoSource` doesn't expose that so we have to deduce its state.
|
||||
bool m_clientIsActive = false;
|
||||
|
||||
public:
|
||||
explicit QtLocationService(location::LocationObserver &, std::string const &);
|
||||
virtual ~QtLocationService() {}
|
||||
virtual void Start();
|
||||
virtual void Stop();
|
||||
|
||||
public slots:
|
||||
void OnLocationUpdate(QGeoPositionInfo const &);
|
||||
void OnErrorOccurred(QGeoPositionInfoSource::Error);
|
||||
void OnSupportedPositioningMethodsChanged();
|
||||
};
|
||||
314
libs/platform/measurement_utils.cpp
Normal file
314
libs/platform/measurement_utils.cpp
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
#include "platform/measurement_utils.hpp"
|
||||
#include "platform/locale.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include "geometry/mercator.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/bits.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/math.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring> // strstr
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
namespace measurement_utils
|
||||
{
|
||||
std::string ToStringPrecision(double d, int pr)
|
||||
{
|
||||
// We assume that the app will be restarted if a user changes device's locale.
|
||||
static auto const locale = platform::GetCurrentLocale();
|
||||
|
||||
return ToStringPrecisionLocale(locale, d, pr);
|
||||
}
|
||||
|
||||
std::string ToStringPrecisionLocale(platform::Locale const & loc, double d, int pr)
|
||||
{
|
||||
std::string formatBuf("%0.0f");
|
||||
if (pr > 0)
|
||||
{
|
||||
if (pr < 10)
|
||||
formatBuf[3] = '0' + pr; // E.g. replace %0.0f with %0.1f
|
||||
else
|
||||
LOG(LERROR, ("Too big unsupported precision", pr));
|
||||
}
|
||||
|
||||
char textBuf[50];
|
||||
int const n = std::snprintf(textBuf, sizeof(textBuf), formatBuf.c_str(), d);
|
||||
if (n < 0 || n >= static_cast<int>(sizeof(textBuf)))
|
||||
{
|
||||
LOG(LERROR, ("snprintf", pr, d, "failed with", n));
|
||||
return std::to_string(static_cast<int64_t>(d));
|
||||
}
|
||||
|
||||
std::string out(textBuf);
|
||||
|
||||
// std::locale does not work on Android NDK, so decimal and grouping (thousands) separator
|
||||
// shall be customized manually here.
|
||||
|
||||
if (pr)
|
||||
{
|
||||
// Value with decimals. Set locale decimal separator.
|
||||
if (loc.m_decimalSeparator != ".")
|
||||
out.replace(out.size() - pr - 1, 1, loc.m_decimalSeparator);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Value with no decimals. Check if it's equal or bigger than 10000 to
|
||||
// insert the grouping (thousands) separator characters.
|
||||
if (out.size() > 4 && !loc.m_groupingSeparator.empty())
|
||||
for (long pos = static_cast<long>(out.size()) - 3; pos > 0; pos -= 3)
|
||||
out.insert(pos, loc.m_groupingSeparator);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string_view DebugPrint(Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case Units::Imperial: return "Units::Imperial";
|
||||
case Units::Metric: return "Units::Metric";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
Units GetMeasurementUnits()
|
||||
{
|
||||
Units units = Units::Metric;
|
||||
settings::TryGet(settings::kMeasurementUnits, units);
|
||||
return units;
|
||||
}
|
||||
|
||||
double ToSpeedKmPH(double speed, Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case Units::Imperial: return MiphToKmph(speed);
|
||||
case Units::Metric: return speed;
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
std::string FormatLatLonAsDMSImpl(double value, char positive, char negative, int dac)
|
||||
{
|
||||
using namespace base;
|
||||
|
||||
std::ostringstream sstream;
|
||||
sstream << std::setfill('0');
|
||||
|
||||
// Degrees
|
||||
double i;
|
||||
double d = std::modf(std::fabs(value), &i);
|
||||
sstream << std::setw(2) << i << "°";
|
||||
|
||||
// Minutes
|
||||
d = std::modf(d * 60.0, &i);
|
||||
sstream << std::setw(2) << i << "′";
|
||||
|
||||
// Seconds
|
||||
d = d * 60.0;
|
||||
if (dac == 0)
|
||||
d = std::round(d);
|
||||
|
||||
d = std::modf(d, &i);
|
||||
sstream << std::setw(2) << i;
|
||||
|
||||
if (dac > 0)
|
||||
sstream << strings::to_string_dac(d, dac).substr(1);
|
||||
|
||||
sstream << "″";
|
||||
|
||||
// This condition is too heavy for production purposes (but more correct).
|
||||
// if (std::round(value * 3600.0 * pow(10, dac)) != 0)
|
||||
if (!AlmostEqualULPs(value, 0.0))
|
||||
{
|
||||
char postfix = positive;
|
||||
if (value < 0.0)
|
||||
postfix = negative;
|
||||
|
||||
sstream << postfix;
|
||||
}
|
||||
|
||||
return sstream.str();
|
||||
}
|
||||
|
||||
std::string FormatLatLonAsDMS(double lat, double lon, bool withComma, int dac)
|
||||
{
|
||||
return (FormatLatLonAsDMSImpl(lat, 'N', 'S', dac) + (withComma ? ", " : " ") +
|
||||
FormatLatLonAsDMSImpl(lon, 'E', 'W', dac));
|
||||
}
|
||||
|
||||
void FormatLatLonAsDMS(double lat, double lon, std::string & latText, std::string & lonText, int dac)
|
||||
{
|
||||
latText = FormatLatLonAsDMSImpl(lat, 'N', 'S', dac);
|
||||
lonText = FormatLatLonAsDMSImpl(lon, 'E', 'W', dac);
|
||||
}
|
||||
|
||||
void FormatMercatorAsDMS(m2::PointD const & mercator, std::string & lat, std::string & lon, int dac)
|
||||
{
|
||||
lat = FormatLatLonAsDMSImpl(mercator::YToLat(mercator.y), 'N', 'S', dac);
|
||||
lon = FormatLatLonAsDMSImpl(mercator::XToLon(mercator.x), 'E', 'W', dac);
|
||||
}
|
||||
|
||||
std::string FormatMercatorAsDMS(m2::PointD const & mercator, int dac)
|
||||
{
|
||||
return FormatLatLonAsDMS(mercator::YToLat(mercator.y), mercator::XToLon(mercator.x), dac);
|
||||
}
|
||||
|
||||
// @TODO take into account decimal points or commas as separators in different locales
|
||||
std::string FormatLatLon(double lat, double lon, int dac)
|
||||
{
|
||||
return strings::to_string_dac(lat, dac) + " " + strings::to_string_dac(lon, dac);
|
||||
}
|
||||
|
||||
std::string FormatLatLon(double lat, double lon, bool withComma, int dac)
|
||||
{
|
||||
return strings::to_string_dac(lat, dac) + (withComma ? ", " : " ") + strings::to_string_dac(lon, dac);
|
||||
}
|
||||
|
||||
void FormatLatLon(double lat, double lon, std::string & latText, std::string & lonText, int dac)
|
||||
{
|
||||
latText = strings::to_string_dac(lat, dac);
|
||||
lonText = strings::to_string_dac(lon, dac);
|
||||
}
|
||||
|
||||
std::string FormatMercator(m2::PointD const & mercator, int dac)
|
||||
{
|
||||
return FormatLatLon(mercator::YToLat(mercator.y), mercator::XToLon(mercator.x), dac);
|
||||
}
|
||||
|
||||
void FormatMercator(m2::PointD const & mercator, std::string & lat, std::string & lon, int dac)
|
||||
{
|
||||
lat = strings::to_string_dac(mercator::YToLat(mercator.y), dac);
|
||||
lon = strings::to_string_dac(mercator::XToLon(mercator.x), dac);
|
||||
}
|
||||
|
||||
double MpsToUnits(double metersPerSecond, Units units)
|
||||
{
|
||||
switch (units)
|
||||
{
|
||||
case Units::Imperial: return KmphToMiph(MpsToKmph(metersPerSecond));
|
||||
case Units::Metric: return MpsToKmph(metersPerSecond);
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
long FormatSpeed(double metersPerSecond, Units units)
|
||||
{
|
||||
return std::lround(MpsToUnits(metersPerSecond, units));
|
||||
}
|
||||
|
||||
std::string FormatSpeedNumeric(double metersPerSecond, Units units)
|
||||
{
|
||||
return std::to_string(FormatSpeed(metersPerSecond, units));
|
||||
}
|
||||
|
||||
std::string FormatOsmLink(double lat, double lon, int zoom)
|
||||
{
|
||||
static constexpr char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~";
|
||||
|
||||
// Same as (lon + 180) / 360 * 1UL << 32, but without warnings.
|
||||
double constexpr factor = (1 << 30) / 90.0;
|
||||
auto const x = static_cast<uint32_t>(std::lround((lon + 180.0) * factor));
|
||||
auto const y = static_cast<uint32_t>(std::lround((lat + 90.0) * factor * 2.0));
|
||||
uint64_t const code = bits::BitwiseMerge(y, x);
|
||||
std::string osmUrl = "https://osm.org/go/";
|
||||
|
||||
for (int i = 0; i < (zoom + 10) / 3; ++i)
|
||||
{
|
||||
uint64_t const digit = (code >> (58 - 6 * i)) & 0x3f;
|
||||
ASSERT_LESS(digit, ARRAY_SIZE(chars), ());
|
||||
osmUrl += chars[digit];
|
||||
}
|
||||
|
||||
for (int i = 0; i < (zoom + 8) % 3; ++i)
|
||||
osmUrl += "-";
|
||||
// ?m tells OSM to display a marker
|
||||
return osmUrl + "?m";
|
||||
}
|
||||
|
||||
bool OSMDistanceToMeters(std::string const & osmRawValue, double & outMeters)
|
||||
{
|
||||
char * stop;
|
||||
char const * s = osmRawValue.c_str();
|
||||
outMeters = strtod(s, &stop);
|
||||
|
||||
// Not a number, was not parsed at all.
|
||||
if (s == stop)
|
||||
return false;
|
||||
|
||||
if (!math::is_finite(outMeters))
|
||||
return false;
|
||||
|
||||
switch (*stop)
|
||||
{
|
||||
// Default units - meters.
|
||||
case 0: return true;
|
||||
|
||||
// Feet and probably inches.
|
||||
case '\'':
|
||||
{
|
||||
outMeters = FeetToMeters(outMeters);
|
||||
s = stop + 1;
|
||||
double const inches = strtod(s, &stop);
|
||||
if (s != stop && *stop == '"' && math::is_finite(inches))
|
||||
outMeters += InchesToMeters(inches);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Inches.
|
||||
case '"': outMeters = InchesToMeters(outMeters); return true;
|
||||
|
||||
// It's probably a range. Use maximum value (if possible) for a range.
|
||||
case '-':
|
||||
{
|
||||
s = stop + 1;
|
||||
double const newValue = strtod(s, &stop);
|
||||
if (s != stop && math::is_finite(newValue))
|
||||
outMeters = newValue;
|
||||
}
|
||||
break;
|
||||
|
||||
// It's probably a list. We don't support them.
|
||||
case ';': return false;
|
||||
}
|
||||
|
||||
while (*stop && strings::IsASCIISpace(*stop))
|
||||
++stop;
|
||||
|
||||
// Default units - meters.
|
||||
if (*stop == 0)
|
||||
return true;
|
||||
|
||||
if (strstr(stop, "nmi") == stop)
|
||||
outMeters = NauticalMilesToMeters(outMeters);
|
||||
else if (strstr(stop, "mi") == stop)
|
||||
outMeters = MilesToMeters(outMeters);
|
||||
else if (strstr(stop, "ft") == stop || strstr(stop, "feet") == stop)
|
||||
outMeters = FeetToMeters(outMeters);
|
||||
else if (strstr(stop, "km") == stop)
|
||||
outMeters = outMeters * 1000;
|
||||
|
||||
// Count all other cases as meters.
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string OSMDistanceToMetersString(std::string const & osmRawValue, bool supportZeroAndNegativeValues,
|
||||
int digitsAfterComma)
|
||||
{
|
||||
double meters;
|
||||
if (OSMDistanceToMeters(osmRawValue, meters))
|
||||
{
|
||||
if (!supportZeroAndNegativeValues && meters <= 0)
|
||||
return {};
|
||||
return strings::to_string_dac(meters, digitsAfterComma);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
} // namespace measurement_utils
|
||||
102
libs/platform/measurement_utils.hpp
Normal file
102
libs/platform/measurement_utils.hpp
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
#pragma once
|
||||
|
||||
#include "geometry/point2d.hpp"
|
||||
#include "platform/locale.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace measurement_utils
|
||||
{
|
||||
enum class Units
|
||||
{
|
||||
Metric = 0,
|
||||
Imperial = 1
|
||||
};
|
||||
|
||||
std::string_view DebugPrint(Units units);
|
||||
|
||||
Units GetMeasurementUnits();
|
||||
|
||||
inline double MetersToMiles(double m)
|
||||
{
|
||||
return m * 0.000621371192;
|
||||
}
|
||||
inline double MilesToMeters(double mi)
|
||||
{
|
||||
return mi * 1609.344;
|
||||
}
|
||||
inline double MilesToFeet(double mi)
|
||||
{
|
||||
return mi * 5280.0;
|
||||
}
|
||||
inline double MiphToKmph(double miph)
|
||||
{
|
||||
return MilesToMeters(miph) / 1000.0;
|
||||
}
|
||||
inline double KmphToMiph(double kmph)
|
||||
{
|
||||
return MetersToMiles(kmph * 1000.0);
|
||||
}
|
||||
inline double MpsToKmph(double mps)
|
||||
{
|
||||
return mps * 3.6;
|
||||
}
|
||||
inline double MetersToFeet(double m)
|
||||
{
|
||||
return m * 3.2808399;
|
||||
}
|
||||
inline double FeetToMeters(double ft)
|
||||
{
|
||||
return ft * 0.3048;
|
||||
}
|
||||
inline double FeetToMiles(double ft)
|
||||
{
|
||||
return ft * 0.00018939;
|
||||
}
|
||||
inline double InchesToMeters(double in)
|
||||
{
|
||||
return in / 39.370;
|
||||
}
|
||||
inline double NauticalMilesToMeters(double nmi)
|
||||
{
|
||||
return nmi * 1852;
|
||||
}
|
||||
inline double constexpr KmphToMps(double kmph)
|
||||
{
|
||||
return kmph * 1000 / 3600;
|
||||
}
|
||||
|
||||
double ToSpeedKmPH(double speed, Units units);
|
||||
double MpsToUnits(double metersPerSecond, Units units);
|
||||
|
||||
/// @return Speed value in km/h for Metric and in mph for Imperial.
|
||||
long FormatSpeed(double metersPerSecond, Units units);
|
||||
/// @return Speed value string (without suffix) in km/h for Metric and in mph for Imperial.
|
||||
std::string FormatSpeedNumeric(double metersPerSecond, Units units);
|
||||
|
||||
/// @param[in] dac Digits after comma in seconds.
|
||||
/// Use dac == 3 for our common conversions to DMS.
|
||||
std::string FormatLatLonAsDMS(double lat, double lon, bool withComma, int dac = 3);
|
||||
void FormatLatLonAsDMS(double lat, double lon, std::string & latText, std::string & lonText, int dac = 3);
|
||||
std::string FormatMercatorAsDMS(m2::PointD const & mercator, int dac = 3);
|
||||
void FormatMercatorAsDMS(m2::PointD const & mercator, std::string & lat, std::string & lon, int dac = 3);
|
||||
|
||||
/// Default dac == 6 for the simple decimal formatting.
|
||||
std::string FormatLatLon(double lat, double lon, int dac = 6);
|
||||
std::string FormatLatLon(double lat, double lon, bool withComma, int dac = 6);
|
||||
void FormatLatLon(double lat, double lon, std::string & latText, std::string & lonText, int dac = 6);
|
||||
std::string FormatMercator(m2::PointD const & mercator, int dac = 6);
|
||||
void FormatMercator(m2::PointD const & mercator, std::string & lat, std::string & lon, int dac = 6);
|
||||
|
||||
std::string FormatOsmLink(double lat, double lon, int zoom);
|
||||
|
||||
/// Converts OSM distance (height, ele etc.) to meters.
|
||||
/// @returns false if fails.
|
||||
bool OSMDistanceToMeters(std::string const & osmRawValue, double & outMeters);
|
||||
/// Converts OSM distance (height, ele etc.) to meters std::string.
|
||||
/// @returns empty std::string on failure.
|
||||
std::string OSMDistanceToMetersString(std::string const & osmRawValue, bool supportZeroAndNegativeValues = true,
|
||||
int digitsAfterComma = 2);
|
||||
std::string ToStringPrecision(double d, int pr);
|
||||
std::string ToStringPrecisionLocale(platform::Locale const & locale, double d, int pr);
|
||||
} // namespace measurement_utils
|
||||
52
libs/platform/mwm_traits.cpp
Normal file
52
libs/platform/mwm_traits.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#include "platform/mwm_traits.hpp"
|
||||
|
||||
namespace version
|
||||
{
|
||||
MwmTraits::MwmTraits(MwmVersion const & version) : m_version(version) {}
|
||||
|
||||
MwmTraits::SearchIndexFormat MwmTraits::GetSearchIndexFormat() const
|
||||
{
|
||||
return SearchIndexFormat::CompressedBitVectorWithHeader;
|
||||
}
|
||||
|
||||
MwmTraits::HouseToStreetTableFormat MwmTraits::GetHouseToStreetTableFormat() const
|
||||
{
|
||||
return HouseToStreetTableFormat::HouseToStreetTableWithHeader;
|
||||
}
|
||||
|
||||
MwmTraits::CentersTableFormat MwmTraits::GetCentersTableFormat() const
|
||||
{
|
||||
return CentersTableFormat::EliasFanoMapWithHeader;
|
||||
}
|
||||
|
||||
std::string DebugPrint(MwmTraits::SearchIndexFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case MwmTraits::SearchIndexFormat::FeaturesWithRankAndCenter: return "FeaturesWithRankAndCenter";
|
||||
case MwmTraits::SearchIndexFormat::CompressedBitVector: return "CompressedBitVector";
|
||||
case MwmTraits::SearchIndexFormat::CompressedBitVectorWithHeader: return "CompressedBitVectorWithHeader";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
std::string DebugPrint(MwmTraits::HouseToStreetTableFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case MwmTraits::HouseToStreetTableFormat::HouseToStreetTableWithHeader: return "HouseToStreetTableWithHeader";
|
||||
case MwmTraits::HouseToStreetTableFormat::Unknown: return "Unknown";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
std::string DebugPrint(MwmTraits::CentersTableFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case MwmTraits::CentersTableFormat::PlainEliasFanoMap: return "PlainEliasFanoMap";
|
||||
case MwmTraits::CentersTableFormat::EliasFanoMapWithHeader: return "EliasFanoMapWithHeader";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
} // namespace version
|
||||
71
libs/platform/mwm_traits.hpp
Normal file
71
libs/platform/mwm_traits.hpp
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/mwm_version.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace version
|
||||
{
|
||||
// This is a wrapper around mwm's version. Allows users to get
|
||||
// information about versions of some data structures in mwm.
|
||||
class MwmTraits
|
||||
{
|
||||
public:
|
||||
enum class SearchIndexFormat
|
||||
{
|
||||
// A list of features with their ranks and centers
|
||||
// is stored behind every node of the search trie.
|
||||
// This format corresponds to ValueList<FeatureWithRankAndCenter>.
|
||||
FeaturesWithRankAndCenter,
|
||||
|
||||
// A compressed bit vector of feature indices is
|
||||
// stored behind every node of the search trie.
|
||||
// This format corresponds to ValueList<Uint64IndexValue>.
|
||||
CompressedBitVector,
|
||||
|
||||
// A compressed bit vector of feature indices is
|
||||
// stored behind every node of the search trie.
|
||||
// This format corresponds to ValueList<Uint64IndexValue>.
|
||||
// Section has header.
|
||||
CompressedBitVectorWithHeader,
|
||||
};
|
||||
|
||||
enum class HouseToStreetTableFormat
|
||||
{
|
||||
// Versioning is independent of MwmTraits: section format depends on the section header.
|
||||
HouseToStreetTableWithHeader,
|
||||
|
||||
// The format of relation is unknown. Most likely, an error has occured.
|
||||
Unknown
|
||||
};
|
||||
|
||||
enum class CentersTableFormat
|
||||
{
|
||||
// Centers table encoded without any header. Coding params from mwm header are used.
|
||||
PlainEliasFanoMap,
|
||||
|
||||
// Centers table has its own header with version and coding params.
|
||||
EliasFanoMapWithHeader,
|
||||
};
|
||||
|
||||
explicit MwmTraits(MwmVersion const & version);
|
||||
|
||||
SearchIndexFormat GetSearchIndexFormat() const;
|
||||
|
||||
HouseToStreetTableFormat GetHouseToStreetTableFormat() const;
|
||||
|
||||
CentersTableFormat GetCentersTableFormat() const;
|
||||
|
||||
bool HasIsolines() const;
|
||||
|
||||
private:
|
||||
Format GetFormat() const { return m_version.GetFormat(); }
|
||||
uint32_t GetVersion() const { return m_version.GetVersion(); }
|
||||
|
||||
MwmVersion m_version;
|
||||
};
|
||||
|
||||
std::string DebugPrint(MwmTraits::SearchIndexFormat format);
|
||||
std::string DebugPrint(MwmTraits::HouseToStreetTableFormat format);
|
||||
std::string DebugPrint(MwmTraits::CentersTableFormat format);
|
||||
} // namespace version
|
||||
74
libs/platform/mwm_version.cpp
Normal file
74
libs/platform/mwm_version.cpp
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#include "mwm_version.hpp"
|
||||
|
||||
#include "coding/files_container.hpp"
|
||||
#include "coding/varint.hpp"
|
||||
#include "coding/writer.hpp"
|
||||
|
||||
#include "base/gmtime.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
#include "base/timer.hpp"
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace version
|
||||
{
|
||||
namespace
|
||||
{
|
||||
char const MWM_PROLOG[] = "MWM";
|
||||
}
|
||||
|
||||
MwmVersion MwmVersion::Read(FilesContainerR const & container)
|
||||
{
|
||||
ModelReaderPtr versionReader = container.GetReader(VERSION_FILE_TAG);
|
||||
ReaderSource<ModelReaderPtr> src(versionReader);
|
||||
|
||||
size_t const prologSize = ARRAY_SIZE(MWM_PROLOG);
|
||||
char prolog[prologSize];
|
||||
src.Read(prolog, prologSize);
|
||||
|
||||
if (strcmp(prolog, MWM_PROLOG) != 0)
|
||||
MYTHROW(CorruptedMwmFile, ());
|
||||
|
||||
MwmVersion version;
|
||||
version.m_format = static_cast<Format>(ReadVarUint<uint32_t>(src));
|
||||
version.m_secondsSinceEpoch = ReadVarUint<uint32_t>(src);
|
||||
return version;
|
||||
}
|
||||
|
||||
uint32_t MwmVersion::GetVersion() const
|
||||
{
|
||||
auto const tm = base::GmTime(base::SecondsSinceEpochToTimeT(m_secondsSinceEpoch));
|
||||
return base::GenerateYYMMDD(tm.tm_year, tm.tm_mon, tm.tm_mday);
|
||||
}
|
||||
|
||||
std::string DebugPrint(Format f)
|
||||
{
|
||||
return "v" + strings::to_string(static_cast<uint32_t>(f) + 1);
|
||||
}
|
||||
|
||||
std::string DebugPrint(MwmVersion const & mwmVersion)
|
||||
{
|
||||
std::stringstream s;
|
||||
s << "MwmVersion "
|
||||
<< "{ m_format: " << DebugPrint(mwmVersion.GetFormat())
|
||||
<< ", m_secondsSinceEpoch: " << mwmVersion.GetSecondsSinceEpoch() << " }";
|
||||
return s.str();
|
||||
}
|
||||
|
||||
void WriteVersion(Writer & w, uint64_t secondsSinceEpoch)
|
||||
{
|
||||
w.Write(MWM_PROLOG, ARRAY_SIZE(MWM_PROLOG));
|
||||
|
||||
// write inner data version
|
||||
WriteVarUint(w, static_cast<uint32_t>(Format::lastFormat));
|
||||
WriteVarUint(w, secondsSinceEpoch);
|
||||
}
|
||||
|
||||
uint32_t ReadVersionDate(ModelReaderPtr const & reader)
|
||||
{
|
||||
return MwmVersion::Read(FilesContainerR(reader)).GetVersion();
|
||||
}
|
||||
|
||||
} // namespace version
|
||||
76
libs/platform/mwm_version.hpp
Normal file
76
libs/platform/mwm_version.hpp
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
|
||||
#include "base/exception.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
class FilesContainerR;
|
||||
class Writer;
|
||||
class ModelReaderPtr;
|
||||
|
||||
DECLARE_EXCEPTION(CorruptedMwmFile, RootException);
|
||||
|
||||
namespace version
|
||||
{
|
||||
enum class Format
|
||||
{
|
||||
unknownFormat = -1,
|
||||
v1 = 0, // April 2011
|
||||
v2, // November 2011 (store type index, instead of raw type in mwm)
|
||||
v3, // March 2013 (store type index, instead of raw type in search data)
|
||||
v4, // April 2015 (distinguish и and й in search index)
|
||||
v5, // July 2015 (feature id is the index in vector now).
|
||||
v6, // October 2015 (offsets vector is in mwm now).
|
||||
v7, // November 2015 (supply different search index formats).
|
||||
v8, // February 2016 (long strings in metadata; store seconds since epoch in MwmVersion).
|
||||
// December 2016 (index graph section was added in version 161206, between v8 and v9).
|
||||
v9, // April 2017 (OSRM sections are deleted and replaced by cross mwm section).
|
||||
v10, // April 2020 (dat section renamed to features, compressed metadata index, addr section with
|
||||
// header, sdx section with header, dat section renamed to features, features section with
|
||||
// header).
|
||||
v11, // September 2020 (compressed string storage for metadata).
|
||||
lastFormat = v11
|
||||
};
|
||||
|
||||
std::string DebugPrint(Format f);
|
||||
|
||||
class MwmVersion
|
||||
{
|
||||
public:
|
||||
Format GetFormat() const { return m_format; }
|
||||
uint64_t GetSecondsSinceEpoch() const { return m_secondsSinceEpoch; }
|
||||
/// \return version as YYMMDD.
|
||||
uint32_t GetVersion() const;
|
||||
|
||||
/// @name Used in tests only.
|
||||
/// @{
|
||||
void SetFormat(Format format) { m_format = format; }
|
||||
void SetSecondsSinceEpoch(uint64_t secondsSinceEpoch) { m_secondsSinceEpoch = secondsSinceEpoch; }
|
||||
/// @}
|
||||
|
||||
static MwmVersion Read(FilesContainerR const & container);
|
||||
|
||||
private:
|
||||
/// Data layout format in mwm file.
|
||||
Format m_format{Format::unknownFormat};
|
||||
uint64_t m_secondsSinceEpoch{0};
|
||||
};
|
||||
|
||||
std::string DebugPrint(MwmVersion const & mwmVersion);
|
||||
|
||||
/// Writes latest format and current timestamp to the writer.
|
||||
void WriteVersion(Writer & w, uint64_t secondsSinceEpoch);
|
||||
|
||||
/// Helper function that is used in FindAllLocalMaps.
|
||||
uint32_t ReadVersionDate(ModelReaderPtr const & reader);
|
||||
|
||||
/// \brief This enum sets constants which are used for
|
||||
/// writing test to set a version of mwm which should be processed.
|
||||
enum ForTesting
|
||||
{
|
||||
FOR_TESTING_MWM1 = 991215,
|
||||
FOR_TESTING_MWM2,
|
||||
FOR_TESTING_MWM_LATEST,
|
||||
};
|
||||
} // namespace version
|
||||
30
libs/platform/network_policy.hpp
Normal file
30
libs/platform/network_policy.hpp
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
struct _JNIEnv;
|
||||
class _jobject;
|
||||
typedef _JNIEnv JNIEnv;
|
||||
typedef _jobject * jobject;
|
||||
|
||||
namespace platform
|
||||
{
|
||||
/// Class that is used to allow or disallow remote calls.
|
||||
class NetworkPolicy
|
||||
{
|
||||
// Maker for android.
|
||||
friend NetworkPolicy ToNativeNetworkPolicy(JNIEnv * env, jobject obj);
|
||||
|
||||
friend NetworkPolicy GetCurrentNetworkPolicy();
|
||||
|
||||
public:
|
||||
bool CanUse() const { return m_canUse; }
|
||||
|
||||
private:
|
||||
NetworkPolicy(bool const canUseNetwork) : m_canUse(canUseNetwork) {}
|
||||
|
||||
bool m_canUse = false;
|
||||
};
|
||||
|
||||
extern NetworkPolicy GetCurrentNetworkPolicy();
|
||||
} // namespace platform
|
||||
9
libs/platform/network_policy_dummy.cpp
Normal file
9
libs/platform/network_policy_dummy.cpp
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
#include "platform/network_policy.hpp"
|
||||
|
||||
namespace platform
|
||||
{
|
||||
NetworkPolicy GetCurrentNetworkPolicy()
|
||||
{
|
||||
return NetworkPolicy(true);
|
||||
}
|
||||
} // namespace platform
|
||||
24
libs/platform/network_policy_ios.h
Normal file
24
libs/platform/network_policy_ios.h
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/network_policy.hpp"
|
||||
|
||||
@class NSDate;
|
||||
|
||||
namespace network_policy
|
||||
{
|
||||
enum Stage
|
||||
{
|
||||
Ask,
|
||||
Always,
|
||||
Never,
|
||||
Today,
|
||||
NotToday
|
||||
};
|
||||
|
||||
void SetStage(Stage state);
|
||||
Stage GetStage();
|
||||
|
||||
bool CanUseNetwork();
|
||||
bool IsActivePolicyDate();
|
||||
NSDate* GetPolicyDate();
|
||||
} // namespace network_policy
|
||||
66
libs/platform/network_policy_ios.mm
Normal file
66
libs/platform/network_policy_ios.mm
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
#include "platform/network_policy_ios.h"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#import <Foundation/NSDate.h>
|
||||
#import <Foundation/NSString.h>
|
||||
#import <Foundation/NSUserDefaults.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
NSString * const kNetworkingPolicyTimeStamp = @"NetworkingPolicyTimeStamp";
|
||||
NSString * const kNetworkingPolicyStage = @"NetworkingPolicyStage";
|
||||
NSTimeInterval const kSessionDurationSeconds = 24 * 60 * 60;
|
||||
} // namespace
|
||||
|
||||
namespace network_policy
|
||||
{
|
||||
void SetStage(Stage stage)
|
||||
{
|
||||
NSUserDefaults *ud = NSUserDefaults.standardUserDefaults;
|
||||
[ud setInteger:stage forKey:kNetworkingPolicyStage];
|
||||
[ud setObject:[NSDate dateWithTimeIntervalSinceNow:kSessionDurationSeconds] forKey:kNetworkingPolicyTimeStamp];
|
||||
}
|
||||
|
||||
Stage GetStage()
|
||||
{
|
||||
return (Stage)[NSUserDefaults.standardUserDefaults integerForKey:kNetworkingPolicyStage];
|
||||
}
|
||||
|
||||
NSDate * GetPolicyDate()
|
||||
{
|
||||
return [NSUserDefaults.standardUserDefaults objectForKey:kNetworkingPolicyTimeStamp];
|
||||
}
|
||||
|
||||
bool IsActivePolicyDate()
|
||||
{
|
||||
return [GetPolicyDate() compare:[NSDate date]] == NSOrderedDescending;
|
||||
}
|
||||
|
||||
bool CanUseNetwork()
|
||||
{
|
||||
using ct = Platform::EConnectionType;
|
||||
switch (GetPlatform().ConnectionStatus())
|
||||
{
|
||||
case ct::CONNECTION_NONE: return false;
|
||||
case ct::CONNECTION_WIFI: return true;
|
||||
case ct::CONNECTION_WWAN:
|
||||
switch (GetStage())
|
||||
{
|
||||
case Stage::Ask: return false;
|
||||
case Stage::Always: return true;
|
||||
case Stage::Never: return false;
|
||||
case Stage::Today: return IsActivePolicyDate();
|
||||
case Stage::NotToday: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace network_policy
|
||||
|
||||
namespace platform
|
||||
{
|
||||
NetworkPolicy GetCurrentNetworkPolicy()
|
||||
{
|
||||
return NetworkPolicy(network_policy::CanUseNetwork());
|
||||
}
|
||||
} // namespace platform
|
||||
402
libs/platform/platform.cpp
Normal file
402
libs/platform/platform.cpp
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/internal/file_data.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/random.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <thread>
|
||||
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include "private.h"
|
||||
|
||||
#include <cerrno>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string RandomString(size_t length)
|
||||
{
|
||||
/// @todo Used for temp file name, so lower-upper case is strange here, no?
|
||||
static std::string_view constexpr kCharset =
|
||||
"0123456789"
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
base::UniformRandom<size_t> rand(0, kCharset.size() - 1);
|
||||
std::string str(length, 0);
|
||||
std::generate_n(str.begin(), length, [&rand]() { return kCharset[rand()]; });
|
||||
return str;
|
||||
}
|
||||
|
||||
inline bool IsSpecialDirName(std::string const & dirName)
|
||||
{
|
||||
return dirName == "." || dirName == "..";
|
||||
}
|
||||
|
||||
bool GetFileTypeChecked(std::string const & path, Platform::EFileType & type)
|
||||
{
|
||||
Platform::EError const ret = Platform::GetFileType(path, type);
|
||||
if (ret != Platform::ERR_OK)
|
||||
{
|
||||
LOG(LERROR, ("Can't determine file type for", path, ":", ret));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// static
|
||||
Platform::EError Platform::ErrnoToError()
|
||||
{
|
||||
switch (errno)
|
||||
{
|
||||
case ENOENT: return ERR_FILE_DOES_NOT_EXIST;
|
||||
case EACCES: return ERR_ACCESS_FAILED;
|
||||
case ENOTEMPTY: return ERR_DIRECTORY_NOT_EMPTY;
|
||||
case EEXIST: return ERR_FILE_ALREADY_EXISTS;
|
||||
case ENAMETOOLONG: return ERR_NAME_TOO_LONG;
|
||||
case ENOTDIR: return ERR_NOT_A_DIRECTORY;
|
||||
case ELOOP: return ERR_SYMLINK_LOOP;
|
||||
case EIO: return ERR_IO_ERROR;
|
||||
default: return ERR_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool Platform::RmDirRecursively(std::string const & dirName)
|
||||
{
|
||||
if (dirName.empty() || IsSpecialDirName(dirName))
|
||||
return false;
|
||||
|
||||
bool res = true;
|
||||
|
||||
FilesList allFiles;
|
||||
GetAllFiles(dirName, allFiles);
|
||||
for (std::string const & file : allFiles)
|
||||
{
|
||||
std::string const path = base::JoinPath(dirName, file);
|
||||
|
||||
EFileType type;
|
||||
if (GetFileType(path, type) != ERR_OK)
|
||||
continue;
|
||||
|
||||
if (type == EFileType::Directory)
|
||||
{
|
||||
if (!IsSpecialDirName(file) && !RmDirRecursively(path))
|
||||
res = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!base::DeleteFileX(path))
|
||||
res = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (RmDir(dirName) != ERR_OK)
|
||||
res = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void Platform::SetSettingsDir(std::string const & path)
|
||||
{
|
||||
m_settingsDir = base::AddSlashIfNeeded(path);
|
||||
}
|
||||
|
||||
std::string Platform::SettingsPathForFile(std::string const & file) const
|
||||
{
|
||||
return base::JoinPath(SettingsDir(), file);
|
||||
}
|
||||
|
||||
std::string Platform::WritablePathForFile(std::string const & file) const
|
||||
{
|
||||
return base::JoinPath(WritableDir(), file);
|
||||
}
|
||||
|
||||
std::string Platform::ReadPathForFile(std::string const & file, std::string searchScope) const
|
||||
{
|
||||
if (searchScope.empty())
|
||||
searchScope = "wrf";
|
||||
|
||||
std::string fullPath;
|
||||
for (size_t i = 0; i < searchScope.size(); ++i)
|
||||
{
|
||||
switch (searchScope[i])
|
||||
{
|
||||
case 'w':
|
||||
ASSERT(!m_writableDir.empty(), ());
|
||||
fullPath = base::JoinPath(m_writableDir, file);
|
||||
break;
|
||||
case 'r':
|
||||
ASSERT(!m_resourcesDir.empty(), ());
|
||||
fullPath = base::JoinPath(m_resourcesDir, file);
|
||||
break;
|
||||
case 's':
|
||||
ASSERT(!m_settingsDir.empty(), ());
|
||||
fullPath = base::JoinPath(m_settingsDir, file);
|
||||
break;
|
||||
case 'f': fullPath = file; break;
|
||||
default: CHECK(false, ("Unsupported searchScope:", searchScope)); break;
|
||||
}
|
||||
if (IsFileExistsByFullPath(fullPath))
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
MYTHROW(FileAbsentException, ("File", file, "doesn't exist in the scope", searchScope, "\nw: ", m_writableDir,
|
||||
"\nr: ", m_resourcesDir, "\ns: ", m_settingsDir));
|
||||
}
|
||||
|
||||
std::string Platform::MetaServerUrl() const
|
||||
{
|
||||
return METASERVER_URL;
|
||||
}
|
||||
|
||||
std::string Platform::DefaultUrlsJSON() const
|
||||
{
|
||||
return DEFAULT_URLS_JSON;
|
||||
}
|
||||
|
||||
bool Platform::RemoveFileIfExists(std::string const & filePath)
|
||||
{
|
||||
return IsFileExistsByFullPath(filePath) ? base::DeleteFileX(filePath) : true;
|
||||
}
|
||||
|
||||
std::string Platform::TmpPathForFile() const
|
||||
{
|
||||
size_t constexpr kNameLen = 32;
|
||||
return base::JoinPath(TmpDir(), RandomString(kNameLen));
|
||||
}
|
||||
|
||||
std::string Platform::TmpPathForFile(std::string const & prefix, std::string const & suffix) const
|
||||
{
|
||||
size_t constexpr kRandomLen = 8;
|
||||
return base::JoinPath(TmpDir(), prefix + RandomString(kRandomLen) + suffix);
|
||||
}
|
||||
|
||||
void Platform::GetFontNames(FilesList & res) const
|
||||
{
|
||||
ASSERT(res.empty(), ());
|
||||
|
||||
/// @todo Actually, this list should present once in all our code.
|
||||
char constexpr const * arrDef[] = {
|
||||
"fonts/00_NotoNaskhArabic-Regular.ttf",
|
||||
"fonts/00_NotoSansBengali-Regular.ttf",
|
||||
"fonts/00_NotoSansHebrew-Regular.ttf",
|
||||
"fonts/00_NotoSansMalayalam-Regular.ttf",
|
||||
"fonts/00_NotoSansThai-Regular.ttf",
|
||||
"fonts/00_NotoSerifDevanagari-Regular.ttf",
|
||||
"fonts/01_dejavusans.ttf",
|
||||
"fonts/02_droidsans-fallback.ttf",
|
||||
"fonts/03_jomolhari-id-a3d.ttf",
|
||||
"fonts/04_padauk.ttf",
|
||||
"fonts/05_khmeros.ttf",
|
||||
"fonts/06_code2000.ttf",
|
||||
"fonts/07_roboto_medium.ttf",
|
||||
};
|
||||
res.insert(res.end(), arrDef, arrDef + ARRAY_SIZE(arrDef));
|
||||
|
||||
GetSystemFontNames(res);
|
||||
|
||||
LOG(LINFO, ("Available font files:", (res)));
|
||||
}
|
||||
|
||||
void Platform::GetFilesByExt(std::string const & directory, std::string_view ext, FilesList & outFiles)
|
||||
{
|
||||
// Transform extension mask to regexp (.mwm -> \.mwm$)
|
||||
ASSERT(!ext.empty(), ());
|
||||
ASSERT_EQUAL(ext[0], '.', ());
|
||||
GetFilesByRegExp(directory, boost::regex(std::string("\\").append(ext).append("$")), outFiles);
|
||||
}
|
||||
|
||||
// static
|
||||
void Platform::GetFilesByType(std::string const & directory, unsigned typeMask, TFilesWithType & outFiles)
|
||||
{
|
||||
FilesList allFiles;
|
||||
GetAllFiles(directory, allFiles);
|
||||
for (auto const & file : allFiles)
|
||||
{
|
||||
EFileType type;
|
||||
if (GetFileType(base::JoinPath(directory, file), type) != ERR_OK)
|
||||
continue;
|
||||
if (typeMask & type)
|
||||
outFiles.emplace_back(file, type);
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool Platform::IsDirectory(std::string const & path)
|
||||
{
|
||||
EFileType fileType;
|
||||
if (GetFileType(path, fileType) != ERR_OK)
|
||||
return false;
|
||||
return fileType == EFileType::Directory;
|
||||
}
|
||||
|
||||
// static
|
||||
void Platform::GetFilesRecursively(std::string const & directory, FilesList & filesList)
|
||||
{
|
||||
TFilesWithType files;
|
||||
|
||||
GetFilesByType(directory, EFileType::Regular, files);
|
||||
for (auto const & p : files)
|
||||
{
|
||||
auto const & file = p.first;
|
||||
CHECK_EQUAL(p.second, EFileType::Regular, ("dir:", directory, "file:", file));
|
||||
filesList.push_back(base::JoinPath(directory, file));
|
||||
}
|
||||
|
||||
TFilesWithType subdirs;
|
||||
GetFilesByType(directory, EFileType::Directory, subdirs);
|
||||
|
||||
for (auto const & p : subdirs)
|
||||
{
|
||||
auto const & subdir = p.first;
|
||||
CHECK_EQUAL(p.second, EFileType::Directory, ("dir:", directory, "subdir:", subdir));
|
||||
if (subdir == "." || subdir == "..")
|
||||
continue;
|
||||
|
||||
GetFilesRecursively(base::JoinPath(directory, subdir), filesList);
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::SetWritableDirForTests(std::string const & path)
|
||||
{
|
||||
m_writableDir = base::AddSlashIfNeeded(path);
|
||||
}
|
||||
|
||||
void Platform::SetResourceDir(std::string const & path)
|
||||
{
|
||||
m_resourcesDir = base::AddSlashIfNeeded(path);
|
||||
}
|
||||
|
||||
// static
|
||||
bool Platform::MkDirChecked(std::string const & dirName)
|
||||
{
|
||||
switch (EError const ret = MkDir(dirName))
|
||||
{
|
||||
case ERR_OK: return true;
|
||||
case ERR_FILE_ALREADY_EXISTS:
|
||||
{
|
||||
EFileType type;
|
||||
if (!GetFileTypeChecked(dirName, type))
|
||||
return false;
|
||||
if (type != Directory)
|
||||
{
|
||||
LOG(LERROR, (dirName, "exists, but not a dirName:", type));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
default: LOG(LERROR, (dirName, "can't be created:", ret)); return false;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
bool Platform::MkDirRecursively(std::string const & dirName)
|
||||
{
|
||||
CHECK(!dirName.empty(), ());
|
||||
|
||||
std::string::value_type const sep[] = {base::GetNativeSeparator(), 0};
|
||||
std::string path = dirName.starts_with(sep[0]) ? sep : ".";
|
||||
for (auto const & t : strings::Tokenize(dirName, sep))
|
||||
{
|
||||
path = base::JoinPath(path, std::string{t});
|
||||
if (!IsFileExistsByFullPath(path))
|
||||
{
|
||||
switch (MkDir(path))
|
||||
{
|
||||
case ERR_OK: break;
|
||||
case ERR_FILE_ALREADY_EXISTS:
|
||||
{
|
||||
if (!IsDirectory(path))
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
unsigned Platform::CpuCores()
|
||||
{
|
||||
unsigned const cores = std::thread::hardware_concurrency();
|
||||
return cores > 0 ? cores : 1;
|
||||
}
|
||||
|
||||
void Platform::ShutdownThreads()
|
||||
{
|
||||
ASSERT(m_networkThread && m_fileThread && m_backgroundThread, ());
|
||||
ASSERT(!m_networkThread->IsShutDown(), ());
|
||||
ASSERT(!m_fileThread->IsShutDown(), ());
|
||||
ASSERT(!m_backgroundThread->IsShutDown(), ());
|
||||
|
||||
m_batteryTracker.UnsubscribeAll();
|
||||
|
||||
m_networkThread->ShutdownAndJoin();
|
||||
m_fileThread->ShutdownAndJoin();
|
||||
m_backgroundThread->ShutdownAndJoin();
|
||||
}
|
||||
|
||||
void Platform::RunThreads()
|
||||
{
|
||||
ASSERT(!m_networkThread || m_networkThread->IsShutDown(), ());
|
||||
ASSERT(!m_fileThread || m_fileThread->IsShutDown(), ());
|
||||
ASSERT(!m_backgroundThread || m_backgroundThread->IsShutDown(), ());
|
||||
|
||||
m_networkThread = std::make_unique<base::DelayedThreadPool>();
|
||||
m_fileThread = std::make_unique<base::DelayedThreadPool>();
|
||||
m_backgroundThread = std::make_unique<base::DelayedThreadPool>();
|
||||
}
|
||||
|
||||
void Platform::SetGuiThread(std::unique_ptr<base::TaskLoop> guiThread)
|
||||
{
|
||||
m_guiThread = std::move(guiThread);
|
||||
}
|
||||
|
||||
void Platform::CancelTask(Thread thread, base::TaskLoop::TaskId id)
|
||||
{
|
||||
ASSERT(m_networkThread && m_fileThread && m_backgroundThread, ());
|
||||
switch (thread)
|
||||
{
|
||||
case Thread::File: m_fileThread->Cancel(id); return;
|
||||
case Thread::Network: m_networkThread->Cancel(id); return;
|
||||
case Thread::Gui: CHECK(false, ("Task cancelling for gui thread is not supported yet")); return;
|
||||
case Thread::Background: m_backgroundThread->Cancel(id); return;
|
||||
}
|
||||
}
|
||||
|
||||
std::string DebugPrint(Platform::EError err)
|
||||
{
|
||||
switch (err)
|
||||
{
|
||||
case Platform::ERR_OK: return "Ok";
|
||||
case Platform::ERR_FILE_DOES_NOT_EXIST: return "File does not exist.";
|
||||
case Platform::ERR_ACCESS_FAILED: return "Access failed.";
|
||||
case Platform::ERR_DIRECTORY_NOT_EMPTY: return "Directory not empty.";
|
||||
case Platform::ERR_FILE_ALREADY_EXISTS: return "File already exists.";
|
||||
case Platform::ERR_NAME_TOO_LONG: return "The length of a component of path exceeds {NAME_MAX} characters.";
|
||||
case Platform::ERR_NOT_A_DIRECTORY: return "A component of the path prefix of Path is not a directory.";
|
||||
case Platform::ERR_SYMLINK_LOOP: return "Too many symbolic links were encountered in translating path.";
|
||||
case Platform::ERR_IO_ERROR: return "An I/O error occurred.";
|
||||
case Platform::ERR_UNKNOWN: return "Unknown";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
std::string DebugPrint(Platform::ChargingStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case Platform::ChargingStatus::Unknown: return "Unknown";
|
||||
case Platform::ChargingStatus::Plugged: return "Plugged";
|
||||
case Platform::ChargingStatus::Unplugged: return "Unplugged";
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
345
libs/platform/platform.hpp
Normal file
345
libs/platform/platform.hpp
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/battery_tracker.hpp"
|
||||
#include "platform/country_defines.hpp"
|
||||
#include "platform/gui_thread.hpp"
|
||||
#include "platform/secure_storage.hpp"
|
||||
|
||||
#include "coding/reader.hpp"
|
||||
|
||||
#include "base/assert.hpp"
|
||||
#include "base/exception.hpp"
|
||||
#include "base/task_loop.hpp"
|
||||
#include "base/thread_pool_delayed.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
DECLARE_EXCEPTION(FileAbsentException, RootException);
|
||||
DECLARE_EXCEPTION(FileSystemException, RootException);
|
||||
|
||||
namespace platform
|
||||
{
|
||||
class LocalCountryFile;
|
||||
}
|
||||
|
||||
class Platform;
|
||||
|
||||
extern Platform & GetPlatform();
|
||||
|
||||
class Platform
|
||||
{
|
||||
public:
|
||||
friend class ThreadRunner;
|
||||
|
||||
// ThreadRunner may be subclassed for testing purposes.
|
||||
class ThreadRunner
|
||||
{
|
||||
public:
|
||||
ThreadRunner() { GetPlatform().RunThreads(); }
|
||||
virtual ~ThreadRunner() { GetPlatform().ShutdownThreads(); }
|
||||
};
|
||||
|
||||
enum EError
|
||||
{
|
||||
ERR_OK = 0,
|
||||
ERR_FILE_DOES_NOT_EXIST,
|
||||
ERR_ACCESS_FAILED,
|
||||
ERR_DIRECTORY_NOT_EMPTY,
|
||||
ERR_FILE_ALREADY_EXISTS,
|
||||
ERR_NAME_TOO_LONG,
|
||||
ERR_NOT_A_DIRECTORY,
|
||||
ERR_SYMLINK_LOOP,
|
||||
ERR_IO_ERROR,
|
||||
ERR_UNKNOWN
|
||||
};
|
||||
|
||||
enum EFileType
|
||||
{
|
||||
Unknown = 0x1,
|
||||
Regular = 0x2,
|
||||
Directory = 0x4
|
||||
};
|
||||
|
||||
enum class EConnectionType : uint8_t
|
||||
{
|
||||
CONNECTION_NONE,
|
||||
CONNECTION_WIFI,
|
||||
CONNECTION_WWAN
|
||||
};
|
||||
|
||||
enum class ChargingStatus : uint8_t
|
||||
{
|
||||
Unknown,
|
||||
Plugged,
|
||||
Unplugged
|
||||
};
|
||||
|
||||
enum class Thread : uint8_t
|
||||
{
|
||||
File,
|
||||
Network,
|
||||
Gui,
|
||||
Background,
|
||||
};
|
||||
|
||||
using TFilesWithType = std::vector<std::pair<std::string, EFileType>>;
|
||||
|
||||
protected:
|
||||
/// Usually read-only directory for application resources
|
||||
std::string m_resourcesDir;
|
||||
/// Writable directory to store downloaded map data
|
||||
/// @note on some systems it can point to external ejectable storage
|
||||
std::string m_writableDir;
|
||||
/// Temporary directory, can be cleaned up by the system
|
||||
std::string m_tmpDir;
|
||||
/// Writable directory to store persistent application data
|
||||
std::string m_settingsDir;
|
||||
|
||||
/// Used in Android only to get corret GUI elements layout.
|
||||
bool m_isTablet = false;
|
||||
|
||||
/// Returns last system call error as EError.
|
||||
static EError ErrnoToError();
|
||||
|
||||
/// Platform-dependent secure storage.
|
||||
platform::SecureStorage m_secureStorage;
|
||||
|
||||
std::unique_ptr<base::TaskLoop> m_guiThread;
|
||||
|
||||
std::unique_ptr<base::DelayedThreadPool> m_networkThread;
|
||||
std::unique_ptr<base::DelayedThreadPool> m_fileThread;
|
||||
std::unique_ptr<base::DelayedThreadPool> m_backgroundThread;
|
||||
|
||||
platform::BatteryLevelTracker m_batteryTracker;
|
||||
|
||||
public:
|
||||
Platform();
|
||||
virtual ~Platform() = default;
|
||||
|
||||
static bool IsFileExistsByFullPath(std::string const & filePath);
|
||||
static void DisableBackupForFile(std::string const & filePath);
|
||||
static bool RemoveFileIfExists(std::string const & filePath);
|
||||
|
||||
/// @returns path to current working directory.
|
||||
/// @note In case of an error returns an empty std::string.
|
||||
static std::string GetCurrentWorkingDirectory() noexcept;
|
||||
/// @return always the same writable dir for current user with slash at the end
|
||||
std::string const & WritableDir() const
|
||||
{
|
||||
ASSERT(!m_writableDir.empty(), ());
|
||||
return m_writableDir;
|
||||
}
|
||||
/// Set writable dir — use for testing and linux stuff only
|
||||
void SetWritableDirForTests(std::string const & path);
|
||||
/// @return full path to file in user's writable directory
|
||||
std::string WritablePathForFile(std::string const & file) const;
|
||||
/// Uses m_writeableDir [w], m_resourcesDir [r], m_settingsDir [s].
|
||||
std::string ReadPathForFile(std::string const & file, std::string searchScope = std::string()) const;
|
||||
|
||||
/// @return resource dir (on some platforms it's differ from Writable dir)
|
||||
std::string const & ResourcesDir() const
|
||||
{
|
||||
ASSERT(!m_resourcesDir.empty(), ());
|
||||
return m_resourcesDir;
|
||||
}
|
||||
/// @note! This function is used in generator_tool and unit tests.
|
||||
/// Client app should not replace default resource dir.
|
||||
void SetResourceDir(std::string const & path);
|
||||
|
||||
/// Creates the directory in the filesystem.
|
||||
[[nodiscard]] static EError MkDir(std::string const & dirName);
|
||||
|
||||
/// Creates the directory. Returns true on success.
|
||||
/// Returns false and logs the reason on failure.
|
||||
[[nodiscard]] static bool MkDirChecked(std::string const & dirName);
|
||||
|
||||
// Creates the directory path dirName.
|
||||
// The function creates all parent directories necessary to create the directory.
|
||||
// Returns true if successful; otherwise returns false.
|
||||
// If the path already exists when this function is called, it returns true.
|
||||
// If only some intermediate directories were created, the function returns false
|
||||
// and does not restore the previous state of the file system.
|
||||
[[nodiscard]] static bool MkDirRecursively(std::string const & dirName);
|
||||
|
||||
/// Removes empty directory from the filesystem.
|
||||
static EError RmDir(std::string const & dirName);
|
||||
|
||||
/// Removes directory from the filesystem.
|
||||
/// @note Directory can be non empty.
|
||||
/// @note If function fails, directory can be partially removed.
|
||||
static bool RmDirRecursively(std::string const & dirName);
|
||||
|
||||
/// @return path for directory with temporary files with slash at the end
|
||||
std::string const & TmpDir() const { return m_tmpDir; }
|
||||
/// @return full path to file in the temporary directory
|
||||
std::string TmpPathForFile(std::string const & file) const { return TmpDir() + file; }
|
||||
/// @return full random path to temporary file.
|
||||
std::string TmpPathForFile() const;
|
||||
/// @return full partially random path to temporary file.
|
||||
std::string TmpPathForFile(std::string const & prefix, std::string const & suffix) const;
|
||||
|
||||
/// @return full path to the file where data for unit tests is stored.
|
||||
std::string TestsDataPathForFile(std::string const & file) const { return ReadPathForFile(file); }
|
||||
|
||||
/// @return path for directory in the persistent memory, can be the same
|
||||
/// as WritableDir, but on some platforms it's different
|
||||
std::string const & SettingsDir() const { return m_settingsDir; }
|
||||
void SetSettingsDir(std::string const & path);
|
||||
/// @return full path to file in the settings directory
|
||||
std::string SettingsPathForFile(std::string const & file) const;
|
||||
|
||||
/// @return reader for file decriptor.
|
||||
/// @throws FileAbsentException
|
||||
/// @param[in] file name or full path which we want to read
|
||||
/// @param[in] searchScope looks for file in dirs in given order: \n
|
||||
/// [w]ritable, [r]esources, [s]ettings, by [f]ull path, [e]xternal resources,
|
||||
std::unique_ptr<ModelReader> GetReader(std::string const & file, std::string searchScope = std::string()) const;
|
||||
|
||||
/// @name File operations
|
||||
//@{
|
||||
using FilesList = std::vector<std::string>;
|
||||
/// Retrieves files list contained in given directory
|
||||
/// @param directory directory path with slash at the end
|
||||
//@{
|
||||
/// @param ext files extension to find, like ".mwm".
|
||||
static void GetFilesByExt(std::string const & directory, std::string_view ext, FilesList & outFiles);
|
||||
static void GetFilesByRegExp(std::string const & directory, boost::regex const & regexp, FilesList & outFiles);
|
||||
static void GetAllFiles(std::string const & directory, FilesList & outFiles);
|
||||
//@}
|
||||
|
||||
static void GetFilesByType(std::string const & directory, unsigned typeMask, TFilesWithType & outFiles);
|
||||
|
||||
static void GetFilesRecursively(std::string const & directory, FilesList & filesList);
|
||||
|
||||
static bool IsDirectoryEmpty(std::string const & directory);
|
||||
// Returns true if |path| refers to a directory. Returns false otherwise or on error.
|
||||
static bool IsDirectory(std::string const & path);
|
||||
|
||||
static EError GetFileType(std::string const & path, EFileType & type);
|
||||
|
||||
/// @return false if file is not exist
|
||||
/// @note Check files in Writable dir first, and in ReadDir if not exist in Writable dir
|
||||
bool GetFileSizeByName(std::string const & fileName, uint64_t & size) const;
|
||||
/// @return false if file is not exist
|
||||
/// @note Try do not use in client production code
|
||||
static bool GetFileSizeByFullPath(std::string const & filePath, uint64_t & size);
|
||||
//@}
|
||||
|
||||
/// @return 0 in case of failure.
|
||||
static time_t GetFileCreationTime(std::string const & path);
|
||||
/// @return 0 in case of failure.
|
||||
static time_t GetFileModificationTime(std::string const & path);
|
||||
|
||||
/// Used to check available free storage space for downloading.
|
||||
enum TStorageStatus
|
||||
{
|
||||
STORAGE_OK = 0,
|
||||
STORAGE_DISCONNECTED,
|
||||
NOT_ENOUGH_SPACE
|
||||
};
|
||||
TStorageStatus GetWritableStorageStatus(uint64_t neededSize) const;
|
||||
|
||||
// Please note, that number of active cores can vary at runtime.
|
||||
// DO NOT assume for the same return value between calls.
|
||||
static unsigned CpuCores();
|
||||
|
||||
void GetFontNames(FilesList & res) const;
|
||||
|
||||
// TODO: Optimize for each platform/device.
|
||||
int VideoMemoryLimit() const;
|
||||
// TODO: Optimize for each platform/device.
|
||||
int PreCachingDepth() const;
|
||||
|
||||
std::string DeviceName() const;
|
||||
|
||||
std::string DeviceModel() const;
|
||||
|
||||
/// @return string version as displayed to the user.
|
||||
std::string Version() const;
|
||||
|
||||
/// @return integer version in yyMMdd format.
|
||||
int32_t IntVersion() const;
|
||||
|
||||
/// @return url for clients to download maps
|
||||
std::string MetaServerUrl() const;
|
||||
|
||||
/// @return JSON-encoded list of urls if metaserver is unreachable
|
||||
std::string DefaultUrlsJSON() const;
|
||||
|
||||
bool IsTablet() const { return m_isTablet; }
|
||||
|
||||
/// @return information about kinds of memory which are relevant for a platform.
|
||||
/// This method is implemented for iOS and Android only.
|
||||
/// @TODO remove as its not used anywhere?
|
||||
std::string GetMemoryInfo() const;
|
||||
|
||||
static EConnectionType ConnectionStatus();
|
||||
static bool IsConnected() { return ConnectionStatus() != EConnectionType::CONNECTION_NONE; }
|
||||
|
||||
static ChargingStatus GetChargingStatus();
|
||||
|
||||
// Returns current battery level. Possible values are from 0 to 100.
|
||||
// Returns 100 when actual level is unknown.
|
||||
static uint8_t GetBatteryLevel();
|
||||
|
||||
void SetupMeasurementSystem() const;
|
||||
|
||||
platform::SecureStorage & GetSecureStorage() { return m_secureStorage; }
|
||||
|
||||
/// \brief Placing an executable object |task| on a queue of |thread|. Then the object will be
|
||||
/// executed on |thread|.
|
||||
/// \note |task| cannot be moved in case of |Thread::Gui|. This way unique_ptr cannot be used
|
||||
/// in |task|. Use shared_ptr instead.
|
||||
template <typename Task>
|
||||
base::TaskLoop::PushResult RunTask(Thread thread, Task && task)
|
||||
{
|
||||
ASSERT(m_networkThread && m_fileThread && m_backgroundThread, ());
|
||||
switch (thread)
|
||||
{
|
||||
case Thread::File: return m_fileThread->Push(std::forward<Task>(task));
|
||||
case Thread::Network: return m_networkThread->Push(std::forward<Task>(task));
|
||||
case Thread::Gui: return m_guiThread->Push(std::forward<Task>(task));
|
||||
case Thread::Background: return m_backgroundThread->Push(std::forward<Task>(task));
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
template <typename Task>
|
||||
base::TaskLoop::PushResult RunDelayedTask(Thread thread, base::DelayedThreadPool::Duration const & delay,
|
||||
Task && task)
|
||||
{
|
||||
ASSERT(m_networkThread && m_fileThread && m_backgroundThread, ());
|
||||
switch (thread)
|
||||
{
|
||||
case Thread::File: return m_fileThread->PushDelayed(delay, std::forward<Task>(task));
|
||||
case Thread::Network: return m_networkThread->PushDelayed(delay, std::forward<Task>(task));
|
||||
case Thread::Gui: CHECK(false, ("Delayed tasks for gui thread are not supported yet")); return {};
|
||||
case Thread::Background: return m_backgroundThread->PushDelayed(delay, std::forward<Task>(task));
|
||||
}
|
||||
UNREACHABLE();
|
||||
}
|
||||
|
||||
void CancelTask(Thread thread, base::TaskLoop::TaskId id);
|
||||
|
||||
// Use this method for testing purposes only.
|
||||
void SetGuiThread(std::unique_ptr<base::TaskLoop> guiThread);
|
||||
|
||||
platform::BatteryLevelTracker & GetBatteryTracker() { return m_batteryTracker; }
|
||||
|
||||
private:
|
||||
void RunThreads();
|
||||
void ShutdownThreads();
|
||||
|
||||
void GetSystemFontNames(FilesList & res) const;
|
||||
};
|
||||
|
||||
std::string DebugPrint(Platform::EError err);
|
||||
std::string DebugPrint(Platform::ChargingStatus status);
|
||||
273
libs/platform/platform_android.cpp
Normal file
273
libs/platform/platform_android.cpp
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
#include "platform/constants.hpp"
|
||||
#include "platform/measurement_utils.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/platform_unix_impl.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include "coding/zip_reader.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/string_utils.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h> // for sysconf
|
||||
|
||||
Platform::Platform()
|
||||
{
|
||||
/// @see initialization routine in android/sdk/src/main/cpp/com/.../Platform.hpp
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
namespace
|
||||
{
|
||||
class DbgLogger
|
||||
{
|
||||
public:
|
||||
explicit DbgLogger(std::string const & file) : m_file(file) {}
|
||||
|
||||
~DbgLogger() { LOG(LDEBUG, ("Source for file", m_file, "is", m_src)); }
|
||||
|
||||
void SetSource(char src) { m_src = src; }
|
||||
|
||||
private:
|
||||
std::string const & m_file;
|
||||
char m_src;
|
||||
};
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
std::unique_ptr<ModelReader> Platform::GetReader(std::string const & file, std::string searchScope) const
|
||||
{
|
||||
std::string ext = base::GetFileExtension(file);
|
||||
strings::AsciiToLower(ext);
|
||||
ASSERT(!ext.empty(), ());
|
||||
|
||||
uint32_t const logPageSize = (ext == DATA_FILE_EXTENSION) ? READER_CHUNK_LOG_SIZE : 10;
|
||||
uint32_t const logPageCount = (ext == DATA_FILE_EXTENSION) ? READER_CHUNK_LOG_COUNT : 4;
|
||||
|
||||
if (searchScope.empty())
|
||||
{
|
||||
if (file[0] == '/')
|
||||
searchScope = "f";
|
||||
else
|
||||
{
|
||||
ASSERT(ext != ".kml" && ext != ".kmb" && ext != ".kmz", ("BookmarkManager is responsible for that"));
|
||||
|
||||
if (ext == DATA_FILE_EXTENSION)
|
||||
if (file.starts_with(WORLD_COASTS_FILE_NAME) || file.starts_with(WORLD_FILE_NAME))
|
||||
searchScope = "wsr";
|
||||
else
|
||||
searchScope = "w";
|
||||
else if (file == SETTINGS_FILE_NAME)
|
||||
searchScope = "s";
|
||||
else
|
||||
searchScope = "rw";
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
DbgLogger logger(file);
|
||||
#endif
|
||||
|
||||
for (char const s : searchScope)
|
||||
{
|
||||
#ifdef DEBUG
|
||||
logger.SetSource(s);
|
||||
#endif
|
||||
|
||||
switch (s)
|
||||
{
|
||||
case 'w':
|
||||
{
|
||||
auto const path = base::JoinPath(m_writableDir, file);
|
||||
if (IsFileExistsByFullPath(path))
|
||||
return std::make_unique<FileReader>(path, logPageSize, logPageCount);
|
||||
break;
|
||||
}
|
||||
|
||||
case 's':
|
||||
{
|
||||
auto const path = base::JoinPath(m_settingsDir, file);
|
||||
if (IsFileExistsByFullPath(path))
|
||||
return std::make_unique<FileReader>(path, logPageSize, logPageCount);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'f':
|
||||
if (IsFileExistsByFullPath(file))
|
||||
return make_unique<FileReader>(file, logPageSize, logPageCount);
|
||||
break;
|
||||
|
||||
case 'r':
|
||||
ASSERT_EQUAL(file.find("assets/"), std::string::npos, ());
|
||||
try
|
||||
{
|
||||
return make_unique<ZipFileReader>(m_resourcesDir, "assets/" + file, logPageSize, logPageCount);
|
||||
}
|
||||
catch (Reader::OpenException const & e)
|
||||
{
|
||||
LOG(LWARNING, ("Can't get reader:", e.what()));
|
||||
}
|
||||
break;
|
||||
|
||||
default: CHECK(false, ("Unsupported source:", s)); break;
|
||||
}
|
||||
}
|
||||
|
||||
LOG(LWARNING, ("Can't get reader for:", file, "in scope", searchScope));
|
||||
MYTHROW(FileAbsentException, ("File not found", file));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Platform::GetFilesByRegExp(std::string const & directory, boost::regex const & regexp, FilesList & res)
|
||||
{
|
||||
if (ZipFileReader::IsZip(directory))
|
||||
{
|
||||
// Get files list inside zip file
|
||||
typedef ZipFileReader::FileList FilesT;
|
||||
FilesT fList;
|
||||
ZipFileReader::FilesList(directory, fList);
|
||||
|
||||
for (FilesT::iterator it = fList.begin(); it != fList.end(); ++it)
|
||||
{
|
||||
std::string & name = it->first;
|
||||
if (boost::regex_search(name.begin(), name.end(), regexp))
|
||||
{
|
||||
// Remove assets/ prefix - clean files are needed for fonts white/blacklisting logic
|
||||
size_t const ASSETS_LENGTH = 7;
|
||||
if (name.find("assets/") == 0)
|
||||
name.erase(0, ASSETS_LENGTH);
|
||||
|
||||
res.push_back(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
pl::EnumerateFilesByRegExp(directory, regexp, res);
|
||||
}
|
||||
|
||||
void Platform::GetAllFiles(std::string const & directory, FilesList & res)
|
||||
{
|
||||
if (ZipFileReader::IsZip(directory))
|
||||
{
|
||||
// Get files list inside zip file
|
||||
typedef ZipFileReader::FileList FilesT;
|
||||
FilesT fList;
|
||||
ZipFileReader::FilesList(directory, fList);
|
||||
|
||||
for (FilesT::iterator it = fList.begin(); it != fList.end(); ++it)
|
||||
{
|
||||
std::string & name = it->first;
|
||||
// Remove assets/ prefix - clean files are needed for fonts white/blacklisting logic
|
||||
size_t const ASSETS_LENGTH = 7;
|
||||
if (name.find("assets/") == 0)
|
||||
name.erase(0, ASSETS_LENGTH);
|
||||
|
||||
res.push_back(name);
|
||||
}
|
||||
}
|
||||
else
|
||||
pl::EnumerateFiles(directory, res);
|
||||
}
|
||||
|
||||
int Platform::VideoMemoryLimit() const
|
||||
{
|
||||
return 10 * 1024 * 1024;
|
||||
}
|
||||
|
||||
int Platform::PreCachingDepth() const
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
bool Platform::GetFileSizeByName(std::string const & fileName, uint64_t & size) const
|
||||
{
|
||||
try
|
||||
{
|
||||
size = ReaderPtr<Reader>(GetReader(fileName)).Size();
|
||||
return true;
|
||||
}
|
||||
catch (RootException const & ex)
|
||||
{
|
||||
LOG(LWARNING, ("Can't get file size for:", fileName));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
Platform::EError Platform::MkDir(std::string const & dirName)
|
||||
{
|
||||
if (0 != mkdir(dirName.c_str(), 0755))
|
||||
return ErrnoToError();
|
||||
return Platform::ERR_OK;
|
||||
}
|
||||
|
||||
void Platform::SetupMeasurementSystem() const
|
||||
{
|
||||
auto units = measurement_utils::Units::Metric;
|
||||
if (settings::Get(settings::kMeasurementUnits, units))
|
||||
return;
|
||||
// @TODO Add correct implementation
|
||||
units = measurement_utils::Units::Metric;
|
||||
settings::Set(settings::kMeasurementUnits, units);
|
||||
}
|
||||
|
||||
void Platform::GetSystemFontNames(FilesList & res) const
|
||||
{
|
||||
bool wasRoboto = false;
|
||||
|
||||
std::string const path = "/system/fonts/";
|
||||
pl::EnumerateFiles(path, [&](char const * entry)
|
||||
{
|
||||
std::string name(entry);
|
||||
if (name != "Roboto-Medium.ttf" && name != "Roboto-Regular.ttf")
|
||||
{
|
||||
if (!name.starts_with("NotoNaskh") && !name.starts_with("NotoSans"))
|
||||
return;
|
||||
|
||||
if (name.find("-Regular") == std::string::npos)
|
||||
return;
|
||||
}
|
||||
else
|
||||
wasRoboto = true;
|
||||
|
||||
res.push_back(path + name);
|
||||
});
|
||||
|
||||
if (!wasRoboto)
|
||||
{
|
||||
std::string droidSans = path + "DroidSans.ttf";
|
||||
if (IsFileExistsByFullPath(droidSans))
|
||||
res.push_back(std::move(droidSans));
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
time_t Platform::GetFileCreationTime(std::string const & path)
|
||||
{
|
||||
struct stat st;
|
||||
if (0 == stat(path.c_str(), &st))
|
||||
return st.st_atim.tv_sec;
|
||||
|
||||
LOG(LERROR, ("GetFileCreationTime stat failed for", path, "with error", strerror(errno)));
|
||||
// TODO(AB): Refactor to return std::optional<time_t>.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// static
|
||||
time_t Platform::GetFileModificationTime(std::string const & path)
|
||||
{
|
||||
struct stat st;
|
||||
if (0 == stat(path.c_str(), &st))
|
||||
return st.st_mtim.tv_sec;
|
||||
|
||||
LOG(LERROR, ("GetFileModificationTime stat failed for", path, "with error", strerror(errno)));
|
||||
// TODO(AB): Refactor to return std::optional<time_t>.
|
||||
return 0;
|
||||
}
|
||||
85
libs/platform/platform_ios.h
Normal file
85
libs/platform/platform_ios.h
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#pragma once
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
NSDictionary<NSString *, NSString *> * const kDeviceModelsBeforeMetalDriver = @{
|
||||
@"i386" : @"Simulator",
|
||||
@"iPad1,1" : @"iPad WiFi",
|
||||
@"iPad1,2" : @"iPad GSM",
|
||||
@"iPad2,1" : @"iPad 2 WiFi",
|
||||
@"iPad2,2" : @"iPad 2 GSM",
|
||||
@"iPad2,3" : @"iPad 2 GSM EV-DO",
|
||||
@"iPad2,4" : @"iPad 2",
|
||||
@"iPad2,5" : @"iPad Mini WiFi",
|
||||
@"iPad2,6" : @"iPad Mini GSM",
|
||||
@"iPad2,7" : @"iPad Mini CDMA",
|
||||
@"iPad3,1" : @"iPad 3rd gen. WiFi",
|
||||
@"iPad3,2" : @"iPad 3rd gen. GSM",
|
||||
@"iPad3,3" : @"iPad 3rd gen. CDMA",
|
||||
@"iPad3,4" : @"iPad 4th gen. WiFi",
|
||||
@"iPad3,5" : @"iPad 4th gen. GSM",
|
||||
@"iPad3,6" : @"iPad 4th gen. CDMA",
|
||||
@"iPad4,1" : @"iPad Air WiFi",
|
||||
@"iPad4,2" : @"iPad Air GSM",
|
||||
@"iPad4,3" : @"iPad Air CDMA",
|
||||
@"iPad4,4" : @"iPad Mini 2nd gen. WiFi",
|
||||
@"iPad4,5" : @"iPad Mini 2nd gen. GSM",
|
||||
@"iPad4,6" : @"iPad Mini 2nd gen. CDMA",
|
||||
@"iPad5,3" : @"iPad Air 2 WiFi",
|
||||
@"iPad5,4" : @"iPad Air 2 GSM",
|
||||
@"iPhone1,1" : @"iPhone",
|
||||
@"iPhone1,2" : @"iPhone 3G",
|
||||
@"iPhone2,1" : @"iPhone 3GS",
|
||||
@"iPhone3,1" : @"iPhone 4 GSM",
|
||||
@"iPhone3,2" : @"iPhone 4 CDMA",
|
||||
@"iPhone3,3" : @"iPhone 4 GSM EV-DO",
|
||||
@"iPhone4,1" : @"iPhone 4S",
|
||||
@"iPhone4,2" : @"iPhone 4S",
|
||||
@"iPhone4,3" : @"iPhone 4S",
|
||||
@"iPhone5,1" : @"iPhone 5",
|
||||
@"iPhone5,2" : @"iPhone 5",
|
||||
@"iPhone5,3" : @"iPhone 5c",
|
||||
@"iPhone5,4" : @"iPhone 5c",
|
||||
@"iPhone6,1" : @"iPhone 5s",
|
||||
@"iPhone6,2" : @"iPhone 5s",
|
||||
@"iPhone7,1" : @"iPhone 6 Plus",
|
||||
@"iPhone7,2" : @"iPhone 6",
|
||||
@"iPod1,1" : @"iPod Touch",
|
||||
@"iPod2,1" : @"iPod Touch 2nd gen.",
|
||||
@"iPod3,1" : @"iPod Touch 3rd gen.",
|
||||
@"iPod4,1" : @"iPod Touch 4th gen.",
|
||||
@"iPod5,1" : @"iPod Touch 5th gen.",
|
||||
@"x86_64" : @"Simulator"
|
||||
};
|
||||
NSDictionary<NSString *, NSString *> * const kDeviceModelsWithiOS10MetalDriver = @{
|
||||
@"iPad6,3" : @"iPad Pro (9.7 inch) WiFi",
|
||||
@"iPad6,4" : @"iPad Pro (9.7 inch) GSM",
|
||||
@"iPad6,7" : @"iPad Pro (12.9 inch) WiFi",
|
||||
@"iPad6,8" : @"iPad Pro (12.9 inch) GSM",
|
||||
@"iPhone8,1" : @"iPhone 6s",
|
||||
@"iPhone8,2" : @"iPhone 6s Plus",
|
||||
@"iPhone8,4" : @"iPhone SE"
|
||||
};
|
||||
NSDictionary<NSString *, NSString *> * const kDeviceModelsWithMetalDriver = @{
|
||||
@"iPad6,11" : @"iPad 5th gen. WiFi",
|
||||
@"iPad6,12" : @"iPad 5th gen. GSM",
|
||||
@"iPad7,1" : @"iPad Pro (12.9 inch) 2nd gen. WiFi",
|
||||
@"iPad7,2" : @"iPad Pro (12.9 inch) 2nd gen. GSM",
|
||||
@"iPad7,3" : @"iPad Pro (10.5-inch) WiFi",
|
||||
@"iPad7,4" : @"iPad Pro (10.5-inch) GSM",
|
||||
@"iPhone9,1" : @"iPhone 7",
|
||||
@"iPhone9,3" : @"iPhone 7",
|
||||
@"iPhone9,2" : @"iPhone 7 Plus",
|
||||
@"iPhone9,4" : @"iPhone 7 Plus",
|
||||
@"iPhone10,1" : @"iPhone 8",
|
||||
@"iPhone10,2" : @"iPhone 8 Plus",
|
||||
@"iPhone10,3" : @"iPhone X",
|
||||
@"iPhone10,4" : @"iPhone 8",
|
||||
@"iPhone10,5" : @"iPhone 8 Plus",
|
||||
@"iPhone10,6" : @"iPhone X",
|
||||
};
|
||||
} // platform
|
||||
282
libs/platform/platform_ios.mm
Normal file
282
libs/platform/platform_ios.mm
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
#include "platform/platform_ios.h"
|
||||
#include "platform/constants.hpp"
|
||||
#include "platform/gui_thread.hpp"
|
||||
#include "platform/measurement_utils.hpp"
|
||||
#include "platform/platform_unix_impl.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include "coding/file_reader.hpp"
|
||||
|
||||
#include "std/target_os.hpp"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include <ifaddrs.h>
|
||||
|
||||
#include <mach/mach.h>
|
||||
|
||||
#include <net/if.h>
|
||||
#include <net/if_dl.h>
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/xattr.h>
|
||||
|
||||
#import <CoreFoundation/CFURL.h>
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <netinet/in.h>
|
||||
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
Platform::Platform()
|
||||
{
|
||||
m_isTablet = (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad);
|
||||
|
||||
NSBundle * bundle = NSBundle.mainBundle;
|
||||
NSString * path = [bundle resourcePath];
|
||||
m_resourcesDir = path.UTF8String;
|
||||
m_resourcesDir += "/";
|
||||
|
||||
NSArray * dirPaths =
|
||||
NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString * docsDir = dirPaths.firstObject;
|
||||
m_writableDir = docsDir.UTF8String;
|
||||
m_writableDir += "/";
|
||||
m_settingsDir = m_writableDir;
|
||||
|
||||
NSString * tmpDir = NSTemporaryDirectory();
|
||||
if (tmpDir)
|
||||
m_tmpDir = tmpDir.UTF8String;
|
||||
else
|
||||
{
|
||||
m_tmpDir = NSHomeDirectory().UTF8String;
|
||||
m_tmpDir += "/tmp/";
|
||||
}
|
||||
|
||||
m_guiThread = std::make_unique<platform::GuiThread>();
|
||||
|
||||
UIDevice * device = UIDevice.currentDevice;
|
||||
device.batteryMonitoringEnabled = YES;
|
||||
|
||||
LOG(LINFO, ("Device:", device.model.UTF8String, "SystemName:",
|
||||
device.systemName.UTF8String, "SystemVersion:",
|
||||
device.systemVersion.UTF8String));
|
||||
}
|
||||
|
||||
//static
|
||||
void Platform::DisableBackupForFile(std::string const & filePath)
|
||||
{
|
||||
// We need to disable iCloud backup for downloaded files.
|
||||
// This is the reason for rejecting from the AppStore
|
||||
// https://developer.apple.com/library/iOS/qa/qa1719/_index.html
|
||||
CFURLRef url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault,
|
||||
reinterpret_cast<unsigned char const *>(filePath.c_str()),
|
||||
filePath.size(),
|
||||
0);
|
||||
CFErrorRef err;
|
||||
BOOL valueRaw = YES;
|
||||
CFNumberRef value = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, &valueRaw);
|
||||
if (!CFURLSetResourcePropertyForKey(url, kCFURLIsExcludedFromBackupKey, value, &err))
|
||||
LOG(LERROR, ("Error:", err, "while disabling iCloud backup for file:", filePath.c_str()));
|
||||
|
||||
CFRelease(value);
|
||||
CFRelease(url);
|
||||
}
|
||||
|
||||
// static
|
||||
Platform::EError Platform::MkDir(std::string const & dirName)
|
||||
{
|
||||
if (::mkdir(dirName.c_str(), 0755))
|
||||
return ErrnoToError();
|
||||
return Platform::ERR_OK;
|
||||
}
|
||||
|
||||
void Platform::GetFilesByRegExp(std::string const & directory, boost::regex const & regexp, FilesList & res)
|
||||
{
|
||||
pl::EnumerateFilesByRegExp(directory, regexp, res);
|
||||
}
|
||||
|
||||
void Platform::GetAllFiles(std::string const & directory, FilesList & res)
|
||||
{
|
||||
pl::EnumerateFiles(directory, res);
|
||||
}
|
||||
|
||||
bool Platform::GetFileSizeByName(std::string const & fileName, uint64_t & size) const
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetFileSizeByFullPath(ReadPathForFile(fileName), size);
|
||||
}
|
||||
catch (RootException const &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<ModelReader> Platform::GetReader(std::string const & file, std::string searchScope) const
|
||||
{
|
||||
return std::make_unique<FileReader>(ReadPathForFile(file, std::move(searchScope)), READER_CHUNK_LOG_SIZE,
|
||||
READER_CHUNK_LOG_COUNT);
|
||||
}
|
||||
|
||||
int Platform::VideoMemoryLimit() const { return 8 * 1024 * 1024; }
|
||||
|
||||
int Platform::PreCachingDepth() const { return 2; }
|
||||
|
||||
std::string Platform::GetMemoryInfo() const
|
||||
{
|
||||
struct task_basic_info info;
|
||||
mach_msg_type_number_t size = sizeof(info);
|
||||
kern_return_t const kerr =
|
||||
task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size);
|
||||
std::stringstream ss;
|
||||
if (kerr == KERN_SUCCESS)
|
||||
{
|
||||
ss << "Memory info: Resident_size = " << info.resident_size / 1024
|
||||
<< "KB; virtual_size = " << info.resident_size / 1024
|
||||
<< "KB; suspend_count = " << info.suspend_count << " policy = " << info.policy;
|
||||
}
|
||||
else
|
||||
{
|
||||
ss << "Error with task_info(): " << mach_error_string(kerr);
|
||||
}
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
std::string Platform::DeviceName() const { return UIDevice.currentDevice.name.UTF8String; }
|
||||
|
||||
std::string Platform::DeviceModel() const
|
||||
{
|
||||
utsname systemInfo;
|
||||
uname(&systemInfo);
|
||||
NSString * deviceModel = @(systemInfo.machine);
|
||||
if (auto m = platform::kDeviceModelsBeforeMetalDriver[deviceModel])
|
||||
deviceModel = m;
|
||||
else if (auto m = platform::kDeviceModelsWithiOS10MetalDriver[deviceModel])
|
||||
deviceModel = m;
|
||||
else if (auto m = platform::kDeviceModelsWithMetalDriver[deviceModel])
|
||||
deviceModel = m;
|
||||
return deviceModel.UTF8String;
|
||||
}
|
||||
|
||||
std::string Platform::Version() const
|
||||
{
|
||||
/// @note Do not change version format, it is parsed on server side.
|
||||
NSBundle * mainBundle = [NSBundle mainBundle];
|
||||
NSString * version = [mainBundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
||||
NSString * build = [mainBundle objectForInfoDictionaryKey:@"CFBundleVersion"];
|
||||
return std::string{version.UTF8String} + '-' + build.UTF8String + '-' + OMIM_OS_NAME;
|
||||
}
|
||||
|
||||
int32_t Platform::IntVersion() const
|
||||
{
|
||||
NSString * version = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
|
||||
int year = 0;
|
||||
int month = 0;
|
||||
int day = 0;
|
||||
int rc = sscanf(version.UTF8String, "%d.%d.%d", &year, &month, &day);
|
||||
CHECK_EQUAL(rc, 3, ("Failed to parse version"));
|
||||
CHECK(year > 2000 && year < 3000, ("Invalid year"));
|
||||
CHECK(month > 0 && month <= 12, ("Invalid month"));
|
||||
CHECK(day > 0 && day <= 31, ("Invalid day"));
|
||||
return (int32_t)(year - 2000) * 10000 + month * 100 + day;
|
||||
}
|
||||
|
||||
Platform::EConnectionType Platform::ConnectionStatus()
|
||||
{
|
||||
struct sockaddr_in zero;
|
||||
bzero(&zero, sizeof(zero));
|
||||
zero.sin_len = sizeof(zero);
|
||||
zero.sin_family = AF_INET;
|
||||
SCNetworkReachabilityRef reachability =
|
||||
SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)&zero);
|
||||
if (!reachability)
|
||||
return EConnectionType::CONNECTION_NONE;
|
||||
SCNetworkReachabilityFlags flags;
|
||||
bool const gotFlags = SCNetworkReachabilityGetFlags(reachability, &flags);
|
||||
CFRelease(reachability);
|
||||
if (!gotFlags || ((flags & kSCNetworkReachabilityFlagsReachable) == 0))
|
||||
return EConnectionType::CONNECTION_NONE;
|
||||
SCNetworkReachabilityFlags userActionRequired = kSCNetworkReachabilityFlagsConnectionRequired |
|
||||
kSCNetworkReachabilityFlagsInterventionRequired;
|
||||
if ((flags & userActionRequired) == userActionRequired)
|
||||
return EConnectionType::CONNECTION_NONE;
|
||||
if ((flags & kSCNetworkReachabilityFlagsIsWWAN) == kSCNetworkReachabilityFlagsIsWWAN)
|
||||
return EConnectionType::CONNECTION_WWAN;
|
||||
else
|
||||
return EConnectionType::CONNECTION_WIFI;
|
||||
}
|
||||
|
||||
Platform::ChargingStatus Platform::GetChargingStatus()
|
||||
{
|
||||
switch (UIDevice.currentDevice.batteryState)
|
||||
{
|
||||
case UIDeviceBatteryStateUnknown: return Platform::ChargingStatus::Unknown;
|
||||
case UIDeviceBatteryStateUnplugged: return Platform::ChargingStatus::Unplugged;
|
||||
case UIDeviceBatteryStateCharging:
|
||||
case UIDeviceBatteryStateFull: return Platform::ChargingStatus::Plugged;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t Platform::GetBatteryLevel()
|
||||
{
|
||||
auto const level = UIDevice.currentDevice.batteryLevel;
|
||||
|
||||
ASSERT_GREATER_OR_EQUAL(level, -1.0, ());
|
||||
ASSERT_LESS_OR_EQUAL(level, 1.0, ());
|
||||
|
||||
if (level == -1.0)
|
||||
return 100;
|
||||
|
||||
auto const result = static_cast<uint8_t>(level * 100);
|
||||
|
||||
CHECK_LESS_OR_EQUAL(result, 100, ());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Platform::SetupMeasurementSystem() const
|
||||
{
|
||||
auto units = measurement_utils::Units::Metric;
|
||||
if (settings::Get(settings::kMeasurementUnits, units))
|
||||
return;
|
||||
BOOL const isMetric =
|
||||
[[[NSLocale autoupdatingCurrentLocale] objectForKey:NSLocaleUsesMetricSystem] boolValue];
|
||||
units = isMetric ? measurement_utils::Units::Metric : measurement_utils::Units::Imperial;
|
||||
settings::Set(settings::kMeasurementUnits, units);
|
||||
}
|
||||
|
||||
void Platform::GetSystemFontNames(FilesList & res) const
|
||||
{
|
||||
}
|
||||
|
||||
// static
|
||||
time_t Platform::GetFileCreationTime(std::string const & path)
|
||||
{
|
||||
struct stat st;
|
||||
if (0 == stat(path.c_str(), &st))
|
||||
return st.st_birthtimespec.tv_sec;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// static
|
||||
time_t Platform::GetFileModificationTime(std::string const & path)
|
||||
{
|
||||
struct stat st;
|
||||
if (0 == stat(path.c_str(), &st))
|
||||
return st.st_mtimespec.tv_sec;
|
||||
return 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
extern Platform & GetPlatform()
|
||||
{
|
||||
static Platform platform;
|
||||
return platform;
|
||||
}
|
||||
300
libs/platform/platform_linux.cpp
Normal file
300
libs/platform/platform_linux.cpp
Normal file
|
|
@ -0,0 +1,300 @@
|
|||
#include "platform/platform.hpp"
|
||||
#include "private.h"
|
||||
|
||||
#include "platform/socket.hpp"
|
||||
|
||||
#include "coding/file_reader.hpp"
|
||||
|
||||
#include "base/exception.hpp"
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/macros.hpp"
|
||||
#include "base/scope_guard.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional> // bind
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h> // strrchr
|
||||
#include <unistd.h> // access, readlink
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <linux/limits.h> // PATH_MAX
|
||||
#include <netinet/in.h>
|
||||
|
||||
#include <QStandardPaths> // writableLocation GenericConfigLocation
|
||||
|
||||
namespace
|
||||
{
|
||||
// Returns directory where binary resides, including slash at the end.
|
||||
std::optional<std::string> GetExecutableDir()
|
||||
{
|
||||
char path[PATH_MAX] = {};
|
||||
if (::readlink("/proc/self/exe", path, ARRAY_SIZE(path)) <= 0)
|
||||
return {};
|
||||
*(strrchr(path, '/') + 1) = '\0';
|
||||
return path;
|
||||
}
|
||||
|
||||
// Returns true if EULA file exists in a directory.
|
||||
bool IsWelcomeExist(std::string const & dir)
|
||||
{
|
||||
return Platform::IsFileExistsByFullPath(base::JoinPath(dir, "welcome.html"));
|
||||
}
|
||||
|
||||
// Returns string value of an environment variable.
|
||||
std::optional<std::string> GetEnv(char const * var)
|
||||
{
|
||||
char const * value = ::getenv(var);
|
||||
if (value == nullptr)
|
||||
return {};
|
||||
return value;
|
||||
}
|
||||
|
||||
bool IsDirWritable(std::string const & dir)
|
||||
{
|
||||
return ::access(dir.c_str(), W_OK) == 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace platform
|
||||
{
|
||||
std::unique_ptr<Socket> CreateSocket()
|
||||
{
|
||||
return std::unique_ptr<Socket>();
|
||||
}
|
||||
} // namespace platform
|
||||
|
||||
Platform::Platform()
|
||||
{
|
||||
using base::JoinPath;
|
||||
// Current executable's path with a trailing slash.
|
||||
auto const execDir = GetExecutableDir();
|
||||
CHECK(execDir, ("Can't retrieve the path to executable"));
|
||||
// Home directory without a trailing slash.
|
||||
auto const homeDir = GetEnv("HOME");
|
||||
CHECK(homeDir, ("Can't retrieve home directory"));
|
||||
|
||||
// XDG config directory, usually ~/.config/CoMaps/
|
||||
m_settingsDir =
|
||||
JoinPath(QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation).toStdString(), "CoMaps");
|
||||
if (!IsFileExistsByFullPath(JoinPath(m_settingsDir, SETTINGS_FILE_NAME)) && !MkDirRecursively(m_settingsDir))
|
||||
MYTHROW(FileSystemException, ("Can't create directory", m_settingsDir));
|
||||
m_settingsDir += '/';
|
||||
|
||||
// Override dirs from the env.
|
||||
if (auto const dir = GetEnv("MWM_WRITABLE_DIR"))
|
||||
m_writableDir = *dir;
|
||||
|
||||
if (auto const dir = GetEnv("MWM_RESOURCES_DIR"))
|
||||
m_resourcesDir = *dir;
|
||||
else
|
||||
{ // Guess the existing resources directory.
|
||||
std::string const dirsToScan[] = {
|
||||
"./data", // symlink in the current folder
|
||||
"../data", // 'build' folder inside the repo
|
||||
JoinPath(*execDir, "..", "comaps", "data"), // build-omim-{debug,release}
|
||||
JoinPath(*execDir, "..", "share"), // installed version with packages
|
||||
JoinPath(*execDir, "..", "CoMaps"), // installed version without packages
|
||||
JoinPath(*execDir, "..", "share", "comaps", "data"), // flatpak-build
|
||||
};
|
||||
for (auto const & dir : dirsToScan)
|
||||
{
|
||||
if (IsWelcomeExist(dir))
|
||||
{
|
||||
m_resourcesDir = dir;
|
||||
if (m_writableDir.empty() && IsDirWritable(dir))
|
||||
m_writableDir = m_resourcesDir;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use ~/.local/share/CoMaps if resources directory was not writable.
|
||||
if (!m_resourcesDir.empty() && m_writableDir.empty())
|
||||
{
|
||||
// The writableLocation does the same for AppDataLocation, AppLocalDataLocation,
|
||||
// and GenericDataLocation. Provided, that test mode is not enabled, then
|
||||
// first it checks ${XDG_DATA_HOME}, if empty then it falls back to ${HOME}/.local/share
|
||||
m_writableDir = JoinPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation).toStdString(), "CoMaps");
|
||||
|
||||
if (!MkDirRecursively(m_writableDir))
|
||||
MYTHROW(FileSystemException, ("Can't create writable directory:", m_writableDir));
|
||||
}
|
||||
// Here one or both m_resourcesDir and m_writableDir still may be empty.
|
||||
// Tests or binary may initialize them later.
|
||||
using base::AddSlashIfNeeded;
|
||||
if (!m_writableDir.empty())
|
||||
m_writableDir = AddSlashIfNeeded(m_writableDir);
|
||||
if (!m_resourcesDir.empty())
|
||||
m_resourcesDir = AddSlashIfNeeded(m_resourcesDir);
|
||||
|
||||
// Select directory for temporary files.
|
||||
for (auto const & dir : {GetEnv("TMPDIR"), GetEnv("TMP"), GetEnv("TEMP"), {"/tmp"}})
|
||||
{
|
||||
if (dir && IsFileExistsByFullPath(*dir) && IsDirWritable(*dir))
|
||||
{
|
||||
m_tmpDir = AddSlashIfNeeded(*dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_guiThread = std::make_unique<platform::GuiThread>();
|
||||
|
||||
LOG(LDEBUG, ("Resources directory:", m_resourcesDir));
|
||||
LOG(LDEBUG, ("Writable directory:", m_writableDir));
|
||||
LOG(LDEBUG, ("Tmp directory:", m_tmpDir));
|
||||
LOG(LDEBUG, ("Settings directory:", m_settingsDir));
|
||||
}
|
||||
|
||||
std::string Platform::DeviceName() const
|
||||
{
|
||||
return OMIM_OS_NAME;
|
||||
}
|
||||
|
||||
std::string Platform::DeviceModel() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
Platform::EConnectionType Platform::ConnectionStatus()
|
||||
{
|
||||
int socketFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||
SCOPE_GUARD(closeSocket, std::bind(&close, socketFd));
|
||||
if (socketFd < 0)
|
||||
return EConnectionType::CONNECTION_NONE;
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(80);
|
||||
inet_pton(AF_INET, DEFAULT_CONNECTION_CHECK_IP, &addr.sin_addr);
|
||||
|
||||
if (connect(socketFd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) < 0)
|
||||
return EConnectionType::CONNECTION_NONE;
|
||||
|
||||
return EConnectionType::CONNECTION_WIFI;
|
||||
}
|
||||
|
||||
Platform::ChargingStatus Platform::GetChargingStatus()
|
||||
{
|
||||
return Platform::ChargingStatus::Plugged;
|
||||
}
|
||||
|
||||
uint8_t Platform::GetBatteryLevel()
|
||||
{
|
||||
// This value is always 100 for desktop.
|
||||
return 100;
|
||||
}
|
||||
|
||||
void Platform::GetSystemFontNames(FilesList & res) const
|
||||
{
|
||||
char constexpr const * const fontsWhitelist[] = {
|
||||
"Roboto-Medium.ttf",
|
||||
"Roboto-Regular.ttf",
|
||||
"DroidSansFallback.ttf",
|
||||
"DroidSansFallbackFull.ttf",
|
||||
"DroidSans.ttf",
|
||||
"DroidSansArabic.ttf",
|
||||
"DroidSansSemc.ttf",
|
||||
"DroidSansSemcCJK.ttf",
|
||||
"DroidNaskh-Regular.ttf",
|
||||
"Lohit-Bengali.ttf",
|
||||
"Lohit-Devanagari.ttf",
|
||||
"Lohit-Tamil.ttf",
|
||||
"PakType Naqsh.ttf",
|
||||
"wqy-microhei.ttc",
|
||||
"Jomolhari.ttf",
|
||||
"Padauk.ttf",
|
||||
"KhmerOS.ttf",
|
||||
"Umpush.ttf",
|
||||
"DroidSansThai.ttf",
|
||||
"DroidSansArmenian.ttf",
|
||||
"DroidSansEthiopic-Regular.ttf",
|
||||
"DroidSansGeorgian.ttf",
|
||||
"DroidSansHebrew-Regular.ttf",
|
||||
"DroidSansHebrew.ttf",
|
||||
"DroidSansJapanese.ttf",
|
||||
"LTe50872.ttf",
|
||||
"LTe50259.ttf",
|
||||
"DevanagariOTS.ttf",
|
||||
"FreeSans.ttf",
|
||||
"DejaVuSans.ttf",
|
||||
"arial.ttf",
|
||||
"AbyssinicaSIL-R.ttf",
|
||||
};
|
||||
|
||||
std::string const systemFontsPath[] = {
|
||||
"/usr/share/fonts/truetype/roboto/",
|
||||
"/usr/share/fonts/truetype/droid/",
|
||||
"/usr/share/fonts/truetype/dejavu/",
|
||||
"/usr/share/fonts/truetype/ttf-dejavu/",
|
||||
"/usr/share/fonts/truetype/wqy/",
|
||||
"/usr/share/fonts/truetype/freefont/",
|
||||
"/usr/share/fonts/truetype/padauk/",
|
||||
"/usr/share/fonts/truetype/dzongkha/",
|
||||
"/usr/share/fonts/truetype/ttf-khmeros-core/",
|
||||
"/usr/share/fonts/truetype/tlwg/",
|
||||
"/usr/share/fonts/truetype/abyssinica/",
|
||||
"/usr/share/fonts/truetype/paktype/",
|
||||
};
|
||||
|
||||
for (auto font : fontsWhitelist)
|
||||
{
|
||||
for (auto sysPath : systemFontsPath)
|
||||
{
|
||||
std::string path = sysPath + font;
|
||||
if (IsFileExistsByFullPath(path))
|
||||
res.push_back(std::move(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
time_t Platform::GetFileCreationTime(std::string const & path)
|
||||
{
|
||||
// In older Linux versions there is no reliable way to get file creation time with GLIBC.
|
||||
// Musl supports statx since 2018.
|
||||
#if !defined(__GLIBC__) || (__GLIBC__ >= 2 && __GLIBC_MINOR__ >= 28)
|
||||
struct statx st;
|
||||
if (0 == statx(AT_FDCWD, path.c_str(), 0, STATX_BTIME, &st))
|
||||
{
|
||||
// Orbstack on MacOS returns zero birth time, see https://github.com/orbstack/orbstack/issues/2064
|
||||
if (st.stx_btime.tv_sec != 0)
|
||||
return st.stx_btime.tv_sec;
|
||||
|
||||
LOG(LWARNING, ("statx returned zero birth time for", path,
|
||||
", using the earliest time from access, modification or status change instead."));
|
||||
return std::min(st.stx_atime.tv_sec, std::min(st.stx_mtime.tv_sec, st.stx_ctime.tv_sec));
|
||||
}
|
||||
|
||||
LOG(LERROR, ("GetFileCreationTime statx failed for", path, "with error", strerror(errno)));
|
||||
#else
|
||||
struct stat st;
|
||||
if (0 == stat(path.c_str(), &st))
|
||||
return std::min(st.st_atim.tv_sec, st.st_mtim.tv_sec);
|
||||
|
||||
LOG(LERROR, ("GetFileCreationTime stat failed for", path, "with error", strerror(errno)));
|
||||
#endif
|
||||
// TODO(AB): Refactor to return std::optional<time_t>.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// static
|
||||
time_t Platform::GetFileModificationTime(std::string const & path)
|
||||
{
|
||||
struct stat st;
|
||||
if (0 == stat(path.c_str(), &st))
|
||||
return st.st_mtim.tv_sec;
|
||||
|
||||
LOG(LERROR, ("GetFileModificationTime stat failed for", path, "with error", strerror(errno)));
|
||||
// TODO(AB): Refactor to return std::optional<time_t>.
|
||||
return 0;
|
||||
}
|
||||
209
libs/platform/platform_mac.mm
Normal file
209
libs/platform/platform_mac.mm
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
#include "platform/platform.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include "std/target_os.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <Foundation/NSAutoreleasePool.h>
|
||||
#include <Foundation/NSBundle.h>
|
||||
#include <Foundation/NSFileManager.h>
|
||||
#include <Foundation/NSPathUtilities.h>
|
||||
#include <IOKit/IOKitLib.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <sys/sysctl.h>
|
||||
|
||||
#include <dispatch/dispatch.h>
|
||||
|
||||
#import <SystemConfiguration/SystemConfiguration.h>
|
||||
#import <netinet/in.h>
|
||||
|
||||
|
||||
Platform::Platform()
|
||||
{
|
||||
// CoMaps.app/Content/Resources or omim-build-debug for tests.
|
||||
std::string const resourcesPath = NSBundle.mainBundle.resourcePath.UTF8String;
|
||||
// CoMaps.app or omim-build-debug for tests.
|
||||
std::string const bundlePath = NSBundle.mainBundle.bundlePath.UTF8String;
|
||||
// Current working directory, can be overrided for Xcode projects in the scheme's settings.
|
||||
std::string const currentDir = [NSFileManager.defaultManager currentDirectoryPath].UTF8String;
|
||||
|
||||
char const * envResourcesDir = ::getenv("MWM_RESOURCES_DIR");
|
||||
char const * envWritableDir = ::getenv("MWM_WRITABLE_DIR");
|
||||
|
||||
if (envResourcesDir && envWritableDir)
|
||||
{
|
||||
m_resourcesDir = envResourcesDir;
|
||||
m_writableDir = envWritableDir;
|
||||
}
|
||||
else if (resourcesPath == bundlePath)
|
||||
{
|
||||
// we're the console app, probably unit test, and path is our directory
|
||||
m_resourcesDir = bundlePath + "/../../data/";
|
||||
if (!IsFileExistsByFullPath(m_resourcesDir))
|
||||
{
|
||||
// Check development environment without symlink but with git repo
|
||||
std::string const repoPath = bundlePath + "/../../../omim/data/";
|
||||
if (IsFileExistsByFullPath(repoPath))
|
||||
m_resourcesDir = repoPath;
|
||||
else
|
||||
m_resourcesDir = "./data/";
|
||||
}
|
||||
m_writableDir = m_resourcesDir;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_resourcesDir = resourcesPath + "/";
|
||||
std::string const paths[] =
|
||||
{
|
||||
// Developers can set a symlink to the data folder.
|
||||
m_resourcesDir + "../../../data/",
|
||||
// Check development environment without a symlink but with a git repo.
|
||||
m_resourcesDir + "../../../../omim/data/",
|
||||
m_resourcesDir + "../../../../organicmaps/data/",
|
||||
// Working directory is set to the data folder or any project's subfolder.
|
||||
currentDir + "/../data",
|
||||
// Working directory is set to the project's root.
|
||||
currentDir + "/data",
|
||||
// Working directory is set to the build folder with binaries.
|
||||
currentDir + "/../omim/data",
|
||||
currentDir + "/../organicmaps/data",
|
||||
};
|
||||
// Find the writable path.
|
||||
for (auto const & path : paths)
|
||||
{
|
||||
if (IsFileExistsByFullPath(path))
|
||||
{
|
||||
m_writableDir = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Xcode-launched Mac projects are built into a non-standard folder and may need
|
||||
// a customized working directory.
|
||||
if (m_writableDir.empty())
|
||||
{
|
||||
for (char const * keyword : {"/omim/", "/organicmaps/"})
|
||||
{
|
||||
if (auto const p = currentDir.rfind(keyword); p != std::string::npos)
|
||||
{
|
||||
m_writableDir = m_resourcesDir = currentDir.substr(0, p) + keyword + "data/";
|
||||
break;
|
||||
}
|
||||
if (auto const p = m_resourcesDir.rfind(keyword); p != std::string::npos)
|
||||
{
|
||||
m_writableDir = m_resourcesDir.substr(0, p) + keyword + "data/";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_writableDir.empty())
|
||||
{
|
||||
NSArray * dirPaths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
|
||||
NSString * supportDir = [dirPaths objectAtIndex:0];
|
||||
m_writableDir = supportDir.UTF8String;
|
||||
#ifdef BUILD_DESIGNER
|
||||
m_writableDir += "/CoMapsData.Designer/";
|
||||
#else // BUILD_DESIGNER
|
||||
m_writableDir += "/CoMapsData/";
|
||||
#endif // BUILD_DESIGNER
|
||||
::mkdir(m_writableDir.c_str(), 0755);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_resourcesDir.empty())
|
||||
m_resourcesDir = ".";
|
||||
m_resourcesDir = base::AddSlashIfNeeded(m_resourcesDir);
|
||||
m_writableDir = base::AddSlashIfNeeded(m_writableDir);
|
||||
|
||||
m_settingsDir = m_writableDir;
|
||||
|
||||
NSString * tempDir = NSTemporaryDirectory();
|
||||
if (tempDir == nil)
|
||||
tempDir = @"/tmp";
|
||||
m_tmpDir = tempDir.UTF8String;
|
||||
base::AddSlashIfNeeded(m_tmpDir);
|
||||
|
||||
m_guiThread = std::make_unique<platform::GuiThread>();
|
||||
|
||||
LOG(LDEBUG, ("Resources Directory:", m_resourcesDir));
|
||||
LOG(LDEBUG, ("Writable Directory:", m_writableDir));
|
||||
LOG(LDEBUG, ("Tmp Directory:", m_tmpDir));
|
||||
LOG(LDEBUG, ("Settings Directory:", m_settingsDir));
|
||||
}
|
||||
|
||||
std::string Platform::DeviceName() const
|
||||
{
|
||||
return OMIM_OS_NAME;
|
||||
}
|
||||
|
||||
std::string Platform::DeviceModel() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
Platform::EConnectionType Platform::ConnectionStatus()
|
||||
{
|
||||
struct sockaddr_in zero;
|
||||
memset(&zero, 0, sizeof(zero));
|
||||
zero.sin_len = sizeof(zero);
|
||||
zero.sin_family = AF_INET;
|
||||
SCNetworkReachabilityRef reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, reinterpret_cast<const struct sockaddr*>(&zero));
|
||||
if (!reachability)
|
||||
return EConnectionType::CONNECTION_NONE;
|
||||
SCNetworkReachabilityFlags flags;
|
||||
bool const gotFlags = SCNetworkReachabilityGetFlags(reachability, &flags);
|
||||
CFRelease(reachability);
|
||||
if (!gotFlags || ((flags & kSCNetworkReachabilityFlagsReachable) == 0))
|
||||
return EConnectionType::CONNECTION_NONE;
|
||||
SCNetworkReachabilityFlags userActionRequired = kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsInterventionRequired;
|
||||
if ((flags & userActionRequired) == userActionRequired)
|
||||
return EConnectionType::CONNECTION_NONE;
|
||||
return EConnectionType::CONNECTION_WIFI;
|
||||
}
|
||||
|
||||
// static
|
||||
Platform::ChargingStatus Platform::GetChargingStatus()
|
||||
{
|
||||
return Platform::ChargingStatus::Plugged;
|
||||
}
|
||||
|
||||
uint8_t Platform::GetBatteryLevel()
|
||||
{
|
||||
// This value is always 100 for desktop.
|
||||
return 100;
|
||||
}
|
||||
|
||||
void Platform::GetSystemFontNames(FilesList & res) const
|
||||
{
|
||||
}
|
||||
|
||||
// static
|
||||
time_t Platform::GetFileCreationTime(std::string const & path)
|
||||
{
|
||||
struct stat st;
|
||||
if (0 == stat(path.c_str(), &st))
|
||||
return st.st_birthtimespec.tv_sec;
|
||||
|
||||
LOG(LERROR, ("GetFileCreationTime stat failed for", path, "with error", strerror(errno)));
|
||||
// TODO(AB): Refactor to return std::optional<time_t>.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// static
|
||||
time_t Platform::GetFileModificationTime(std::string const & path)
|
||||
{
|
||||
struct stat st;
|
||||
if (0 == stat(path.c_str(), &st))
|
||||
return st.st_mtimespec.tv_sec;
|
||||
|
||||
LOG(LERROR, ("GetFileModificationTime stat failed for", path, "with error", strerror(errno)));
|
||||
// TODO(AB): Refactor to return std::optional<time_t>.
|
||||
return 0;
|
||||
}
|
||||
94
libs/platform/platform_qt.cpp
Normal file
94
libs/platform/platform_qt.cpp
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#include "platform/constants.hpp"
|
||||
#include "platform/measurement_utils.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include "coding/file_reader.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QLocale>
|
||||
|
||||
std::unique_ptr<ModelReader> Platform::GetReader(std::string const & file, std::string searchScope) const
|
||||
{
|
||||
return std::make_unique<FileReader>(ReadPathForFile(file, std::move(searchScope)), READER_CHUNK_LOG_SIZE,
|
||||
READER_CHUNK_LOG_COUNT);
|
||||
}
|
||||
|
||||
bool Platform::GetFileSizeByName(std::string const & fileName, uint64_t & size) const
|
||||
{
|
||||
try
|
||||
{
|
||||
return GetFileSizeByFullPath(ReadPathForFile(fileName), size);
|
||||
}
|
||||
catch (RootException const &)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::GetFilesByRegExp(std::string const & directory, boost::regex const & regexp, FilesList & outFiles)
|
||||
{
|
||||
QDir dir(QString::fromUtf8(directory.c_str()));
|
||||
int const count = dir.count();
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
std::string name = dir[i].toStdString();
|
||||
if (boost::regex_search(name.begin(), name.end(), regexp))
|
||||
outFiles.push_back(std::move(name));
|
||||
}
|
||||
}
|
||||
|
||||
void Platform::GetAllFiles(std::string const & directory, FilesList & outFiles)
|
||||
{
|
||||
QDir dir(QString::fromUtf8(directory.c_str()));
|
||||
|
||||
for (int i = 0; i < dir.count(); ++i)
|
||||
outFiles.push_back(dir[i].toStdString());
|
||||
}
|
||||
|
||||
int Platform::PreCachingDepth() const
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
|
||||
int Platform::VideoMemoryLimit() const
|
||||
{
|
||||
return 20 * 1024 * 1024;
|
||||
}
|
||||
|
||||
// static
|
||||
Platform::EError Platform::MkDir(std::string const & dirName)
|
||||
{
|
||||
if (QDir().exists(dirName.c_str()))
|
||||
return Platform::ERR_FILE_ALREADY_EXISTS;
|
||||
if (!QDir().mkdir(dirName.c_str()))
|
||||
{
|
||||
LOG(LWARNING, ("Can't create directory: ", dirName));
|
||||
return Platform::ERR_UNKNOWN;
|
||||
}
|
||||
return Platform::ERR_OK;
|
||||
}
|
||||
|
||||
void Platform::SetupMeasurementSystem() const
|
||||
{
|
||||
auto units = measurement_utils::Units::Metric;
|
||||
if (settings::Get(settings::kMeasurementUnits, units))
|
||||
return;
|
||||
bool const isMetric = QLocale::system().measurementSystem() == QLocale::MetricSystem;
|
||||
units = isMetric ? measurement_utils::Units::Metric : measurement_utils::Units::Imperial;
|
||||
settings::Set(settings::kMeasurementUnits, units);
|
||||
}
|
||||
|
||||
extern Platform & GetPlatform()
|
||||
{
|
||||
static Platform platform;
|
||||
return platform;
|
||||
}
|
||||
11
libs/platform/platform_qt_version.cpp.in
Normal file
11
libs/platform/platform_qt_version.cpp.in
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
#include "platform/platform.hpp"
|
||||
|
||||
std::string Platform::Version() const
|
||||
{
|
||||
return "@OM_VERSION@";
|
||||
}
|
||||
|
||||
int32_t Platform::IntVersion() const
|
||||
{
|
||||
return @OM_INT_VERSION@;
|
||||
}
|
||||
28
libs/platform/platform_tests/CMakeLists.txt
Normal file
28
libs/platform/platform_tests/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
project(platform_tests)
|
||||
|
||||
set(SRC
|
||||
country_file_tests.cpp
|
||||
distance_tests.cpp
|
||||
duration_tests.cpp
|
||||
downloader_tests/downloader_test.cpp
|
||||
downloader_utils_tests.cpp
|
||||
get_text_by_id_tests.cpp
|
||||
glaze_test.cpp
|
||||
jansson_test.cpp
|
||||
language_test.cpp
|
||||
local_country_file_tests.cpp
|
||||
location_test.cpp
|
||||
measurement_tests.cpp
|
||||
platform_test.cpp
|
||||
meta_config_tests.cpp
|
||||
utm_mgrs_utils_tests.cpp
|
||||
)
|
||||
|
||||
omim_add_test(${PROJECT_NAME} ${SRC} REQUIRE_QT REQUIRE_SERVER)
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
platform_tests_support
|
||||
platform
|
||||
cppjansson
|
||||
glaze::glaze
|
||||
)
|
||||
33
libs/platform/platform_tests/country_file_tests.cpp
Normal file
33
libs/platform/platform_tests/country_file_tests.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
#include "platform/country_file.hpp"
|
||||
#include "platform/mwm_version.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
UNIT_TEST(CountryFile_Smoke)
|
||||
{
|
||||
{
|
||||
CountryFile cf("One");
|
||||
TEST_EQUAL("One", cf.GetName(), ());
|
||||
auto const mapFileName = cf.GetFileName(MapFileType::Map);
|
||||
|
||||
TEST_EQUAL("One" DATA_FILE_EXTENSION, mapFileName, ());
|
||||
TEST_EQUAL(0, cf.GetRemoteSize(), ());
|
||||
}
|
||||
|
||||
{
|
||||
CountryFile cf("Three", 666, "xxxSHAxxx");
|
||||
TEST_EQUAL("Three", cf.GetName(), ());
|
||||
auto const mapFileName = cf.GetFileName(MapFileType::Map);
|
||||
|
||||
TEST_EQUAL("Three" DATA_FILE_EXTENSION, mapFileName, ());
|
||||
TEST_EQUAL(666, cf.GetRemoteSize(), ());
|
||||
TEST_EQUAL("xxxSHAxxx", cf.GetSha1(), ());
|
||||
}
|
||||
}
|
||||
} // namespace platform
|
||||
365
libs/platform/platform_tests/distance_tests.cpp
Normal file
365
libs/platform/platform_tests/distance_tests.cpp
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/distance.hpp"
|
||||
#include "platform/measurement_utils.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
std::string MakeDistanceStr(std::string value, Distance::Units unit)
|
||||
{
|
||||
static Locale const loc = GetCurrentLocale();
|
||||
|
||||
constexpr char kHardCodedGroupingSeparator = ',';
|
||||
if (auto found = value.find(kHardCodedGroupingSeparator); found != std::string::npos)
|
||||
value.replace(found, 1, loc.m_groupingSeparator);
|
||||
|
||||
constexpr char kHardCodedDecimalSeparator = '.';
|
||||
if (auto found = value.find(kHardCodedDecimalSeparator); found != std::string::npos)
|
||||
value.replace(found, 1, loc.m_decimalSeparator);
|
||||
|
||||
return value.append(kNarrowNonBreakingSpace).append(DebugPrint(unit));
|
||||
}
|
||||
|
||||
struct ScopedSettings
|
||||
{
|
||||
/// Saves/restores previous units and sets new units for a scope.
|
||||
explicit ScopedSettings(measurement_utils::Units newUnits) : m_oldUnits(measurement_utils::Units::Metric)
|
||||
{
|
||||
m_wasSet = settings::Get(settings::kMeasurementUnits, m_oldUnits);
|
||||
settings::Set(settings::kMeasurementUnits, newUnits);
|
||||
}
|
||||
|
||||
~ScopedSettings()
|
||||
{
|
||||
if (m_wasSet)
|
||||
settings::Set(settings::kMeasurementUnits, m_oldUnits);
|
||||
else
|
||||
settings::Delete(settings::kMeasurementUnits);
|
||||
}
|
||||
|
||||
bool m_wasSet;
|
||||
measurement_utils::Units m_oldUnits;
|
||||
};
|
||||
|
||||
UNIT_TEST(Distance_InititalDistance)
|
||||
{
|
||||
Distance const d;
|
||||
TEST(!d.IsValid(), ());
|
||||
TEST_ALMOST_EQUAL_ULPS(d.GetDistance(), -1.0, ());
|
||||
TEST_EQUAL(d.GetDistanceString(), "", ());
|
||||
TEST_EQUAL(d.ToString(), "", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_CreateFormatted)
|
||||
{
|
||||
using enum Distance::Units;
|
||||
{
|
||||
ScopedSettings const guard(measurement_utils::Units::Metric);
|
||||
|
||||
Distance const d = Distance::CreateFormatted(100);
|
||||
TEST_EQUAL(d.GetUnits(), Meters, ());
|
||||
TEST_ALMOST_EQUAL_ULPS(d.GetDistance(), 100.0, ());
|
||||
TEST_EQUAL(d.GetDistanceString(), "100", ());
|
||||
TEST_EQUAL(d.ToString(), MakeDistanceStr("100", Meters), ());
|
||||
}
|
||||
{
|
||||
ScopedSettings const guard(measurement_utils::Units::Imperial);
|
||||
|
||||
Distance const d = Distance::CreateFormatted(100);
|
||||
TEST_EQUAL(d.GetUnits(), Feet, ());
|
||||
TEST_ALMOST_EQUAL_ULPS(d.GetDistance(), 330.0, ());
|
||||
TEST_EQUAL(d.GetDistanceString(), "330", ());
|
||||
TEST_EQUAL(d.ToString(), MakeDistanceStr("330", Feet), ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_CreateAltitudeFormatted)
|
||||
{
|
||||
using enum Distance::Units;
|
||||
{
|
||||
ScopedSettings const guard(measurement_utils::Units::Metric);
|
||||
|
||||
TEST_EQUAL(Distance::FormatAltitude(5), MakeDistanceStr("5", Meters), ());
|
||||
TEST_EQUAL(Distance::FormatAltitude(-8849), MakeDistanceStr("-8849", Meters), ());
|
||||
TEST_EQUAL(Distance::FormatAltitude(12345), MakeDistanceStr("12,345", Meters), ());
|
||||
}
|
||||
{
|
||||
ScopedSettings const guard(measurement_utils::Units::Imperial);
|
||||
|
||||
TEST_EQUAL(Distance::FormatAltitude(10000), MakeDistanceStr("32,808", Feet), ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_IsLowUnits)
|
||||
{
|
||||
using enum Distance::Units;
|
||||
TEST_EQUAL(Distance(0.0, Meters).IsLowUnits(), true, ());
|
||||
TEST_EQUAL(Distance(0.0, Feet).IsLowUnits(), true, ());
|
||||
TEST_EQUAL(Distance(0.0, Kilometers).IsLowUnits(), false, ());
|
||||
TEST_EQUAL(Distance(0.0, Miles).IsLowUnits(), false, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_IsHighUnits)
|
||||
{
|
||||
using enum Distance::Units;
|
||||
TEST_EQUAL(Distance(0.0, Meters).IsHighUnits(), false, ());
|
||||
TEST_EQUAL(Distance(0.0, Feet).IsHighUnits(), false, ());
|
||||
TEST_EQUAL(Distance(0.0, Kilometers).IsHighUnits(), true, ());
|
||||
TEST_EQUAL(Distance(0.0, Miles).IsHighUnits(), true, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_To)
|
||||
{
|
||||
struct TestData
|
||||
{
|
||||
double initialDistance;
|
||||
Distance::Units initialUnits;
|
||||
Distance::Units to;
|
||||
double newDistance;
|
||||
Distance::Units newUnits;
|
||||
};
|
||||
|
||||
using enum Distance::Units;
|
||||
// clang-format off
|
||||
TestData constexpr testData[] = {
|
||||
{0.1, Meters, Feet, 0, Feet},
|
||||
{0.3, Meters, Feet, 1, Feet},
|
||||
{0.3048, Meters, Feet, 1, Feet},
|
||||
{0.4573, Meters, Feet, 2, Feet},
|
||||
{0.9, Meters, Feet, 3, Feet},
|
||||
{3, Meters, Feet, 10, Feet},
|
||||
{30.17, Meters, Feet, 99, Feet},
|
||||
{30.33, Meters, Feet, 100, Feet},
|
||||
{30.49, Meters, Feet, 100, Feet},
|
||||
{33.5, Meters, Feet, 110, Feet},
|
||||
{302, Meters, Feet, 990, Feet},
|
||||
{304.7, Meters, Feet, 0.2, Miles},
|
||||
{304.8, Meters, Feet, 0.2, Miles},
|
||||
{402.3, Meters, Feet, 0.2, Miles},
|
||||
{402.4, Meters, Feet, 0.3, Miles},
|
||||
{482.8, Meters, Feet, 0.3, Miles},
|
||||
{1609.3, Meters, Feet, 1.0, Miles},
|
||||
{1610, Meters, Feet, 1.0, Miles},
|
||||
{1770, Meters, Feet, 1.1, Miles},
|
||||
{15933, Meters, Feet, 9.9, Miles},
|
||||
{16093, Meters, Feet, 10, Miles},
|
||||
{16093.5, Meters, Feet, 10, Miles},
|
||||
{16898.464, Meters, Feet, 11, Miles},
|
||||
{16898.113, Meters, Kilometers, 17, Kilometers},
|
||||
{302, Meters, Miles, 990, Feet},
|
||||
{994, Meters, Kilometers, 990, Meters},
|
||||
{995, Meters, Kilometers, 1.0, Kilometers},
|
||||
{0.1, Kilometers, Meters, 100, Meters},
|
||||
{0.3, Kilometers, Kilometers, 300, Meters},
|
||||
{12, Kilometers, Feet, 7.5, Miles},
|
||||
{0.1, Kilometers, Feet, 330, Feet},
|
||||
{110, Feet, Meters, 34, Meters},
|
||||
{1100, Feet, Kilometers, 340, Meters},
|
||||
{1100, Feet, Meters, 340, Meters},
|
||||
{1100, Feet, Miles, 0.2, Miles},
|
||||
{0.2, Miles, Meters, 320, Meters},
|
||||
{11, Miles, Meters, 18, Kilometers},
|
||||
{11, Miles, Kilometers, 18, Kilometers},
|
||||
{0.1, Miles, Feet, 530, Feet},
|
||||
};
|
||||
|
||||
// clang-format on
|
||||
for (TestData const & data : testData)
|
||||
{
|
||||
Distance const formatted = Distance(data.initialDistance, data.initialUnits).To(data.to).GetFormattedDistance();
|
||||
TEST_ALMOST_EQUAL_ULPS(formatted.GetDistance(), data.newDistance, (data.initialDistance));
|
||||
TEST_EQUAL(formatted.GetUnits(), data.newUnits, ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_ToPlatformUnitsFormatted)
|
||||
{
|
||||
using enum Distance::Units;
|
||||
{
|
||||
ScopedSettings const guard(measurement_utils::Units::Metric);
|
||||
|
||||
Distance d{11, Feet};
|
||||
Distance newDistance = d.ToPlatformUnitsFormatted();
|
||||
|
||||
TEST_EQUAL(newDistance.GetUnits(), Meters, (d.ToString()));
|
||||
TEST_ALMOST_EQUAL_ULPS(newDistance.GetDistance(), 3.0, (d.ToString()));
|
||||
TEST_EQUAL(newDistance.GetDistanceString(), "3", (d.ToString()));
|
||||
TEST_EQUAL(newDistance.ToString(), MakeDistanceStr("3", Meters), (d.ToString()));
|
||||
|
||||
d = Distance{11, Kilometers};
|
||||
newDistance = d.ToPlatformUnitsFormatted();
|
||||
|
||||
TEST_EQUAL(newDistance.GetUnits(), Kilometers, (d.ToString()));
|
||||
TEST_ALMOST_EQUAL_ULPS(newDistance.GetDistance(), 11.0, (d.ToString()));
|
||||
TEST_EQUAL(newDistance.GetDistanceString(), "11", (d.ToString()));
|
||||
TEST_EQUAL(newDistance.ToString(), MakeDistanceStr("11", Kilometers), (d.ToString()));
|
||||
}
|
||||
|
||||
{
|
||||
ScopedSettings const guard(measurement_utils::Units::Imperial);
|
||||
|
||||
Distance d{11, Feet};
|
||||
Distance newDistance = d.ToPlatformUnitsFormatted();
|
||||
|
||||
TEST_EQUAL(newDistance.GetUnits(), Feet, (d.ToString()));
|
||||
TEST_ALMOST_EQUAL_ULPS(newDistance.GetDistance(), 11.0, (d.ToString()));
|
||||
TEST_EQUAL(newDistance.GetDistanceString(), "11", (d.ToString()));
|
||||
TEST_EQUAL(newDistance.ToString(), MakeDistanceStr("11", Feet), (d.ToString()));
|
||||
|
||||
d = Distance{11, Kilometers};
|
||||
newDistance = d.ToPlatformUnitsFormatted();
|
||||
|
||||
TEST_EQUAL(newDistance.GetUnits(), Miles, (d.ToString()));
|
||||
TEST_ALMOST_EQUAL_ULPS(newDistance.GetDistance(), 6.8, (d.ToString()));
|
||||
TEST_EQUAL(newDistance.GetDistanceString(), "6.8", (d.ToString()));
|
||||
TEST_EQUAL(newDistance.ToString(), MakeDistanceStr("6.8", Miles), (d.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_GetUnits)
|
||||
{
|
||||
using enum Distance::Units;
|
||||
TEST_EQUAL(Distance(1234).GetUnits(), Meters, ());
|
||||
TEST_EQUAL(Distance(1234, Kilometers).GetUnits(), Kilometers, ());
|
||||
TEST_EQUAL(Distance(1234, Feet).GetUnits(), Feet, ());
|
||||
TEST_EQUAL(Distance(1234, Miles).GetUnits(), Miles, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_GetUnitsString)
|
||||
{
|
||||
using enum Distance::Units;
|
||||
TEST_EQUAL(Distance(1234).GetUnitsString(), "m", ());
|
||||
TEST_EQUAL(Distance(1234, Meters).GetUnitsString(), "m", ());
|
||||
TEST_EQUAL(Distance(1234, Kilometers).GetUnitsString(), "km", ());
|
||||
TEST_EQUAL(Distance(1234, Feet).GetUnitsString(), "ft", ());
|
||||
TEST_EQUAL(Distance(1234, Miles).GetUnitsString(), "mi", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(Distance_FormattedDistance)
|
||||
{
|
||||
struct TestData
|
||||
{
|
||||
Distance distance;
|
||||
double formattedDistance;
|
||||
Distance::Units formattedUnits;
|
||||
std::string formattedDistanceStringInUsLocale;
|
||||
};
|
||||
|
||||
using enum Distance::Units;
|
||||
// clang-format off
|
||||
TestData const testData[] = {
|
||||
// From Meters to Meters
|
||||
{Distance(0, Meters), 0, Meters, "0"},
|
||||
{Distance(0.3, Meters), 0, Meters, "0"},
|
||||
{Distance(0.9, Meters), 1, Meters, "1"},
|
||||
{Distance(1, Meters), 1, Meters, "1"},
|
||||
{Distance(1.234, Meters), 1, Meters, "1"},
|
||||
{Distance(9.99, Meters), 10, Meters, "10"},
|
||||
{Distance(10.01, Meters), 10, Meters, "10"},
|
||||
{Distance(10.4, Meters), 10, Meters, "10"},
|
||||
{Distance(10.5, Meters), 11, Meters, "11"},
|
||||
{Distance(10.51, Meters), 11, Meters, "11"},
|
||||
{Distance(64.2, Meters), 64, Meters, "64"},
|
||||
{Distance(99, Meters), 99, Meters, "99"},
|
||||
{Distance(100, Meters), 100, Meters, "100"},
|
||||
{Distance(101, Meters), 100, Meters, "100"},
|
||||
{Distance(109, Meters), 110, Meters, "110"},
|
||||
{Distance(991, Meters), 990, Meters, "990"},
|
||||
|
||||
// From Kilometers to Kilometers
|
||||
{Distance(0, Kilometers), 0, Meters, "0"},
|
||||
{Distance(0.3, Kilometers), 300, Meters, "300"},
|
||||
{Distance(1.234, Kilometers), 1.2, Kilometers, "1.2"},
|
||||
{Distance(10, Kilometers), 10, Kilometers, "10"},
|
||||
{Distance(11, Kilometers), 11, Kilometers, "11"},
|
||||
{Distance(54, Kilometers), 54, Kilometers, "54"},
|
||||
{Distance(99.99, Kilometers), 100, Kilometers, "100"},
|
||||
{Distance(100.01, Kilometers), 100, Kilometers, "100"},
|
||||
{Distance(115, Kilometers), 115, Kilometers, "115"},
|
||||
{Distance(999, Kilometers), 999, Kilometers, "999"},
|
||||
{Distance(1000, Kilometers), 1000, Kilometers, "1000"},
|
||||
{Distance(1049.99, Kilometers), 1050, Kilometers, "1050"},
|
||||
{Distance(1050, Kilometers), 1050, Kilometers, "1050"},
|
||||
{Distance(1050.01, Kilometers), 1050, Kilometers, "1050"},
|
||||
{Distance(1234, Kilometers), 1234, Kilometers, "1234"},
|
||||
{Distance(12345, Kilometers), 12345, Kilometers, "12,345"},
|
||||
|
||||
// From Feet to Feet
|
||||
{Distance(0, Feet), 0, Feet, "0"},
|
||||
{Distance(1, Feet), 1, Feet, "1"},
|
||||
{Distance(9.99, Feet), 10, Feet, "10"},
|
||||
{Distance(10.01, Feet), 10, Feet, "10"},
|
||||
{Distance(95, Feet), 95, Feet, "95"},
|
||||
{Distance(125, Feet), 130, Feet, "130"},
|
||||
{Distance(991, Feet), 990, Feet, "990"},
|
||||
|
||||
// From Miles to Miles
|
||||
{Distance(0, Miles), 0, Feet, "0"},
|
||||
{Distance(0.1, Miles), 530, Feet, "530"},
|
||||
{Distance(1, Miles), 1.0, Miles, "1.0"},
|
||||
{Distance(1.234, Miles), 1.2, Miles, "1.2"},
|
||||
{Distance(9.99, Miles), 10, Miles, "10"},
|
||||
{Distance(10.01, Miles), 10, Miles, "10"},
|
||||
{Distance(11, Miles), 11, Miles, "11"},
|
||||
{Distance(54, Miles), 54, Miles, "54"},
|
||||
{Distance(145, Miles), 145, Miles, "145"},
|
||||
{Distance(999, Miles), 999, Miles, "999"},
|
||||
{Distance(1149.99, Miles), 1150, Miles, "1150"},
|
||||
{Distance(1150, Miles), 1150, Miles, "1150"},
|
||||
{Distance(1150.01, Miles), 1150, Miles, "1150"},
|
||||
{Distance(12345.0, Miles), 12345, Miles, "12,345"},
|
||||
|
||||
// From Meters to Kilometers
|
||||
{Distance(999, Meters), 1.0, Kilometers, "1.0"},
|
||||
{Distance(1000, Meters), 1.0, Kilometers, "1.0"},
|
||||
{Distance(1001, Meters), 1.0, Kilometers, "1.0"},
|
||||
{Distance(1100, Meters), 1.1, Kilometers, "1.1"},
|
||||
{Distance(1140, Meters), 1.1, Kilometers, "1.1"},
|
||||
{Distance(1151, Meters), 1.2, Kilometers, "1.2"},
|
||||
{Distance(1500, Meters), 1.5, Kilometers, "1.5"},
|
||||
{Distance(1549.9, Meters), 1.5, Kilometers, "1.5"},
|
||||
{Distance(1550, Meters), 1.6, Kilometers, "1.6"},
|
||||
{Distance(1551, Meters), 1.6, Kilometers, "1.6"},
|
||||
{Distance(9949, Meters), 9.9, Kilometers, "9.9"},
|
||||
{Distance(9992, Meters), 10, Kilometers, "10"},
|
||||
{Distance(10000, Meters), 10, Kilometers, "10"},
|
||||
{Distance(10499.9, Meters), 10, Kilometers, "10"},
|
||||
{Distance(10501, Meters), 11, Kilometers, "11"},
|
||||
{Distance(101'001, Meters), 101, Kilometers, "101"},
|
||||
{Distance(101'999, Meters), 102, Kilometers, "102"},
|
||||
{Distance(287'386, Meters), 287, Kilometers, "287"},
|
||||
|
||||
// From Feet to Miles
|
||||
{Distance(999, Feet), 0.2, Miles, "0.2"},
|
||||
{Distance(1000, Feet), 0.2, Miles, "0.2"},
|
||||
{Distance(1150, Feet), 0.2, Miles, "0.2"},
|
||||
{Distance(5280, Feet), 1.0, Miles, "1.0"},
|
||||
{Distance(7920, Feet), 1.5, Miles, "1.5"},
|
||||
{Distance(10560, Feet), 2.0, Miles, "2.0"},
|
||||
{Distance(100'000, Feet), 19, Miles, "19"},
|
||||
{Distance(285'120, Feet), 54, Miles, "54"},
|
||||
{Distance(633'547, Feet), 120, Miles, "120"},
|
||||
{Distance(633'600, Feet), 120, Miles, "120"},
|
||||
{Distance(633'653, Feet), 120, Miles, "120"},
|
||||
{Distance(999'999, Feet), 189, Miles, "189"},
|
||||
};
|
||||
|
||||
// clang-format on
|
||||
for (auto const & [distance, formattedDistance, formattedUnits, formattedDistanceStringInUsLocale] : testData)
|
||||
{
|
||||
Distance const formatted = distance.GetFormattedDistance();
|
||||
// Run two times to verify that nothing breaks after second format
|
||||
for (auto const & d : {formatted, formatted.GetFormattedDistance()})
|
||||
{
|
||||
TEST_ALMOST_EQUAL_ULPS(d.GetDistance(), formattedDistance, (distance));
|
||||
TEST_EQUAL(d.GetUnits(), formattedUnits, (distance));
|
||||
auto const formattedString = MakeDistanceStr(formattedDistanceStringInUsLocale, formattedUnits);
|
||||
TEST_EQUAL(d.ToString(), formattedString, (distance));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace platform
|
||||
|
|
@ -0,0 +1,613 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/chunks_download_strategy.hpp"
|
||||
#include "platform/http_request.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "coding/file_reader.hpp"
|
||||
#include "coding/file_writer.hpp"
|
||||
#include "coding/internal/file_data.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
#include "base/std_serialization.hpp"
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace downloader_test
|
||||
{
|
||||
using namespace downloader;
|
||||
using namespace std::placeholders;
|
||||
using std::bind, std::string, std::vector;
|
||||
|
||||
char constexpr kTestUrl1[] = "http://localhost:34568/unit_tests/1.txt";
|
||||
char constexpr kTestUrl404[] = "http://localhost:34568/unit_tests/notexisting_unittest";
|
||||
char constexpr kTestUrlBigFile[] = "http://localhost:34568/unit_tests/47kb.file";
|
||||
|
||||
// Should match file size in tools/python/ResponseProvider.py
|
||||
int constexpr kBigFileSize = 47684;
|
||||
|
||||
class DownloadObserver
|
||||
{
|
||||
bool m_progressWasCalled;
|
||||
// Chunked downloads can return one status per chunk (thread).
|
||||
vector<DownloadStatus> m_statuses;
|
||||
// Interrupt download after this number of chunks
|
||||
int m_chunksToFail;
|
||||
base::ScopedLogLevelChanger const m_debugLogLevel;
|
||||
|
||||
public:
|
||||
DownloadObserver() : m_chunksToFail(-1), m_debugLogLevel(LDEBUG) { Reset(); }
|
||||
|
||||
void CancelDownloadOnGivenChunk(int chunksToFail) { m_chunksToFail = chunksToFail; }
|
||||
|
||||
void Reset()
|
||||
{
|
||||
m_progressWasCalled = false;
|
||||
m_statuses.clear();
|
||||
}
|
||||
|
||||
void TestOk()
|
||||
{
|
||||
TEST_NOT_EQUAL(0, m_statuses.size(), ("Observer was not called."));
|
||||
TEST(m_progressWasCalled, ("Download progress wasn't called"));
|
||||
for (auto const & status : m_statuses)
|
||||
TEST_EQUAL(status, DownloadStatus::Completed, ());
|
||||
}
|
||||
|
||||
void TestFailed()
|
||||
{
|
||||
TEST_NOT_EQUAL(0, m_statuses.size(), ("Observer was not called."));
|
||||
for (auto const & status : m_statuses)
|
||||
TEST_EQUAL(status, DownloadStatus::Failed, ());
|
||||
}
|
||||
|
||||
void TestFileNotFound()
|
||||
{
|
||||
TEST_NOT_EQUAL(0, m_statuses.size(), ("Observer was not called."));
|
||||
for (auto const & status : m_statuses)
|
||||
TEST_EQUAL(status, DownloadStatus::FileNotFound, ());
|
||||
}
|
||||
|
||||
void OnDownloadProgress(HttpRequest & request)
|
||||
{
|
||||
m_progressWasCalled = true;
|
||||
TEST_EQUAL(request.GetStatus(), DownloadStatus::InProgress, ());
|
||||
|
||||
// Cancel download if needed
|
||||
if (m_chunksToFail != -1)
|
||||
{
|
||||
--m_chunksToFail;
|
||||
if (m_chunksToFail == 0)
|
||||
{
|
||||
m_chunksToFail = -1;
|
||||
LOG(LINFO, ("Download canceled"));
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virtual void OnDownloadFinish(HttpRequest & request)
|
||||
{
|
||||
auto const status = request.GetStatus();
|
||||
m_statuses.emplace_back(status);
|
||||
TEST(status != DownloadStatus::InProgress, ());
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
};
|
||||
|
||||
struct CancelDownload
|
||||
{
|
||||
void OnProgress(HttpRequest & request)
|
||||
{
|
||||
TEST_GREATER(request.GetData().size(), 0, ());
|
||||
delete &request;
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
void OnFinish(HttpRequest &) { TEST(false, ("Should be never called")); }
|
||||
};
|
||||
|
||||
struct DeleteOnFinish
|
||||
{
|
||||
void OnProgress(HttpRequest & request) { TEST_GREATER(request.GetData().size(), 0, ()); }
|
||||
void OnFinish(HttpRequest & request)
|
||||
{
|
||||
delete &request;
|
||||
QCoreApplication::quit();
|
||||
}
|
||||
};
|
||||
|
||||
UNIT_TEST(DownloaderSimpleGet)
|
||||
{
|
||||
DownloadObserver observer;
|
||||
auto const MakeRequest = [&observer](string const & url)
|
||||
{
|
||||
return HttpRequest::Get(url, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
|
||||
bind(&DownloadObserver::OnDownloadProgress, &observer, _1));
|
||||
};
|
||||
|
||||
{
|
||||
// simple success case
|
||||
std::unique_ptr<HttpRequest> const request{MakeRequest(kTestUrl1)};
|
||||
// wait until download is finished
|
||||
QCoreApplication::exec();
|
||||
observer.TestOk();
|
||||
TEST_EQUAL(request->GetData(), "Test1", ());
|
||||
}
|
||||
|
||||
observer.Reset();
|
||||
{
|
||||
// We DO NOT SUPPORT redirects to avoid data corruption when downloading mwm files
|
||||
std::unique_ptr<HttpRequest> const request{MakeRequest("http://localhost:34568/unit_tests/permanent")};
|
||||
QCoreApplication::exec();
|
||||
observer.TestFailed();
|
||||
TEST_EQUAL(request->GetData().size(), 0, (request->GetData()));
|
||||
}
|
||||
|
||||
observer.Reset();
|
||||
{
|
||||
// fail case 404
|
||||
std::unique_ptr<HttpRequest> const request{MakeRequest(kTestUrl404)};
|
||||
QCoreApplication::exec();
|
||||
observer.TestFileNotFound();
|
||||
TEST_EQUAL(request->GetData().size(), 0, (request->GetData()));
|
||||
}
|
||||
|
||||
observer.Reset();
|
||||
{
|
||||
// fail case not existing host
|
||||
std::unique_ptr<HttpRequest> const request{MakeRequest("http://not-valid-host123532.ath.cx")};
|
||||
QCoreApplication::exec();
|
||||
observer.TestFailed();
|
||||
TEST_EQUAL(request->GetData().size(), 0, (request->GetData()));
|
||||
}
|
||||
|
||||
{
|
||||
// cancel download in the middle of the progress
|
||||
CancelDownload canceler;
|
||||
// should be deleted in canceler
|
||||
HttpRequest::Get(kTestUrlBigFile, bind(&CancelDownload::OnFinish, &canceler, _1),
|
||||
bind(&CancelDownload::OnProgress, &canceler, _1));
|
||||
QCoreApplication::exec();
|
||||
}
|
||||
|
||||
observer.Reset();
|
||||
{
|
||||
// https success case
|
||||
std::unique_ptr<HttpRequest> const request{MakeRequest("https://github.com")};
|
||||
// wait until download is finished
|
||||
QCoreApplication::exec();
|
||||
observer.TestOk();
|
||||
TEST_GREATER(request->GetData().size(), 0, ());
|
||||
}
|
||||
|
||||
{
|
||||
// Delete request at the end of successful download
|
||||
DeleteOnFinish deleter;
|
||||
// should be deleted in deleter on finish
|
||||
HttpRequest::Get(kTestUrl1, bind(&DeleteOnFinish::OnFinish, &deleter, _1),
|
||||
bind(&DeleteOnFinish::OnProgress, &deleter, _1));
|
||||
QCoreApplication::exec();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This test sometimes fails on CI. Reasons are unknown.
|
||||
#ifndef OMIM_OS_MAC
|
||||
UNIT_TEST(DownloaderSimplePost)
|
||||
{
|
||||
// simple success case
|
||||
string const postData = "{\"jsonKey\":\"jsonValue\"}";
|
||||
DownloadObserver observer;
|
||||
std::unique_ptr<HttpRequest> const request{HttpRequest::PostJson(
|
||||
"http://localhost:34568/unit_tests/post.php", postData, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
|
||||
bind(&DownloadObserver::OnDownloadProgress, &observer, _1))};
|
||||
// wait until download is finished
|
||||
QCoreApplication::exec();
|
||||
observer.TestOk();
|
||||
TEST_EQUAL(request->GetData(), postData, ());
|
||||
}
|
||||
#endif
|
||||
|
||||
UNIT_TEST(ChunksDownloadStrategy)
|
||||
{
|
||||
vector<string> const servers = {"UrlOfServer1", "UrlOfServer2", "UrlOfServer3"};
|
||||
|
||||
typedef std::pair<int64_t, int64_t> RangeT;
|
||||
RangeT const R1{0, 249}, R2{250, 499}, R3{500, 749}, R4{750, 800};
|
||||
|
||||
int64_t constexpr kFileSize = 800;
|
||||
int64_t constexpr kChunkSize = 250;
|
||||
ChunksDownloadStrategy strategy(servers);
|
||||
strategy.InitChunks(kFileSize, kChunkSize);
|
||||
|
||||
string s1;
|
||||
RangeT r1;
|
||||
TEST_EQUAL(strategy.NextChunk(s1, r1), ChunksDownloadStrategy::ENextChunk, ());
|
||||
|
||||
string s2;
|
||||
RangeT r2;
|
||||
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::ENextChunk, ());
|
||||
|
||||
string s3;
|
||||
RangeT r3;
|
||||
TEST_EQUAL(strategy.NextChunk(s3, r3), ChunksDownloadStrategy::ENextChunk, ());
|
||||
|
||||
string sEmpty;
|
||||
RangeT rEmpty;
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
|
||||
TEST(s1 != s2 && s2 != s3 && s3 != s1, (s1, s2, s3));
|
||||
|
||||
TEST(r1 != r2 && r2 != r3 && r3 != r1, (r1, r2, r3));
|
||||
TEST(r1 == R1 || r1 == R2 || r1 == R3 || r1 == R4, (r1));
|
||||
TEST(r2 == R1 || r2 == R2 || r2 == R3 || r2 == R4, (r2));
|
||||
TEST(r3 == R1 || r3 == R2 || r3 == R3 || r3 == R4, (r3));
|
||||
|
||||
strategy.ChunkFinished(true, r1);
|
||||
|
||||
string s4;
|
||||
RangeT r4;
|
||||
TEST_EQUAL(strategy.NextChunk(s4, r4), ChunksDownloadStrategy::ENextChunk, ());
|
||||
TEST_EQUAL(s4, s1, ());
|
||||
TEST(r4 != r1 && r4 != r2 && r4 != r3, (r4));
|
||||
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
|
||||
strategy.ChunkFinished(false, r2);
|
||||
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
|
||||
strategy.ChunkFinished(true, r4);
|
||||
|
||||
string s5;
|
||||
RangeT r5;
|
||||
TEST_EQUAL(strategy.NextChunk(s5, r5), ChunksDownloadStrategy::ENextChunk, ());
|
||||
TEST_EQUAL(s5, s4, (s5, s4));
|
||||
TEST_EQUAL(r5, r2, ());
|
||||
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
|
||||
strategy.ChunkFinished(true, r5);
|
||||
|
||||
// 3rd is still alive here
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
|
||||
strategy.ChunkFinished(true, r3);
|
||||
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::EDownloadSucceeded, ());
|
||||
TEST_EQUAL(strategy.NextChunk(sEmpty, rEmpty), ChunksDownloadStrategy::EDownloadSucceeded, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(ChunksDownloadStrategyFAIL)
|
||||
{
|
||||
vector<string> const servers = {"UrlOfServer1", "UrlOfServer2"};
|
||||
|
||||
typedef std::pair<int64_t, int64_t> RangeT;
|
||||
|
||||
int64_t constexpr kFileSize = 800;
|
||||
int64_t constexpr kChunkSize = 250;
|
||||
ChunksDownloadStrategy strategy(servers);
|
||||
strategy.InitChunks(kFileSize, kChunkSize);
|
||||
|
||||
string s1;
|
||||
RangeT r1;
|
||||
TEST_EQUAL(strategy.NextChunk(s1, r1), ChunksDownloadStrategy::ENextChunk, ());
|
||||
|
||||
string s2;
|
||||
RangeT r2;
|
||||
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::ENextChunk, ());
|
||||
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
|
||||
strategy.ChunkFinished(false, r1);
|
||||
|
||||
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
|
||||
strategy.ChunkFinished(false, r2);
|
||||
|
||||
TEST_EQUAL(strategy.NextChunk(s2, r2), ChunksDownloadStrategy::EDownloadFailed, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(ChunksDownloadStrategyDynamicChunks)
|
||||
{
|
||||
vector<string> const servers = {"UrlOfServer1", "UrlOfServer2"};
|
||||
|
||||
typedef std::pair<int64_t, int64_t> RangeT;
|
||||
|
||||
string url;
|
||||
ChunksDownloadStrategy strategy(servers);
|
||||
|
||||
// Small 1MB file - one chunk
|
||||
strategy.InitChunks(1024 * 1024, 0);
|
||||
RangeT const R11{0, 1024 * 1024 - 1};
|
||||
RangeT r1;
|
||||
TEST_EQUAL(strategy.NextChunk(url, r1), ChunksDownloadStrategy::ENextChunk, ());
|
||||
RangeT rEmpty;
|
||||
TEST_EQUAL(strategy.NextChunk(url, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
TEST(r1 == R11, (r1));
|
||||
|
||||
// Small 1MB+1b file - 2 chunks
|
||||
strategy = ChunksDownloadStrategy(servers);
|
||||
strategy.InitChunks(1024 * 1024 + 1, 0);
|
||||
RangeT const R21{0, 1024 * 1024 - 1}, R22{1024 * 1024, 1024 * 1024};
|
||||
TEST_EQUAL(strategy.NextChunk(url, r1), ChunksDownloadStrategy::ENextChunk, ());
|
||||
RangeT r2;
|
||||
TEST_EQUAL(strategy.NextChunk(url, r2), ChunksDownloadStrategy::ENextChunk, ());
|
||||
TEST_EQUAL(strategy.NextChunk(url, rEmpty), ChunksDownloadStrategy::ENoFreeServers, ());
|
||||
TEST(r1 == R21 && r2 == R22, (r1, r2));
|
||||
|
||||
// Big 200MB file - 5MB chunks
|
||||
strategy = ChunksDownloadStrategy(servers);
|
||||
strategy.InitChunks(200 * 1024 * 1024, 0);
|
||||
RangeT const R31{0, 5 * 1024 * 1024 - 1}, R32{5 * 1024 * 1024, 2 * 5 * 1024 * 1024 - 1};
|
||||
TEST_EQUAL(strategy.NextChunk(url, r1), ChunksDownloadStrategy::ENextChunk, ());
|
||||
TEST_EQUAL(strategy.NextChunk(url, r2), ChunksDownloadStrategy::ENextChunk, ());
|
||||
TEST(r1 == R31 && r2 == R32, (r1, r2));
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
string ReadFileAsString(string const & file)
|
||||
{
|
||||
try
|
||||
{
|
||||
FileReader f(file);
|
||||
string s;
|
||||
f.ReadAsString(s);
|
||||
return s;
|
||||
}
|
||||
catch (FileReader::Exception const &)
|
||||
{
|
||||
TEST(false, ("File ", file, " should exist"));
|
||||
return {};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void FinishDownloadSuccess(string const & file)
|
||||
{
|
||||
TEST(base::DeleteFileX(file), ("Result file should present on success"));
|
||||
|
||||
uint64_t size;
|
||||
TEST(!base::GetFileSize(file + DOWNLOADING_FILE_EXTENSION, size), ("No downloading file on success"));
|
||||
TEST(!base::GetFileSize(file + RESUME_FILE_EXTENSION, size), ("No resume file on success"));
|
||||
}
|
||||
|
||||
void FinishDownloadFail(string const & file)
|
||||
{
|
||||
uint64_t size;
|
||||
TEST(!base::GetFileSize(file, size), ("No result file on fail"));
|
||||
|
||||
(void)base::DeleteFileX(file + DOWNLOADING_FILE_EXTENSION);
|
||||
|
||||
TEST(base::DeleteFileX(file + RESUME_FILE_EXTENSION), ("Resume file should present on fail"));
|
||||
}
|
||||
|
||||
void DeleteTempDownloadFiles()
|
||||
{
|
||||
// Remove data from previously failed files.
|
||||
|
||||
// Get regexp like this: (\.downloading3$|\.resume3$)
|
||||
string const regexp = "(\\" RESUME_FILE_EXTENSION "$|\\" DOWNLOADING_FILE_EXTENSION "$)";
|
||||
|
||||
Platform::FilesList files;
|
||||
Platform::GetFilesByRegExp(".", regexp, files);
|
||||
for (Platform::FilesList::iterator it = files.begin(); it != files.end(); ++it)
|
||||
FileWriter::DeleteFileX(*it);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
UNIT_TEST(DownloadChunks)
|
||||
{
|
||||
string const kFileName = "some_downloader_test_file";
|
||||
|
||||
// remove data from previously failed files
|
||||
DeleteTempDownloadFiles();
|
||||
|
||||
vector<string> urls = {kTestUrl1, kTestUrl1};
|
||||
int64_t fileSize = 5;
|
||||
|
||||
DownloadObserver observer;
|
||||
auto const MakeRequest = [&](int64_t chunkSize)
|
||||
{
|
||||
return HttpRequest::GetFile(urls, kFileName, fileSize, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
|
||||
bind(&DownloadObserver::OnDownloadProgress, &observer, _1), chunkSize);
|
||||
};
|
||||
|
||||
{
|
||||
// should use only one thread
|
||||
std::unique_ptr<HttpRequest> const request{MakeRequest(512 * 1024)};
|
||||
// wait until download is finished
|
||||
QCoreApplication::exec();
|
||||
observer.TestOk();
|
||||
TEST_EQUAL(request->GetData(), kFileName, ());
|
||||
TEST_EQUAL(ReadFileAsString(kFileName), "Test1", ());
|
||||
FinishDownloadSuccess(kFileName);
|
||||
}
|
||||
|
||||
observer.Reset();
|
||||
urls = {kTestUrlBigFile, kTestUrlBigFile, kTestUrlBigFile};
|
||||
fileSize = 5;
|
||||
{
|
||||
// 3 threads - fail, because of invalid size
|
||||
[[maybe_unused]] std::unique_ptr<HttpRequest> const request{MakeRequest(2048)};
|
||||
// wait until download is finished
|
||||
QCoreApplication::exec();
|
||||
observer.TestFailed();
|
||||
FinishDownloadFail(kFileName);
|
||||
}
|
||||
|
||||
observer.Reset();
|
||||
urls = {kTestUrlBigFile, kTestUrlBigFile, kTestUrlBigFile};
|
||||
fileSize = kBigFileSize;
|
||||
{
|
||||
// 3 threads - succeeded
|
||||
[[maybe_unused]] std::unique_ptr<HttpRequest> const request{MakeRequest(2048)};
|
||||
// wait until download is finished
|
||||
QCoreApplication::exec();
|
||||
observer.TestOk();
|
||||
FinishDownloadSuccess(kFileName);
|
||||
}
|
||||
|
||||
observer.Reset();
|
||||
urls = {kTestUrlBigFile, kTestUrl1, kTestUrl404};
|
||||
fileSize = kBigFileSize;
|
||||
{
|
||||
// 3 threads with only one valid url - succeeded
|
||||
[[maybe_unused]] std::unique_ptr<HttpRequest> const request{MakeRequest(2048)};
|
||||
// wait until download is finished
|
||||
QCoreApplication::exec();
|
||||
observer.TestOk();
|
||||
FinishDownloadSuccess(kFileName);
|
||||
}
|
||||
|
||||
observer.Reset();
|
||||
urls = {kTestUrlBigFile, kTestUrlBigFile};
|
||||
fileSize = 12345;
|
||||
{
|
||||
// 2 threads and all points to file with invalid size - fail
|
||||
[[maybe_unused]] std::unique_ptr<HttpRequest> const request{MakeRequest(2048)};
|
||||
// wait until download is finished
|
||||
QCoreApplication::exec();
|
||||
observer.TestFailed();
|
||||
FinishDownloadFail(kFileName);
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
int64_t constexpr beg1 = 123, end1 = 1230, beg2 = 44000, end2 = 47683;
|
||||
|
||||
struct ResumeChecker
|
||||
{
|
||||
size_t m_counter;
|
||||
ResumeChecker() : m_counter(0) {}
|
||||
|
||||
void OnProgress(HttpRequest & request)
|
||||
{
|
||||
if (m_counter == 0)
|
||||
{
|
||||
TEST_EQUAL(request.GetProgress().m_bytesDownloaded, beg2, ());
|
||||
TEST_EQUAL(request.GetProgress().m_bytesTotal, kBigFileSize, ());
|
||||
}
|
||||
else if (m_counter == 1)
|
||||
{
|
||||
TEST_EQUAL(request.GetProgress().m_bytesDownloaded, kBigFileSize, ());
|
||||
TEST_EQUAL(request.GetProgress().m_bytesTotal, kBigFileSize, ());
|
||||
}
|
||||
else
|
||||
{
|
||||
TEST(false, ("Progress should be called exactly 2 times"));
|
||||
}
|
||||
++m_counter;
|
||||
}
|
||||
|
||||
void OnFinish(HttpRequest & request)
|
||||
{
|
||||
TEST_EQUAL(request.GetStatus(), DownloadStatus::Completed, ());
|
||||
QCoreApplication::exit();
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
UNIT_TEST(DownloadResumeChunks)
|
||||
{
|
||||
string const FILENAME = "some_test_filename_12345";
|
||||
string const RESUME_FILENAME = FILENAME + RESUME_FILE_EXTENSION;
|
||||
string const DOWNLOADING_FILENAME = FILENAME + DOWNLOADING_FILE_EXTENSION;
|
||||
|
||||
// remove data from previously failed files
|
||||
DeleteTempDownloadFiles();
|
||||
|
||||
vector<string> urls = {kTestUrlBigFile};
|
||||
|
||||
// 1st step - download full file
|
||||
{
|
||||
DownloadObserver observer;
|
||||
|
||||
std::unique_ptr<HttpRequest> const request(
|
||||
HttpRequest::GetFile(urls, FILENAME, kBigFileSize, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
|
||||
bind(&DownloadObserver::OnDownloadProgress, &observer, _1)));
|
||||
|
||||
QCoreApplication::exec();
|
||||
|
||||
observer.TestOk();
|
||||
|
||||
uint64_t size;
|
||||
TEST(!base::GetFileSize(RESUME_FILENAME, size), ("No resume file on success"));
|
||||
}
|
||||
|
||||
// 2nd step - mark some file blocks as not downloaded
|
||||
{
|
||||
// to substitute temporary not fully downloaded file
|
||||
TEST(base::RenameFileX(FILENAME, DOWNLOADING_FILENAME), ());
|
||||
|
||||
FileWriter f(DOWNLOADING_FILENAME, FileWriter::OP_WRITE_EXISTING);
|
||||
f.Seek(beg1);
|
||||
char b1[end1 - beg1 + 1] = {0};
|
||||
f.Write(b1, ARRAY_SIZE(b1));
|
||||
|
||||
f.Seek(beg2);
|
||||
char b2[end2 - beg2 + 1] = {0};
|
||||
f.Write(b2, ARRAY_SIZE(b2));
|
||||
|
||||
ChunksDownloadStrategy strategy((vector<string>()));
|
||||
strategy.AddChunk(std::make_pair(int64_t(0), beg1 - 1), ChunksDownloadStrategy::CHUNK_COMPLETE);
|
||||
strategy.AddChunk(std::make_pair(beg1, end1), ChunksDownloadStrategy::CHUNK_FREE);
|
||||
strategy.AddChunk(std::make_pair(end1 + 1, beg2 - 1), ChunksDownloadStrategy::CHUNK_COMPLETE);
|
||||
strategy.AddChunk(std::make_pair(beg2, end2), ChunksDownloadStrategy::CHUNK_FREE);
|
||||
|
||||
strategy.SaveChunks(kBigFileSize, RESUME_FILENAME);
|
||||
}
|
||||
|
||||
// 3rd step - check that resume works
|
||||
{
|
||||
ResumeChecker checker;
|
||||
std::unique_ptr<HttpRequest> const request(HttpRequest::GetFile(urls, FILENAME, kBigFileSize,
|
||||
bind(&ResumeChecker::OnFinish, &checker, _1),
|
||||
bind(&ResumeChecker::OnProgress, &checker, _1)));
|
||||
QCoreApplication::exec();
|
||||
|
||||
FinishDownloadSuccess(FILENAME);
|
||||
}
|
||||
}
|
||||
|
||||
// Unit test with forcible canceling of http request
|
||||
UNIT_TEST(DownloadResumeChunksWithCancel)
|
||||
{
|
||||
string const FILENAME = "some_test_filename_12345";
|
||||
|
||||
// remove data from previously failed files
|
||||
DeleteTempDownloadFiles();
|
||||
|
||||
vector<string> urls = {kTestUrlBigFile};
|
||||
|
||||
DownloadObserver observer;
|
||||
|
||||
int arrCancelChunks[] = {1, 3, 10, 15, 20, 0};
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(arrCancelChunks); ++i)
|
||||
{
|
||||
if (arrCancelChunks[i] > 0)
|
||||
observer.CancelDownloadOnGivenChunk(arrCancelChunks[i]);
|
||||
|
||||
std::unique_ptr<HttpRequest> const request(
|
||||
HttpRequest::GetFile(urls, FILENAME, kBigFileSize, bind(&DownloadObserver::OnDownloadFinish, &observer, _1),
|
||||
bind(&DownloadObserver::OnDownloadProgress, &observer, _1), 1024, false));
|
||||
|
||||
QCoreApplication::exec();
|
||||
}
|
||||
|
||||
observer.TestOk();
|
||||
|
||||
FinishDownloadSuccess(FILENAME);
|
||||
}
|
||||
|
||||
} // namespace downloader_test
|
||||
125
libs/platform/platform_tests/downloader_utils_tests.cpp
Normal file
125
libs/platform/platform_tests/downloader_utils_tests.cpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/downloader_utils.hpp"
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
#include "platform/mwm_version.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/servers_list.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
|
||||
UNIT_TEST(Downloader_GetFilePathByUrl)
|
||||
{
|
||||
{
|
||||
std::string const mwmName = "Luna";
|
||||
std::string const fileName = platform::GetFileName(mwmName, MapFileType::Map);
|
||||
int64_t const dataVersion = version::FOR_TESTING_MWM1;
|
||||
int64_t const diffVersion = 0;
|
||||
MapFileType const fileType = MapFileType::Map;
|
||||
|
||||
auto const path = platform::GetFileDownloadPath(dataVersion, mwmName, fileType);
|
||||
|
||||
auto const url = downloader::GetFileDownloadUrl(fileName, dataVersion, diffVersion);
|
||||
auto const resultPath = downloader::GetFilePathByUrl(url);
|
||||
|
||||
TEST_EQUAL(path, resultPath, ());
|
||||
}
|
||||
{
|
||||
std::string const mwmName = "Luna";
|
||||
std::string const fileName = platform::GetFileName(mwmName, MapFileType::Diff);
|
||||
int64_t const dataVersion = version::FOR_TESTING_MWM2;
|
||||
int64_t const diffVersion = version::FOR_TESTING_MWM1;
|
||||
MapFileType const fileType = MapFileType::Diff;
|
||||
|
||||
auto const path = platform::GetFileDownloadPath(dataVersion, mwmName, fileType);
|
||||
|
||||
auto const url = downloader::GetFileDownloadUrl(fileName, dataVersion, diffVersion);
|
||||
auto const resultPath = downloader::GetFilePathByUrl(url);
|
||||
|
||||
TEST_EQUAL(path, resultPath, ());
|
||||
}
|
||||
|
||||
TEST_EQUAL(downloader::GetFilePathByUrl("/maps/220314/Belarus_Brest Region.mwm"),
|
||||
base::JoinPath(GetPlatform().WritableDir(), "220314/Belarus_Brest Region.mwm.ready"), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(Downloader_IsUrlSupported)
|
||||
{
|
||||
std::string const mwmName = "Luna";
|
||||
|
||||
std::string fileName = platform::GetFileName(mwmName, MapFileType::Map);
|
||||
int64_t dataVersion = version::FOR_TESTING_MWM1;
|
||||
int64_t diffVersion = 0;
|
||||
|
||||
auto url = downloader::GetFileDownloadUrl(fileName, dataVersion, diffVersion);
|
||||
TEST(downloader::IsUrlSupported(url), ());
|
||||
TEST(downloader::IsUrlSupported("maps/991215/Luna.mwm"), ());
|
||||
TEST(downloader::IsUrlSupported("maps/0/Luna.mwm"), ());
|
||||
TEST(!downloader::IsUrlSupported("maps/x/Luna.mwm"), ());
|
||||
TEST(!downloader::IsUrlSupported("macarena/0/Luna.mwm"), ());
|
||||
TEST(!downloader::IsUrlSupported("/hack/maps/0/Luna.mwm"), ());
|
||||
TEST(!downloader::IsUrlSupported("0/Luna.mwm"), ());
|
||||
TEST(!downloader::IsUrlSupported("maps/0/Luna"), ());
|
||||
TEST(!downloader::IsUrlSupported("0/Luna.mwm"), ());
|
||||
TEST(!downloader::IsUrlSupported("Luna.mwm"), ());
|
||||
TEST(!downloader::IsUrlSupported("Luna"), ());
|
||||
|
||||
fileName = platform::GetFileName(mwmName, MapFileType::Diff);
|
||||
diffVersion = version::FOR_TESTING_MWM1;
|
||||
url = downloader::GetFileDownloadUrl(fileName, dataVersion, diffVersion);
|
||||
TEST(downloader::IsUrlSupported(url), ());
|
||||
TEST(downloader::IsUrlSupported("diffs/991215/991215/Luna.mwmdiff"), ());
|
||||
TEST(downloader::IsUrlSupported("diffs/0/0/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/x/0/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/0/x/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/x/x/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("beefs/0/0/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/0/0/Luna.mwmdiff.f"), ());
|
||||
TEST(!downloader::IsUrlSupported("maps/diffs/0/0/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/0/0/Luna"), ());
|
||||
TEST(!downloader::IsUrlSupported("0/0/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/0/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/0"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/0/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("diffs/Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("Luna.mwmdiff"), ());
|
||||
TEST(!downloader::IsUrlSupported("Luna"), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(Downloader_ParseMetaConfig)
|
||||
{
|
||||
std::optional<downloader::MetaConfig> cfg;
|
||||
|
||||
TEST((cfg = downloader::ParseMetaConfig(R"([ "https://url1/", "https://url2/" ])")), ());
|
||||
TEST_EQUAL(cfg->m_serversList.size(), 2, ());
|
||||
TEST_EQUAL(cfg->m_serversList[0], "https://url1/", ());
|
||||
TEST_EQUAL(cfg->m_serversList[1], "https://url2/", ());
|
||||
|
||||
TEST((cfg = downloader::ParseMetaConfig(R"(
|
||||
{
|
||||
"servers": [ "https://url1/", "https://url2/" ],
|
||||
"settings": {
|
||||
"DonateUrl": "value1",
|
||||
"NY": "value2",
|
||||
"key3": "value3"
|
||||
}
|
||||
}
|
||||
)")),
|
||||
());
|
||||
TEST_EQUAL(cfg->m_serversList.size(), 2, ());
|
||||
TEST_EQUAL(cfg->m_serversList[0], "https://url1/", ());
|
||||
TEST_EQUAL(cfg->m_serversList[1], "https://url2/", ());
|
||||
TEST_EQUAL(cfg->m_settings.size(), 2, ()); // "key3" is ignored
|
||||
TEST_EQUAL(cfg->m_settings["DonateUrl"], "value1", ());
|
||||
TEST_EQUAL(cfg->m_settings["NY"], "value2", ());
|
||||
|
||||
TEST(!downloader::ParseMetaConfig(R"(broken json)"), ());
|
||||
|
||||
TEST(!downloader::ParseMetaConfig(R"([])"), ());
|
||||
|
||||
TEST(!downloader::ParseMetaConfig(R"({})"), ());
|
||||
|
||||
TEST(!downloader::ParseMetaConfig(R"({"no_servers": "invalid"})"), ());
|
||||
|
||||
TEST(!downloader::ParseMetaConfig(R"({"servers": "invalid"})"), ());
|
||||
}
|
||||
118
libs/platform/platform_tests/duration_tests.cpp
Normal file
118
libs/platform/platform_tests/duration_tests.cpp
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/duration.hpp"
|
||||
|
||||
#include <chrono>
|
||||
|
||||
namespace platform
|
||||
{
|
||||
using std::chrono::duration_cast, std::chrono::seconds, std::chrono::minutes, std::chrono::hours, std::chrono::days;
|
||||
|
||||
struct TestData
|
||||
{
|
||||
struct Duration
|
||||
{
|
||||
days m_days;
|
||||
hours m_hours;
|
||||
minutes m_minutes;
|
||||
seconds m_seconds;
|
||||
std::string result;
|
||||
|
||||
Duration(long days, long hours, long minutes, long seconds, std::string const & result)
|
||||
: m_days(days)
|
||||
, m_hours(hours)
|
||||
, m_minutes(minutes)
|
||||
, m_seconds(seconds)
|
||||
, result(result)
|
||||
{}
|
||||
|
||||
long Seconds() const
|
||||
{
|
||||
return (duration_cast<seconds>(m_days) + duration_cast<seconds>(m_hours) + duration_cast<seconds>(m_minutes) +
|
||||
m_seconds)
|
||||
.count();
|
||||
}
|
||||
};
|
||||
|
||||
Locale m_locale;
|
||||
std::vector<Duration> m_duration;
|
||||
|
||||
constexpr TestData(Locale locale, std::vector<Duration> duration) : m_locale(locale), m_duration(duration) {}
|
||||
};
|
||||
|
||||
Locale GetLocale(std::string const & language)
|
||||
{
|
||||
Locale locale;
|
||||
locale.m_language = language;
|
||||
return locale;
|
||||
}
|
||||
/*
|
||||
Localized string cannot be retrieved from the app target bundle during the tests execution
|
||||
and the platform::GetLocalizedString will return the same string as the input ("minute", "hour" etc).
|
||||
This is why the expectation strings are not explicit.
|
||||
*/
|
||||
|
||||
auto const m = Duration::GetUnitsString(Duration::Units::Minutes);
|
||||
auto const h = Duration::GetUnitsString(Duration::Units::Hours);
|
||||
auto const d = Duration::GetUnitsString(Duration::Units::Days);
|
||||
|
||||
UNIT_TEST(Duration_AllUnits)
|
||||
{
|
||||
TestData const testData[] = {
|
||||
{GetLocale("en"),
|
||||
{{0, 0, 0, 0, "0" + m},
|
||||
{0, 0, 0, 30, "0" + m},
|
||||
{0, 0, 0, 59, "0" + m},
|
||||
{0, 0, 1, 0, "1" + m},
|
||||
{0, 0, 1, 59, "1" + m},
|
||||
{0, 0, 60, 0, "1" + h},
|
||||
{0, 0, 123, 0, "2" + h + kNonBreakingSpace + "3" + m},
|
||||
{0, 3, 0, 0, "3" + h},
|
||||
{0, 24, 0, 0, "1" + d},
|
||||
{4, 0, 0, 0, "4" + d},
|
||||
{1, 2, 3, 0, "1" + d + kNonBreakingSpace + "2" + h + kNonBreakingSpace + "3" + m},
|
||||
{1, 0, 15, 0, "1" + d + kNonBreakingSpace + "15" + m},
|
||||
{0, 15, 1, 0, "15" + h + kNonBreakingSpace + "1" + m},
|
||||
{1, 15, 0, 0, "1" + d + kNonBreakingSpace + "15" + h},
|
||||
{15, 0, 10, 0, "15" + d + kNonBreakingSpace + "10" + m},
|
||||
{15, 15, 15, 0, "15" + d + kNonBreakingSpace + "15" + h + kNonBreakingSpace + "15" + m}}},
|
||||
};
|
||||
|
||||
for (auto const & data : testData)
|
||||
{
|
||||
for (auto const & dataDuration : data.m_duration)
|
||||
{
|
||||
auto const duration = Duration(dataDuration.Seconds());
|
||||
auto durationStr = duration.GetLocalizedString(
|
||||
{Duration::Units::Days, Duration::Units::Hours, Duration::Units::Minutes}, data.m_locale);
|
||||
TEST_EQUAL(durationStr, dataDuration.result, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(Duration_Localization)
|
||||
{
|
||||
TestData const testData[] = {
|
||||
// en
|
||||
{GetLocale("en"), {{1, 2, 3, 0, "1" + d + kNonBreakingSpace + "2" + h + kNonBreakingSpace + "3" + m}}},
|
||||
// ru (narrow spacing between number and unit)
|
||||
{GetLocale("ru"),
|
||||
{{1, 2, 3, 0,
|
||||
"1" + kNarrowNonBreakingSpace + d + kNonBreakingSpace + "2" + kNarrowNonBreakingSpace + h + kNonBreakingSpace +
|
||||
"3" + kNarrowNonBreakingSpace + m}}},
|
||||
// zh (no spacings)
|
||||
{GetLocale("zh"), {{1, 2, 3, 0, "1" + d + "2" + h + "3" + m}}}};
|
||||
|
||||
for (auto const & data : testData)
|
||||
{
|
||||
for (auto const & duration : data.m_duration)
|
||||
{
|
||||
auto const durationStr =
|
||||
Duration(duration.Seconds())
|
||||
.GetLocalizedString({Duration::Units::Days, Duration::Units::Hours, Duration::Units::Minutes},
|
||||
data.m_locale);
|
||||
TEST_EQUAL(durationStr, duration.result, ());
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace platform
|
||||
120
libs/platform/platform_tests/get_text_by_id_tests.cpp
Normal file
120
libs/platform/platform_tests/get_text_by_id_tests.cpp
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/get_text_by_id.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
UNIT_TEST(GetTextByIdEnglishTest)
|
||||
{
|
||||
std::string const shortJson =
|
||||
"\
|
||||
{\
|
||||
\"make_a_slight_right_turn\":\"Make a slight right turn.\",\
|
||||
\"in_900_meters\":\"In nine hundred meters.\",\
|
||||
\"then\":\"Then.\",\
|
||||
\"in_1_mile\":\"In one mile.\"\
|
||||
}";
|
||||
|
||||
auto getEnglish = platform::ForTestingGetTextByIdFactory(shortJson, "en");
|
||||
TEST_EQUAL((*getEnglish)("make_a_slight_right_turn"), "Make a slight right turn.", ());
|
||||
TEST_EQUAL((*getEnglish)("in_900_meters"), "In nine hundred meters.", ());
|
||||
TEST_EQUAL((*getEnglish)("then"), "Then.", ());
|
||||
TEST_EQUAL((*getEnglish)("in_1_mile"), "In one mile.", ());
|
||||
|
||||
TEST_EQUAL((*getEnglish)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getEnglish)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getEnglish)(""), "", ());
|
||||
TEST_EQUAL((*getEnglish)(" "), "", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GetTextByIdRussianTest)
|
||||
{
|
||||
std::string const shortJson =
|
||||
"\
|
||||
{\
|
||||
\"in_800_meters\":\"Через восемьсот метров.\",\
|
||||
\"make_a_slight_right_turn\":\"Держитесь правее.\",\
|
||||
\"take_the_6_exit\":\"Сверните на шестой съезд.\",\
|
||||
\"in_1_mile\":\"Через одну милю.\"\
|
||||
}";
|
||||
|
||||
auto getRussian = platform::ForTestingGetTextByIdFactory(shortJson, "ru");
|
||||
TEST_EQUAL((*getRussian)("in_800_meters"), "Через восемьсот метров.", ());
|
||||
TEST_EQUAL((*getRussian)("make_a_slight_right_turn"), "Держитесь правее.", ());
|
||||
TEST_EQUAL((*getRussian)("take_the_6_exit"), "Сверните на шестой съезд.", ());
|
||||
TEST_EQUAL((*getRussian)("in_1_mile"), "Через одну милю.", ());
|
||||
|
||||
TEST_EQUAL((*getRussian)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getRussian)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getRussian)(""), "", ());
|
||||
TEST_EQUAL((*getRussian)(" "), "", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GetTextByIdKoreanTest)
|
||||
{
|
||||
std::string const shortJson =
|
||||
"\
|
||||
{\
|
||||
\"in_700_meters\":\"700 미터 앞\",\
|
||||
\"make_a_right_turn\":\"우회전입니다.\",\
|
||||
\"take_the_5_exit\":\"다섯 번째 출구입니다.\",\
|
||||
\"in_5000_feet\":\"5000피트 앞\"\
|
||||
}";
|
||||
|
||||
auto getKorean = platform::ForTestingGetTextByIdFactory(shortJson, "ko");
|
||||
TEST_EQUAL((*getKorean)("in_700_meters"), "700 미터 앞", ());
|
||||
TEST_EQUAL((*getKorean)("make_a_right_turn"), "우회전입니다.", ());
|
||||
TEST_EQUAL((*getKorean)("take_the_5_exit"), "다섯 번째 출구입니다.", ());
|
||||
TEST_EQUAL((*getKorean)("in_5000_feet"), "5000피트 앞", ());
|
||||
|
||||
TEST_EQUAL((*getKorean)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getKorean)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getKorean)(""), "", ());
|
||||
TEST_EQUAL((*getKorean)(" "), "", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GetTextByIdArabicTest)
|
||||
{
|
||||
std::string const shortJson =
|
||||
"\
|
||||
{\
|
||||
\"in_1_kilometer\":\"بعد كيلو متر واحدٍ\",\
|
||||
\"leave_the_roundabout\":\"اخرج من الطريق الدوار\",\
|
||||
\"take_the_3_exit\":\"اسلك المخرج الثالث\",\
|
||||
\"in_4000_feet\":\"بعد 4000 قدم\"\
|
||||
}";
|
||||
|
||||
auto getArabic = platform::ForTestingGetTextByIdFactory(shortJson, "ar");
|
||||
TEST_EQUAL((*getArabic)("in_1_kilometer"), "بعد كيلو متر واحدٍ", ());
|
||||
TEST_EQUAL((*getArabic)("leave_the_roundabout"), "اخرج من الطريق الدوار", ());
|
||||
TEST_EQUAL((*getArabic)("take_the_3_exit"), "اسلك المخرج الثالث", ());
|
||||
TEST_EQUAL((*getArabic)("in_4000_feet"), "بعد 4000 قدم", ());
|
||||
|
||||
TEST_EQUAL((*getArabic)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getArabic)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getArabic)(""), "", ());
|
||||
TEST_EQUAL((*getArabic)(" "), "", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GetTextByIdFrenchTest)
|
||||
{
|
||||
std::string const shortJson =
|
||||
"\
|
||||
{\
|
||||
\"in_1_5_kilometers\":\"Dans un virgule cinq kilomètre.\",\
|
||||
\"enter_the_roundabout\":\"Prenez le rond-point.\",\
|
||||
\"take_the_2_exit\":\"Prenez la deuxième sortie.\",\
|
||||
\"in_3500_feet\":\"Dans trois mille cinq cents pieds.\"\
|
||||
}";
|
||||
|
||||
auto getFrench = platform::ForTestingGetTextByIdFactory(shortJson, "fr");
|
||||
TEST_EQUAL((*getFrench)("in_1_5_kilometers"), "Dans un virgule cinq kilomètre.", ());
|
||||
TEST_EQUAL((*getFrench)("enter_the_roundabout"), "Prenez le rond-point.", ());
|
||||
TEST_EQUAL((*getFrench)("take_the_2_exit"), "Prenez la deuxième sortie.", ());
|
||||
TEST_EQUAL((*getFrench)("in_3500_feet"), "Dans trois mille cinq cents pieds.", ());
|
||||
|
||||
TEST_EQUAL((*getFrench)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getFrench)("some_nonexistent_key"), "", ());
|
||||
TEST_EQUAL((*getFrench)(""), "", ());
|
||||
TEST_EQUAL((*getFrench)(" "), "", ());
|
||||
}
|
||||
90
libs/platform/platform_tests/glaze_test.cpp
Normal file
90
libs/platform/platform_tests/glaze_test.cpp
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "glaze/json.hpp"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
enum class LocationSource
|
||||
{
|
||||
GPS,
|
||||
Network,
|
||||
Unknown
|
||||
};
|
||||
|
||||
// If enumeration should be represented as string instead of integer in JSON,
|
||||
// then enum should be declared in the global namespace.
|
||||
template <>
|
||||
struct glz::meta<LocationSource>
|
||||
{
|
||||
using enum LocationSource;
|
||||
static constexpr auto value = glz::enumerate(GPS, Network, Unknown);
|
||||
};
|
||||
|
||||
// For our test macros.
|
||||
std::string DebugPrint(LocationSource source)
|
||||
{
|
||||
return std::string{glz::get_enum_name(source)};
|
||||
}
|
||||
|
||||
namespace glaze_test
|
||||
{
|
||||
struct Location
|
||||
{
|
||||
LocationSource source;
|
||||
double latitude;
|
||||
double longitude;
|
||||
double accuracy;
|
||||
std::optional<double> altitude;
|
||||
std::optional<double> altitudeAccuracy;
|
||||
};
|
||||
|
||||
struct AccessToken
|
||||
{
|
||||
Location location;
|
||||
std::string accessToken;
|
||||
};
|
||||
|
||||
UNIT_TEST(Glaze_Smoke)
|
||||
{
|
||||
std::string_view constexpr fullJsonWithComments = R"({
|
||||
"location": {
|
||||
"source": "GPS", // Where the location data comes from.
|
||||
"latitude": 47.3345141,
|
||||
"longitude": 8.5312839,
|
||||
"accuracy": 22,
|
||||
"altitude": 100.5,
|
||||
"altitudeAccuracy": 5.0
|
||||
},
|
||||
/* Access token for authentication
|
||||
* This token is used to access protected resources.
|
||||
*/
|
||||
"accessToken": "2:vC65Xv0mxMtsNVf4:hY5YSIkuFfnAU77z"
|
||||
})";
|
||||
|
||||
AccessToken token;
|
||||
auto error = glz::read_jsonc(token, fullJsonWithComments);
|
||||
TEST(!error, (glz::format_error(error, fullJsonWithComments)));
|
||||
|
||||
TEST_EQUAL(token.location.source, LocationSource::GPS, ());
|
||||
TEST_EQUAL(token.location.latitude, 47.3345141, ());
|
||||
TEST_EQUAL(token.location.longitude, 8.5312839, ());
|
||||
TEST_EQUAL(token.location.accuracy, 22.0, ());
|
||||
TEST_EQUAL(token.location.altitude, 100.5, ());
|
||||
TEST_EQUAL(token.location.altitudeAccuracy, 5.0, ());
|
||||
TEST_EQUAL(token.accessToken, "2:vC65Xv0mxMtsNVf4:hY5YSIkuFfnAU77z", ());
|
||||
|
||||
std::string_view constexpr partialJson =
|
||||
R"({"location":{"source":"Network","latitude":47.3345141,"longitude":8.5312839,"accuracy":22},"accessToken":""})";
|
||||
|
||||
token.location.source = LocationSource::Network;
|
||||
token.location.altitude = {};
|
||||
token.location.altitudeAccuracy = {};
|
||||
token.accessToken = {};
|
||||
|
||||
std::string buffer;
|
||||
error = glz::write_json(token, buffer);
|
||||
TEST(!error, (glz::format_error(error, "Failed to write JSON")));
|
||||
TEST_EQUAL(buffer, partialJson, ());
|
||||
}
|
||||
} // namespace glaze_test
|
||||
45
libs/platform/platform_tests/jansson_test.cpp
Normal file
45
libs/platform/platform_tests/jansson_test.cpp
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "cppjansson/cppjansson.hpp"
|
||||
|
||||
UNIT_TEST(Jansson_Smoke)
|
||||
{
|
||||
char * savedLocale = setlocale(LC_NUMERIC, "C");
|
||||
|
||||
// char const * str = "{\"location\":{\"latitude\":47.383333,\"longitude\":8.533333,"
|
||||
// "\"accuracy\":18000.0},\"access_token\":\"2:6aOjM2IAoPMaweWN:txhu5LpkRkLVb3u3\"}";
|
||||
|
||||
char const * str =
|
||||
"{\"location\":{\"latitude\":47.3345141,\"longitude\":8.5312839,"
|
||||
"\"accuracy\":22.0},\"access_token\":\"2:vC65Xv0mxMtsNVf4:hY5YSIkuFfnAU77z\"}";
|
||||
|
||||
base::Json root(str);
|
||||
TEST(json_is_object(root.get()), ());
|
||||
json_t * location = json_object_get(root.get(), "location");
|
||||
TEST(json_is_object(location), ());
|
||||
|
||||
json_t * lat = json_object_get(location, "latitude");
|
||||
TEST(json_is_real(lat), ());
|
||||
TEST_ALMOST_EQUAL_ULPS(json_real_value(lat), 47.3345141, ());
|
||||
|
||||
json_t * lon = json_object_get(location, "longitude");
|
||||
TEST(json_is_real(lon), ());
|
||||
TEST_ALMOST_EQUAL_ULPS(json_real_value(lon), 8.5312839, ());
|
||||
|
||||
json_t * acc = json_object_get(location, "accuracy");
|
||||
TEST(json_is_real(acc), ());
|
||||
TEST_ALMOST_EQUAL_ULPS(json_real_value(acc), 22.0, ());
|
||||
|
||||
bool wasException = false;
|
||||
try
|
||||
{
|
||||
base::Json invalid("{asd]");
|
||||
}
|
||||
catch (base::Json::Exception const &)
|
||||
{
|
||||
wasException = true;
|
||||
}
|
||||
TEST(wasException, ());
|
||||
|
||||
setlocale(LC_NUMERIC, savedLocale);
|
||||
}
|
||||
33
libs/platform/platform_tests/language_test.cpp
Normal file
33
libs/platform/platform_tests/language_test.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/preferred_languages.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
UNIT_TEST(LangNormalize_Smoke)
|
||||
{
|
||||
char const * arr1[] = {"en", "en-GB", "zh", "es-SP", "zh-penyn", "en-US", "ru_RU", "es"};
|
||||
char const * arr2[] = {"en", "en", "zh", "es", "zh", "en", "ru", "es"};
|
||||
static_assert(ARRAY_SIZE(arr1) == ARRAY_SIZE(arr2), "");
|
||||
|
||||
for (size_t i = 0; i < ARRAY_SIZE(arr1); ++i)
|
||||
TEST_EQUAL(arr2[i], languages::Normalize(arr1[i]), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(PrefLanguages_Smoke)
|
||||
{
|
||||
std::string s = languages::GetPreferred();
|
||||
TEST(!s.empty(), ());
|
||||
LOG(LINFO, ("Preferred langs:", s));
|
||||
|
||||
s = languages::GetCurrentOrig();
|
||||
TEST(!s.empty(), ());
|
||||
LOG(LINFO, ("Current original lang:", s));
|
||||
|
||||
s = languages::GetCurrentNorm();
|
||||
TEST(!s.empty(), ());
|
||||
LOG(LINFO, ("Current normalized lang:", s));
|
||||
}
|
||||
349
libs/platform/platform_tests/local_country_file_tests.cpp
Normal file
349
libs/platform/platform_tests/local_country_file_tests.cpp
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/country_file.hpp"
|
||||
#include "platform/local_country_file.hpp"
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
#include "platform/mwm_version.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/platform_tests_support/scoped_dir.hpp"
|
||||
#include "platform/platform_tests_support/scoped_file.hpp"
|
||||
#include "platform/settings.hpp"
|
||||
|
||||
#include "coding/file_writer.hpp"
|
||||
#include "coding/internal/file_data.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/scope_guard.hpp"
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
namespace local_country_file_tests
|
||||
{
|
||||
using platform::CountryFile;
|
||||
using platform::LocalCountryFile;
|
||||
using platform::tests_support::ScopedDir;
|
||||
using platform::tests_support::ScopedFile;
|
||||
|
||||
// Checks that all unsigned numbers less than 10 ^ 18 can be parsed as
|
||||
// a timestamp.
|
||||
UNIT_TEST(LocalCountryFile_ParseVersion)
|
||||
{
|
||||
using namespace platform;
|
||||
|
||||
int64_t version = 0;
|
||||
TEST(ParseVersion("1", version), ());
|
||||
TEST_EQUAL(version, 1, ());
|
||||
|
||||
TEST(ParseVersion("141010", version), ());
|
||||
TEST_EQUAL(version, 141010, ());
|
||||
|
||||
TEST(ParseVersion("150309", version), ());
|
||||
TEST_EQUAL(version, 150309, ());
|
||||
|
||||
TEST(!ParseVersion("2111225", version), ()); // too many digits
|
||||
TEST(!ParseVersion("00000000000000000000000000000000123", version), ());
|
||||
|
||||
TEST(!ParseVersion("", version), ());
|
||||
TEST(!ParseVersion("150309 ", version), ());
|
||||
TEST(!ParseVersion(" 150309", version), ());
|
||||
TEST(!ParseVersion("-150309", version), ());
|
||||
TEST(!ParseVersion("just string", version), ());
|
||||
}
|
||||
|
||||
// Checks basic functionality of LocalCountryFile.
|
||||
UNIT_TEST(LocalCountryFile_Smoke)
|
||||
{
|
||||
CountryFile countryFile("TestCountry", 1 /* size */, "sha1");
|
||||
LocalCountryFile localFile("/test-dir", countryFile, 150309);
|
||||
|
||||
TEST_EQUAL("/test-dir/TestCountry" DATA_FILE_EXTENSION, localFile.GetPath(MapFileType::Map), ());
|
||||
|
||||
// Not synced with disk yet.
|
||||
TEST(!localFile.HasFiles(), ());
|
||||
|
||||
TEST(!localFile.OnDisk(MapFileType::Map), ());
|
||||
TEST(!localFile.OnDisk(MapFileType::Diff), ());
|
||||
|
||||
TEST_EQUAL("/test-dir", localFile.GetDirectory(), ());
|
||||
|
||||
TEST_EQUAL(0, localFile.GetSize(MapFileType::Map), ());
|
||||
TEST_EQUAL(0, localFile.GetSize(MapFileType::Diff), ());
|
||||
|
||||
TEST_EQUAL(150309, localFile.GetVersion(), ());
|
||||
}
|
||||
|
||||
// Creates test country map file and checks sync-with-disk functionality.
|
||||
UNIT_TEST(LocalCountryFile_DiskFiles)
|
||||
{
|
||||
Platform & platform = GetPlatform();
|
||||
|
||||
CountryFile countryFile("TestCountry", 1 /* size */, "sha1");
|
||||
|
||||
for (int64_t version : {1, 150312})
|
||||
{
|
||||
LocalCountryFile localFile(platform.WritableDir(), countryFile, version);
|
||||
TEST(!localFile.OnDisk(MapFileType::Map), ());
|
||||
TEST(!localFile.OnDisk(MapFileType::Diff), ());
|
||||
|
||||
std::string const mapFileName = countryFile.GetFileName(MapFileType::Map);
|
||||
std::string const mapFileContents("map");
|
||||
ScopedFile testMapFile(mapFileName, mapFileContents);
|
||||
|
||||
localFile.SyncWithDisk();
|
||||
TEST(localFile.OnDisk(MapFileType::Map), ());
|
||||
TEST(!localFile.OnDisk(MapFileType::Diff), ());
|
||||
TEST_EQUAL(mapFileContents.size(), localFile.GetSize(MapFileType::Map), ());
|
||||
|
||||
localFile.SyncWithDisk();
|
||||
TEST(localFile.OnDisk(MapFileType::Map), ());
|
||||
TEST_EQUAL(mapFileContents.size(), localFile.GetSize(MapFileType::Map), ());
|
||||
|
||||
localFile.DeleteFromDisk(MapFileType::Map);
|
||||
TEST(!testMapFile.Exists(), (testMapFile, "wasn't deleted by LocalCountryFile."));
|
||||
testMapFile.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(LocalCountryFile_CleanupMapFiles)
|
||||
{
|
||||
Platform & platform = GetPlatform();
|
||||
std::string const mapsDir = platform.WritableDir();
|
||||
|
||||
// Two fake directories for test country files and indexes.
|
||||
ScopedDir dir3("3");
|
||||
ScopedDir dir4("4");
|
||||
|
||||
ScopedDir absentCountryIndexesDir(dir4, "Absent");
|
||||
ScopedDir irelandIndexesDir(dir4, "Ireland");
|
||||
|
||||
CountryFile irelandFile("Ireland");
|
||||
|
||||
LocalCountryFile irelandLocalFile(dir4.GetFullPath(), irelandFile, 4 /* version */);
|
||||
ScopedFile irelandMapFile(dir4, irelandFile, MapFileType::Map);
|
||||
|
||||
// Check FindAllLocalMaps()
|
||||
std::vector<LocalCountryFile> localFiles;
|
||||
FindAllLocalMapsAndCleanup(4 /* latestVersion */, localFiles);
|
||||
TEST(base::IsExist(localFiles, irelandLocalFile), (irelandLocalFile, localFiles));
|
||||
|
||||
irelandLocalFile.SyncWithDisk();
|
||||
TEST(irelandLocalFile.OnDisk(MapFileType::Map), ());
|
||||
irelandLocalFile.DeleteFromDisk(MapFileType::Map);
|
||||
TEST(!irelandMapFile.Exists(), (irelandMapFile));
|
||||
irelandMapFile.Reset();
|
||||
|
||||
TEST(!dir3.Exists(), ("Empty directory", dir3, "wasn't removed."));
|
||||
dir3.Reset();
|
||||
|
||||
TEST(dir4.Exists(), ());
|
||||
|
||||
// Useless CountryIndexes::DeleteFromDisk calls are removed.
|
||||
// TEST(!absentCountryIndexesDir.Exists(), ("Indexes for absent country weren't deleted."));
|
||||
// absentCountryIndexesDir.Reset();
|
||||
|
||||
TEST(irelandIndexesDir.Exists(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(LocalCountryFile_CleanupPartiallyDownloadedFiles)
|
||||
{
|
||||
ScopedDir dataDir("101008");
|
||||
auto const DataFilePath = [&dataDir](char const * file) { return dataDir.GetRelativePath() + "/" + file; };
|
||||
|
||||
ScopedDir oldDir("101009");
|
||||
ScopedDir latestDir("101010");
|
||||
|
||||
ScopedFile toBeDeleted[] = {
|
||||
{DataFilePath("Ireland.mwm.ready"), ScopedFile::Mode::Create},
|
||||
{DataFilePath("Netherlands.mwm.routing.downloading2"), ScopedFile::Mode::Create},
|
||||
{DataFilePath("Germany.mwm.ready3"), ScopedFile::Mode::Create},
|
||||
{DataFilePath("UK_England.mwm.resume4"), ScopedFile::Mode::Create},
|
||||
{base::JoinPath(oldDir.GetRelativePath(), "Russia_Central.mwm.downloading"), ScopedFile::Mode::Create}};
|
||||
|
||||
ScopedFile toBeKept[] = {
|
||||
{DataFilePath("Italy.mwm"), ScopedFile::Mode::Create},
|
||||
{DataFilePath("Spain.mwm"), ScopedFile::Mode::Create},
|
||||
{DataFilePath("Spain.mwm.routing"), ScopedFile::Mode::Create},
|
||||
{base::JoinPath(latestDir.GetRelativePath(), "Russia_Southern.mwm.downloading"), ScopedFile::Mode::Create}};
|
||||
|
||||
platform::CleanupMapsDirectory(101010 /* latestVersion */);
|
||||
|
||||
for (ScopedFile & file : toBeDeleted)
|
||||
{
|
||||
TEST(!file.Exists(), (file));
|
||||
file.Reset();
|
||||
}
|
||||
TEST(!oldDir.Exists(), (oldDir));
|
||||
oldDir.Reset();
|
||||
|
||||
for (ScopedFile & file : toBeKept)
|
||||
TEST(file.Exists(), (file));
|
||||
|
||||
TEST(latestDir.Exists(), (latestDir));
|
||||
TEST(dataDir.Exists(), (dataDir));
|
||||
}
|
||||
|
||||
// Creates test-dir and following files:
|
||||
// * test-dir/Ireland.mwm
|
||||
// * test-dir/Netherlands.mwm
|
||||
// * test-dir/Netherlands.mwm.routing
|
||||
// After that, checks that FindAllLocalMapsInDirectory() correctly finds all created files.
|
||||
UNIT_TEST(LocalCountryFile_DirectoryLookup)
|
||||
{
|
||||
// This tests creates a map file for Ireland and map + routing files
|
||||
// for Netherlands in a test directory.
|
||||
CountryFile const irelandFile("Ireland");
|
||||
CountryFile const netherlandsFile("Netherlands");
|
||||
|
||||
ScopedDir testDir("test-dir");
|
||||
|
||||
ScopedFile testIrelandMapFile(testDir, irelandFile, MapFileType::Map);
|
||||
ScopedFile testNetherlandsMapFile(testDir, netherlandsFile, MapFileType::Map);
|
||||
|
||||
std::vector<LocalCountryFile> localFiles;
|
||||
FindAllLocalMapsInDirectoryAndCleanup(testDir.GetFullPath(), 150309 /* version */, -1 /* latestVersion */,
|
||||
localFiles);
|
||||
std::sort(localFiles.begin(), localFiles.end());
|
||||
for (LocalCountryFile & localFile : localFiles)
|
||||
localFile.SyncWithDisk();
|
||||
|
||||
LocalCountryFile expectedIrelandFile(testDir.GetFullPath(), irelandFile, 150309);
|
||||
expectedIrelandFile.SyncWithDisk();
|
||||
|
||||
LocalCountryFile expectedNetherlandsFile(testDir.GetFullPath(), netherlandsFile, 150309);
|
||||
expectedNetherlandsFile.SyncWithDisk();
|
||||
|
||||
std::vector expectedLocalFiles = {expectedIrelandFile, expectedNetherlandsFile};
|
||||
std::sort(expectedLocalFiles.begin(), expectedLocalFiles.end());
|
||||
TEST_EQUAL(expectedLocalFiles, localFiles, ());
|
||||
}
|
||||
|
||||
// Creates directory 010101 and 010101/Italy.mwm file. After that,
|
||||
// checks that this file will be recognized as a map file for Italy
|
||||
// with version 010101. Also, checks that World.mwm and
|
||||
// WorldCoasts.mwm exist in writable dir.
|
||||
UNIT_TEST(LocalCountryFile_AllLocalFilesLookup)
|
||||
{
|
||||
CountryFile const italyFile("Italy");
|
||||
|
||||
ScopedDir testDir("10101");
|
||||
|
||||
settings::Delete("LastMigration");
|
||||
|
||||
ScopedFile testItalyMapFile(testDir, italyFile, MapFileType::Map);
|
||||
|
||||
std::vector<LocalCountryFile> localFiles;
|
||||
FindAllLocalMapsAndCleanup(10101 /* latestVersion */, localFiles);
|
||||
std::multiset<LocalCountryFile> localFilesSet(localFiles.begin(), localFiles.end());
|
||||
|
||||
bool worldFound = false;
|
||||
bool worldCoastsFound = false;
|
||||
for (auto const & file : localFiles)
|
||||
{
|
||||
// With the new concepts, World mwm files have valid version.
|
||||
if (file.GetCountryName() == WORLD_FILE_NAME)
|
||||
{
|
||||
worldFound = true;
|
||||
TEST_NOT_EQUAL(0, file.GetVersion(), (file));
|
||||
}
|
||||
if (file.GetCountryName() == WORLD_COASTS_FILE_NAME)
|
||||
{
|
||||
worldCoastsFound = true;
|
||||
TEST_NOT_EQUAL(0, file.GetVersion(), (file));
|
||||
}
|
||||
}
|
||||
TEST(worldFound, ());
|
||||
TEST(worldCoastsFound, ());
|
||||
|
||||
LocalCountryFile expectedItalyFile(testDir.GetFullPath(), italyFile, 10101);
|
||||
TEST_EQUAL(1, localFilesSet.count(expectedItalyFile), (localFiles));
|
||||
}
|
||||
|
||||
UNIT_TEST(LocalCountryFile_PreparePlaceForCountryFiles)
|
||||
{
|
||||
Platform & platform = GetPlatform();
|
||||
|
||||
CountryFile italyFile("Italy");
|
||||
LocalCountryFile expectedItalyFile(platform.WritableDir(), italyFile, 0 /* version */);
|
||||
auto italyLocalFile = PreparePlaceForCountryFiles(0 /* version */, italyFile);
|
||||
TEST(italyLocalFile.get(), ());
|
||||
TEST_EQUAL(expectedItalyFile, *italyLocalFile, ());
|
||||
|
||||
ScopedDir directoryForV1("1");
|
||||
|
||||
CountryFile germanyFile("Germany");
|
||||
LocalCountryFile expectedGermanyFile(directoryForV1.GetFullPath(), germanyFile, 1 /* version */);
|
||||
auto germanyLocalFile = PreparePlaceForCountryFiles(1 /* version */, germanyFile);
|
||||
TEST(germanyLocalFile.get(), ());
|
||||
TEST_EQUAL(expectedGermanyFile, *germanyLocalFile, ());
|
||||
|
||||
CountryFile franceFile("France");
|
||||
LocalCountryFile expectedFranceFile(directoryForV1.GetFullPath(), franceFile, 1 /* version */);
|
||||
auto franceLocalFile = PreparePlaceForCountryFiles(1 /* version */, franceFile);
|
||||
TEST(franceLocalFile.get(), ());
|
||||
TEST_EQUAL(expectedFranceFile, *franceLocalFile, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(LocalCountryFile_CountryIndexes)
|
||||
{
|
||||
using platform::CountryIndexes;
|
||||
|
||||
ScopedDir testDir("101010");
|
||||
|
||||
CountryFile germanyFile("Germany");
|
||||
LocalCountryFile germanyLocalFile(testDir.GetFullPath(), germanyFile, 101010 /* version */);
|
||||
TEST_EQUAL(base::JoinPath(germanyLocalFile.GetDirectory(), germanyFile.GetName()),
|
||||
CountryIndexes::IndexesDir(germanyLocalFile), ());
|
||||
CountryIndexes::PreparePlaceOnDisk(germanyLocalFile);
|
||||
|
||||
auto const bitsPath = CountryIndexes::GetPath(germanyLocalFile, CountryIndexes::Index::Bits);
|
||||
TEST(!Platform::IsFileExistsByFullPath(bitsPath), (bitsPath));
|
||||
{
|
||||
FileWriter writer(bitsPath);
|
||||
std::string const contents = "bits index";
|
||||
writer.Write(contents.data(), contents.size());
|
||||
}
|
||||
TEST(Platform::IsFileExistsByFullPath(bitsPath), (bitsPath));
|
||||
|
||||
TEST(CountryIndexes::DeleteFromDisk(germanyLocalFile), ("Can't delete indexes for:", germanyLocalFile));
|
||||
|
||||
TEST(!Platform::IsFileExistsByFullPath(bitsPath), (bitsPath));
|
||||
}
|
||||
|
||||
UNIT_TEST(LocalCountryFile_DoNotDeleteUserFiles)
|
||||
{
|
||||
using platform::CountryIndexes;
|
||||
|
||||
base::ScopedLogLevelChanger const criticalLogLevel(LCRITICAL);
|
||||
|
||||
ScopedDir testDir("101010");
|
||||
|
||||
CountryFile germanyFile("Germany");
|
||||
LocalCountryFile germanyLocalFile(testDir.GetFullPath(), germanyFile, 101010 /* version */);
|
||||
CountryIndexes::PreparePlaceOnDisk(germanyLocalFile);
|
||||
|
||||
auto const userFilePath = base::JoinPath(CountryIndexes::IndexesDir(germanyLocalFile), "user-data.txt");
|
||||
{
|
||||
FileWriter writer(userFilePath);
|
||||
std::string const data = "user data";
|
||||
writer.Write(data.data(), data.size());
|
||||
}
|
||||
TEST(!CountryIndexes::DeleteFromDisk(germanyLocalFile), ("Indexes dir should not be deleted for:", germanyLocalFile));
|
||||
|
||||
TEST(base::DeleteFileX(userFilePath), ("Can't delete test file:", userFilePath));
|
||||
TEST(CountryIndexes::DeleteFromDisk(germanyLocalFile), ("Can't delete indexes for:", germanyLocalFile));
|
||||
}
|
||||
|
||||
UNIT_TEST(LocalCountryFile_MakeTemporary)
|
||||
{
|
||||
auto const path = GetPlatform().WritablePathForFile("minsk-pass" DATA_FILE_EXTENSION);
|
||||
LocalCountryFile file = LocalCountryFile::MakeTemporary(path);
|
||||
TEST_EQUAL(file.GetPath(MapFileType::Map), path, ());
|
||||
}
|
||||
|
||||
} // namespace local_country_file_tests
|
||||
39
libs/platform/platform_tests/location_test.cpp
Normal file
39
libs/platform/platform_tests/location_test.cpp
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/location.hpp"
|
||||
|
||||
UNIT_TEST(IsLatValid)
|
||||
{
|
||||
TEST(location::IsLatValid(35.), ());
|
||||
TEST(location::IsLatValid(-35.), ());
|
||||
TEST(!location::IsLatValid(0.), ());
|
||||
TEST(!location::IsLatValid(100.), ());
|
||||
TEST(!location::IsLatValid(-99.), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(IsLonValid)
|
||||
{
|
||||
TEST(location::IsLonValid(135.), ());
|
||||
TEST(location::IsLonValid(-35.), ());
|
||||
TEST(!location::IsLonValid(0.), ());
|
||||
TEST(!location::IsLonValid(200.), ());
|
||||
TEST(!location::IsLonValid(-199.), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(AngleToBearing)
|
||||
{
|
||||
TEST_ALMOST_EQUAL_ULPS(location::AngleToBearing(0.), 90., ());
|
||||
TEST_ALMOST_EQUAL_ULPS(location::AngleToBearing(30.), 60., ());
|
||||
TEST_ALMOST_EQUAL_ULPS(location::AngleToBearing(100.), 350., ());
|
||||
TEST_ALMOST_EQUAL_ULPS(location::AngleToBearing(370.), 80., ());
|
||||
TEST_ALMOST_EQUAL_ULPS(location::AngleToBearing(-370.), 100., ());
|
||||
}
|
||||
|
||||
UNIT_TEST(BearingToAngle)
|
||||
{
|
||||
TEST_ALMOST_EQUAL_ULPS(location::BearingToAngle(0.), 90., ());
|
||||
TEST_ALMOST_EQUAL_ULPS(location::BearingToAngle(30.), 60., ());
|
||||
TEST_ALMOST_EQUAL_ULPS(location::BearingToAngle(100.), 350., ());
|
||||
TEST_ALMOST_EQUAL_ULPS(location::BearingToAngle(370.), 80., ());
|
||||
TEST_ALMOST_EQUAL_ULPS(location::AngleToBearing(-370.), 100., ());
|
||||
}
|
||||
183
libs/platform/platform_tests/measurement_tests.cpp
Normal file
183
libs/platform/platform_tests/measurement_tests.cpp
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/measurement_utils.hpp"
|
||||
|
||||
#include "base/math.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
using namespace measurement_utils;
|
||||
using namespace platform;
|
||||
|
||||
std::string AddGroupingSeparators(std::string const & valueString, std::string const & groupingSeparator)
|
||||
{
|
||||
std::string out(valueString);
|
||||
|
||||
if (out.size() > 4 && !groupingSeparator.empty())
|
||||
for (int pos = out.size() - 3; pos > 0; pos -= 3)
|
||||
out.insert(pos, groupingSeparator);
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
UNIT_TEST(LatLonToDMS_Origin)
|
||||
{
|
||||
TEST_EQUAL(FormatLatLonAsDMS(0, 0, false), "00°00′00″ 00°00′00″", ());
|
||||
TEST_EQUAL(FormatLatLonAsDMS(0, 0, false, 3), "00°00′00″ 00°00′00″", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(LatLonToDMS_Rounding)
|
||||
{
|
||||
// Here and after data is from Wiki: http://bit.ly/datafotformatingtest
|
||||
// Boston
|
||||
TEST_EQUAL(FormatLatLonAsDMS(42.358056, -71.063611, false, 0), "42°21′29″N 71°03′49″W", ());
|
||||
// Minsk
|
||||
TEST_EQUAL(FormatLatLonAsDMS(53.916667, 27.55, false, 0), "53°55′00″N 27°33′00″E", ());
|
||||
// Rio
|
||||
TEST_EQUAL(FormatLatLonAsDMS(-22.908333, -43.196389, false, 0), "22°54′30″S 43°11′47″W", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(LatLonToDMS_NoRounding)
|
||||
{
|
||||
// Paris
|
||||
TEST_EQUAL(FormatLatLonAsDMS(48.8567, 2.3508, false, 2), "48°51′24.12″N 02°21′02.88″E", ());
|
||||
// Vatican
|
||||
TEST_EQUAL(FormatLatLonAsDMS(41.904, 12.453, false, 2), "41°54′14.4″N 12°27′10.8″E", ());
|
||||
|
||||
TEST_EQUAL(FormatLatLonAsDMS(21.981112, -159.371112, false, 2), "21°58′52″N 159°22′16″W", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(FormatOsmLink)
|
||||
{
|
||||
// Zero point
|
||||
TEST_EQUAL(FormatOsmLink(0, 0, 5), "https://osm.org/go/wAAAA-?m", ());
|
||||
// Eifel tower
|
||||
TEST_EQUAL(FormatOsmLink(48.85825, 2.29450, 15), "https://osm.org/go/0BOdUs9e--?m", ());
|
||||
// Buenos Aires
|
||||
TEST_EQUAL(FormatOsmLink(-34.6061, -58.4360, 10), "https://osm.org/go/Mnx6SB?m", ());
|
||||
|
||||
// Formally, lat = -90 and lat = 90 are the same for OSM links, but Mercator is valid until 85.
|
||||
auto link = FormatOsmLink(-90, -180, 10);
|
||||
TEST(link == "https://osm.org/go/AAAAAA?m" || link == "https://osm.org/go/~~~~~~?m", (link));
|
||||
link = FormatOsmLink(90, 180, 10);
|
||||
TEST(link == "https://osm.org/go/AAAAAA?m" || link == "https://osm.org/go/~~~~~~?m", (link));
|
||||
}
|
||||
|
||||
UNIT_TEST(FormatSpeedNumeric)
|
||||
{
|
||||
TEST_EQUAL(FormatSpeedNumeric(10, Units::Metric), "36", ());
|
||||
TEST_EQUAL(FormatSpeedNumeric(1, Units::Metric), "4", ());
|
||||
|
||||
TEST_EQUAL(FormatSpeedNumeric(10, Units::Imperial), "22", ());
|
||||
TEST_EQUAL(FormatSpeedNumeric(1, Units::Imperial), "2", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(OSMDistanceToMetersString)
|
||||
{
|
||||
TEST_EQUAL(OSMDistanceToMetersString(""), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("INF"), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("NAN"), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("not a number"), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("10й"), "10", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("0"), "0", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("0.0"), "0", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("0.0000000"), "0", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("22.5"), "22.5", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("+21.5"), "21.5", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("1e+2"), "100", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("5 метров"), "5", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString(" 4.4 "), "4.4", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("8-15"), "15", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("8-15 ft"), "4.57", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("8-й километр"), "8", ());
|
||||
// Do not support lists for distance values.
|
||||
TEST_EQUAL(OSMDistanceToMetersString("8;9;10"), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("8;9;10 meters"), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("8;9;10 km"), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("10;20!111"), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("10;20\""), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("-100.3"), "-100.3", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("99.0000000"), "99", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("8900.000023"), "8900", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("-300.9999"), "-301", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("-300.9"), "-300.9", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("15 m"), "15", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("15.9 m"), "15.9", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("15.9m"), "15.9", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("3000 ft"), "914.4", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("3000ft"), "914.4", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("100 feet"), "30.48", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("100feet"), "30.48", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("3 nmi"), "5556", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("3 mi"), "4828.03", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("3.3 km"), "3300", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("105'"), "32", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("11'"), "3.35", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("11 3\""), "11", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("11 3'"), "11", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("11\"'"), "0.28", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("11'\""), "3.35", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("11'4\""), "3.45", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("11\""), "0.28", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("100 \""), "100", ());
|
||||
|
||||
TEST_EQUAL(OSMDistanceToMetersString("0", false), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("-15", false), "", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("15.12345", false, 1), "15.1", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("15.123", false, 4), "15.123", ());
|
||||
TEST_EQUAL(OSMDistanceToMetersString("15.654321", true, 1), "15.7", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(UnitsConversion)
|
||||
{
|
||||
double const kEps = 1e-5;
|
||||
TEST(AlmostEqualAbs(MilesToMeters(MetersToMiles(1000.0)), 1000.0, kEps), ());
|
||||
TEST(AlmostEqualAbs(MilesToMeters(1.0), 1609.344, kEps), ());
|
||||
|
||||
TEST(AlmostEqualAbs(MiphToKmph(KmphToMiph(100.0)), 100.0, kEps), ());
|
||||
TEST(AlmostEqualAbs(MiphToKmph(100.0), 160.9344, kEps), (MiphToKmph(100.0)));
|
||||
|
||||
TEST(AlmostEqualAbs(ToSpeedKmPH(100.0, Units::Imperial), 160.9344, kEps), ());
|
||||
TEST(AlmostEqualAbs(ToSpeedKmPH(100.0, Units::Metric), 100.0, kEps), ());
|
||||
|
||||
TEST(AlmostEqualAbs(KmphToMps(3.6), 1.0, kEps), ());
|
||||
TEST(AlmostEqualAbs(MpsToKmph(1.0), 3.6, kEps), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(ToStringPrecisionLocale)
|
||||
{
|
||||
double d1 = 9.8;
|
||||
int pr1 = 1;
|
||||
|
||||
double d2 = 12345.0;
|
||||
int pr2 = 0;
|
||||
std::string d2String("12345");
|
||||
|
||||
struct TestData
|
||||
{
|
||||
std::string localeName;
|
||||
std::string d1String;
|
||||
};
|
||||
|
||||
TestData testData[] = {// Locale name , Decimal
|
||||
{"en_US.UTF-8", "9.8"},
|
||||
{"es_ES.UTF-8", "9,8"},
|
||||
{"fr_FR.UTF-8", "9,8"},
|
||||
{"ru_RU.UTF-8", "9,8"}};
|
||||
|
||||
for (TestData const & data : testData)
|
||||
{
|
||||
Locale loc;
|
||||
|
||||
if (!GetLocale(data.localeName, loc))
|
||||
{
|
||||
std::cout << "Locale '" << data.localeName << "' not found!! Skipping test..." << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
TEST_EQUAL(measurement_utils::ToStringPrecisionLocale(loc, d1, pr1), data.d1String, ());
|
||||
TEST_EQUAL(measurement_utils::ToStringPrecisionLocale(loc, d2, pr2),
|
||||
AddGroupingSeparators(d2String, loc.m_groupingSeparator), ());
|
||||
}
|
||||
}
|
||||
62
libs/platform/platform_tests/meta_config_tests.cpp
Normal file
62
libs/platform/platform_tests/meta_config_tests.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/servers_list.hpp"
|
||||
|
||||
#include "cppjansson/cppjansson.hpp"
|
||||
|
||||
using namespace downloader;
|
||||
|
||||
UNIT_TEST(MetaConfig_JSONParser_OldFormat)
|
||||
{
|
||||
std::string oldFormatJson = R"(["http://url1", "http://url2", "http://url3"])";
|
||||
auto result = ParseMetaConfig(oldFormatJson);
|
||||
TEST(result.has_value(), ());
|
||||
TEST_EQUAL(result->m_serversList.size(), 3, ());
|
||||
TEST_EQUAL(result->m_serversList[0], "http://url1", ());
|
||||
TEST_EQUAL(result->m_serversList[1], "http://url2", ());
|
||||
TEST_EQUAL(result->m_serversList[2], "http://url3", ());
|
||||
TEST(result->m_settings.empty(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(MetaConfig_JSONParser_InvalidJSON)
|
||||
{
|
||||
std::string invalidJson = R"({"servers": ["http://url1", "http://url2")";
|
||||
auto result = ParseMetaConfig(invalidJson);
|
||||
TEST(!result.has_value(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(MetaConfig_JSONParser_EmptyServersList)
|
||||
{
|
||||
std::string emptyServersJson = R"({"servers": []})";
|
||||
auto result = ParseMetaConfig(emptyServersJson);
|
||||
TEST(!result.has_value(), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(MetaConfig_JSONParser_NewFormatWithoutProducts)
|
||||
{
|
||||
std::string newFormatJson = R"({
|
||||
"servers": ["http://url1", "http://url2"],
|
||||
"settings": {
|
||||
"DonateUrl": "value1",
|
||||
"key2": "value2"
|
||||
}
|
||||
})";
|
||||
auto result = ParseMetaConfig(newFormatJson);
|
||||
TEST(result.has_value(), ());
|
||||
TEST_EQUAL(result->m_serversList.size(), 2, ());
|
||||
TEST_EQUAL(result->m_serversList[0], "http://url1", ());
|
||||
TEST_EQUAL(result->m_serversList[1], "http://url2", ());
|
||||
TEST_EQUAL(result->m_settings.size(), 1, ());
|
||||
TEST_EQUAL(result->m_settings["DonateUrl"], "value1", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(MetaConfig_JSONParser_MissingServersKey)
|
||||
{
|
||||
std::string missingServersJson = R"({
|
||||
"settings": {
|
||||
"key1": "value1"
|
||||
}
|
||||
})";
|
||||
auto result = ParseMetaConfig(missingServersJson);
|
||||
TEST(!result.has_value(), ("JSON shouldn't be parsed without 'servers' key"));
|
||||
}
|
||||
334
libs/platform/platform_tests/platform_test.cpp
Normal file
334
libs/platform/platform_tests/platform_test.cpp
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/mwm_version.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "platform/platform_tests_support/scoped_file.hpp"
|
||||
|
||||
#include "coding/file_writer.hpp"
|
||||
#include "coding/internal/file_data.hpp"
|
||||
|
||||
#include "base/file_name_utils.hpp"
|
||||
#include "base/logging.hpp"
|
||||
#include "base/scope_guard.hpp"
|
||||
#include "base/stl_helpers.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <boost/regex.hpp>
|
||||
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
char const * TEST_FILE_NAME = "some_temporary_unit_test_file.tmp";
|
||||
|
||||
void CheckFilesPresence(std::string const & baseDir, unsigned typeMask,
|
||||
std::initializer_list<std::pair<std::string, size_t>> const & files)
|
||||
{
|
||||
Platform::TFilesWithType fwts;
|
||||
Platform::GetFilesByType(baseDir, typeMask, fwts);
|
||||
|
||||
std::multiset<std::string> filesSet;
|
||||
for (auto const & fwt : fwts)
|
||||
filesSet.insert(fwt.first);
|
||||
|
||||
for (auto const & file : files)
|
||||
TEST_EQUAL(filesSet.count(file.first), file.second, (file.first, file.second));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
UNIT_TEST(WritableDir)
|
||||
{
|
||||
std::string const path = GetPlatform().WritableDir() + TEST_FILE_NAME;
|
||||
|
||||
try
|
||||
{
|
||||
base::FileData f(path, base::FileData::Op::WRITE_TRUNCATE);
|
||||
}
|
||||
catch (Writer::OpenException const &)
|
||||
{
|
||||
LOG(LCRITICAL, ("Can't create file", path));
|
||||
return;
|
||||
}
|
||||
|
||||
base::DeleteFileX(path);
|
||||
}
|
||||
|
||||
UNIT_TEST(WritablePathForFile)
|
||||
{
|
||||
Platform & pl = GetPlatform();
|
||||
std::string const p1 = base::JoinPath(pl.WritableDir(), TEST_FILE_NAME);
|
||||
std::string const p2 = pl.WritablePathForFile(TEST_FILE_NAME);
|
||||
TEST_EQUAL(p1, p2, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GetReader)
|
||||
{
|
||||
char const * NON_EXISTING_FILE = "mgbwuerhsnmbui45efhdbn34.tmp";
|
||||
char const * arr[] = {"symbols/mdpi/light/symbols.sdf", "classificator.txt", "minsk-pass.mwm"};
|
||||
|
||||
Platform & p = GetPlatform();
|
||||
for (size_t i = 0; i < ARRAY_SIZE(arr); ++i)
|
||||
{
|
||||
ReaderPtr<Reader> r = p.GetReader(arr[i]);
|
||||
TEST_GREATER(r.Size(), 0, ("File should exist!"));
|
||||
}
|
||||
|
||||
bool wasException = false;
|
||||
try
|
||||
{
|
||||
ReaderPtr<Reader> r = p.GetReader(NON_EXISTING_FILE);
|
||||
}
|
||||
catch (FileAbsentException const &)
|
||||
{
|
||||
wasException = true;
|
||||
}
|
||||
TEST_EQUAL(wasException, true, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GetFilesInDir_Smoke)
|
||||
{
|
||||
Platform & pl = GetPlatform();
|
||||
Platform::FilesList files1, files2;
|
||||
|
||||
std::string const dir = pl.ResourcesDir();
|
||||
|
||||
pl.GetFilesByExt(dir, DATA_FILE_EXTENSION, files1);
|
||||
TEST_GREATER(files1.size(), 0, (dir, "folder should contain some data files"));
|
||||
|
||||
TEST(base::IsExist(files1, "minsk-pass.mwm"), ());
|
||||
|
||||
pl.GetFilesByRegExp(dir, boost::regex(".*\\" + DATA_FILE_EXTENSION + "$"), files2);
|
||||
TEST_EQUAL(files1, files2, ());
|
||||
|
||||
files1.clear();
|
||||
pl.GetFilesByExt(dir, ".dsa", files1);
|
||||
TEST_EQUAL(files1.size(), 0, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(DirsRoutines)
|
||||
{
|
||||
std::string const baseDir = GetPlatform().WritableDir();
|
||||
std::string const testDir = base::JoinPath(baseDir, "test-dir");
|
||||
std::string const testFile = base::JoinPath(testDir, "test-file");
|
||||
|
||||
TEST(!Platform::IsFileExistsByFullPath(testDir), ());
|
||||
TEST_EQUAL(Platform::MkDir(testDir), Platform::ERR_OK, ());
|
||||
|
||||
TEST(Platform::IsFileExistsByFullPath(testDir), ());
|
||||
TEST(Platform::IsDirectoryEmpty(testDir), ());
|
||||
|
||||
{
|
||||
FileWriter writer(testFile);
|
||||
}
|
||||
TEST(!Platform::IsDirectoryEmpty(testDir), ());
|
||||
FileWriter::DeleteFileX(testFile);
|
||||
|
||||
TEST_EQUAL(Platform::RmDir(testDir), Platform::ERR_OK, ());
|
||||
|
||||
TEST(!Platform::IsFileExistsByFullPath(testDir), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GetFilesByType)
|
||||
{
|
||||
std::string const kTestDirBaseName = "test-dir";
|
||||
std::string const kTestFileBaseName = "test-file";
|
||||
|
||||
std::string const baseDir = GetPlatform().WritableDir();
|
||||
|
||||
std::string const testDir = base::JoinPath(baseDir, kTestDirBaseName);
|
||||
TEST_EQUAL(Platform::MkDir(testDir), Platform::ERR_OK, ());
|
||||
SCOPE_GUARD(removeTestDir, bind(&Platform::RmDir, testDir));
|
||||
|
||||
std::string const testFile = base::JoinPath(baseDir, kTestFileBaseName);
|
||||
TEST(!Platform::IsFileExistsByFullPath(testFile), ());
|
||||
{
|
||||
FileWriter writer(testFile);
|
||||
}
|
||||
TEST(Platform::IsFileExistsByFullPath(testFile), ());
|
||||
SCOPE_GUARD(removeTestFile, bind(FileWriter::DeleteFileX, testFile));
|
||||
|
||||
CheckFilesPresence(baseDir, Platform::EFileType::Directory,
|
||||
{{
|
||||
kTestDirBaseName, 1 /* present */
|
||||
},
|
||||
{
|
||||
kTestFileBaseName, 0 /* not present */
|
||||
}});
|
||||
CheckFilesPresence(baseDir, Platform::EFileType::Regular,
|
||||
{{
|
||||
kTestDirBaseName, 0 /* not present */
|
||||
},
|
||||
{
|
||||
kTestFileBaseName, 1 /* present */
|
||||
}});
|
||||
CheckFilesPresence(baseDir, Platform::EFileType::Directory | Platform::EFileType::Regular,
|
||||
{{
|
||||
kTestDirBaseName, 1 /* present */
|
||||
},
|
||||
{
|
||||
kTestFileBaseName, 1 /* present */
|
||||
}});
|
||||
}
|
||||
|
||||
UNIT_TEST(GetFileSize)
|
||||
{
|
||||
Platform & pl = GetPlatform();
|
||||
uint64_t size = 123141;
|
||||
TEST(!pl.GetFileSizeByName("adsmngfuwrbfyfwe", size), ());
|
||||
TEST(!pl.IsFileExistsByFullPath("adsmngfuwrbfyfwe"), ());
|
||||
|
||||
char const kContent[] = "HOHOHO";
|
||||
size_t const kSize = ARRAY_SIZE(kContent);
|
||||
std::string const fileName = pl.WritablePathForFile(TEST_FILE_NAME);
|
||||
{
|
||||
FileWriter testFile(fileName);
|
||||
testFile.Write(kContent, kSize);
|
||||
}
|
||||
size = 0;
|
||||
TEST(Platform::GetFileSizeByFullPath(fileName, size), ());
|
||||
TEST_EQUAL(size, kSize, ());
|
||||
|
||||
FileWriter::DeleteFileX(fileName);
|
||||
TEST(!pl.IsFileExistsByFullPath(fileName), ());
|
||||
|
||||
{
|
||||
FileWriter testFile(fileName);
|
||||
testFile.Write(kContent, kSize);
|
||||
}
|
||||
size = 0;
|
||||
TEST(pl.GetFileSizeByName(TEST_FILE_NAME, size), ());
|
||||
TEST_EQUAL(size, kSize, ());
|
||||
|
||||
FileWriter::DeleteFileX(fileName);
|
||||
}
|
||||
|
||||
UNIT_TEST(CpuCores)
|
||||
{
|
||||
int const coresNum = GetPlatform().CpuCores();
|
||||
TEST_GREATER(coresNum, 0, ());
|
||||
TEST_LESS_OR_EQUAL(coresNum, 128, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GetWritableStorageStatus)
|
||||
{
|
||||
TEST_EQUAL(Platform::STORAGE_OK, GetPlatform().GetWritableStorageStatus(100000), ());
|
||||
TEST_EQUAL(Platform::NOT_ENOUGH_SPACE, GetPlatform().GetWritableStorageStatus(uint64_t(-1)), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(RmDirRecursively)
|
||||
{
|
||||
std::string const testDir1 = base::JoinPath(GetPlatform().WritableDir(), "test_dir1");
|
||||
TEST_EQUAL(Platform::MkDir(testDir1), Platform::ERR_OK, ());
|
||||
SCOPE_GUARD(removeTestDir1, bind(&Platform::RmDir, testDir1));
|
||||
|
||||
std::string const testDir2 = base::JoinPath(testDir1, "test_dir2");
|
||||
TEST_EQUAL(Platform::MkDir(testDir2), Platform::ERR_OK, ());
|
||||
SCOPE_GUARD(removeTestDir2, bind(&Platform::RmDir, testDir2));
|
||||
|
||||
std::string const filePath = base::JoinPath(testDir2, "test_file");
|
||||
{
|
||||
FileWriter testFile(filePath);
|
||||
testFile.Write("HOHOHO", 6);
|
||||
}
|
||||
SCOPE_GUARD(removeTestFile, bind(&base::DeleteFileX, filePath));
|
||||
|
||||
TEST(Platform::IsFileExistsByFullPath(filePath), ());
|
||||
TEST(Platform::IsFileExistsByFullPath(testDir1), ());
|
||||
TEST(Platform::IsFileExistsByFullPath(testDir2), ());
|
||||
|
||||
TEST_EQUAL(Platform::ERR_DIRECTORY_NOT_EMPTY, Platform::RmDir(testDir1), ());
|
||||
|
||||
TEST(Platform::IsFileExistsByFullPath(filePath), ());
|
||||
TEST(Platform::IsFileExistsByFullPath(testDir1), ());
|
||||
TEST(Platform::IsFileExistsByFullPath(testDir2), ());
|
||||
|
||||
TEST(Platform::RmDirRecursively(testDir1), ());
|
||||
|
||||
TEST(!Platform::IsFileExistsByFullPath(filePath), ());
|
||||
TEST(!Platform::IsFileExistsByFullPath(testDir1), ());
|
||||
TEST(!Platform::IsFileExistsByFullPath(testDir2), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(MkDirRecursively)
|
||||
{
|
||||
using namespace platform::tests_support;
|
||||
auto const writablePath = GetPlatform().WritableDir();
|
||||
auto const workPath = base::JoinPath(writablePath, "MkDirRecursively");
|
||||
auto const resetDir = [](std::string const & path)
|
||||
{
|
||||
if (Platform::IsFileExistsByFullPath(path) && !Platform::RmDirRecursively(path))
|
||||
return false;
|
||||
|
||||
return Platform::MkDirChecked(path);
|
||||
};
|
||||
|
||||
CHECK(resetDir(workPath), ());
|
||||
auto const path = base::JoinPath(workPath, "test1", "test2", "test3");
|
||||
TEST(Platform::MkDirRecursively(path), ());
|
||||
TEST(Platform::IsFileExistsByFullPath(path), ());
|
||||
TEST(Platform::IsDirectory(path), ());
|
||||
|
||||
CHECK(resetDir(workPath), ());
|
||||
auto const filePath = base::JoinPath(workPath, "test1");
|
||||
SCOPE_GUARD(removeTestFile, bind(&base::DeleteFileX, filePath));
|
||||
{
|
||||
FileWriter testFile(filePath);
|
||||
}
|
||||
|
||||
TEST(!Platform::MkDirRecursively(path), ());
|
||||
TEST(!Platform::IsFileExistsByFullPath(path), ());
|
||||
|
||||
CHECK(Platform::RmDirRecursively(workPath), ());
|
||||
}
|
||||
|
||||
UNIT_TEST(Platform_ThreadRunner)
|
||||
{
|
||||
{
|
||||
Platform::ThreadRunner m_runner;
|
||||
|
||||
bool called = false;
|
||||
GetPlatform().RunTask(Platform::Thread::File, [&called]
|
||||
{
|
||||
called = true;
|
||||
testing::Notify();
|
||||
});
|
||||
testing::Wait();
|
||||
|
||||
TEST(called, ());
|
||||
}
|
||||
|
||||
GetPlatform().RunTask(Platform::Thread::File, []
|
||||
{
|
||||
TEST(false, ("The task must not be posted when thread runner is dead. "
|
||||
"But app must not be crashed. It is normal behaviour during destruction"));
|
||||
});
|
||||
}
|
||||
|
||||
UNIT_TEST(GetFileCreationTime_GetFileModificationTime)
|
||||
{
|
||||
auto const now = std::time(nullptr);
|
||||
|
||||
std::string_view constexpr kContent{"HOHOHO"};
|
||||
std::string const fileName = GetPlatform().WritablePathForFile(TEST_FILE_NAME);
|
||||
{
|
||||
FileWriter testFile(fileName);
|
||||
testFile.Write(kContent.data(), kContent.size());
|
||||
}
|
||||
SCOPE_GUARD(removeTestFile, bind(&base::DeleteFileX, fileName));
|
||||
|
||||
auto const creationTime = Platform::GetFileCreationTime(fileName);
|
||||
TEST_GREATER_OR_EQUAL(creationTime, now, ());
|
||||
|
||||
{
|
||||
FileWriter testFile(fileName);
|
||||
testFile.Write(kContent.data(), kContent.size());
|
||||
}
|
||||
|
||||
auto const modificationTime = Platform::GetFileModificationTime(fileName);
|
||||
TEST_GREATER_OR_EQUAL(modificationTime, creationTime, ());
|
||||
}
|
||||
69
libs/platform/platform_tests/utm_mgrs_utils_tests.cpp
Normal file
69
libs/platform/platform_tests/utm_mgrs_utils_tests.cpp
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "platform/utm_mgrs_utils.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace utm_mgrs_utils;
|
||||
|
||||
UNIT_TEST(FormatUTM)
|
||||
{
|
||||
TEST_EQUAL(FormatUTM(42.0, -93.0), "15T 500000 4649776", ());
|
||||
TEST_EQUAL(FormatUTM(31.77597, 35.23406), "36R 711554 3517777", ());
|
||||
TEST_EQUAL(FormatUTM(-34.81449, -58.54016), "21H 359135 6146448", ());
|
||||
TEST_EQUAL(FormatUTM(36.2361322, -115.0820944), "11S 672349 4011845", ());
|
||||
|
||||
TEST_EQUAL(FormatUTM(50.77535, 6.08389), "32U 294409 5628898", ());
|
||||
TEST_EQUAL(FormatUTM(40.71435, -74.00597), "18T 583960 4507523", ());
|
||||
TEST_EQUAL(FormatUTM(-41.28646, 174.77624), "60G 313784 5427057", ());
|
||||
TEST_EQUAL(FormatUTM(-33.92487, 18.42406), "34H 261878 6243186", ());
|
||||
TEST_EQUAL(FormatUTM(-32.89018, -68.84405), "19H 514586 6360877", ());
|
||||
TEST_EQUAL(FormatUTM(64.83778, -147.71639), "6W 466013 7190568", ());
|
||||
TEST_EQUAL(FormatUTM(56.79680, -5.00601), "30V 377486 6296562", ());
|
||||
TEST_EQUAL(FormatUTM(84, -5.00601), "30X 476594 9328501", ());
|
||||
|
||||
TEST_EQUAL(FormatUTM(84.644103, 3.000009), "", ()); // Latitude limit exceeded.
|
||||
TEST_EQUAL(FormatUTM(12.016469, 188.0), "", ());
|
||||
}
|
||||
|
||||
UNIT_TEST(FormatMGRS)
|
||||
{
|
||||
TEST_EQUAL(FormatMGRS(42.0, -93.0, 5), "15T WG 00000 49776", ());
|
||||
TEST_EQUAL(FormatMGRS(31.77597, 35.23406, 5), "36R YA 11554 17776", ());
|
||||
TEST_EQUAL(FormatMGRS(-34.81449, -58.54016, 5), "21H UB 59135 46447", ());
|
||||
TEST_EQUAL(FormatMGRS(36.2361322, -115.0820944, 5), "11S PA 72349 11844", ());
|
||||
TEST_EQUAL(FormatMGRS(36.2361322, -115.0820944, 4), "11S PA 7234 1184", ());
|
||||
TEST_EQUAL(FormatMGRS(36.2361322, -115.0820944, 3), "11S PA 723 118", ());
|
||||
TEST_EQUAL(FormatMGRS(36.2361322, -115.0820944, 2), "11S PA 72 11", ());
|
||||
TEST_EQUAL(FormatMGRS(36.2361322, -115.0820944, 1), "11S PA 7 1", ());
|
||||
|
||||
TEST_EQUAL(FormatMGRS(84.644103, 3.000009, 5), "", ()); // Some converters generate string "Z AB 31142 05767"
|
||||
TEST_EQUAL(FormatMGRS(-81.016469, 8.745519, 5), "", ()); // Some converters generate string "B BX 51947 87732"
|
||||
TEST_EQUAL(FormatMGRS(12.016469, 188.0, 2), "", ());
|
||||
|
||||
// Test data from https://s3.amazonaws.com/mgrs.io/mgrsToGeo_WE.txt
|
||||
TEST_EQUAL(FormatMGRS(0.000005, 0.304981, 5), "31N BA 00000 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(4.518520, 48.296629, 5), "39N TF 00000 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(9.045438, 50.090125, 5), "39P VL 00000 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(63.115494, 91.017708, 5), "46V DR 00000 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(40.626644, 137.364687, 5), "53T QF 00000 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(0.000005, -4.797048, 5), "30N UF 00000 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(0.000005, -0.592324, 5), "30N YF 67993 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(0.000005, 6.592333, 5), "32N KF 32007 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(0.000005, 54.592333, 5), "40N BF 32007 00000", ());
|
||||
TEST_EQUAL(FormatMGRS(76.463950, 58.675211, 5), "40X EK 43763 87577", ());
|
||||
TEST_EQUAL(FormatMGRS(-49.578078, 85.150396, 5), "45F UF 66291 06635", ());
|
||||
TEST_EQUAL(FormatMGRS(-45.089793, 132.812340, 5), "53G LL 27848 04746", ());
|
||||
TEST_EQUAL(FormatMGRS(4.514602, 138.603457, 5), "54N TK 34069 99447", ());
|
||||
TEST_EQUAL(FormatMGRS(-67.547521, -142.303616, 5), "07D DF 44443 06997", ());
|
||||
TEST_EQUAL(FormatMGRS(-62.908852, -154.887008, 5), "05E ML 04130 23160", ());
|
||||
TEST_EQUAL(FormatMGRS(4.514602, -144.603447, 5), "06N YK 65931 99447", ());
|
||||
TEST_EQUAL(FormatMGRS(4.514602, -137.396543, 5), "08N KK 34069 99447", ());
|
||||
TEST_EQUAL(FormatMGRS(9.028532, -144.637149, 5), "06P YQ 59760 98847", ());
|
||||
TEST_EQUAL(FormatMGRS(54.109208, -60.059588, 5), "20U PE 92211 99669", ());
|
||||
TEST_EQUAL(FormatMGRS(54.109208, -53.940397, 5), "22U CE 07789 99669", ());
|
||||
TEST_EQUAL(FormatMGRS(-45.089793, -53.187660, 5), "22G CR 27848 04746", ());
|
||||
TEST_EQUAL(FormatMGRS(-31.596034, -18.161583, 5), "27J YF 69322 00842", ());
|
||||
TEST_EQUAL(FormatMGRS(-22.559756, -11.111475, 5), "29K KR 82882 03678", ());
|
||||
TEST_EQUAL(FormatMGRS(0.000005, 0.592333, 5), "31N BA 32007 00000", ());
|
||||
}
|
||||
23
libs/platform/platform_tests_support/CMakeLists.txt
Normal file
23
libs/platform/platform_tests_support/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
project(platform_tests_support)
|
||||
|
||||
set(SRC
|
||||
async_gui_thread.hpp
|
||||
helpers.hpp
|
||||
scoped_dir.cpp
|
||||
scoped_dir.hpp
|
||||
scoped_file.cpp
|
||||
scoped_file.hpp
|
||||
scoped_mwm.cpp
|
||||
scoped_mwm.hpp
|
||||
test_socket.cpp
|
||||
test_socket.hpp
|
||||
writable_dir_changer.cpp
|
||||
writable_dir_changer.hpp
|
||||
)
|
||||
|
||||
omim_add_library(${PROJECT_NAME} ${SRC})
|
||||
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
platform
|
||||
indexer # feature::DataHeader::Save
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue