Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

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

View 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

View 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

View 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

View 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

View 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

View 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

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

View 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();
}

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

View 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

View 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
View 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

View 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

View 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

View 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

View 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
View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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;
};

View 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

View 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

View 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

View 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()];
}
}

View file

@ -0,0 +1,6 @@
#include "platform/http_uploader_background.hpp"
namespace platform
{
void HttpUploaderBackground::Upload() const {}
} // namespace platform

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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

View 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

View 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

View 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

View 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

View 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
View 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

View 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()

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

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

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

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

View 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();
};

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,9 @@
#include "platform/network_policy.hpp"
namespace platform
{
NetworkPolicy GetCurrentNetworkPolicy()
{
return NetworkPolicy(true);
}
} // namespace platform

View 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

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

View 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;
}

View 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

View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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@;
}

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

View 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

View 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

View file

@ -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

View 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"})"), ());
}

View 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

View 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)(" "), "", ());
}

View 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

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

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

View 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

View 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., ());
}

View 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°0000″ 00°0000″", ());
TEST_EQUAL(FormatLatLonAsDMS(0, 0, false, 3), "00°0000″ 00°0000″", ());
}
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°2129″N 71°0349″W", ());
// Minsk
TEST_EQUAL(FormatLatLonAsDMS(53.916667, 27.55, false, 0), "53°5500″N 27°3300″E", ());
// Rio
TEST_EQUAL(FormatLatLonAsDMS(-22.908333, -43.196389, false, 0), "22°5430″S 43°1147″W", ());
}
UNIT_TEST(LatLonToDMS_NoRounding)
{
// Paris
TEST_EQUAL(FormatLatLonAsDMS(48.8567, 2.3508, false, 2), "48°5124.12″N 02°2102.88″E", ());
// Vatican
TEST_EQUAL(FormatLatLonAsDMS(41.904, 12.453, false, 2), "41°5414.4″N 12°2710.8″E", ());
TEST_EQUAL(FormatLatLonAsDMS(21.981112, -159.371112, false, 2), "21°5852″N 159°2216″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), ());
}
}

View 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"));
}

View 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, ());
}

View 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", ());
}

View 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