diff options
author | Alex Zolotarev <alex@maps.me> | 2015-03-04 18:22:30 +0300 |
---|---|---|
committer | Alex Zolotarev <alex@maps.me> | 2015-09-23 02:38:07 +0300 |
commit | 4f3b0f713e6e3c9b010f30194a5782fa7971822d (patch) | |
tree | 903fff132c7d6cba2071278cae36a0e4ca7fa295 /3party/Alohalytics/src/apple/alohalytics_objc.mm | |
parent | 3363aba81c28935c23de60f5874ce11c77796868 (diff) |
[alohalytics] Updated library version (now with iOS support).
Diffstat (limited to '3party/Alohalytics/src/apple/alohalytics_objc.mm')
-rw-r--r-- | 3party/Alohalytics/src/apple/alohalytics_objc.mm | 369 |
1 files changed, 369 insertions, 0 deletions
diff --git a/3party/Alohalytics/src/apple/alohalytics_objc.mm b/3party/Alohalytics/src/apple/alohalytics_objc.mm new file mode 100644 index 0000000000..b9812feebb --- /dev/null +++ b/3party/Alohalytics/src/apple/alohalytics_objc.mm @@ -0,0 +1,369 @@ +/******************************************************************************* +The MIT License (MIT) + +Copyright (c) 2015 Alexander Zolotarev <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 "../alohalytics_objc.h" +#include "../alohalytics.h" +#include "../logger.h" + +#include <utility> // std::pair +#include <sys/xattr.h> +#include <TargetConditionals.h> // TARGET_OS_IPHONE + +#import <CoreFoundation/CoreFoundation.h> +#import <CoreFoundation/CFURL.h> +#import <Foundation/NSURL.h> +#if (TARGET_OS_IPHONE > 0) // Works for all iOS devices, including iPad. +#import <UIKit/UIDevice.h> +#import <UIKit/UIScreen.h> +#import <UIKit/UIApplication.h> +#import <AdSupport/ASIdentifierManager.h> +#endif // TARGET_OS_IPHONE + +using namespace alohalytics; + +namespace { +// Conversion from [possible nil] NSString to std::string. +static std::string ToStdString(NSString * nsString) { + if (nsString) { + return std::string([nsString UTF8String]); + } + return std::string(); +} + +// Additional check if object can be represented as a string. +static std::string ToStdStringSafe(id object) { + if ([object isKindOfClass:[NSString class]]) { + return ToStdString(object); + } else if ([object isKindOfClass:[NSObject class]]) { + return ToStdString(((NSObject *)object).description); + } + return "ERROR: Trying to log neither NSString nor NSObject-inherited object."; +} + +// Safe conversion from [possible nil] NSDictionary. +static TStringMap ToStringMap(NSDictionary * nsDictionary) { + TStringMap map; + for (NSString * key in nsDictionary) { + map[ToStdString(key)] = ToStdStringSafe([nsDictionary objectForKey:key]); + } + return map; +} + +// Safe conversion from [possible nil] NSArray. +static TStringMap ToStringMap(NSArray * nsArray) { + TStringMap map; + std::string key; + for (id item in nsArray) { + if (key.empty()) { + key = ToStdStringSafe(item); + map[key] = ""; + } else { + map[key] = ToStdStringSafe(item); + key.clear(); + } + } + return map; +} + +// Safe extraction from [possible nil] CLLocation to alohalytics::Location. +static Location ExtractLocation(CLLocation * l) { + Location extracted; + if (!l) { + return extracted; + } + // Validity of values is checked according to Apple's documentation: + // https://developer.apple.com/library/ios/documentation/CoreLocation/Reference/CLLocation_Class/ + if (l.horizontalAccuracy >= 0) { + extracted.SetLatLon([l.timestamp timeIntervalSince1970] * 1000., + l.coordinate.latitude, l.coordinate.longitude, + l.horizontalAccuracy); + } + if (l.verticalAccuracy >= 0) { + extracted.SetAltitude(l.altitude, l.verticalAccuracy); + } + if (l.speed >= 0) { + extracted.SetSpeed(l.speed); + } + if (l.course >= 0) { + extracted.SetBearing(l.course); + } + // We don't know location source on iOS. + return extracted; +} + +// Returns string representing uint64_t timestamp of given file or directory (modification date in millis from 1970). +static std::string PathTimestampMillis(NSString * path) { + NSDictionary * attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; + if (attributes) { + NSDate * date = [attributes objectForKey:NSFileModificationDate]; + return std::to_string(static_cast<uint64_t>([date timeIntervalSince1970] * 1000.)); + } + return std::string("0"); +} + +#if (TARGET_OS_IPHONE > 0) +static std::string RectToString(CGRect const & rect) { + return std::to_string(static_cast<int>(rect.origin.x)) + " " + std::to_string(static_cast<int>(rect.origin.y)) + " " + + std::to_string(static_cast<int>(rect.size.width)) + " " + std::to_string(static_cast<int>(rect.size.height)); +} + +// Logs some basic device's info. +static void LogSystemInformation() { + UIDevice * device = [UIDevice currentDevice]; + UIScreen * screen = [UIScreen mainScreen]; + std::string preferredLanguages; + for (NSString * lang in [NSLocale preferredLanguages]) { + preferredLanguages += [lang UTF8String] + std::string(" "); + } + std::string preferredLocalizations; + for (NSString * loc in [[NSBundle mainBundle] preferredLocalizations]) { + preferredLocalizations += [loc UTF8String] + std::string(" "); + } + NSLocale * locale = [NSLocale currentLocale]; + std::string userInterfaceIdiom = "phone"; + if (device.userInterfaceIdiom == UIUserInterfaceIdiomPad) { + userInterfaceIdiom = "pad"; + } else if (device.userInterfaceIdiom == UIUserInterfaceIdiomUnspecified) { + userInterfaceIdiom = "unspecified"; + } + alohalytics::TStringMap info = {{"deviceName", ToStdString(device.name)}, + {"deviceSystemName", ToStdString(device.systemName)}, + {"deviceSystemVersion", ToStdString(device.systemVersion)}, + {"deviceModel", ToStdString(device.model)}, + {"deviceUserInterfaceIdiom", userInterfaceIdiom}, + {"screens", std::to_string([UIScreen screens].count)}, + {"screenBounds", RectToString(screen.bounds)}, + {"screenScale", std::to_string(screen.scale)}, + {"preferredLanguages", preferredLanguages}, + {"preferredLocalizations", preferredLocalizations}, + {"localeIdentifier", ToStdString([locale objectForKey:NSLocaleIdentifier])}, + {"calendarIdentifier", ToStdString([[locale objectForKey:NSLocaleCalendar] calendarIdentifier])}, + {"localeMeasurementSystem", ToStdString([locale objectForKey:NSLocaleMeasurementSystem])}, + {"localeDecimalSeparator", ToStdString([locale objectForKey:NSLocaleDecimalSeparator])}, + }; + if (device.systemVersion.floatValue >= 8.0) { + info.emplace("screenNativeBounds", RectToString(screen.nativeBounds)); + info.emplace("screenNativeScale", std::to_string(screen.nativeScale)); + } + Stats & instance = Stats::Instance(); + instance.LogEvent("$iosDeviceInfo", info); + + info.clear(); + if (device.systemVersion.floatValue >= 6.0) { + if (device.identifierForVendor) { + info.emplace("identifierForVendor", ToStdString(device.identifierForVendor.UUIDString)); + } + if (NSClassFromString(@"ASIdentifierManager")) { + ASIdentifierManager * manager = [ASIdentifierManager sharedManager]; + info.emplace("isAdvertisingTrackingEnabled", manager.isAdvertisingTrackingEnabled ? "YES" : "NO"); + if (manager.advertisingIdentifier) { + info.emplace("advertisingIdentifier", ToStdString(manager.advertisingIdentifier.UUIDString)); + } + } + } + if (!info.empty()) { + instance.LogEvent("$iosDeviceIds", info); + } +} +#endif // TARGET_OS_IPHONE + +// Returns <unique id, true if it's the very-first app launch>. +static std::pair<std::string, bool> InstallationId() { + bool firstLaunch = false; + NSUserDefaults * userDataBase = [NSUserDefaults standardUserDefaults]; + NSString * installationId = [userDataBase objectForKey:@"AlohalyticsInstallationId"]; + if (installationId == nil) { + CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault); + // All iOS IDs start with I: + installationId = [@"I:" stringByAppendingString:(NSString *)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, uuid))]; + CFRelease(uuid); + [userDataBase setValue:installationId forKey:@"AlohalyticsInstallationId"]; + [userDataBase synchronize]; + firstLaunch = true; + } + return std::make_pair([installationId UTF8String], firstLaunch); +} + +// Returns path to store statistics files. +static std::string StoragePath() { + // Store files in special directory which is not backed up automatically. + NSArray * paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES); + NSString * directory = [[paths firstObject] stringByAppendingString:@"/Alohalytics/"]; + NSFileManager * fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:directory]) { + if (![fm createDirectoryAtPath:directory withIntermediateDirectories:YES attributes:nil error:nil]) { + // TODO(AlexZ): Probably we need to log this case to the server in the future. + NSLog(@"Alohalytics ERROR: Can't create directory %@.", directory); + } +#if (TARGET_OS_IPHONE > 0) + // Disable iCloud backup for storage folder: https://developer.apple.com/library/iOS/qa/qa1719/_index.html + const std::string storagePath = [directory UTF8String]; + if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_5_1) { + CFURLRef url = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, + reinterpret_cast<unsigned char const *>(storagePath.c_str()), + storagePath.size(), + 0); + CFErrorRef err; + signed char valueOfCFBooleanYes = 1; + CFNumberRef value = CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, &valueOfCFBooleanYes); + if (!CFURLSetResourcePropertyForKey(url, kCFURLIsExcludedFromBackupKey, value, &err)) { + NSLog(@"Alohalytics ERROR while disabling iCloud backup for directory %@", directory); + } + CFRelease(value); + CFRelease(url); + } else { + static char const * attrName = "com.apple.MobileBackup"; + u_int8_t valueOfBooleanYes = 1; + const int result = ::setxattr(storagePath.c_str(), attrName, &valueOfBooleanYes, sizeof(valueOfBooleanYes), 0, 0); + if (result != 0) { + NSLog(@"Alohalytics ERROR while disabling iCloud backup for directory %@", directory); + } + } +#endif // TARGET_OS_IPHONE + } + if (directory) { + return [directory UTF8String]; + } + return std::string("Alohalytics ERROR: Can't retrieve valid storage path."); +} + +#if (TARGET_OS_IPHONE > 0) +static alohalytics::TStringMap ParseLaunchOptions(NSDictionary * options) { + TStringMap parsed; + + NSURL * url = [options objectForKey:UIApplicationLaunchOptionsURLKey]; + if (url) { + parsed.emplace("UIApplicationLaunchOptionsURLKey", ToStdString([url absoluteString])); + } + NSString * source = [options objectForKey:UIApplicationLaunchOptionsSourceApplicationKey]; + if (source) { + parsed.emplace("UIApplicationLaunchOptionsSourceApplicationKey", ToStdString(source)); + } + return parsed; +} +#endif // TARGET_OS_IPHONE +} // namespace + +@implementation Alohalytics + ++ (void)setDebugMode:(BOOL)enable { + Stats::Instance().SetDebugMode(enable); +} + ++ (void)setup:(NSString *)serverUrl withLaunchOptions:(NSDictionary *)options { + [Alohalytics setup:serverUrl andFirstLaunch:YES withLaunchOptions:options]; +} + ++ (void)setup:(NSString *)serverUrl andFirstLaunch:(BOOL)isFirstLaunch withLaunchOptions:(NSDictionary *)options { + const auto installationId = InstallationId(); + Stats & instance = Stats::Instance(); + instance.SetClientId(installationId.first) + .SetServerUrl([serverUrl UTF8String]) + .SetStoragePath(StoragePath()); + + // Calculate some basic statistics about installations/updates/launches. + NSUserDefaults * userDataBase = [NSUserDefaults standardUserDefaults]; + NSString * installedVersion = [userDataBase objectForKey:@"AlohalyticsInstalledVersion"]; + bool forceUpload = false; + if (installationId.second && isFirstLaunch && installedVersion == nil) { + NSString * version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; + // Documents folder modification time can be interpreted as a "first app launch time" or an approx. "app install time". + // App bundle modification time can be interpreted as an "app update time". + instance.LogEvent("$install", {{"CFBundleShortVersionString", [version UTF8String]}, + {"documentsTimestampMillis", PathTimestampMillis([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject])}, + {"bundleTimestampMillis", PathTimestampMillis([[NSBundle mainBundle] executablePath])}}); + [userDataBase setValue:version forKey:@"AlohalyticsInstalledVersion"]; + [userDataBase synchronize]; +#if (TARGET_OS_IPHONE > 0) + LogSystemInformation(); +#else + static_cast<void>(options); // Unused variable warning fix. +#endif // TARGET_OS_IPHONE + forceUpload = true; + } else { + NSString * version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; + if (installedVersion == nil || ![installedVersion isEqualToString:version]) { + instance.LogEvent("$update", {{"CFBundleShortVersionString", [version UTF8String]}, + {"documentsTimestampMillis", PathTimestampMillis([NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject])}, + {"bundleTimestampMillis", PathTimestampMillis([[NSBundle mainBundle] executablePath])}}); + [userDataBase setValue:version forKey:@"AlohalyticsInstalledVersion"]; + [userDataBase synchronize]; +#if (TARGET_OS_IPHONE > 0) + LogSystemInformation(); +#endif // TARGET_OS_IPHONE + forceUpload = true; + } + } + instance.LogEvent("$launch" +#if (TARGET_OS_IPHONE > 0) + , ParseLaunchOptions(options) +#endif // TARGET_OS_IPHONE + ); + // Force uploading to get first-time install information before uninstall. + if (forceUpload) { + instance.Upload(); + } +} + ++ (void)forceUpload { + Stats::Instance().Upload(); +} + ++ (void)logEvent:(NSString *)event { + Stats::Instance().LogEvent(ToStdString(event)); +} + ++ (void)logEvent:(NSString *)event atLocation:(CLLocation *)location { + Stats::Instance().LogEvent(ToStdString(event), ExtractLocation(location)); +} + ++ (void)logEvent:(NSString *)event withValue:(NSString *)value { + Stats::Instance().LogEvent(ToStdString(event), ToStdString(value)); +} + ++ (void)logEvent:(NSString *)event withValue:(NSString *)value atLocation:(CLLocation *)location { + Stats::Instance().LogEvent(ToStdString(event), ToStdString(value), ExtractLocation(location)); +} + ++ (void)logEvent:(NSString *)event withKeyValueArray:(NSArray *)array { + Stats::Instance().LogEvent(ToStdString(event), ToStringMap(array)); +} + ++ (void)logEvent:(NSString *)event withKeyValueArray:(NSArray *)array atLocation:(CLLocation *)location { + Stats::Instance().LogEvent(ToStdString(event), ToStringMap(array), ExtractLocation(location)); +} + ++ (void)logEvent:(NSString *)event withDictionary:(NSDictionary *)dictionary { + Stats::Instance().LogEvent(ToStdString(event), ToStringMap(dictionary)); +} + ++ (void)logEvent:(NSString *)event withDictionary:(NSDictionary *)dictionary atLocation:(CLLocation *)location { + Stats::Instance().LogEvent(ToStdString(event), ToStringMap(dictionary), ExtractLocation(location)); +} + +@end |