diff --git a/cfg/central-signing/ProductionSigning.xcconfig b/cfg/central-signing/ProductionSigning.xcconfig new file mode 100644 index 00000000000..29251b2ceeb --- /dev/null +++ b/cfg/central-signing/ProductionSigning.xcconfig @@ -0,0 +1,2 @@ +CODE_SIGN_IDENTITY = iPhone Distribution +PROVISIONING_PROFILE_SPECIFIER = 6WQYJS4JUL/com sap sailing ios SAPTracker enterprise diff --git a/cfg/central-signing/ProductionSigningExportOptions.plist b/cfg/central-signing/ProductionSigningExportOptions.plist new file mode 100644 index 00000000000..67d0b3f9417 --- /dev/null +++ b/cfg/central-signing/ProductionSigningExportOptions.plist @@ -0,0 +1,13 @@ + + + + + method + enterprise + provisioningProfiles + + com.sap.sailing.ios.SAPTracker.enterprise + com sap sailing ios SAPTracker enterprise + + + diff --git a/cfg/files2sign.json b/cfg/files2sign.json index c403414dfd2..ed0af5de200 100644 --- a/cfg/files2sign.json +++ b/cfg/files2sign.json @@ -1,22 +1,22 @@ -{ - "ANDROID":[ - { - "version": "1.4.63", - "groupid": "com.sap.sailing", - "file": "src/java/com.sap.sailing.www/apps/com.sap.sailing.racecommittee.app-unsigned.apk", - "artifactid": "com.sap.sailing.racecommittee.app" - }, - { - "version": "1.4.63", - "groupid": "com.sap.sailing", - "file": "src/java/com.sap.sailing.www/apps/com.sap.sailing.android.tracking.app-unsigned.apk", - "artifactid": "com.sap.sailing.android.tracking.app" - }, - { - "version": "1.4.63", - "groupid": "com.sap.sailing", - "file": "src/java/com.sap.sailing.www/apps/com.sap.sailing.android.buoy.positioning.app-unsigned.apk", - "artifactid": "com.sap.sailing.android.buoy.positioning.app" - } - ] -} +{ + "ANDROID":[ + { + "version": "1.4.63", + "groupid": "com.sap.sailing", + "file": "src/java/com.sap.sailing.www/apps/com.sap.sailing.racecommittee.app-unsigned.apk", + "artifactid": "com.sap.sailing.racecommittee.app" + }, + { + "version": "1.4.63", + "groupid": "com.sap.sailing", + "file": "src/java/com.sap.sailing.www/apps/com.sap.sailing.android.tracking.app-unsigned.apk", + "artifactid": "com.sap.sailing.android.tracking.app" + }, + { + "version": "1.4.63", + "groupid": "com.sap.sailing", + "file": "src/java/com.sap.sailing.www/apps/com.sap.sailing.android.buoy.positioning.app-unsigned.apk", + "artifactid": "com.sap.sailing.android.buoy.positioning.app" + } + ] +} diff --git a/configuration/installPluginsForEclipse2019-03.sh b/configuration/installPluginsForEclipse2019-03.sh old mode 100644 new mode 100755 index a7e620b9097..266d020cc8d --- a/configuration/installPluginsForEclipse2019-03.sh +++ b/configuration/installPluginsForEclipse2019-03.sh @@ -36,15 +36,15 @@ installPlugins http://p2.sapsailing.com/p2/sdbg com.github.sdbg.feature.feature. echo "Installing EasyShell..." installPlugins http://anb0s.github.io/EasyShell de.anbos.eclipse.easyshell.feature.feature.group -echo "Installing BIRT charts (requirement for MAT)..." -installPlugins https://download.eclipse.org/birt/update-site/photon-interim/ org.eclipse.birt.chart.feature.group - echo "Installing Memory Analyzer..." installPlugins http://download.eclipse.org/releases/2019-03 org.eclipse.mat.feature.feature.group,org.eclipse.mat.chart.feature.feature.group echo "Installing SAP JVM Tools (profiler) ..." installPlugins https://tools.hana.ondemand.com/oxygen com.sap.jvm.profiling.feature.group +echo "Installing UMLet ..." +installPlugins https://www.umlet.com/umlet_latest/repository/ umlet-eclipse-feature.feature.group + echo "Installing javax.xml.bind for news feed polling (see https://stackoverflow.com/questions/52528693/eclipse-internal-error-polling-news-feeds)..." installPlugins http://download.eclipse.org/tools/orbit/downloads/drops/R20180905201904/repository javax.xml.bind diff --git a/configuration/installPluginsForEclipseMars.sh b/configuration/installPluginsForEclipseMars.sh deleted file mode 100644 index 3399162b36e..00000000000 --- a/configuration/installPluginsForEclipseMars.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/bin/bash - -# You need an installation of the Mars release of "Eclipse IDE for Eclipse Committers" matching you OS and JDK (32 vs 64 Bit): -# http://www.eclipse.org/downloads/packages/eclipse-ide-eclipse-committers/marsr - -if [[ $1 == "" ]]; then - echo "You need to specify the Eclipse installation directory" - exit 1 -fi - -installPath="$1" - -installPlugins() { - "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -installIU $2 - echo "" -} - -updatePlugins() { - "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -uninstallIU $2 -installIU $2 - echo "" -} - -# Not necessary if using Eclipse for JEE developers -echo "Installing webtools (WTP/WST/JPT) and m2e that are required by the GWT plugin..." -installPlugins http://download.eclipse.org/releases/mars org.eclipse.wst.web_ui.feature.feature.group,org.eclipse.jst.web_ui.feature.feature.group,org.eclipse.wst.xml_ui.feature.feature.group,org.eclipse.jpt.common.feature.feature.group,org.eclipse.jpt.jpa.feature.feature.group,org.eclipse.m2e.feature.feature.group,org.eclipse.m2e.wtp.feature.feature.group - -echo "Installing GWT plugin..." -installPlugins http://storage.googleapis.com/gwt-eclipse-plugin/release com.google.gdt.eclipse.suite.e44.feature.feature.group - -echo "Installing Android Tools..." -installPlugins https://dl-ssl.google.com/android/eclipse com.android.ide.eclipse.adt.feature.feature.group - -echo "Installing GWT SDM debug bridge..." -installPlugins http://p2.sapsailing.com/p2/sdbg com.github.sdbg.feature.feature.group - -echo "Installing EasyShell..." -installPlugins http://anb0s.github.io/EasyShell com.tetrade.eclipse.plugins.easyshell.feature.feature.group - -echo "Installing BIRT charts (requirement for MAT)..." -installPlugins http://download.eclipse.org/birt/update-site/4.6 org.eclipse.birt.chart.feature.group - -echo "Installing Memory Analyzer..." -installPlugins http://download.eclipse.org/mat/1.6/update-site/ org.eclipse.mat.feature.feature.group,org.eclipse.mat.chart.feature.feature.group - -echo "Installing latest version of Code Recommenders ..." -updatePlugins http://download.eclipse.org/recommenders/updates/stable/ org.eclipse.recommenders.rcp.feature.feature.group,org.eclipse.recommenders.mylyn.rcp.feature.feature.group,org.eclipse.recommenders.snipmatch.rcp.feature.feature.group - -echo "Installing latest version of EGit ..." -updatePlugins http://download.eclipse.org/egit/updates org.eclipse.jgit.feature.group,org.eclipse.jgit.http.apache.feature.group,org.eclipse.egit.feature.group,org.eclipse.egit.mylyn.feature.group - -echo "Installing latest version of Mylyn ..." -updatePlugins http://download.eclipse.org/mylyn/releases/latest org.eclipse.mylyn_feature.feature.group,org.eclipse.mylyn.bugzilla_feature.feature.group,org.eclipse.mylyn.builds.feature.group,org.eclipse.mylyn.commons.feature.group,org.eclipse.mylyn.commons.identity.feature.group,org.eclipse.mylyn.commons.notifications.feature.group,org.eclipse.mylyn.commons.repositories.feature.group,org.eclipse.mylyn.commons.repositories.http.feature.group,org.eclipse.mylyn.context_feature.feature.group,org.eclipse.mylyn.discovery.feature.group,org.eclipse.mylyn.gerrit.feature.feature.group,org.eclipse.mylyn.git.feature.group,org.eclipse.mylyn.hudson.feature.group,org.eclipse.mylyn.java_feature.feature.group,org.eclipse.mylyn.monitor.feature.group,org.eclipse.mylyn.reviews.feature.feature.group,org.eclipse.mylyn.team_feature.feature.group,org.eclipse.mylyn.versions.feature.group,org.eclipse.mylyn.wikitext_feature.feature.group - -echo "Installation completed!" diff --git a/configuration/installPluginsForEclipseNeon.sh b/configuration/installPluginsForEclipseNeon.sh deleted file mode 100644 index 3e1d30fd860..00000000000 --- a/configuration/installPluginsForEclipseNeon.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# You need an installation of the Neon SR2 release of "Eclipse IDE for Eclipse Committers" matching you OS and JDK (32 vs 64 Bit): -# http://www.eclipse.org/downloads/packages/eclipse-ide-eclipse-committers/neon2 - -if [[ $1 == "" ]]; then - echo "You need to specify the Eclipse installation directory" - exit 1 -fi - -installPath="$1" - -installPlugins() { - "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -installIU $2 - echo "" -} - -updatePlugins() { - "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -uninstallIU $2 -installIU $2 - echo "" -} - -# Not necessary if using Eclipse for JEE developers -echo "Installing webtools (WTP/WST/JPT) and m2e that are required by the GWT plugin..." -installPlugins http://download.eclipse.org/releases/neon org.eclipse.wst.web_ui.feature.feature.group,org.eclipse.jst.web_ui.feature.feature.group,org.eclipse.wst.xml_ui.feature.feature.group,org.eclipse.jpt.common.feature.feature.group,org.eclipse.jpt.jpa.feature.feature.group,org.eclipse.m2e.feature.feature.group,org.eclipse.m2e.wtp.feature.feature.group - -echo "Installing GWT plugin..." -installPlugins http://storage.googleapis.com/gwt-eclipse-plugin/release com.google.gdt.eclipse.suite.e44.feature.feature.group - -echo "Installing Android Tools..." -installPlugins https://dl-ssl.google.com/android/eclipse com.android.ide.eclipse.adt.feature.feature.group - -echo "Installing GWT SDM debug bridge..." -installPlugins http://p2.sapsailing.com/p2/sdbg com.github.sdbg.feature.feature.group - -echo "Installing EasyShell..." -installPlugins http://anb0s.github.io/EasyShell de.anbos.eclipse.easyshell.feature.feature.group - -echo "Installing BIRT charts (requirement for MAT)..." -installPlugins http://download.eclipse.org/birt/update-site/4.6 org.eclipse.birt.chart.feature.group - -echo "Installing Memory Analyzer..." -installPlugins http://download.eclipse.org/mat/1.6/update-site/ org.eclipse.mat.feature.feature.group,org.eclipse.mat.chart.feature.feature.group - -echo "Installing latest version of Code Recommenders (Neon release version has bugs)..." -updatePlugins http://download.eclipse.org/recommenders/updates/stable/ org.eclipse.recommenders.rcp.feature.feature.group,org.eclipse.recommenders.mylyn.rcp.feature.feature.group,org.eclipse.recommenders.snipmatch.rcp.feature.feature.group,org.eclipse.recommenders.news.rcp.feature.feature.group - -echo "Installing latest version of EGit (Neon release version has bugs)..." -updatePlugins http://download.eclipse.org/egit/updates org.eclipse.jgit.feature.group,org.eclipse.jgit.http.apache.feature.group,org.eclipse.egit.feature.group,org.eclipse.egit.mylyn.feature.group - -echo "Installing latest version of Mylyn ..." -updatePlugins http://download.eclipse.org/mylyn/releases/latest org.eclipse.mylyn_feature.feature.group,org.eclipse.mylyn.bugzilla_feature.feature.group,org.eclipse.mylyn.builds.feature.group,org.eclipse.mylyn.commons.feature.group,org.eclipse.mylyn.commons.identity.feature.group,org.eclipse.mylyn.commons.notifications.feature.group,org.eclipse.mylyn.commons.repositories.feature.group,org.eclipse.mylyn.commons.repositories.http.feature.group,org.eclipse.mylyn.context_feature.feature.group,org.eclipse.mylyn.discovery.feature.group,org.eclipse.mylyn.gerrit.feature.feature.group,org.eclipse.mylyn.git.feature.group,org.eclipse.mylyn.hudson.feature.group,org.eclipse.mylyn.java_feature.feature.group,org.eclipse.mylyn.monitor.feature.group,org.eclipse.mylyn.reviews.feature.feature.group,org.eclipse.mylyn.team_feature.feature.group,org.eclipse.mylyn.versions.feature.group,org.eclipse.mylyn.wikitext_feature.feature.group - -echo "Installing SAP JVM Tools (profiler) ..." -installPlugins https://tools.hana.ondemand.com/neon com.sap.jvm.profiling.feature.group - -echo "Installation completed!" diff --git a/configuration/installPluginsForEclipseOxygen.sh b/configuration/installPluginsForEclipseOxygen.sh deleted file mode 100644 index 9c263371ac1..00000000000 --- a/configuration/installPluginsForEclipseOxygen.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# You need an installation of the Oxygen SR2 release of "Eclipse IDE for Eclipse Committers" matching you OS and JDK (32 vs 64 Bit): -# http://www.eclipse.org/downloads/packages/eclipse-ide-eclipse-committers/oxygen2 - -if [[ $1 == "" ]]; then - echo "You need to specify the Eclipse installation directory" - exit 1 -fi - -installPath="$1" - -installPlugins() { - "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -installIU $2 - echo "" -} - -updatePlugins() { - "$installPath/eclipse" -application org.eclipse.equinox.p2.director -noSplash -roaming -repository $1 -uninstallIU $2 -installIU $2 - echo "" -} - -# Not necessary if using Eclipse for JEE developers -echo "Installing webtools (WTP/WST/JPT) and m2e that are required by the GWT plugin..." -installPlugins http://download.eclipse.org/releases/oxygen org.eclipse.wst.web_ui.feature.feature.group,org.eclipse.jst.web_ui.feature.feature.group,org.eclipse.wst.xml_ui.feature.feature.group,org.eclipse.jpt.common.feature.feature.group,org.eclipse.jpt.jpa.feature.feature.group,org.eclipse.m2e.feature.feature.group,org.eclipse.m2e.wtp.feature.feature.group - -echo "Installing GWT plugin..." -installPlugins http://storage.googleapis.com/gwt-eclipse-plugin/v3/release com.gwtplugins.eclipse.suite.v3.feature.feature.group - -echo "Installing Android Tools..." -installPlugins https://dl-ssl.google.com/android/eclipse com.android.ide.eclipse.adt.feature.feature.group - -echo "Installing GWT SDM debug bridge..." -installPlugins http://p2.sapsailing.com/p2/sdbg com.github.sdbg.feature.feature.group - -echo "Installing EasyShell..." -installPlugins http://anb0s.github.io/EasyShell de.anbos.eclipse.easyshell.feature.feature.group - -echo "Installing BIRT charts (requirement for MAT)..." -installPlugins http://download.eclipse.org/birt/update-site/4.6 org.eclipse.birt.chart.feature.group - -echo "Installing Memory Analyzer..." -installPlugins http://download.eclipse.org/releases/oxygen org.eclipse.mat.feature.feature.group,org.eclipse.mat.chart.feature.feature.group - -echo "Installing latest version of Code Recommenders..." -updatePlugins http://download.eclipse.org/recommenders/updates/stable/ org.eclipse.recommenders.rcp.feature.feature.group,org.eclipse.recommenders.mylyn.rcp.feature.feature.group,org.eclipse.recommenders.snipmatch.rcp.feature.feature.group,org.eclipse.recommenders.news.rcp.feature.feature.group - -echo "Installing latest version of EGit..." -updatePlugins http://download.eclipse.org/egit/updates org.eclipse.jgit.feature.group,org.eclipse.jgit.http.apache.feature.group,org.eclipse.egit.feature.group,org.eclipse.egit.mylyn.feature.group,org.eclipse.egit.gitflow.feature.feature.group - -echo "Installing latest version of Mylyn ..." -updatePlugins http://download.eclipse.org/mylyn/releases/latest org.eclipse.mylyn_feature.feature.group,org.eclipse.mylyn.bugzilla_feature.feature.group,org.eclipse.mylyn.builds.feature.group,org.eclipse.mylyn.commons.feature.group,org.eclipse.mylyn.commons.identity.feature.group,org.eclipse.mylyn.commons.notifications.feature.group,org.eclipse.mylyn.commons.repositories.feature.group,org.eclipse.mylyn.commons.repositories.http.feature.group,org.eclipse.mylyn.context_feature.feature.group,org.eclipse.mylyn.discovery.feature.group,org.eclipse.mylyn.gerrit.feature.feature.group,org.eclipse.mylyn.git.feature.group,org.eclipse.mylyn.hudson.feature.group,org.eclipse.mylyn.java_feature.feature.group,org.eclipse.mylyn.monitor.feature.group,org.eclipse.mylyn.reviews.feature.feature.group,org.eclipse.mylyn.team_feature.feature.group,org.eclipse.mylyn.versions.feature.group,org.eclipse.mylyn.wikitext_feature.feature.group - -echo "Installing SAP JVM Tools (profiler) ..." -installPlugins https://tools.hana.ondemand.com/oxygen com.sap.jvm.profiling.feature.group - -echo "Installation completed!" diff --git a/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/project.pbxproj b/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/project.pbxproj old mode 100755 new mode 100644 index 19507b37c1c..39b01ad2ea5 --- a/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/project.pbxproj +++ b/ios/SAPTracker/src/xcode/SAPTracker.xcodeproj/project.pbxproj @@ -7,6 +7,65 @@ objects = { /* Begin PBXBuildFile section */ + 10080BAA220C5DD200C6E0A2 /* BNCError.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B29220C5DD200C6E0A2 /* BNCError.m */; }; + 10080BAB220C5DD200C6E0A2 /* BNCEncodingUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B2B220C5DD200C6E0A2 /* BNCEncodingUtils.m */; }; + 10080BAC220C5DD200C6E0A2 /* BranchUniversalObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B2C220C5DD200C6E0A2 /* BranchUniversalObject.m */; }; + 10080BAD220C5DD200C6E0A2 /* BNCApplication.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B2D220C5DD200C6E0A2 /* BNCApplication.m */; }; + 10080BAE220C5DD200C6E0A2 /* NSMutableDictionary+Branch.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B2E220C5DD200C6E0A2 /* NSMutableDictionary+Branch.m */; }; + 10080BAF220C5DD200C6E0A2 /* BNCDebug.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B32220C5DD200C6E0A2 /* BNCDebug.m */; }; + 10080BB0220C5DD200C6E0A2 /* BNCDeepLinkViewControllerInstance.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B34220C5DD200C6E0A2 /* BNCDeepLinkViewControllerInstance.m */; }; + 10080BB1220C5DD200C6E0A2 /* BNCContentDiscoveryManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B35220C5DD200C6E0A2 /* BNCContentDiscoveryManager.m */; }; + 10080BB2220C5DD200C6E0A2 /* Branch+Validator.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B36220C5DD200C6E0A2 /* Branch+Validator.m */; }; + 10080BB3220C5DD200C6E0A2 /* NSString+Branch.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B38220C5DD200C6E0A2 /* NSString+Branch.m */; }; + 10080BB4220C5DD200C6E0A2 /* BNCSystemObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B39220C5DD200C6E0A2 /* BNCSystemObserver.m */; }; + 10080BB5220C5DD200C6E0A2 /* BNCCommerceEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B3A220C5DD200C6E0A2 /* BNCCommerceEvent.m */; }; + 10080BB6220C5DD200C6E0A2 /* BNCThreads.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B3C220C5DD200C6E0A2 /* BNCThreads.m */; }; + 10080BB7220C5DD200C6E0A2 /* BNCPreferenceHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B3F220C5DD200C6E0A2 /* BNCPreferenceHelper.m */; }; + 10080BB8220C5DD200C6E0A2 /* BNCServerRequestQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B45220C5DD200C6E0A2 /* BNCServerRequestQueue.m */; }; + 10080BB9220C5DD200C6E0A2 /* BNCServerRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B46220C5DD200C6E0A2 /* BNCServerRequest.m */; }; + 10080BBA220C5DD200C6E0A2 /* BNCNetworkService.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B48220C5DD200C6E0A2 /* BNCNetworkService.m */; }; + 10080BBB220C5DD200C6E0A2 /* BNCServerInterface.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B49220C5DD200C6E0A2 /* BNCServerInterface.m */; }; + 10080BBC220C5DD200C6E0A2 /* BranchLogoutRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B4C220C5DD200C6E0A2 /* BranchLogoutRequest.m */; }; + 10080BBD220C5DD200C6E0A2 /* BranchInstallRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B4D220C5DD200C6E0A2 /* BranchInstallRequest.m */; }; + 10080BBE220C5DD200C6E0A2 /* BranchSetIdentityRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B4F220C5DD200C6E0A2 /* BranchSetIdentityRequest.m */; }; + 10080BBF220C5DD200C6E0A2 /* BranchUserCompletedActionRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B50220C5DD200C6E0A2 /* BranchUserCompletedActionRequest.m */; }; + 10080BC0220C5DD200C6E0A2 /* BranchRegisterViewRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B51220C5DD200C6E0A2 /* BranchRegisterViewRequest.m */; }; + 10080BC1220C5DD200C6E0A2 /* BranchCloseRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B52220C5DD200C6E0A2 /* BranchCloseRequest.m */; }; + 10080BC2220C5DD200C6E0A2 /* BranchSpotlightUrlRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B54220C5DD200C6E0A2 /* BranchSpotlightUrlRequest.m */; }; + 10080BC3220C5DD200C6E0A2 /* BranchLoadRewardsRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B55220C5DD200C6E0A2 /* BranchLoadRewardsRequest.m */; }; + 10080BC4220C5DD200C6E0A2 /* BranchCreditHistoryRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B5C220C5DD200C6E0A2 /* BranchCreditHistoryRequest.m */; }; + 10080BC5220C5DD200C6E0A2 /* BranchRedeemRewardsRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B5F220C5DD200C6E0A2 /* BranchRedeemRewardsRequest.m */; }; + 10080BC6220C5DD200C6E0A2 /* BranchShortUrlRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B62220C5DD200C6E0A2 /* BranchShortUrlRequest.m */; }; + 10080BC7220C5DD200C6E0A2 /* BranchOpenRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B63220C5DD200C6E0A2 /* BranchOpenRequest.m */; }; + 10080BC8220C5DD200C6E0A2 /* BranchShortUrlSyncRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B64220C5DD200C6E0A2 /* BranchShortUrlSyncRequest.m */; }; + 10080BC9220C5DD200C6E0A2 /* BNCServerResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B68220C5DD200C6E0A2 /* BNCServerResponse.m */; }; + 10080BCA220C5DD200C6E0A2 /* BNCConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B6B220C5DD200C6E0A2 /* BNCConfig.m */; }; + 10080BCB220C5DD200C6E0A2 /* BranchConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B6C220C5DD200C6E0A2 /* BranchConstants.m */; }; + 10080BCC220C5DD200C6E0A2 /* BranchContentDiscoveryManifest.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B6E220C5DD200C6E0A2 /* BranchContentDiscoveryManifest.m */; }; + 10080BCD220C5DD200C6E0A2 /* BNCLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B6F220C5DD200C6E0A2 /* BNCLog.m */; }; + 10080BCE220C5DD200C6E0A2 /* BranchContentDiscoverer.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B70220C5DD200C6E0A2 /* BranchContentDiscoverer.m */; }; + 10080BCF220C5DD200C6E0A2 /* BNCDeviceInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B71220C5DD200C6E0A2 /* BNCDeviceInfo.m */; }; + 10080BD0220C5DD200C6E0A2 /* BNCFabricAnswers.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B73220C5DD200C6E0A2 /* BNCFabricAnswers.m */; }; + 10080BD1220C5DD200C6E0A2 /* BNCURLBlackList.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B75220C5DD200C6E0A2 /* BNCURLBlackList.m */; }; + 10080BD2220C5DD200C6E0A2 /* BNCCrashlyticsWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B77220C5DD200C6E0A2 /* BNCCrashlyticsWrapper.m */; }; + 10080BD3220C5DD200C6E0A2 /* BranchCSSearchableItemAttributeSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B7A220C5DD200C6E0A2 /* BranchCSSearchableItemAttributeSet.m */; }; + 10080BD4220C5DD200C6E0A2 /* BNCStrongMatchHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B7B220C5DD200C6E0A2 /* BNCStrongMatchHelper.m */; }; + 10080BD5220C5DD200C6E0A2 /* BranchActivityItemProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B7C220C5DD200C6E0A2 /* BranchActivityItemProvider.m */; }; + 10080BD6220C5DD200C6E0A2 /* BNCKeyChain.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B81220C5DD200C6E0A2 /* BNCKeyChain.m */; }; + 10080BD7220C5DD200C6E0A2 /* BranchDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B83220C5DD200C6E0A2 /* BranchDelegate.m */; }; + 10080BD8220C5DD200C6E0A2 /* BNCLinkData.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B84220C5DD200C6E0A2 /* BNCLinkData.m */; }; + 10080BD9220C5DD200C6E0A2 /* Branch.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B85220C5DD200C6E0A2 /* Branch.m */; }; + 10080BDA220C5DD200C6E0A2 /* BNCAvailability.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B86220C5DD200C6E0A2 /* BNCAvailability.m */; }; + 10080BDB220C5DD200C6E0A2 /* BranchView.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B8B220C5DD200C6E0A2 /* BranchView.m */; }; + 10080BDC220C5DD200C6E0A2 /* BranchContentPathProperties.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B8D220C5DD200C6E0A2 /* BranchContentPathProperties.m */; }; + 10080BDD220C5DD200C6E0A2 /* BranchLinkProperties.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B8F220C5DD200C6E0A2 /* BranchLinkProperties.m */; }; + 10080BDE220C5DD200C6E0A2 /* BNCLinkCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B94220C5DD200C6E0A2 /* BNCLinkCache.m */; }; + 10080BDF220C5DD200C6E0A2 /* BNCSpotlightService.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B9A220C5DD200C6E0A2 /* BNCSpotlightService.m */; }; + 10080BE0220C5DD200C6E0A2 /* BranchShareLink.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B9B220C5DD200C6E0A2 /* BranchShareLink.m */; }; + 10080BE1220C5DD200C6E0A2 /* UIViewController+Branch.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B9C220C5DD200C6E0A2 /* UIViewController+Branch.m */; }; + 10080BE2220C5DD200C6E0A2 /* BranchEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080B9E220C5DD200C6E0A2 /* BranchEvent.m */; }; + 10080BE3220C5DD200C6E0A2 /* BNCLocalization.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080BA0220C5DD200C6E0A2 /* BNCLocalization.m */; }; + 10080BE4220C5DD200C6E0A2 /* BranchViewHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 10080BA2220C5DD200C6E0A2 /* BranchViewHandler.m */; }; 64FDEBDC20D12DCF0086C2F4 /* AFAutoPurgingImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 64FDEBDA20D12DCE0086C2F4 /* AFAutoPurgingImageCache.m */; }; 64FDEBDD20D12DCF0086C2F4 /* AFImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 64FDEBDB20D12DCE0086C2F4 /* AFImageDownloader.m */; }; 7F0658FD19F7D6700077FF95 /* ScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0658FC19F7D6700077FF95 /* ScanViewController.swift */; }; @@ -307,8 +366,6 @@ B3150BB320FF453D006D742A /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3150BB220FF453D006D742A /* MobileCoreServices.framework */; }; B3150BB520FF4548006D742A /* CoreSpotlight.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3150BB420FF4548006D742A /* CoreSpotlight.framework */; }; B3150BB720FF454D006D742A /* iAd.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3150BB620FF454D006D742A /* iAd.framework */; }; - B31EE1A320FF915E0097AA40 /* Branch.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B3150BAA20FF4518006D742A /* Branch.framework */; }; - B31EE1A420FF915E0097AA40 /* Branch.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B3150BAA20FF4518006D742A /* Branch.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C8150BA51DCC6FDB0087C9C2 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C8150BA71DCC6FDB0087C9C2 /* InfoPlist.strings */; }; C8289F3A1F386D8300A76953 /* SignUpRequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8289F391F386D8300A76953 /* SignUpRequestManager.swift */; }; C843D47F1EBC54A300250F01 /* CompetitorCheckIn+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C843D4791EBC54A300250F01 /* CompetitorCheckIn+CoreDataProperties.swift */; }; @@ -374,7 +431,6 @@ C89C5FD01D06CF46002AAAD2 /* TrackingViewModeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C89C5FCF1D06CF46002AAAD2 /* TrackingViewModeCell.swift */; }; C8A4400B1C18153600DF70A0 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = C8A4400A1C18153600DF70A0 /* Settings.bundle */; }; C8B3B9BC1F39E64400B3B6BA /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B3B9BB1F39E64400B3B6BA /* Keychain.swift */; }; - C8B3B9C01F3C3B4400B3B6BA /* SignUp.strings in Resources */ = {isa = PBXBuildFile; fileRef = C8B3B9BF1F3C3B4400B3B6BA /* SignUp.strings */; }; C8B3B9C21F3C6B3900B3B6BA /* SignUpTranslation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8B3B9C11F3C6B3900B3B6BA /* SignUpTranslation.swift */; }; C8BBAC791C52649E004CA551 /* LicenseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8BBAC781C52649E004CA551 /* LicenseViewController.swift */; }; C8BFCCC41D51C53500FC1A27 /* TeamData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8BFCCC31D51C53500FC1A27 /* TeamData.swift */; }; @@ -428,21 +484,136 @@ }; /* End PBXContainerItemProxy section */ -/* Begin PBXCopyFilesBuildPhase section */ - B31EE1A520FF915E0097AA40 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - B31EE1A420FF915E0097AA40 /* Branch.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ + 10080B27220C5DD200C6E0A2 /* BNCLinkData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCLinkData.h; sourceTree = ""; }; + 10080B28220C5DD200C6E0A2 /* BranchDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchDelegate.h; sourceTree = ""; }; + 10080B29220C5DD200C6E0A2 /* BNCError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCError.m; sourceTree = ""; }; + 10080B2A220C5DD200C6E0A2 /* BNCKeyChain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCKeyChain.h; sourceTree = ""; }; + 10080B2B220C5DD200C6E0A2 /* BNCEncodingUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCEncodingUtils.m; sourceTree = ""; }; + 10080B2C220C5DD200C6E0A2 /* BranchUniversalObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchUniversalObject.m; sourceTree = ""; }; + 10080B2D220C5DD200C6E0A2 /* BNCApplication.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCApplication.m; sourceTree = ""; }; + 10080B2E220C5DD200C6E0A2 /* NSMutableDictionary+Branch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableDictionary+Branch.m"; sourceTree = ""; }; + 10080B2F220C5DD200C6E0A2 /* BranchActivityItemProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchActivityItemProvider.h; sourceTree = ""; }; + 10080B30220C5DD200C6E0A2 /* BNCStrongMatchHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCStrongMatchHelper.h; sourceTree = ""; }; + 10080B31220C5DD200C6E0A2 /* BranchCSSearchableItemAttributeSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchCSSearchableItemAttributeSet.h; sourceTree = ""; }; + 10080B32220C5DD200C6E0A2 /* BNCDebug.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCDebug.m; sourceTree = ""; }; + 10080B33220C5DD200C6E0A2 /* BranchLinkProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchLinkProperties.h; sourceTree = ""; }; + 10080B34220C5DD200C6E0A2 /* BNCDeepLinkViewControllerInstance.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCDeepLinkViewControllerInstance.m; sourceTree = ""; }; + 10080B35220C5DD200C6E0A2 /* BNCContentDiscoveryManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCContentDiscoveryManager.m; sourceTree = ""; }; + 10080B36220C5DD200C6E0A2 /* Branch+Validator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "Branch+Validator.m"; sourceTree = ""; }; + 10080B37220C5DD200C6E0A2 /* BranchContentPathProperties.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchContentPathProperties.h; sourceTree = ""; }; + 10080B38220C5DD200C6E0A2 /* NSString+Branch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+Branch.m"; sourceTree = ""; }; + 10080B39220C5DD200C6E0A2 /* BNCSystemObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCSystemObserver.m; sourceTree = ""; }; + 10080B3A220C5DD200C6E0A2 /* BNCCommerceEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCCommerceEvent.m; sourceTree = ""; }; + 10080B3B220C5DD200C6E0A2 /* BranchView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchView.h; sourceTree = ""; }; + 10080B3C220C5DD200C6E0A2 /* BNCThreads.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCThreads.m; sourceTree = ""; }; + 10080B3D220C5DD200C6E0A2 /* BNCAvailability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCAvailability.h; sourceTree = ""; }; + 10080B3E220C5DD200C6E0A2 /* Branch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Branch.h; sourceTree = ""; }; + 10080B3F220C5DD200C6E0A2 /* BNCPreferenceHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCPreferenceHelper.m; sourceTree = ""; }; + 10080B40220C5DD200C6E0A2 /* BNCCallbacks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCCallbacks.h; sourceTree = ""; }; + 10080B41220C5DD200C6E0A2 /* UIViewController+Branch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Branch.h"; sourceTree = ""; }; + 10080B42220C5DD200C6E0A2 /* BranchShareLink.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchShareLink.h; sourceTree = ""; }; + 10080B43220C5DD200C6E0A2 /* BNCFieldDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCFieldDefines.h; sourceTree = ""; }; + 10080B45220C5DD200C6E0A2 /* BNCServerRequestQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCServerRequestQueue.m; sourceTree = ""; }; + 10080B46220C5DD200C6E0A2 /* BNCServerRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCServerRequest.m; sourceTree = ""; }; + 10080B47220C5DD200C6E0A2 /* BNCServerResponse.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCServerResponse.h; sourceTree = ""; }; + 10080B48220C5DD200C6E0A2 /* BNCNetworkService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCNetworkService.m; sourceTree = ""; }; + 10080B49220C5DD200C6E0A2 /* BNCServerInterface.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCServerInterface.m; sourceTree = ""; }; + 10080B4A220C5DD200C6E0A2 /* BNCNetworkServiceProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCNetworkServiceProtocol.h; sourceTree = ""; }; + 10080B4C220C5DD200C6E0A2 /* BranchLogoutRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchLogoutRequest.m; sourceTree = ""; }; + 10080B4D220C5DD200C6E0A2 /* BranchInstallRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchInstallRequest.m; sourceTree = ""; }; + 10080B4E220C5DD200C6E0A2 /* BranchCreditHistoryRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchCreditHistoryRequest.h; sourceTree = ""; }; + 10080B4F220C5DD200C6E0A2 /* BranchSetIdentityRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchSetIdentityRequest.m; sourceTree = ""; }; + 10080B50220C5DD200C6E0A2 /* BranchUserCompletedActionRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchUserCompletedActionRequest.m; sourceTree = ""; }; + 10080B51220C5DD200C6E0A2 /* BranchRegisterViewRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchRegisterViewRequest.m; sourceTree = ""; }; + 10080B52220C5DD200C6E0A2 /* BranchCloseRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchCloseRequest.m; sourceTree = ""; }; + 10080B53220C5DD200C6E0A2 /* BranchRedeemRewardsRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchRedeemRewardsRequest.h; sourceTree = ""; }; + 10080B54220C5DD200C6E0A2 /* BranchSpotlightUrlRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchSpotlightUrlRequest.m; sourceTree = ""; }; + 10080B55220C5DD200C6E0A2 /* BranchLoadRewardsRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchLoadRewardsRequest.m; sourceTree = ""; }; + 10080B56220C5DD200C6E0A2 /* BranchShortUrlSyncRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchShortUrlSyncRequest.h; sourceTree = ""; }; + 10080B57220C5DD200C6E0A2 /* BranchOpenRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchOpenRequest.h; sourceTree = ""; }; + 10080B58220C5DD200C6E0A2 /* BranchShortUrlRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchShortUrlRequest.h; sourceTree = ""; }; + 10080B59220C5DD200C6E0A2 /* BranchLogoutRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchLogoutRequest.h; sourceTree = ""; }; + 10080B5A220C5DD200C6E0A2 /* BranchUserCompletedActionRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchUserCompletedActionRequest.h; sourceTree = ""; }; + 10080B5B220C5DD200C6E0A2 /* BranchRegisterViewRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchRegisterViewRequest.h; sourceTree = ""; }; + 10080B5C220C5DD200C6E0A2 /* BranchCreditHistoryRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchCreditHistoryRequest.m; sourceTree = ""; }; + 10080B5D220C5DD200C6E0A2 /* BranchSetIdentityRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchSetIdentityRequest.h; sourceTree = ""; }; + 10080B5E220C5DD200C6E0A2 /* BranchInstallRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchInstallRequest.h; sourceTree = ""; }; + 10080B5F220C5DD200C6E0A2 /* BranchRedeemRewardsRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchRedeemRewardsRequest.m; sourceTree = ""; }; + 10080B60220C5DD200C6E0A2 /* BranchSpotlightUrlRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchSpotlightUrlRequest.h; sourceTree = ""; }; + 10080B61220C5DD200C6E0A2 /* BranchCloseRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchCloseRequest.h; sourceTree = ""; }; + 10080B62220C5DD200C6E0A2 /* BranchShortUrlRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchShortUrlRequest.m; sourceTree = ""; }; + 10080B63220C5DD200C6E0A2 /* BranchOpenRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchOpenRequest.m; sourceTree = ""; }; + 10080B64220C5DD200C6E0A2 /* BranchShortUrlSyncRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchShortUrlSyncRequest.m; sourceTree = ""; }; + 10080B65220C5DD200C6E0A2 /* BranchLoadRewardsRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchLoadRewardsRequest.h; sourceTree = ""; }; + 10080B66220C5DD200C6E0A2 /* BNCServerRequestQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCServerRequestQueue.h; sourceTree = ""; }; + 10080B67220C5DD200C6E0A2 /* BNCServerRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCServerRequest.h; sourceTree = ""; }; + 10080B68220C5DD200C6E0A2 /* BNCServerResponse.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCServerResponse.m; sourceTree = ""; }; + 10080B69220C5DD200C6E0A2 /* BNCServerInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCServerInterface.h; sourceTree = ""; }; + 10080B6A220C5DD200C6E0A2 /* BNCNetworkService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCNetworkService.h; sourceTree = ""; }; + 10080B6B220C5DD200C6E0A2 /* BNCConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCConfig.m; sourceTree = ""; }; + 10080B6C220C5DD200C6E0A2 /* BranchConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchConstants.m; sourceTree = ""; }; + 10080B6D220C5DD200C6E0A2 /* BNCSpotlightService.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCSpotlightService.h; sourceTree = ""; }; + 10080B6E220C5DD200C6E0A2 /* BranchContentDiscoveryManifest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchContentDiscoveryManifest.m; sourceTree = ""; }; + 10080B6F220C5DD200C6E0A2 /* BNCLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCLog.m; sourceTree = ""; }; + 10080B70220C5DD200C6E0A2 /* BranchContentDiscoverer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchContentDiscoverer.m; sourceTree = ""; }; + 10080B71220C5DD200C6E0A2 /* BNCDeviceInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCDeviceInfo.m; sourceTree = ""; }; + 10080B72220C5DD200C6E0A2 /* BNCLinkCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCLinkCache.h; sourceTree = ""; }; + 10080B73220C5DD200C6E0A2 /* BNCFabricAnswers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCFabricAnswers.m; sourceTree = ""; }; + 10080B74220C5DD200C6E0A2 /* BranchViewHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchViewHandler.h; sourceTree = ""; }; + 10080B75220C5DD200C6E0A2 /* BNCURLBlackList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCURLBlackList.m; sourceTree = ""; }; + 10080B76220C5DD200C6E0A2 /* BNCLocalization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCLocalization.h; sourceTree = ""; }; + 10080B77220C5DD200C6E0A2 /* BNCCrashlyticsWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCCrashlyticsWrapper.m; sourceTree = ""; }; + 10080B78220C5DD200C6E0A2 /* BranchEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchEvent.h; sourceTree = ""; }; + 10080B79220C5DD200C6E0A2 /* BNCDebug.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCDebug.h; sourceTree = ""; }; + 10080B7A220C5DD200C6E0A2 /* BranchCSSearchableItemAttributeSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchCSSearchableItemAttributeSet.m; sourceTree = ""; }; + 10080B7B220C5DD200C6E0A2 /* BNCStrongMatchHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCStrongMatchHelper.m; sourceTree = ""; }; + 10080B7C220C5DD200C6E0A2 /* BranchActivityItemProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchActivityItemProvider.m; sourceTree = ""; }; + 10080B7D220C5DD200C6E0A2 /* BNCApplication.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCApplication.h; sourceTree = ""; }; + 10080B7E220C5DD200C6E0A2 /* NSMutableDictionary+Branch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableDictionary+Branch.h"; sourceTree = ""; }; + 10080B7F220C5DD200C6E0A2 /* BranchUniversalObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchUniversalObject.h; sourceTree = ""; }; + 10080B80220C5DD200C6E0A2 /* BNCEncodingUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCEncodingUtils.h; sourceTree = ""; }; + 10080B81220C5DD200C6E0A2 /* BNCKeyChain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCKeyChain.m; sourceTree = ""; }; + 10080B82220C5DD200C6E0A2 /* BNCError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCError.h; sourceTree = ""; }; + 10080B83220C5DD200C6E0A2 /* BranchDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchDelegate.m; sourceTree = ""; }; + 10080B84220C5DD200C6E0A2 /* BNCLinkData.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCLinkData.m; sourceTree = ""; }; + 10080B85220C5DD200C6E0A2 /* Branch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Branch.m; sourceTree = ""; }; + 10080B86220C5DD200C6E0A2 /* BNCAvailability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCAvailability.m; sourceTree = ""; }; + 10080B87220C5DD200C6E0A2 /* BranchDeepLinkingController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchDeepLinkingController.h; sourceTree = ""; }; + 10080B88220C5DD200C6E0A2 /* BNCThreads.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCThreads.h; sourceTree = ""; }; + 10080B89220C5DD200C6E0A2 /* BNCSystemObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCSystemObserver.h; sourceTree = ""; }; + 10080B8A220C5DD200C6E0A2 /* BNCCommerceEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCCommerceEvent.h; sourceTree = ""; }; + 10080B8B220C5DD200C6E0A2 /* BranchView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchView.m; sourceTree = ""; }; + 10080B8C220C5DD200C6E0A2 /* NSString+Branch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+Branch.h"; sourceTree = ""; }; + 10080B8D220C5DD200C6E0A2 /* BranchContentPathProperties.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchContentPathProperties.m; sourceTree = ""; }; + 10080B8E220C5DD200C6E0A2 /* Branch+Validator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Branch+Validator.h"; sourceTree = ""; }; + 10080B8F220C5DD200C6E0A2 /* BranchLinkProperties.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchLinkProperties.m; sourceTree = ""; }; + 10080B90220C5DD200C6E0A2 /* BNCDeepLinkViewControllerInstance.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCDeepLinkViewControllerInstance.h; sourceTree = ""; }; + 10080B91220C5DD200C6E0A2 /* BNCContentDiscoveryManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCContentDiscoveryManager.h; sourceTree = ""; }; + 10080B92220C5DD200C6E0A2 /* BNCFabricAnswers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCFabricAnswers.h; sourceTree = ""; }; + 10080B93220C5DD200C6E0A2 /* BNCDeviceInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCDeviceInfo.h; sourceTree = ""; }; + 10080B94220C5DD200C6E0A2 /* BNCLinkCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCLinkCache.m; sourceTree = ""; }; + 10080B95220C5DD200C6E0A2 /* BNCLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCLog.h; sourceTree = ""; }; + 10080B96220C5DD200C6E0A2 /* BranchContentDiscoverer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchContentDiscoverer.h; sourceTree = ""; }; + 10080B97220C5DD200C6E0A2 /* BranchContentDiscoveryManifest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchContentDiscoveryManifest.h; sourceTree = ""; }; + 10080B98220C5DD200C6E0A2 /* BNCConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCConfig.h; sourceTree = ""; }; + 10080B99220C5DD200C6E0A2 /* BranchConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BranchConstants.h; sourceTree = ""; }; + 10080B9A220C5DD200C6E0A2 /* BNCSpotlightService.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCSpotlightService.m; sourceTree = ""; }; + 10080B9B220C5DD200C6E0A2 /* BranchShareLink.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchShareLink.m; sourceTree = ""; }; + 10080B9C220C5DD200C6E0A2 /* UIViewController+Branch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+Branch.m"; sourceTree = ""; }; + 10080B9D220C5DD200C6E0A2 /* BNCPreferenceHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCPreferenceHelper.h; sourceTree = ""; }; + 10080B9E220C5DD200C6E0A2 /* BranchEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchEvent.m; sourceTree = ""; }; + 10080B9F220C5DD200C6E0A2 /* BNCCrashlyticsWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCCrashlyticsWrapper.h; sourceTree = ""; }; + 10080BA0220C5DD200C6E0A2 /* BNCLocalization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BNCLocalization.m; sourceTree = ""; }; + 10080BA1220C5DD200C6E0A2 /* BNCURLBlackList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BNCURLBlackList.h; sourceTree = ""; }; + 10080BA2220C5DD200C6E0A2 /* BranchViewHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = BranchViewHandler.m; sourceTree = ""; }; + 10080BA4220C5DD200C6E0A2 /* ANSCompatibility.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ANSCompatibility.h; sourceTree = ""; }; + 10080BA5220C5DD200C6E0A2 /* Fabric.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Fabric.h; sourceTree = ""; }; + 10080BA6220C5DD200C6E0A2 /* Answers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Answers.h; sourceTree = ""; }; + 10080BA7220C5DD200C6E0A2 /* FABKitProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FABKitProtocol.h; sourceTree = ""; }; + 10080BA8220C5DD200C6E0A2 /* Fabric+FABKits.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Fabric+FABKits.h"; sourceTree = ""; }; + 10080BA9220C5DD200C6E0A2 /* FABAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FABAttributes.h; sourceTree = ""; }; + 10C76537224BA455003254D8 /* CoreData 5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "CoreData 5.xcdatamodel"; sourceTree = ""; }; 64FDEBD220D12C390086C2F4 /* AFImageDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AFImageDownloader.h; sourceTree = ""; }; 64FDEBD320D12C390086C2F4 /* AFAutoPurgingImageCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AFAutoPurgingImageCache.h; sourceTree = ""; }; 64FDEBDA20D12DCE0086C2F4 /* AFAutoPurgingImageCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AFAutoPurgingImageCache.m; sourceTree = ""; }; @@ -743,7 +914,6 @@ 9E95C3D51C723AB30016632B /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 9ECBDDAD1C9C4F0300F76AEB /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; B3150B2920FE3280006D742A /* SAPTracker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SAPTracker.entitlements; sourceTree = ""; }; - B3150BAA20FF4518006D742A /* Branch.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Branch.framework; path = SAPTracker/Libraries/Branch.framework; sourceTree = ""; }; B3150BAE20FF452C006D742A /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; B3150BB020FF4532006D742A /* SafariServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SafariServices.framework; path = System/Library/Frameworks/SafariServices.framework; sourceTree = SDKROOT; }; B3150BB220FF453D006D742A /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; @@ -818,7 +988,6 @@ C89C5FCF1D06CF46002AAAD2 /* TrackingViewModeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrackingViewModeCell.swift; sourceTree = ""; }; C8A4400A1C18153600DF70A0 /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; C8B3B9BB1F39E64400B3B6BA /* Keychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; - C8B3B9BF1F3C3B4400B3B6BA /* SignUp.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = SignUp.strings; sourceTree = ""; }; C8B3B9C11F3C6B3900B3B6BA /* SignUpTranslation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignUpTranslation.swift; sourceTree = ""; }; C8BBAC781C52649E004CA551 /* LicenseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LicenseViewController.swift; sourceTree = ""; }; C8BFCCC31D51C53500FC1A27 /* TeamData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TeamData.swift; sourceTree = ""; }; @@ -894,7 +1063,6 @@ files = ( B3150BB720FF454D006D742A /* iAd.framework in Frameworks */, B3150BB520FF4548006D742A /* CoreSpotlight.framework in Frameworks */, - B31EE1A320FF915E0097AA40 /* Branch.framework in Frameworks */, B3150BB320FF453D006D742A /* MobileCoreServices.framework in Frameworks */, B3150BB120FF4532006D742A /* SafariServices.framework in Frameworks */, B3150BAF20FF452C006D742A /* AdSupport.framework in Frameworks */, @@ -915,10 +1083,176 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 10080B25220C5DD200C6E0A2 /* Branch-SDK */ = { + isa = PBXGroup; + children = ( + 10080B26220C5DD200C6E0A2 /* Branch-SDK */, + 10080BA3220C5DD200C6E0A2 /* Fabric */, + ); + path = "Branch-SDK"; + sourceTree = ""; + }; + 10080B26220C5DD200C6E0A2 /* Branch-SDK */ = { + isa = PBXGroup; + children = ( + 10080B27220C5DD200C6E0A2 /* BNCLinkData.h */, + 10080B28220C5DD200C6E0A2 /* BranchDelegate.h */, + 10080B29220C5DD200C6E0A2 /* BNCError.m */, + 10080B2A220C5DD200C6E0A2 /* BNCKeyChain.h */, + 10080B2B220C5DD200C6E0A2 /* BNCEncodingUtils.m */, + 10080B2C220C5DD200C6E0A2 /* BranchUniversalObject.m */, + 10080B2D220C5DD200C6E0A2 /* BNCApplication.m */, + 10080B2E220C5DD200C6E0A2 /* NSMutableDictionary+Branch.m */, + 10080B2F220C5DD200C6E0A2 /* BranchActivityItemProvider.h */, + 10080B30220C5DD200C6E0A2 /* BNCStrongMatchHelper.h */, + 10080B31220C5DD200C6E0A2 /* BranchCSSearchableItemAttributeSet.h */, + 10080B32220C5DD200C6E0A2 /* BNCDebug.m */, + 10080B33220C5DD200C6E0A2 /* BranchLinkProperties.h */, + 10080B34220C5DD200C6E0A2 /* BNCDeepLinkViewControllerInstance.m */, + 10080B35220C5DD200C6E0A2 /* BNCContentDiscoveryManager.m */, + 10080B36220C5DD200C6E0A2 /* Branch+Validator.m */, + 10080B37220C5DD200C6E0A2 /* BranchContentPathProperties.h */, + 10080B38220C5DD200C6E0A2 /* NSString+Branch.m */, + 10080B39220C5DD200C6E0A2 /* BNCSystemObserver.m */, + 10080B3A220C5DD200C6E0A2 /* BNCCommerceEvent.m */, + 10080B3B220C5DD200C6E0A2 /* BranchView.h */, + 10080B3C220C5DD200C6E0A2 /* BNCThreads.m */, + 10080B3D220C5DD200C6E0A2 /* BNCAvailability.h */, + 10080B3E220C5DD200C6E0A2 /* Branch.h */, + 10080B3F220C5DD200C6E0A2 /* BNCPreferenceHelper.m */, + 10080B40220C5DD200C6E0A2 /* BNCCallbacks.h */, + 10080B41220C5DD200C6E0A2 /* UIViewController+Branch.h */, + 10080B42220C5DD200C6E0A2 /* BranchShareLink.h */, + 10080B43220C5DD200C6E0A2 /* BNCFieldDefines.h */, + 10080B44220C5DD200C6E0A2 /* Networking */, + 10080B6B220C5DD200C6E0A2 /* BNCConfig.m */, + 10080B6C220C5DD200C6E0A2 /* BranchConstants.m */, + 10080B6D220C5DD200C6E0A2 /* BNCSpotlightService.h */, + 10080B6E220C5DD200C6E0A2 /* BranchContentDiscoveryManifest.m */, + 10080B6F220C5DD200C6E0A2 /* BNCLog.m */, + 10080B70220C5DD200C6E0A2 /* BranchContentDiscoverer.m */, + 10080B71220C5DD200C6E0A2 /* BNCDeviceInfo.m */, + 10080B72220C5DD200C6E0A2 /* BNCLinkCache.h */, + 10080B73220C5DD200C6E0A2 /* BNCFabricAnswers.m */, + 10080B74220C5DD200C6E0A2 /* BranchViewHandler.h */, + 10080B75220C5DD200C6E0A2 /* BNCURLBlackList.m */, + 10080B76220C5DD200C6E0A2 /* BNCLocalization.h */, + 10080B77220C5DD200C6E0A2 /* BNCCrashlyticsWrapper.m */, + 10080B78220C5DD200C6E0A2 /* BranchEvent.h */, + 10080B79220C5DD200C6E0A2 /* BNCDebug.h */, + 10080B7A220C5DD200C6E0A2 /* BranchCSSearchableItemAttributeSet.m */, + 10080B7B220C5DD200C6E0A2 /* BNCStrongMatchHelper.m */, + 10080B7C220C5DD200C6E0A2 /* BranchActivityItemProvider.m */, + 10080B7D220C5DD200C6E0A2 /* BNCApplication.h */, + 10080B7E220C5DD200C6E0A2 /* NSMutableDictionary+Branch.h */, + 10080B7F220C5DD200C6E0A2 /* BranchUniversalObject.h */, + 10080B80220C5DD200C6E0A2 /* BNCEncodingUtils.h */, + 10080B81220C5DD200C6E0A2 /* BNCKeyChain.m */, + 10080B82220C5DD200C6E0A2 /* BNCError.h */, + 10080B83220C5DD200C6E0A2 /* BranchDelegate.m */, + 10080B84220C5DD200C6E0A2 /* BNCLinkData.m */, + 10080B85220C5DD200C6E0A2 /* Branch.m */, + 10080B86220C5DD200C6E0A2 /* BNCAvailability.m */, + 10080B87220C5DD200C6E0A2 /* BranchDeepLinkingController.h */, + 10080B88220C5DD200C6E0A2 /* BNCThreads.h */, + 10080B89220C5DD200C6E0A2 /* BNCSystemObserver.h */, + 10080B8A220C5DD200C6E0A2 /* BNCCommerceEvent.h */, + 10080B8B220C5DD200C6E0A2 /* BranchView.m */, + 10080B8C220C5DD200C6E0A2 /* NSString+Branch.h */, + 10080B8D220C5DD200C6E0A2 /* BranchContentPathProperties.m */, + 10080B8E220C5DD200C6E0A2 /* Branch+Validator.h */, + 10080B8F220C5DD200C6E0A2 /* BranchLinkProperties.m */, + 10080B90220C5DD200C6E0A2 /* BNCDeepLinkViewControllerInstance.h */, + 10080B91220C5DD200C6E0A2 /* BNCContentDiscoveryManager.h */, + 10080B92220C5DD200C6E0A2 /* BNCFabricAnswers.h */, + 10080B93220C5DD200C6E0A2 /* BNCDeviceInfo.h */, + 10080B94220C5DD200C6E0A2 /* BNCLinkCache.m */, + 10080B95220C5DD200C6E0A2 /* BNCLog.h */, + 10080B96220C5DD200C6E0A2 /* BranchContentDiscoverer.h */, + 10080B97220C5DD200C6E0A2 /* BranchContentDiscoveryManifest.h */, + 10080B98220C5DD200C6E0A2 /* BNCConfig.h */, + 10080B99220C5DD200C6E0A2 /* BranchConstants.h */, + 10080B9A220C5DD200C6E0A2 /* BNCSpotlightService.m */, + 10080B9B220C5DD200C6E0A2 /* BranchShareLink.m */, + 10080B9C220C5DD200C6E0A2 /* UIViewController+Branch.m */, + 10080B9D220C5DD200C6E0A2 /* BNCPreferenceHelper.h */, + 10080B9E220C5DD200C6E0A2 /* BranchEvent.m */, + 10080B9F220C5DD200C6E0A2 /* BNCCrashlyticsWrapper.h */, + 10080BA0220C5DD200C6E0A2 /* BNCLocalization.m */, + 10080BA1220C5DD200C6E0A2 /* BNCURLBlackList.h */, + 10080BA2220C5DD200C6E0A2 /* BranchViewHandler.m */, + ); + path = "Branch-SDK"; + sourceTree = ""; + }; + 10080B44220C5DD200C6E0A2 /* Networking */ = { + isa = PBXGroup; + children = ( + 10080B45220C5DD200C6E0A2 /* BNCServerRequestQueue.m */, + 10080B46220C5DD200C6E0A2 /* BNCServerRequest.m */, + 10080B47220C5DD200C6E0A2 /* BNCServerResponse.h */, + 10080B48220C5DD200C6E0A2 /* BNCNetworkService.m */, + 10080B49220C5DD200C6E0A2 /* BNCServerInterface.m */, + 10080B4A220C5DD200C6E0A2 /* BNCNetworkServiceProtocol.h */, + 10080B4B220C5DD200C6E0A2 /* Requests */, + 10080B66220C5DD200C6E0A2 /* BNCServerRequestQueue.h */, + 10080B67220C5DD200C6E0A2 /* BNCServerRequest.h */, + 10080B68220C5DD200C6E0A2 /* BNCServerResponse.m */, + 10080B69220C5DD200C6E0A2 /* BNCServerInterface.h */, + 10080B6A220C5DD200C6E0A2 /* BNCNetworkService.h */, + ); + path = Networking; + sourceTree = ""; + }; + 10080B4B220C5DD200C6E0A2 /* Requests */ = { + isa = PBXGroup; + children = ( + 10080B4C220C5DD200C6E0A2 /* BranchLogoutRequest.m */, + 10080B4D220C5DD200C6E0A2 /* BranchInstallRequest.m */, + 10080B4E220C5DD200C6E0A2 /* BranchCreditHistoryRequest.h */, + 10080B4F220C5DD200C6E0A2 /* BranchSetIdentityRequest.m */, + 10080B50220C5DD200C6E0A2 /* BranchUserCompletedActionRequest.m */, + 10080B51220C5DD200C6E0A2 /* BranchRegisterViewRequest.m */, + 10080B52220C5DD200C6E0A2 /* BranchCloseRequest.m */, + 10080B53220C5DD200C6E0A2 /* BranchRedeemRewardsRequest.h */, + 10080B54220C5DD200C6E0A2 /* BranchSpotlightUrlRequest.m */, + 10080B55220C5DD200C6E0A2 /* BranchLoadRewardsRequest.m */, + 10080B56220C5DD200C6E0A2 /* BranchShortUrlSyncRequest.h */, + 10080B57220C5DD200C6E0A2 /* BranchOpenRequest.h */, + 10080B58220C5DD200C6E0A2 /* BranchShortUrlRequest.h */, + 10080B59220C5DD200C6E0A2 /* BranchLogoutRequest.h */, + 10080B5A220C5DD200C6E0A2 /* BranchUserCompletedActionRequest.h */, + 10080B5B220C5DD200C6E0A2 /* BranchRegisterViewRequest.h */, + 10080B5C220C5DD200C6E0A2 /* BranchCreditHistoryRequest.m */, + 10080B5D220C5DD200C6E0A2 /* BranchSetIdentityRequest.h */, + 10080B5E220C5DD200C6E0A2 /* BranchInstallRequest.h */, + 10080B5F220C5DD200C6E0A2 /* BranchRedeemRewardsRequest.m */, + 10080B60220C5DD200C6E0A2 /* BranchSpotlightUrlRequest.h */, + 10080B61220C5DD200C6E0A2 /* BranchCloseRequest.h */, + 10080B62220C5DD200C6E0A2 /* BranchShortUrlRequest.m */, + 10080B63220C5DD200C6E0A2 /* BranchOpenRequest.m */, + 10080B64220C5DD200C6E0A2 /* BranchShortUrlSyncRequest.m */, + 10080B65220C5DD200C6E0A2 /* BranchLoadRewardsRequest.h */, + ); + path = Requests; + sourceTree = ""; + }; + 10080BA3220C5DD200C6E0A2 /* Fabric */ = { + isa = PBXGroup; + children = ( + 10080BA4220C5DD200C6E0A2 /* ANSCompatibility.h */, + 10080BA5220C5DD200C6E0A2 /* Fabric.h */, + 10080BA6220C5DD200C6E0A2 /* Answers.h */, + 10080BA7220C5DD200C6E0A2 /* FABKitProtocol.h */, + 10080BA8220C5DD200C6E0A2 /* Fabric+FABKits.h */, + 10080BA9220C5DD200C6E0A2 /* FABAttributes.h */, + ); + path = Fabric; + sourceTree = ""; + }; 7AC6457280AEA3EC18F5E846 /* Frameworks */ = { isa = PBXGroup; children = ( - B3150BAA20FF4518006D742A /* Branch.framework */, B3150BB620FF454D006D742A /* iAd.framework */, B3150BB420FF4548006D742A /* CoreSpotlight.framework */, B3150BB220FF453D006D742A /* MobileCoreServices.framework */, @@ -1292,7 +1626,6 @@ 9ECBDDAD1C9C4F0300F76AEB /* LaunchScreen.storyboard */, C8D5E81C1D479FA800DA0D61 /* Main.storyboard */, C85DB8241F30B680009559CE /* SignUp.storyboard */, - C8B3B9BF1F3C3B4400B3B6BA /* SignUp.strings */, 9E95C3D51C723AB30016632B /* Constants.swift */, 7FD9D3021A1236080058FEAD /* Extensions.swift */, C8B3B9C11F3C6B3900B3B6BA /* SignUpTranslation.swift */, @@ -1589,6 +1922,7 @@ C8F71EA31D8691F700826DE4 /* Libraries */ = { isa = PBXGroup; children = ( + 10080B25220C5DD200C6E0A2 /* Branch-SDK */, C8BFCC1B1D50F17800FC1A27 /* SMTWiFiStatus */, C8F71EA41D8691F700826DE4 /* AFNetworking */, C8F71EB81D8691F700826DE4 /* SVProgressHUD */, @@ -1682,8 +2016,7 @@ 7F83F14C19F154F000C53328 /* Sources */, 7F83F14D19F154F000C53328 /* Frameworks */, 7F83F14E19F154F000C53328 /* Resources */, - 7FF3E4BE1A1DFCF20012F1FB /* ShellScript */, - B31EE1A520FF915E0097AA40 /* Embed Frameworks */, + 7FF3E4BE1A1DFCF20012F1FB /* Run Script */, ); buildRules = ( ); @@ -1723,9 +2056,8 @@ TargetAttributes = { 7F83F14F19F154F000C53328 = { CreatedOnToolsVersion = 6.0.1; - DevelopmentTeam = F9U235EA4J; LastSwiftMigration = 0820; - ProvisioningStyle = Automatic; + ProvisioningStyle = Manual; SystemCapabilities = { com.apple.SafariKeychain = { enabled = 1; @@ -1826,7 +2158,6 @@ 7F14A1681A13B9A6001C1ECB /* GA.png in Resources */, 7F14A10C1A13B9A6001C1ECB /* _british-antarctic-territory.png in Resources */, 7F14A1771A13B9A6001C1ECB /* GW.png in Resources */, - C8B3B9C01F3C3B4400B3B6BA /* SignUp.strings in Resources */, 7F14A1811A13B9A6001C1ECB /* IE.png in Resources */, 7F14A14D1A13B9A6001C1ECB /* CR.png in Resources */, 7F14A1991A13B9A6001C1ECB /* LB.png in Resources */, @@ -2054,18 +2385,19 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 7FF3E4BE1A1DFCF20012F1FB /* ShellScript */ = { + 7FF3E4BE1A1DFCF20012F1FB /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "FILE=\"${SRCROOT}/HockeySDK-iOS/BuildAgent\"\nif [ -f \"$FILE\" ]; then\n\"$FILE\"\nfi"; + shellScript = "FILE=\"${SRCROOT}/HockeySDK-iOS/BuildAgent\"\nif [ -f \"$FILE\" ]; then\n\"$FILE\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -2075,14 +2407,19 @@ buildActionMask = 2147483647; files = ( 64FDEBDC20D12DCF0086C2F4 /* AFAutoPurgingImageCache.m in Sources */, + 10080BCF220C5DD200C6E0A2 /* BNCDeviceInfo.m in Sources */, 64FDEBDD20D12DCF0086C2F4 /* AFImageDownloader.m in Sources */, C867BD091F4B0274007E78E0 /* TrainingCompetitorViewController.swift in Sources */, C85DB82A1F31AAFD009559CE /* LoginViewController.swift in Sources */, 7F1875B31A10F1B800C25BE0 /* TrackingViewController.swift in Sources */, + 10080BCE220C5DD200C6E0A2 /* BranchContentDiscoverer.m in Sources */, + 10080BC8220C5DD200C6E0A2 /* BranchShortUrlSyncRequest.m in Sources */, C8739E081D2AA2A500A89F3C /* Leaderboard+CoreDataProperties.swift in Sources */, C8739E121D2BF14F00A89F3C /* EventData.swift in Sources */, + 10080BAD220C5DD200C6E0A2 /* BNCApplication.m in Sources */, C843D4881EBCADDC00250F01 /* CompetitorSessionController.swift in Sources */, C880646E2087DA1F005FBE57 /* BoatClassData.swift in Sources */, + 10080BC6220C5DD200C6E0A2 /* BranchShortUrlRequest.m in Sources */, C891887A2057F55100E37CC3 /* BoatSessionController.swift in Sources */, C8F71EE31D8691F700826DE4 /* SVIndefiniteAnimatedView.m in Sources */, C8739E0F1D2BEE7900A89F3C /* SessionController.swift in Sources */, @@ -2091,14 +2428,23 @@ 7F8B8D431A373D4400C34B88 /* CourseViewController.swift in Sources */, 7FAFA7951A126B5700D230F1 /* AboutViewController.swift in Sources */, C8739E161D2BF17900A89F3C /* CompetitorData.swift in Sources */, + 10080BAB220C5DD200C6E0A2 /* BNCEncodingUtils.m in Sources */, C8289F3A1F386D8300A76953 /* SignUpRequestManager.swift in Sources */, + 10080BD7220C5DD200C6E0A2 /* BranchDelegate.m in Sources */, + 10080BCC220C5DD200C6E0A2 /* BranchContentDiscoveryManifest.m in Sources */, + 10080BB5220C5DD200C6E0A2 /* BNCCommerceEvent.m in Sources */, + 10080BE0220C5DD200C6E0A2 /* BranchShareLink.m in Sources */, C8739E0C1D2AA2A500A89F3C /* Event+CoreDataProperties.swift in Sources */, C8739E071D2AA2A500A89F3C /* GPSFix.swift in Sources */, C8E0BD0D1F39ADAD002FCCA5 /* KeychainPasswordItem.swift in Sources */, C891887C20580A4100E37CC3 /* TrainingBoatViewController.swift in Sources */, C87A74E11F4EE570003DE396 /* TrainingRequestManager.swift in Sources */, + 10080BB0220C5DD200C6E0A2 /* BNCDeepLinkViewControllerInstance.m in Sources */, + 10080BB7220C5DD200C6E0A2 /* BNCPreferenceHelper.m in Sources */, C89188762057F36300E37CC3 /* BoatSessionViewController.swift in Sources */, C843D4811EBC54A300250F01 /* MarkCheckIn+CoreDataProperties.swift in Sources */, + 10080BB6220C5DD200C6E0A2 /* BNCThreads.m in Sources */, + 10080BAE220C5DD200C6E0A2 /* NSMutableDictionary+Branch.m in Sources */, C843D532201F3050003725F0 /* NewCheckInViewController.swift in Sources */, C8F71EEE1D8691F700826DE4 /* UIProgressView+AFNetworking.m in Sources */, C8CF22A91F62780C00A01E0A /* CheckInDataCollector.swift in Sources */, @@ -2108,26 +2454,44 @@ C89C5FD01D06CF46002AAAD2 /* TrackingViewModeCell.swift in Sources */, C8FF5B7C1EB8CF2C00CF9764 /* MarkData.swift in Sources */, C88F6D311FFBC35F00A616BE /* ErrorHelper.swift in Sources */, + 10080BE3220C5DD200C6E0A2 /* BNCLocalization.m in Sources */, + 10080BC7220C5DD200C6E0A2 /* BranchOpenRequest.m in Sources */, + 10080BC3220C5DD200C6E0A2 /* BranchLoadRewardsRequest.m in Sources */, C8F71EEA1D8691F700826DE4 /* UIActivityIndicatorView+AFNetworking.m in Sources */, 7F1B66F91A0CDCF600904A41 /* BatteryManager.swift in Sources */, + 10080BE1220C5DD200C6E0A2 /* UIViewController+Branch.m in Sources */, + 10080BCB220C5DD200C6E0A2 /* BranchConstants.m in Sources */, 7FA6551A19F6661900A1B92D /* CoreDataManager.swift in Sources */, C8F71EE71D8691F700826DE4 /* SVRadialGradientLayer.m in Sources */, C852BAA21CF5E1EA0093B0C5 /* CheckInRequestManager.swift in Sources */, C8F71EE01D8691F700826DE4 /* AFURLRequestSerialization.m in Sources */, + 10080BCD220C5DD200C6E0A2 /* BNCLog.m in Sources */, C8BBAC791C52649E004CA551 /* LicenseViewController.swift in Sources */, C8BFCCC41D51C53500FC1A27 /* TeamData.swift in Sources */, + 10080BD4220C5DD200C6E0A2 /* BNCStrongMatchHelper.m in Sources */, + 10080BE2220C5DD200C6E0A2 /* BranchEvent.m in Sources */, C843D4911EC0A93600250F01 /* CoreData1-3.xcmappingmodel in Sources */, C89188742057F32300E37CC3 /* RegattaBoatViewController.swift in Sources */, + 10080BCA220C5DD200C6E0A2 /* BNCConfig.m in Sources */, + 10080BBF220C5DD200C6E0A2 /* BranchUserCompletedActionRequest.m in Sources */, + 10080BC2220C5DD200C6E0A2 /* BranchSpotlightUrlRequest.m in Sources */, + 10080BC1220C5DD200C6E0A2 /* BranchCloseRequest.m in Sources */, + 10080BC4220C5DD200C6E0A2 /* BranchCreditHistoryRequest.m in Sources */, C88F6D2B1FEAC4E800A616BE /* LeaderboardAutoCourseData.swift in Sources */, 7F23F0881A1A3DEF00F03F1C /* CheckInController.swift in Sources */, + 10080BD2220C5DD200C6E0A2 /* BNCCrashlyticsWrapper.m in Sources */, C843D47F1EBC54A300250F01 /* CompetitorCheckIn+CoreDataProperties.swift in Sources */, C8F71EDC1D8691F700826DE4 /* AFHTTPSessionManager.m in Sources */, C8F71EE91D8691F700826DE4 /* AFNetworkActivityIndicatorManager.m in Sources */, C84936291D47645F000F30E8 /* CheckInTableViewCell.swift in Sources */, C891886E2057DB4A00E37CC3 /* BoatData.swift in Sources */, + 10080BC5220C5DD200C6E0A2 /* BranchRedeemRewardsRequest.m in Sources */, 7F2C605A19F160E400C32D93 /* LocationManager.swift in Sources */, + 10080BD0220C5DD200C6E0A2 /* BNCFabricAnswers.m in Sources */, C89C5FCC1D06CF0C002AAAD2 /* TrackingViewGPSAccuracyCell.swift in Sources */, + 10080BAF220C5DD200C6E0A2 /* BNCDebug.m in Sources */, C89C5FCA1D06CECD002AAAD2 /* TrackingViewStatusCell.swift in Sources */, + 10080BDD220C5DD200C6E0A2 /* BranchLinkProperties.m in Sources */, C843D4861EBC927800250F01 /* RegattaMarkViewController.swift in Sources */, C8F71EDE1D8691F700826DE4 /* AFSecurityPolicy.m in Sources */, 7F8B8D3F1A37356C00C34B88 /* TrackingPageViewController.swift in Sources */, @@ -2139,47 +2503,71 @@ C89188722057E7D700E37CC3 /* BoatCheckIn+CoreDataProperties.swift in Sources */, C843D4841EBC54A300250F01 /* CheckIn.swift in Sources */, C8BFCCCC1D523C5D00FC1A27 /* TitleView.swift in Sources */, + 10080BBC220C5DD200C6E0A2 /* BranchLogoutRequest.m in Sources */, C8CF22C01F6926E500A01E0A /* CompetitorSessionViewController.swift in Sources */, + 10080BB8220C5DD200C6E0A2 /* BNCServerRequestQueue.m in Sources */, C8F71EF31D86A15F00826DE4 /* SMTWiFiStatus.m in Sources */, 7F8CC2081A10D47A00C56D7F /* SettingsViewController.swift in Sources */, C8739E0D1D2AA2A500A89F3C /* Event.swift in Sources */, C87A74EC1F5068D0003DE396 /* CreateTrainingData.swift in Sources */, C8CF22A31F6194C600A01E0A /* RegattaCheckInController.swift in Sources */, + 10080BDB220C5DD200C6E0A2 /* BranchView.m in Sources */, 7F8B8D411A373B0200C34B88 /* SpeedViewController.swift in Sources */, + 10080BB3220C5DD200C6E0A2 /* NSString+Branch.m in Sources */, 9E95C3D61C723AB30016632B /* Constants.swift in Sources */, C8CF22A51F61965C00A01E0A /* TrainingCheckInController.swift in Sources */, C89188782057F44200E37CC3 /* BoatViewController.swift in Sources */, C8F71EF01D8691F700826DE4 /* UIWebView+AFNetworking.m in Sources */, + 10080BDC220C5DD200C6E0A2 /* BranchContentPathProperties.m in Sources */, + 10080BAA220C5DD200C6E0A2 /* BNCError.m in Sources */, + 10080BB1220C5DD200C6E0A2 /* BNCContentDiscoveryManager.m in Sources */, + 10080BDA220C5DD200C6E0A2 /* BNCAvailability.m in Sources */, C87A74E81F504EBB003DE396 /* CreateEventData.swift in Sources */, 7FD9D3031A1236080058FEAD /* Extensions.swift in Sources */, C8739E091D2AA2A500A89F3C /* Leaderboard.swift in Sources */, + 10080BD1220C5DD200C6E0A2 /* BNCURLBlackList.m in Sources */, C8B3B9BC1F39E64400B3B6BA /* Keychain.swift in Sources */, C8739E141D2BF16300A89F3C /* LeaderboardData.swift in Sources */, C89C5FC51D06A289002AAAD2 /* GradientView.swift in Sources */, C8F71EEF1D8691F700826DE4 /* UIRefreshControl+AFNetworking.m in Sources */, C8F71EE21D8691F700826DE4 /* AFURLSessionManager.m in Sources */, C843D4821EBC54A300250F01 /* MarkCheckIn.swift in Sources */, + 10080BD5220C5DD200C6E0A2 /* BranchActivityItemProvider.m in Sources */, C8F71EDD1D8691F700826DE4 /* AFNetworkReachabilityManager.m in Sources */, + 10080BB4220C5DD200C6E0A2 /* BNCSystemObserver.m in Sources */, C843D4801EBC54A300250F01 /* CompetitorCheckIn.swift in Sources */, C8CF22C41F692B7700A01E0A /* MarkViewController.swift in Sources */, C852BAAB1CF736680093B0C5 /* Preferences.swift in Sources */, + 10080BC9220C5DD200C6E0A2 /* BNCServerResponse.m in Sources */, + 10080BE4220C5DD200C6E0A2 /* BranchViewHandler.m in Sources */, + 10080BB2220C5DD200C6E0A2 /* Branch+Validator.m in Sources */, + 10080BDE220C5DD200C6E0A2 /* BNCLinkCache.m in Sources */, C843D48A1EBCADF500250F01 /* MarkSessionController.swift in Sources */, + 10080BBA220C5DD200C6E0A2 /* BNCNetworkService.m in Sources */, C8CF22C21F692B5300A01E0A /* MarkSessionViewController.swift in Sources */, 7F0658FD19F7D6700077FF95 /* ScanViewController.swift in Sources */, + 10080BD6220C5DD200C6E0A2 /* BNCKeyChain.m in Sources */, C843D4831EBC54A300250F01 /* CheckIn+CoreDataProperties.swift in Sources */, C8F71EEC1D8691F700826DE4 /* UIButton+AFNetworking.m in Sources */, + 10080BD3220C5DD200C6E0A2 /* BranchCSSearchableItemAttributeSet.m in Sources */, C867BD0B1F4B0290007E78E0 /* CreateTrainingViewController.swift in Sources */, C87A74E31F4F0E34003DE396 /* TrainingController.swift in Sources */, + 10080BD9220C5DD200C6E0A2 /* Branch.m in Sources */, + 10080BBE220C5DD200C6E0A2 /* BranchSetIdentityRequest.m in Sources */, C8CF22C61F694EB900A01E0A /* TrainingViewController.swift in Sources */, C8F1B1E11D488B6D004C3379 /* Translation.swift in Sources */, C8F71EE81D8691F700826DE4 /* TSMarkdownParser.m in Sources */, C8739E061D2AA2A500A89F3C /* GPSFix+CoreDataProperties.swift in Sources */, 7F193A8F1A39ACF500D71773 /* TimerViewController.swift in Sources */, C88F6D2F1FEACD7400A616BE /* RegattaRaceCourseData.swift in Sources */, + 10080BB9220C5DD200C6E0A2 /* BNCServerRequest.m in Sources */, C8739E181D2BF2DC00A89F3C /* BaseData.swift in Sources */, C8CF22B41F67C4D800A01E0A /* RegattaRaceColumnAddData.swift in Sources */, 7F83F15619F154F000C53328 /* AppDelegate.swift in Sources */, + 10080BD8220C5DD200C6E0A2 /* BNCLinkData.m in Sources */, C8B3B9C21F3C6B3900B3B6BA /* SignUpTranslation.swift in Sources */, + 10080BC0220C5DD200C6E0A2 /* BranchRegisterViewRequest.m in Sources */, + 10080BAC220C5DD200C6E0A2 /* BranchUniversalObject.m in Sources */, 7F950A911A35E967003C8C5C /* LeaderboardViewController.swift in Sources */, C8F71EED1D8691F700826DE4 /* UIImageView+AFNetworking.m in Sources */, C88F6D2D1FEACC5700A616BE /* LeaderboardGroupData.swift in Sources */, @@ -2189,9 +2577,12 @@ C85DB8281F31AAE0009559CE /* SignUpViewController.swift in Sources */, C89C5FCE1D06CF26002AAAD2 /* TrackingViewGPSFixesCell.swift in Sources */, C8F71EE11D8691F700826DE4 /* AFURLResponseSerialization.m in Sources */, + 10080BBD220C5DD200C6E0A2 /* BranchInstallRequest.m in Sources */, C8F71EE61D8691F700826DE4 /* SVProgressHUD.m in Sources */, 7F0658FF19F7EE910077FF95 /* CheckInData.swift in Sources */, 7F193A911A39E54600D71773 /* GPSAccuracyView.swift in Sources */, + 10080BDF220C5DD200C6E0A2 /* BNCSpotlightService.m in Sources */, + 10080BBB220C5DD200C6E0A2 /* BNCServerInterface.m in Sources */, C8E0BD0A1F39A08B002FCCA5 /* FormularViewController.swift in Sources */, C8CF22BE1F69187800A01E0A /* CompetitorViewController.swift in Sources */, C8F71EE41D8691F700826DE4 /* SVProgressAnimatedView.m in Sources */, @@ -2335,8 +2726,8 @@ CODE_SIGN_ENTITLEMENTS = SAPTracker/SAPTracker.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = F9U235EA4J; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/SAPTracker/Libraries", @@ -2346,7 +2737,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_SWIFT_FLAGS = "-D DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = com.sap.sailing.ios.SAPTracker.dev; + PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2363,8 +2754,8 @@ CODE_SIGN_ENTITLEMENTS = SAPTracker/SAPTracker.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = F9U235EA4J; + CODE_SIGN_STYLE = Manual; + DEVELOPMENT_TEAM = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/SAPTracker/Libraries", @@ -2374,7 +2765,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; OTHER_SWIFT_FLAGS = ""; - PRODUCT_BUNDLE_IDENTIFIER = com.sap.sailing.ios.SAPTracker.dev; + PRODUCT_BUNDLE_IDENTIFIER = ""; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = ""; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -2390,6 +2781,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_IDENTITY = "iPhone Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + DEVELOPMENT_TEAM = ""; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", @@ -2411,6 +2803,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_IDENTITY = "iPhone Distribution"; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = SAPTrackerTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = "com.sap.sailing.ios.$(PRODUCT_NAME:rfc1034identifier)"; @@ -2459,12 +2852,13 @@ 7F44095919F65B5B0021BF22 /* CoreData.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 10C76537224BA455003254D8 /* CoreData 5.xcdatamodel */, C843D522201F1F17003725F0 /* CoreData 4.xcdatamodel */, C8D938B11EB9AFDB00B1B4BC /* CoreData 3.xcdatamodel */, C8739D9A1D25317100A89F3C /* CoreData 2.xcdatamodel */, 7F44095A19F65B5B0021BF22 /* CoreData.xcdatamodel */, ); - currentVersion = C843D522201F1F17003725F0 /* CoreData 4.xcdatamodel */; + currentVersion = 10C76537224BA455003254D8 /* CoreData 5.xcdatamodel */; path = CoreData.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/AppDelegate.swift b/ios/SAPTracker/src/xcode/SAPTracker/AppDelegate.swift index e0afe41d5cc..6ff21e2abb8 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/AppDelegate.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/AppDelegate.swift @@ -8,7 +8,7 @@ import UIKit import CoreData -import Branch +// Branch.io Version: 0.25.11 @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { @@ -41,7 +41,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { setup() Branch.getInstance().initSession(launchOptions: launchOptions) { (params, error) in guard let data = params as? [String: AnyObject] else { return } - if let checkinUrl = data["checkin_url"] as? String { + if let checkinUrl = data["checkinUrl"] as? String { Preferences.newCheckInURL = checkinUrl.removingPercentEncoding } } @@ -50,7 +50,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool { let branchQueryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems - if (branchQueryItems?.first(where: {$0.name == "checkin_url"})) != nil { + if (branchQueryItems?.first(where: {$0.name == "checkinUrl"})) != nil { Branch.getInstance().application(app, open: url, options: options) } else { Preferences.newCheckInURL = urlStringForDeeplink(url: url) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/InfoPlist.strings b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/InfoPlist.strings index 0faec172b97..0878a6aa745 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/InfoPlist.strings +++ b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/InfoPlist.strings @@ -1,11 +1,12 @@ -// -// InfoPlist.strings -// SAPTracker -// -// Created by Raimund Wege on 04.11.16. -// Copyright © 2016 com.sap.sailing. All rights reserved. -// +/* + InfoPlist.strings + SAPTracker + + Created by Raimund Wege on 04.11.16. + Copyright © 2016 com.sap.sailing. All rights reserved. +*/ +"NSLocationAlwaysUsageDescription" = "In order to track your entire race your phone needs to continue tracking also when you switch off or lock the screen which helps save battery and avoids unintentionally stopping the tracking process."; "NSLocationWhenInUseUsageDescription" = "In order to track your entire race your phone needs to continue tracking also when you switch off or lock the screen which helps save battery and avoids unintentionally stopping the tracking process."; "NSLocationAlwaysAndWhenInUseUsageDescription" = "In order to track your entire race your phone needs to continue tracking also when you switch off or lock the screen which helps save battery and avoids unintentionally stopping the tracking process."; "NSCameraUsageDescription" = "The camera is required to perform a check-in using QR code."; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings index 9a7c34ec360..721f4b8d9d2 100644 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings and b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Localizable.strings differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckIn+CoreDataProperties.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckIn+CoreDataProperties.swift index b450c45676a..d033d1cf574 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckIn+CoreDataProperties.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckIn+CoreDataProperties.swift @@ -20,5 +20,6 @@ extension CheckIn { @NSManaged var event: Event @NSManaged var gpsFixes: NSSet? @NSManaged var leaderboard: Leaderboard + @NSManaged var secret: String? } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckIn.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckIn.swift index 06c46182e45..10dea2d61ab 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckIn.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckIn.swift @@ -17,6 +17,7 @@ class CheckIn: NSManagedObject { serverURL = checkInData.serverURL event.updateWithEventData(eventData: checkInData.eventData) leaderboard.updateWithLeaderboardData(leaderboardData: checkInData.leaderboardData) + secret = checkInData.secret } func eventURL() -> URL? { diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckInController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInController.swift index 17f05d12c53..5bcc37c9f1d 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckInController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInController.swift @@ -139,7 +139,8 @@ class CheckInController : NSObject { failure: @escaping (_ error: Error) -> Void) { SVProgressHUD.show() - let checkInRequestManager = CheckInRequestManager(baseURLString: checkInData.serverURL) + let checkInRequestManager = CheckInRequestManager(baseURLString: checkInData.serverURL, + secret: checkInData.secret) checkInRequestManager.postCheckIn(checkInData: checkInData, success: { [weak self] () in SVProgressHUD.popActivity() self?.postCheckInSuccess(checkInData: checkInData, success: success) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckInData.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInData.swift index cefa62b2082..f667dd7fff2 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckInData.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInData.swift @@ -16,6 +16,7 @@ class CheckInData: NSObject { static let LeaderboardName = "leaderboard_name" static let CompetitorID = "competitor_id" static let MarkID = "mark_id" + static let Secret = "secret" } public enum CheckInDataType { @@ -31,6 +32,7 @@ class CheckInData: NSObject { let competitorID: String? let markID: String? let isTraining: Bool + let secret: String? let type: CheckInDataType var eventData = EventData() @@ -44,7 +46,8 @@ class CheckInData: NSObject { eventID: String, leaderboardName: String, competitorID: String, - isTraining: Bool) + isTraining: Bool, + secret: String? = nil) { self.boatID = nil self.competitorID = competitorID @@ -53,6 +56,7 @@ class CheckInData: NSObject { self.markID = nil self.serverURL = serverURL self.isTraining = isTraining + self.secret = secret self.type = CheckInDataType.competitor super.init() } @@ -61,7 +65,8 @@ class CheckInData: NSObject { eventID: String, leaderboardName: String, markID: String, - isTraining: Bool) + isTraining: Bool, + secret: String? = nil) { self.boatID = nil self.competitorID = nil @@ -70,6 +75,7 @@ class CheckInData: NSObject { self.markID = markID self.serverURL = serverURL self.isTraining = isTraining + self.secret = secret type = CheckInDataType.mark super.init() } @@ -78,7 +84,8 @@ class CheckInData: NSObject { eventID: String, leaderboardName: String, boatID: String, - isTraining: Bool) + isTraining: Bool, + secret: String? = nil) { self.boatID = boatID self.competitorID = nil @@ -87,6 +94,7 @@ class CheckInData: NSObject { self.markID = nil self.serverURL = serverURL self.isTraining = isTraining + self.secret = secret type = CheckInDataType.boat super.init() } @@ -97,7 +105,8 @@ class CheckInData: NSObject { eventID: boatCheckIn.event.eventID, leaderboardName: boatCheckIn.leaderboard.name, boatID: boatCheckIn.boatID, - isTraining: boatCheckIn.isTraining.boolValue + isTraining: boatCheckIn.isTraining.boolValue, + secret: boatCheckIn.secret ) } @@ -107,7 +116,8 @@ class CheckInData: NSObject { eventID: competitorCheckIn.event.eventID, leaderboardName: competitorCheckIn.leaderboard.name, competitorID: competitorCheckIn.competitorID, - isTraining: competitorCheckIn.isTraining.boolValue + isTraining: competitorCheckIn.isTraining.boolValue, + secret: competitorCheckIn.secret ) } @@ -117,7 +127,8 @@ class CheckInData: NSObject { eventID: markCheckIn.event.eventID, leaderboardName: markCheckIn.leaderboard.name, markID: markCheckIn.markID, - isTraining: markCheckIn.isTraining.boolValue + isTraining: markCheckIn.isTraining.boolValue, + secret: markCheckIn.secret ) } @@ -125,7 +136,7 @@ class CheckInData: NSObject { // handle the case where a user uses the app to scan a QR code containing a branch.io deeplink let branchQueryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems - let checkinUrl = branchQueryItems?.first(where: {$0.name == "checkin_url"}).map { URL(string: $0.value!)! } + let checkinUrl = branchQueryItems?.first(where: {$0.name == "checkinUrl"}).map { URL(string: $0.value!)! } let unwrappedUrl = checkinUrl ?? url @@ -142,13 +153,16 @@ class CheckInData: NSObject { // Get check-in items guard let eventID = CheckInData.queryItemValue(queryItems: queryItems, itemName: ItemNames.EventID) else { return nil } guard let leaderboardName = CheckInData.queryItemValue(queryItems: queryItems, itemName: ItemNames.LeaderboardName) else { return nil } + let secret = CheckInData.queryItemValue(queryItems: queryItems, itemName: ItemNames.Secret) + if let competitorID = CheckInData.queryItemValue(queryItems: queryItems, itemName: ItemNames.CompetitorID) { self.init( serverURL: serverURL, eventID: eventID, leaderboardName: leaderboardName, competitorID: competitorID, - isTraining: false + isTraining: false, + secret: secret ) } else if let markID = CheckInData.queryItemValue(queryItems: queryItems, itemName: ItemNames.MarkID) { self.init( @@ -156,7 +170,8 @@ class CheckInData: NSObject { eventID: eventID, leaderboardName: leaderboardName, markID: markID, - isTraining: false + isTraining: false, + secret: secret ) } else if let boatID = CheckInData.queryItemValue(queryItems: queryItems, itemName: ItemNames.BoatID) { self.init( @@ -164,7 +179,8 @@ class CheckInData: NSObject { eventID: eventID, leaderboardName: leaderboardName, boatID: boatID, - isTraining: false + isTraining: false, + secret: secret ) } else { logError(name: "\(#function)", error: "unknown check-in type") diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckInDataCollector.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInDataCollector.swift index 08d36df6695..e5fd89a8182 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckInDataCollector.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInDataCollector.swift @@ -26,7 +26,7 @@ class CheckInDataCollector: NSObject { fileprivate let checkInRequestManager: CheckInRequestManager init(checkInData: CheckInData) { - checkInRequestManager = CheckInRequestManager(baseURLString: checkInData.serverURL) + checkInRequestManager = CheckInRequestManager(baseURLString: checkInData.serverURL, secret: checkInData.secret) super.init() } @@ -92,7 +92,7 @@ class CheckInDataCollector: NSObject { success: @escaping (_ collector: CheckInData) -> Void, failure: @escaping (_ error: Error) -> Void) { - checkInRequestManager.getBoat(boatID: boatID, success: { (boatData) in + checkInRequestManager.getBoat(boatID: boatID, leaderboardName: collector.leaderboardName, success: { (boatData) in collector.boatData = boatData success(collector) }) { error in @@ -108,7 +108,7 @@ class CheckInDataCollector: NSObject { success: @escaping (_ collector: CheckInData) -> Void, failure: @escaping (_ error: Error) -> Void) { - checkInRequestManager.getCompetitor(competitorID: competitorID, success: { (competitorData) in + checkInRequestManager.getCompetitor(competitorID: competitorID, leaderboardName: collector.leaderboardName, success: { (competitorData) in collector.competitorData = competitorData self.getCompetitorDataSuccess( collector: collector, @@ -127,7 +127,7 @@ class CheckInDataCollector: NSObject { success: @escaping (_ collector: CheckInData) -> Void, failure: @escaping (_ error: Error) -> Void) { - checkInRequestManager.getTeamImageURL(competitorID: competitorID, result: { (imageURL) in + checkInRequestManager.getTeamImageURL(competitorID: competitorID, leaderboardName: collector.leaderboardName, result: { (imageURL) in collector.teamImageURL = imageURL success(collector) }) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CheckInRequestManager.swift b/ios/SAPTracker/src/xcode/SAPTracker/CheckInRequestManager.swift index 7bbb816a88a..f611862c2d7 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CheckInRequestManager.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CheckInRequestManager.swift @@ -71,8 +71,10 @@ class CheckInRequestManager: NSObject { static let FixesSpeed = "speed" static let FixesTimestamp = "timestamp" static let FromMillis = "fromMillis" + static let LeaderboardName = "leaderboardName" static let MarkID = "markId" static let PushDeviceID = "pushDeviceId" + static let Secret = "secret" static let ToMillis = "toMillis" } @@ -84,12 +86,14 @@ class CheckInRequestManager: NSObject { static let IOS = "iOS" } + private let secret: String? let baseURLString: String let manager: AFHTTPSessionManager let sessionManager: AFURLSessionManager - init(baseURLString: String = "") { + init(baseURLString: String = "", secret: String? = nil) { self.baseURLString = baseURLString + self.secret = secret manager = AFHTTPSessionManager(baseURL: URL(string: baseURLString)) manager.requestSerializer = AFJSONRequestSerializer() as AFHTTPRequestSerializer manager.requestSerializer.timeoutInterval = Application.RequestTimeout @@ -108,9 +112,15 @@ class CheckInRequestManager: NSObject { { let encodedEventID = eventID.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" let urlString = "\(basePathString)/events/\(encodedEventID)" + let parameter: [String:String]? + switch self.secret { + case .some(let secret): parameter = [BodyKeys.Secret:secret] + case .none: parameter = nil + } + manager.get( urlString, - parameters: nil, + parameters: parameter, progress: nil, success: { (requestOperation, responseObject) in self.getEventSuccess(responseObject: responseObject, success: success) }, failure: { (requestOperation, error) in self.getEventFailure(error: error, failure: failure) } @@ -136,9 +146,15 @@ class CheckInRequestManager: NSObject { { let encodedLeaderboardName = leaderboardName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" let urlString = "\(basePathString)/leaderboards/\(encodedLeaderboardName)" + let parameter: [String:String]? + switch self.secret { + case .some(let secret): parameter = [BodyKeys.Secret:secret] + case .none: parameter = nil + } + manager.get( urlString, - parameters: nil, + parameters: parameter, progress: nil, success: { (requestOperation, responseObject) in self.getLeaderboardSuccess(responseObject: responseObject, success: success) }, failure: { (requestOperation, error) in self.getLeaderboardFailure(error: error, failure: failure) } @@ -159,14 +175,21 @@ class CheckInRequestManager: NSObject { func getBoat( boatID: String, + leaderboardName: String, success: @escaping (_ boatData: BoatData) -> Void, failure: @escaping (_ error: Error) -> Void) { let encodedBoatID = boatID.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed) ?? "" let urlString = "\(basePathString)/boats/\(encodedBoatID)" + var parameters = [String:String]() + parameters[BodyKeys.LeaderboardName] = leaderboardName + if let secret = secret { + parameters[BodyKeys.Secret] = secret + } + manager.get( urlString, - parameters: nil, + parameters: parameters, progress: nil, success: { (requestOperation, responseObject) in self.getBoatSuccess(responseObject: responseObject, success: success) }, failure: { (requestOperation, error) in self.getBoatFailure(error: error, failure: failure) } @@ -187,14 +210,21 @@ class CheckInRequestManager: NSObject { func getCompetitor( competitorID: String, + leaderboardName: String, success: @escaping (_ competitorData: CompetitorData) -> Void, failure: @escaping (_ error: Error) -> Void) { let encodedCompetitorID = competitorID.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed) ?? "" let urlString = "\(basePathString)/competitors/\(encodedCompetitorID)" + var parameters = [String:String]() + parameters[BodyKeys.LeaderboardName] = leaderboardName + if let secret = secret { + parameters[BodyKeys.Secret] = secret + } + manager.get( urlString, - parameters: nil, + parameters: parameters, progress: nil, success: { (requestOperation, responseObject) in self.getCompetitorSuccess(responseObject: responseObject, success: success) }, failure: { (requestOperation, error) in self.getCompetitorFailure(error: error, failure: failure) } @@ -222,9 +252,15 @@ class CheckInRequestManager: NSObject { let encodedLeaderboardName = leaderboardName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" let encodedMarkID = markID.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed) ?? "" let urlString = "\(basePathString)/leaderboards/\(encodedLeaderboardName)/marks/\(encodedMarkID)" + let parameter: [String:String]? + switch self.secret { + case .some(let secret): parameter = [BodyKeys.Secret:secret] + case .none: parameter = nil + } + manager.get( urlString, - parameters: nil, + parameters: parameter, progress: nil, success: { (requestOperation, responseObject) in self.getMarkSuccess(responseObject: responseObject, success: success) }, failure: { (requestOperation, error) in self.getMarkFailure(error: error, failure: failure) } @@ -245,14 +281,20 @@ class CheckInRequestManager: NSObject { func getTeam( competitorID: String, + leaderboardName: String, success: @escaping (_ teamData: TeamData) -> Void, failure: @escaping (_ error: Error) -> Void) { let encodedCompetitorID = competitorID.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlPathAllowed) ?? "" let urlString = "\(basePathString)/competitors/\(encodedCompetitorID)/team" + var parameters = [String:String]() + parameters[BodyKeys.LeaderboardName] = leaderboardName + if let secret = secret { + parameters[BodyKeys.Secret] = secret + } manager.get( urlString, - parameters: nil, + parameters: parameters, progress: nil, success: { (requestOperation, responseObject) in self.getTeamSuccess(responseObject: responseObject, success: success) }, failure: { (requestOperation, error) in self.getTeamFailure(error: error, failure: failure) } @@ -269,9 +311,10 @@ class CheckInRequestManager: NSObject { failure(CheckInRequestManagerError.getTeamFailed) } - func getTeamImageURL(competitorID: String, result: @escaping (_ imageURL: String?) -> Void) { + func getTeamImageURL(competitorID: String, leaderboardName: String, result: @escaping (_ imageURL: String?) -> Void) { getTeam( competitorID: competitorID, + leaderboardName: leaderboardName, success: { (teamData) in result(teamData.imageURL) }, failure: { (error) in result(nil) } // No image but that's ok ) @@ -290,6 +333,9 @@ class CheckInRequestManager: NSObject { body[BodyKeys.DeviceUUID] = Preferences.uuid as AnyObject? body[BodyKeys.PushDeviceID] = "" as AnyObject? body[BodyKeys.FromMillis] = millisSince1970() + if let secret = checkInData.secret { + body[BodyKeys.Secret] = secret as AnyObject? + } switch checkInData.type { case .boat: body[BodyKeys.BoatID] = checkInData.boatData.boatID as AnyObject? @@ -334,6 +380,9 @@ class CheckInRequestManager: NSObject { var body = [String: AnyObject]() body[BodyKeys.DeviceUUID] = Preferences.uuid as AnyObject? body[BodyKeys.ToMillis] = millisSince1970() + if let secret = checkIn.secret { + body[BodyKeys.Secret] = secret as AnyObject? + } if let competitorCheckIn = checkIn as? CompetitorCheckIn { body[BodyKeys.CompetitorID] = competitorCheckIn.competitorID as AnyObject? } else if let markCheckIn = checkIn as? MarkCheckIn { @@ -409,15 +458,26 @@ class CheckInRequestManager: NSObject { func postTeamImageData( imageData: Data, - competitorID: String, + competitorCheckIn: CompetitorCheckIn, success: @escaping (_ teamImageURL: String) -> Void, failure: @escaping (_ error: Error) -> Void) { - guard let teamImageURL = URL(string: "\(baseURLString)\(basePathString)/competitors/\(competitorID)/team/image") else { + let competitorID = competitorCheckIn.competitorID + let leaderboardName = competitorCheckIn.leaderboard.name + + guard var components = URLComponents(string: "\(baseURLString)\(basePathString)/competitors/\(competitorID)/team/image") else { failure(CheckInRequestManagerError.teamImageURLIsInvalid) return } - + components.queryItems = [URLQueryItem(name: BodyKeys.LeaderboardName, value: leaderboardName)] + if let secret = competitorCheckIn.secret { + components.queryItems?.append(URLQueryItem(name: BodyKeys.Secret, value: secret)) + } + guard let teamImageURL = components.url else { + failure(CheckInRequestManagerError.teamImageURLIsInvalid) + return + } + postTeamImageData( imageData: imageData, teamImageURL: teamImageURL, diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionController.swift index 68653c67bfe..68f59205371 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorSessionController.swift @@ -14,13 +14,13 @@ class CompetitorSessionController: SessionController { func postTeamImageData( imageData: Data, - competitorID: String, + competitorCheckIn: CompetitorCheckIn, success: @escaping (_ teamImageURL: String) -> Void, failure: @escaping (_ error: Error) -> Void ) { checkInRequestManager.postTeamImageData( imageData: imageData, - competitorID: competitorID, + competitorCheckIn: competitorCheckIn, success: success, failure: failure ) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorViewController.swift index 4b52115539e..f5d05a59f44 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CompetitorViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/CompetitorViewController.swift @@ -218,7 +218,7 @@ extension CompetitorViewController: UIImagePickerControllerDelegate { fileprivate func uploadTeamImageData(imageData: Data!) { SVProgressHUD.show() - competitorSessionController.postTeamImageData(imageData: imageData, competitorID: competitorCheckIn.competitorID, success: { (teamImageURL) in + competitorSessionController.postTeamImageData(imageData: imageData, competitorCheckIn: competitorCheckIn, success: { (teamImageURL) in SVProgressHUD.popActivity() self.uploadTeamImageDataSuccess(teamImageURL: teamImageURL) }) { (error) in diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/.xccurrentversion b/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/.xccurrentversion index 1d5ea989f87..2145ac780f8 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/.xccurrentversion +++ b/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - CoreData 4.xcdatamodel + CoreData 5.xcdatamodel diff --git a/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents b/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents new file mode 100644 index 00000000000..b3a24e8bb98 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/CoreData.xcdatamodeld/CoreData 5.xcdatamodel/contents @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/1024x0w.jpg b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/1024x0w.jpg new file mode 100644 index 00000000000..c7122b563a2 Binary files /dev/null and b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/1024x0w.jpg differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/Contents.json index 140d558a03a..b77d112031f 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/SAPTracker/src/xcode/SAPTracker/Images.xcassets/AppIcon.appiconset/Contents.json @@ -99,8 +99,9 @@ "scale" : "2x" }, { - "idiom" : "ios-marketing", "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "1024x0w.jpg", "scale" : "1x" } ], diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Info.plist b/ios/SAPTracker/src/xcode/SAPTracker/Info.plist index 3e18d436e87..1988ae2d2e5 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Info.plist +++ b/ios/SAPTracker/src/xcode/SAPTracker/Info.plist @@ -9,7 +9,7 @@ CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) + com.sap.sailing.ios.SAPTracker CFBundleInfoDictionaryVersion 6.0 CFBundleName @@ -28,14 +28,6 @@ comsapsailingtracker - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - prefs - - CFBundleVersion 2.0.0-rc2 @@ -46,12 +38,6 @@ NSAllowsArbitraryLoads - NSLocationAlwaysUsageDescription - - branch_app_domain - sailinsight-app.sapsailing.com - branch_key - key_live_ges90RzIjjgM0vg8wbTtYcdmtCeYJMAZ UIBackgroundModes fetch @@ -78,5 +64,9 @@ UIViewControllerBasedStatusBarAppearance + branch_app_domain + sailinsight-app.sapsailing.com + branch_key + key_live_ges90RzIjjgM0vg8wbTtYcdmtCeYJMAZ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCApplication.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCApplication.h new file mode 100644 index 00000000000..52f88e84730 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCApplication.h @@ -0,0 +1,54 @@ +/** + @file BNCApplication.h + @package Branch-SDK + @brief Current application and extension info. + + @author Edward Smith + @date January 8, 2018 + @copyright Copyright © 2018 Branch. All rights reserved. +*/ + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +@interface BNCApplication : NSObject + +/// A reference to the current running application. ++ (BNCApplication*_Nonnull) currentApplication; + +/// The bundle identifier of the current +@property (atomic, readonly) NSString*_Nullable bundleID; + +/// The bundle display name from the info plist. +@property (atomic, readonly) NSString*_Nullable displayName; + +/// The bundle short display name from the info plist. +@property (atomic, readonly) NSString*_Nullable shortDisplayName; + +/// The short version ID as is typically shown to the user, like in iTunes or the app store. +@property (atomic, readonly) NSString*_Nullable displayVersionString; + +/// The version ID for developers use. +@property (atomic, readonly) NSString*_Nullable versionString; + +/// The creation date of the current executable. +@property (atomic, readonly) NSDate*_Nullable currentBuildDate; + +/// The creating date of the exectuble the first time it was recorded by Branch. +@property (atomic, readonly) NSDate*_Nullable firstInstallBuildDate; + +/// The date this app was installed on this device. +@property (atomic, readonly) NSDate*_Nullable currentInstallDate; + +/// The date this app was first installed on this device. +@property (atomic, readonly) NSDate*_Nullable firstInstallDate; + +/// Returns a dictionary of device / identity pairs. +@property (atomic, readonly) NSDictionary*_Nonnull deviceKeyIdentityValueDictionary; + +/// The team identifier for the app. +@property (atomic, readonly) NSString*_Nullable teamID; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCApplication.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCApplication.m new file mode 100644 index 00000000000..f9adcfbd43f --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCApplication.m @@ -0,0 +1,169 @@ +/** + @file BNCApplication.m + @package Branch-SDK + @brief Current application and extension info. + + @author Edward Smith + @date January 8, 2018 + @copyright Copyright © 2018 Branch. All rights reserved. +*/ + +#import "BNCApplication.h" +#import "BNCLog.h" +#import "BNCKeyChain.h" + +static NSString*const kBranchKeychainService = @"BranchKeychainService"; +static NSString*const kBranchKeychainDevicesKey = @"BranchKeychainDevices"; +static NSString*const kBranchKeychainFirstBuildKey = @"BranchKeychainFirstBuild"; +static NSString*const kBranchKeychainFirstInstalldKey = @"BranchKeychainFirstInstall"; + +#pragma mark - BNCApplication + +@implementation BNCApplication + ++ (BNCApplication*) currentApplication { + static BNCApplication *bnc_currentApplication = nil; + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^{ + bnc_currentApplication = [BNCApplication createCurrentApplication]; + }); + return bnc_currentApplication; +} + ++ (BNCApplication*) createCurrentApplication { + BNCApplication *application = [[BNCApplication alloc] init]; + if (!application) return application; + NSDictionary *info = [NSBundle mainBundle].infoDictionary; + + application->_bundleID = [NSBundle mainBundle].bundleIdentifier; + application->_displayName = info[@"CFBundleDisplayName"]; + application->_shortDisplayName = info[@"CFBundleName"]; + + application->_displayVersionString = info[@"CFBundleShortVersionString"]; + application->_versionString = info[@"CFBundleVersion"]; + + application->_firstInstallBuildDate = [BNCApplication firstInstallBuildDate]; + application->_currentBuildDate = [BNCApplication currentBuildDate]; + + application->_firstInstallDate = [BNCApplication firstInstallDate]; + application->_currentInstallDate = [BNCApplication currentInstallDate]; + + NSString*group = [BNCKeyChain securityAccessGroup]; + if (group) { + NSRange range = [group rangeOfString:@"."]; + if (range.location != NSNotFound) { + application->_teamID = [[group substringToIndex:range.location] copy]; + } + } + + return application; +} + ++ (NSDate*) currentBuildDate { + NSURL *appURL = nil; + NSURL *bundleURL = [NSBundle mainBundle].bundleURL; + NSDictionary *info = [NSBundle mainBundle].infoDictionary; + NSString *appName = info[(__bridge NSString*)kCFBundleExecutableKey]; + if (appName.length > 0 && bundleURL) { + appURL = [bundleURL URLByAppendingPathComponent:appName]; + } else { + NSString *path = [[NSProcessInfo processInfo].arguments firstObject]; + if (path) appURL = [NSURL fileURLWithPath:path]; + } + if (appURL == nil) + return nil; + + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSDictionary *attributes = [fileManager attributesOfItemAtPath:appURL.path error:&error]; + if (error) { + BNCLogError(@"Can't get build date: %@.", error); + return nil; + } + NSDate * buildDate = [attributes fileCreationDate]; + if (buildDate == nil || [buildDate timeIntervalSince1970] <= 0.0) { + BNCLogError(@"Invalid build date: %@.", buildDate); + } + return buildDate; +} + ++ (NSDate*) firstInstallBuildDate { + NSError *error = nil; + NSDate *firstBuildDate = + [BNCKeyChain retrieveValueForService:kBranchKeychainService + key:kBranchKeychainFirstBuildKey + error:&error]; + if (firstBuildDate) + return firstBuildDate; + + firstBuildDate = [self currentBuildDate]; + error = [BNCKeyChain storeValue:firstBuildDate + forService:kBranchKeychainService + key:kBranchKeychainFirstBuildKey + cloudAccessGroup:nil]; + if (error) BNCLogError(@"Keychain store: %@.", error); + return firstBuildDate; +} + ++ (NSDate*) currentInstallDate { + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *libraryURL = + [[fileManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] firstObject]; + NSDictionary *attributes = [fileManager attributesOfItemAtPath:libraryURL.path error:&error]; + if (error) { + BNCLogError(@"Can't get library date: %@.", error); + return nil; + } + NSDate *installDate = [attributes fileCreationDate]; + if (installDate == nil || [installDate timeIntervalSince1970] <= 0.0) { + BNCLogError(@"Invalid install date."); + } + return installDate; +} + ++ (NSDate*) firstInstallDate { + NSError *error = nil; + NSDate* firstInstallDate = + [BNCKeyChain retrieveValueForService:kBranchKeychainService + key:kBranchKeychainFirstInstalldKey + error:&error]; + if (firstInstallDate) + return firstInstallDate; + + firstInstallDate = [self currentInstallDate]; + error = [BNCKeyChain storeValue:firstInstallDate + forService:kBranchKeychainService + key:kBranchKeychainFirstInstalldKey + cloudAccessGroup:nil]; + if (error) BNCLogError(@"Keychain store: %@.", error); + return firstInstallDate; +} + +- (NSDictionary*) deviceKeyIdentityValueDictionary { + @synchronized (self.class) { + NSError *error = nil; + NSDictionary *deviceDictionary = + [BNCKeyChain retrieveValueForService:kBranchKeychainService + key:kBranchKeychainDevicesKey + error:&error]; + if (error) BNCLogWarning(@"While retrieving deviceKeyIdentityValueDictionary: %@.", error); + if (!deviceDictionary) deviceDictionary = @{}; + return deviceDictionary; + } +} + +@end + +@implementation BNCApplication (BNCTest) + +- (void) setAppOriginalInstallDate:(NSDate*)originalInstallDate + firstInstallDate:(NSDate*)firstInstallDate + lastUpdateDate:(NSDate*)lastUpdateDate { + self->_currentInstallDate = firstInstallDate; // latest_install_time + self->_firstInstallDate = originalInstallDate; // first_install_time + self->_currentBuildDate = lastUpdateDate; // lastest_update_time +} + +@end + diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCAvailability.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCAvailability.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCAvailability.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCAvailability.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCAvailability.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCAvailability.m new file mode 100644 index 00000000000..7dbf548d752 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCAvailability.m @@ -0,0 +1,14 @@ +// +// BNCAvailability.m +// Branch-SDK +// +// Created by Edward on 10/26/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import "BNCAvailability.h" + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < 100000 +// Usually nothing goes here. +#endif + diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCCallbacks.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCallbacks.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCCallbacks.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCallbacks.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCCommerceEvent.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCommerceEvent.h similarity index 99% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCCommerceEvent.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCommerceEvent.h index 224d4334de6..e69bd4182d1 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCCommerceEvent.h +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCommerceEvent.h @@ -263,7 +263,7 @@ NSArray*_Nonnull BNCCurrencyAllCurrencies(void); @end -@interface BranchCommerceEventRequest : BNCServerRequest +@interface BranchCommerceEventRequest : BNCServerRequest - (instancetype _Nonnull) initWithCommerceEvent:(BNCCommerceEvent*_Nonnull)commerceEvent metadata:(NSDictionary*_Nullable)dictionary diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCommerceEvent.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCommerceEvent.m new file mode 100644 index 00000000000..5ed23483f6a --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCommerceEvent.m @@ -0,0 +1,622 @@ +// +// BNCCommerceEvent.h +// Branch-SDK +// +// Created by Edward Smith on 12/14/16. +// Copyright (c) 2016 Branch Metrics. All rights reserved. +// + +#import "BNCCommerceEvent.h" +#import "BranchConstants.h" +#import "BNCLog.h" + +#pragma mark BNCProductCategory + +BNCProductCategory BNCProductCategoryAnimalSupplies = @"Animals & Pet Supplies"; +BNCProductCategory BNCProductCategoryApparel = @"Apparel & Accessories"; +BNCProductCategory BNCProductCategoryArtsEntertainment = @"Arts & Entertainment"; +BNCProductCategory BNCProductCategoryBabyToddler = @"Baby & Toddler"; +BNCProductCategory BNCProductCategoryBusinessIndustrial = @"Business & Industrial"; +BNCProductCategory BNCProductCategoryCamerasOptics = @"Cameras & Optics"; +BNCProductCategory BNCProductCategoryElectronics = @"Electronics"; +BNCProductCategory BNCProductCategoryFoodBeverageTobacco = @"Food, Beverages & Tobacco"; +BNCProductCategory BNCProductCategoryFurniture = @"Furniture"; +BNCProductCategory BNCProductCategoryHardware = @"Hardware"; +BNCProductCategory BNCProductCategoryHealthBeauty = @"Health & Beauty"; +BNCProductCategory BNCProductCategoryHomeGarden = @"Home & Garden"; +BNCProductCategory BNCProductCategoryLuggageBags = @"Luggage & Bags"; +BNCProductCategory BNCProductCategoryMature = @"Mature"; +BNCProductCategory BNCProductCategoryMedia = @"Media"; +BNCProductCategory BNCProductCategoryOfficeSupplies = @"Office Supplies"; +BNCProductCategory BNCProductCategoryReligious = @"Religious & Ceremonial"; +BNCProductCategory BNCProductCategorySoftware = @"Software"; +BNCProductCategory BNCProductCategorySportingGoods = @"Sporting Goods"; +BNCProductCategory BNCProductCategoryToysGames = @"Toys & Games"; +BNCProductCategory BNCProductCategoryVehiclesParts = @"Vehicles & Parts"; + +NSArray* BNCProductCategoryAllCategories(void) { + return @[ + BNCProductCategoryAnimalSupplies, + BNCProductCategoryApparel, + BNCProductCategoryArtsEntertainment, + BNCProductCategoryBabyToddler, + BNCProductCategoryBusinessIndustrial, + BNCProductCategoryCamerasOptics, + BNCProductCategoryElectronics, + BNCProductCategoryFoodBeverageTobacco, + BNCProductCategoryFurniture, + BNCProductCategoryHardware, + BNCProductCategoryHealthBeauty, + BNCProductCategoryHomeGarden, + BNCProductCategoryLuggageBags, + BNCProductCategoryMature, + BNCProductCategoryMedia, + BNCProductCategoryOfficeSupplies, + BNCProductCategoryReligious, + BNCProductCategorySoftware, + BNCProductCategorySportingGoods, + BNCProductCategoryToysGames, + BNCProductCategoryVehiclesParts, + ]; +} + +#pragma mark - BNCCurrency + +BNCCurrency BNCCurrencyAED = @"AED"; +BNCCurrency BNCCurrencyAFN = @"AFN"; +BNCCurrency BNCCurrencyALL = @"ALL"; +BNCCurrency BNCCurrencyAMD = @"AMD"; +BNCCurrency BNCCurrencyANG = @"ANG"; +BNCCurrency BNCCurrencyAOA = @"AOA"; +BNCCurrency BNCCurrencyARS = @"ARS"; +BNCCurrency BNCCurrencyAUD = @"AUD"; +BNCCurrency BNCCurrencyAWG = @"AWG"; +BNCCurrency BNCCurrencyAZN = @"AZN"; +BNCCurrency BNCCurrencyBAM = @"BAM"; +BNCCurrency BNCCurrencyBBD = @"BBD"; + +BNCCurrency BNCCurrencyBDT = @"BDT"; +BNCCurrency BNCCurrencyBGN = @"BGN"; +BNCCurrency BNCCurrencyBHD = @"BHD"; +BNCCurrency BNCCurrencyBIF = @"BIF"; +BNCCurrency BNCCurrencyBMD = @"BMD"; +BNCCurrency BNCCurrencyBND = @"BND"; +BNCCurrency BNCCurrencyBOB = @"BOB"; +BNCCurrency BNCCurrencyBOV = @"BOV"; +BNCCurrency BNCCurrencyBRL = @"BRL"; +BNCCurrency BNCCurrencyBSD = @"BSD"; +BNCCurrency BNCCurrencyBTN = @"BTN"; +BNCCurrency BNCCurrencyBWP = @"BWP"; + +BNCCurrency BNCCurrencyBYN = @"BYN"; +BNCCurrency BNCCurrencyBYR = @"BYR"; +BNCCurrency BNCCurrencyBZD = @"BZD"; +BNCCurrency BNCCurrencyCAD = @"CAD"; +BNCCurrency BNCCurrencyCDF = @"CDF"; +BNCCurrency BNCCurrencyCHE = @"CHE"; +BNCCurrency BNCCurrencyCHF = @"CHF"; +BNCCurrency BNCCurrencyCHW = @"CHW"; +BNCCurrency BNCCurrencyCLF = @"CLF"; +BNCCurrency BNCCurrencyCLP = @"CLP"; +BNCCurrency BNCCurrencyCNY = @"CNY"; +BNCCurrency BNCCurrencyCOP = @"COP"; + +BNCCurrency BNCCurrencyCOU = @"COU"; +BNCCurrency BNCCurrencyCRC = @"CRC"; +BNCCurrency BNCCurrencyCUC = @"CUC"; +BNCCurrency BNCCurrencyCUP = @"CUP"; +BNCCurrency BNCCurrencyCVE = @"CVE"; +BNCCurrency BNCCurrencyCZK = @"CZK"; +BNCCurrency BNCCurrencyDJF = @"DJF"; +BNCCurrency BNCCurrencyDKK = @"DKK"; +BNCCurrency BNCCurrencyDOP = @"DOP"; +BNCCurrency BNCCurrencyDZD = @"DZD"; +BNCCurrency BNCCurrencyEGP = @"EGP"; +BNCCurrency BNCCurrencyERN = @"ERN"; + +BNCCurrency BNCCurrencyETB = @"ETB"; +BNCCurrency BNCCurrencyEUR = @"EUR"; +BNCCurrency BNCCurrencyFJD = @"FJD"; +BNCCurrency BNCCurrencyFKP = @"FKP"; +BNCCurrency BNCCurrencyGBP = @"GBP"; +BNCCurrency BNCCurrencyGEL = @"GEL"; +BNCCurrency BNCCurrencyGHS = @"GHS"; +BNCCurrency BNCCurrencyGIP = @"GIP"; +BNCCurrency BNCCurrencyGMD = @"GMD"; +BNCCurrency BNCCurrencyGNF = @"GNF"; +BNCCurrency BNCCurrencyGTQ = @"GTQ"; +BNCCurrency BNCCurrencyGYD = @"GYD"; + +BNCCurrency BNCCurrencyHKD = @"HKD"; +BNCCurrency BNCCurrencyHNL = @"HNL"; +BNCCurrency BNCCurrencyHRK = @"HRK"; +BNCCurrency BNCCurrencyHTG = @"HTG"; +BNCCurrency BNCCurrencyHUF = @"HUF"; +BNCCurrency BNCCurrencyIDR = @"IDR"; +BNCCurrency BNCCurrencyILS = @"ILS"; +BNCCurrency BNCCurrencyINR = @"INR"; +BNCCurrency BNCCurrencyIQD = @"IQD"; +BNCCurrency BNCCurrencyIRR = @"IRR"; +BNCCurrency BNCCurrencyISK = @"ISK"; +BNCCurrency BNCCurrencyJMD = @"JMD"; + +BNCCurrency BNCCurrencyJOD = @"JOD"; +BNCCurrency BNCCurrencyJPY = @"JPY"; +BNCCurrency BNCCurrencyKES = @"KES"; +BNCCurrency BNCCurrencyKGS = @"KGS"; +BNCCurrency BNCCurrencyKHR = @"KHR"; +BNCCurrency BNCCurrencyKMF = @"KMF"; +BNCCurrency BNCCurrencyKPW = @"KPW"; +BNCCurrency BNCCurrencyKRW = @"KRW"; +BNCCurrency BNCCurrencyKWD = @"KWD"; +BNCCurrency BNCCurrencyKYD = @"KYD"; +BNCCurrency BNCCurrencyKZT = @"KZT"; +BNCCurrency BNCCurrencyLAK = @"LAK"; + +BNCCurrency BNCCurrencyLBP = @"LBP"; +BNCCurrency BNCCurrencyLKR = @"LKR"; +BNCCurrency BNCCurrencyLRD = @"LRD"; +BNCCurrency BNCCurrencyLSL = @"LSL"; +BNCCurrency BNCCurrencyLYD = @"LYD"; +BNCCurrency BNCCurrencyMAD = @"MAD"; +BNCCurrency BNCCurrencyMDL = @"MDL"; +BNCCurrency BNCCurrencyMGA = @"MGA"; +BNCCurrency BNCCurrencyMKD = @"MKD"; +BNCCurrency BNCCurrencyMMK = @"MMK"; +BNCCurrency BNCCurrencyMNT = @"MNT"; +BNCCurrency BNCCurrencyMOP = @"MOP"; + +BNCCurrency BNCCurrencyMRO = @"MRO"; +BNCCurrency BNCCurrencyMUR = @"MUR"; +BNCCurrency BNCCurrencyMVR = @"MVR"; +BNCCurrency BNCCurrencyMWK = @"MWK"; +BNCCurrency BNCCurrencyMXN = @"MXN"; +BNCCurrency BNCCurrencyMXV = @"MXV"; +BNCCurrency BNCCurrencyMYR = @"MYR"; +BNCCurrency BNCCurrencyMZN = @"MZN"; +BNCCurrency BNCCurrencyNAD = @"NAD"; +BNCCurrency BNCCurrencyNGN = @"NGN"; +BNCCurrency BNCCurrencyNIO = @"NIO"; +BNCCurrency BNCCurrencyNOK = @"NOK"; + +BNCCurrency BNCCurrencyNPR = @"NPR"; +BNCCurrency BNCCurrencyNZD = @"NZD"; +BNCCurrency BNCCurrencyOMR = @"OMR"; +BNCCurrency BNCCurrencyPAB = @"PAB"; +BNCCurrency BNCCurrencyPEN = @"PEN"; +BNCCurrency BNCCurrencyPGK = @"PGK"; +BNCCurrency BNCCurrencyPHP = @"PHP"; +BNCCurrency BNCCurrencyPKR = @"PKR"; +BNCCurrency BNCCurrencyPLN = @"PLN"; +BNCCurrency BNCCurrencyPYG = @"PYG"; +BNCCurrency BNCCurrencyQAR = @"QAR"; +BNCCurrency BNCCurrencyRON = @"RON"; + +BNCCurrency BNCCurrencyRSD = @"RSD"; +BNCCurrency BNCCurrencyRUB = @"RUB"; +BNCCurrency BNCCurrencyRWF = @"RWF"; +BNCCurrency BNCCurrencySAR = @"SAR"; +BNCCurrency BNCCurrencySBD = @"SBD"; +BNCCurrency BNCCurrencySCR = @"SCR"; +BNCCurrency BNCCurrencySDG = @"SDG"; +BNCCurrency BNCCurrencySEK = @"SEK"; +BNCCurrency BNCCurrencySGD = @"SGD"; +BNCCurrency BNCCurrencySHP = @"SHP"; +BNCCurrency BNCCurrencySLL = @"SLL"; +BNCCurrency BNCCurrencySOS = @"SOS"; + +BNCCurrency BNCCurrencySRD = @"SRD"; +BNCCurrency BNCCurrencySSP = @"SSP"; +BNCCurrency BNCCurrencySTD = @"STD"; +BNCCurrency BNCCurrencySYP = @"SYP"; +BNCCurrency BNCCurrencySZL = @"SZL"; +BNCCurrency BNCCurrencyTHB = @"THB"; +BNCCurrency BNCCurrencyTJS = @"TJS"; +BNCCurrency BNCCurrencyTMT = @"TMT"; +BNCCurrency BNCCurrencyTND = @"TND"; +BNCCurrency BNCCurrencyTOP = @"TOP"; +BNCCurrency BNCCurrencyTRY = @"TRY"; +BNCCurrency BNCCurrencyTTD = @"TTD"; + +BNCCurrency BNCCurrencyTWD = @"TWD"; +BNCCurrency BNCCurrencyTZS = @"TZS"; +BNCCurrency BNCCurrencyUAH = @"UAH"; +BNCCurrency BNCCurrencyUGX = @"UGX"; +BNCCurrency BNCCurrencyUSD = @"USD"; +BNCCurrency BNCCurrencyUSN = @"USN"; +BNCCurrency BNCCurrencyUYI = @"UYI"; +BNCCurrency BNCCurrencyUYU = @"UYU"; +BNCCurrency BNCCurrencyUZS = @"UZS"; +BNCCurrency BNCCurrencyVEF = @"VEF"; +BNCCurrency BNCCurrencyVND = @"VND"; +BNCCurrency BNCCurrencyVUV = @"VUV"; + +BNCCurrency BNCCurrencyWST = @"WST"; +BNCCurrency BNCCurrencyXAF = @"XAF"; +BNCCurrency BNCCurrencyXAG = @"XAG"; +BNCCurrency BNCCurrencyXAU = @"XAU"; +BNCCurrency BNCCurrencyXBA = @"XBA"; +BNCCurrency BNCCurrencyXBB = @"XBB"; +BNCCurrency BNCCurrencyXBC = @"XBC"; +BNCCurrency BNCCurrencyXBD = @"XBD"; +BNCCurrency BNCCurrencyXCD = @"XCD"; +BNCCurrency BNCCurrencyXDR = @"XDR"; +BNCCurrency BNCCurrencyXFU = @"XFU"; +BNCCurrency BNCCurrencyXOF = @"XOF"; + +BNCCurrency BNCCurrencyXPD = @"XPD"; +BNCCurrency BNCCurrencyXPF = @"XPF"; +BNCCurrency BNCCurrencyXPT = @"XPT"; +BNCCurrency BNCCurrencyXSU = @"XSU"; +BNCCurrency BNCCurrencyXTS = @"XTS"; +BNCCurrency BNCCurrencyXUA = @"XUA"; +BNCCurrency BNCCurrencyXXX = @"XXX"; +BNCCurrency BNCCurrencyYER = @"YER"; +BNCCurrency BNCCurrencyZAR = @"ZAR"; +BNCCurrency BNCCurrencyZMW = @"ZMW"; + +NSArray* BNCCurrencyAllCurrencies(void) { + return @[ + BNCCurrencyAED, + BNCCurrencyAFN, + BNCCurrencyALL, + BNCCurrencyAMD, + BNCCurrencyANG, + BNCCurrencyAOA, + BNCCurrencyARS, + BNCCurrencyAUD, + BNCCurrencyAWG, + BNCCurrencyAZN, + BNCCurrencyBAM, + BNCCurrencyBBD, + + BNCCurrencyBDT, + BNCCurrencyBGN, + BNCCurrencyBHD, + BNCCurrencyBIF, + BNCCurrencyBMD, + BNCCurrencyBND, + BNCCurrencyBOB, + BNCCurrencyBOV, + BNCCurrencyBRL, + BNCCurrencyBSD, + BNCCurrencyBTN, + BNCCurrencyBWP, + + BNCCurrencyBYN, + BNCCurrencyBYR, + BNCCurrencyBZD, + BNCCurrencyCAD, + BNCCurrencyCDF, + BNCCurrencyCHE, + BNCCurrencyCHF, + BNCCurrencyCHW, + BNCCurrencyCLF, + BNCCurrencyCLP, + BNCCurrencyCNY, + BNCCurrencyCOP, + + BNCCurrencyCOU, + BNCCurrencyCRC, + BNCCurrencyCUC, + BNCCurrencyCUP, + BNCCurrencyCVE, + BNCCurrencyCZK, + BNCCurrencyDJF, + BNCCurrencyDKK, + BNCCurrencyDOP, + BNCCurrencyDZD, + BNCCurrencyEGP, + BNCCurrencyERN, + + BNCCurrencyETB, + BNCCurrencyEUR, + BNCCurrencyFJD, + BNCCurrencyFKP, + BNCCurrencyGBP, + BNCCurrencyGEL, + BNCCurrencyGHS, + BNCCurrencyGIP, + BNCCurrencyGMD, + BNCCurrencyGNF, + BNCCurrencyGTQ, + BNCCurrencyGYD, + + BNCCurrencyHKD, + BNCCurrencyHNL, + BNCCurrencyHRK, + BNCCurrencyHTG, + BNCCurrencyHUF, + BNCCurrencyIDR, + BNCCurrencyILS, + BNCCurrencyINR, + BNCCurrencyIQD, + BNCCurrencyIRR, + BNCCurrencyISK, + BNCCurrencyJMD, + + BNCCurrencyJOD, + BNCCurrencyJPY, + BNCCurrencyKES, + BNCCurrencyKGS, + BNCCurrencyKHR, + BNCCurrencyKMF, + BNCCurrencyKPW, + BNCCurrencyKRW, + BNCCurrencyKWD, + BNCCurrencyKYD, + BNCCurrencyKZT, + BNCCurrencyLAK, + + BNCCurrencyLBP, + BNCCurrencyLKR, + BNCCurrencyLRD, + BNCCurrencyLSL, + BNCCurrencyLYD, + BNCCurrencyMAD, + BNCCurrencyMDL, + BNCCurrencyMGA, + BNCCurrencyMKD, + BNCCurrencyMMK, + BNCCurrencyMNT, + BNCCurrencyMOP, + + BNCCurrencyMRO, + BNCCurrencyMUR, + BNCCurrencyMVR, + BNCCurrencyMWK, + BNCCurrencyMXN, + BNCCurrencyMXV, + BNCCurrencyMYR, + BNCCurrencyMZN, + BNCCurrencyNAD, + BNCCurrencyNGN, + BNCCurrencyNIO, + BNCCurrencyNOK, + + BNCCurrencyNPR, + BNCCurrencyNZD, + BNCCurrencyOMR, + BNCCurrencyPAB, + BNCCurrencyPEN, + BNCCurrencyPGK, + BNCCurrencyPHP, + BNCCurrencyPKR, + BNCCurrencyPLN, + BNCCurrencyPYG, + BNCCurrencyQAR, + BNCCurrencyRON, + + BNCCurrencyRSD, + BNCCurrencyRUB, + BNCCurrencyRWF, + BNCCurrencySAR, + BNCCurrencySBD, + BNCCurrencySCR, + BNCCurrencySDG, + BNCCurrencySEK, + BNCCurrencySGD, + BNCCurrencySHP, + BNCCurrencySLL, + BNCCurrencySOS, + + BNCCurrencySRD, + BNCCurrencySSP, + BNCCurrencySTD, + BNCCurrencySYP, + BNCCurrencySZL, + BNCCurrencyTHB, + BNCCurrencyTJS, + BNCCurrencyTMT, + BNCCurrencyTND, + BNCCurrencyTOP, + BNCCurrencyTRY, + BNCCurrencyTTD, + + BNCCurrencyTWD, + BNCCurrencyTZS, + BNCCurrencyUAH, + BNCCurrencyUGX, + BNCCurrencyUSD, + BNCCurrencyUSN, + BNCCurrencyUYI, + BNCCurrencyUYU, + BNCCurrencyUZS, + BNCCurrencyVEF, + BNCCurrencyVND, + BNCCurrencyVUV, + + BNCCurrencyWST, + BNCCurrencyXAF, + BNCCurrencyXAG, + BNCCurrencyXAU, + BNCCurrencyXBA, + BNCCurrencyXBB, + BNCCurrencyXBC, + BNCCurrencyXBD, + BNCCurrencyXCD, + BNCCurrencyXDR, + BNCCurrencyXFU, + BNCCurrencyXOF, + + BNCCurrencyXPD, + BNCCurrencyXPF, + BNCCurrencyXPT, + BNCCurrencyXSU, + BNCCurrencyXTS, + BNCCurrencyXUA, + BNCCurrencyXXX, + BNCCurrencyYER, + BNCCurrencyZAR, + BNCCurrencyZMW, + ]; +} + +#pragma mark - BNCProduct + +@implementation BNCProduct + +- (NSMutableDictionary*) dictionary { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + #define assign(x) \ + do { if (self.x != nil) { dictionary[@#x] = self.x; } } while (0) + + assign(sku); + assign(name); + assign(price); + assign(quantity); + assign(brand); + assign(category); + assign(variant); + + #undef assign + + return dictionary; +} + +- (NSString*) description { + return [NSString stringWithFormat: + @"Name: %@ Sku: %@ Price: %@ Quantity: %@ Brand: %@ Category: %@ Variant: %@", + self.name, + self.sku, + self.price, + self.quantity, + self.brand, + self.category, + self.variant]; +} + +@end + +#pragma mark - BNCCommerceEvent + +@implementation BNCCommerceEvent : NSObject + +- (NSDictionary*) dictionary { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + #define assign(x) \ + do { if (self.x) { dictionary[@#x] = self.x; } } while (0) + + assign(revenue); + assign(currency); + if (self.transactionID) { + dictionary[@"transaction_id"] = self.transactionID; + } + assign(shipping); + assign(tax); + assign(coupon); + assign(affiliation); + + NSMutableArray *products = [NSMutableArray arrayWithCapacity:self.products.count]; + for (BNCProduct *product in self.products) { + NSDictionary * d = [product dictionary]; + if (d) [products addObject:d]; + } + dictionary[@"products"] = products; + + #undef assign + + return dictionary; +} + +- (NSString*) description { + return [NSString stringWithFormat: + @"Revenue: %@ Currency: %@ TxID: %@ Shipping: %@ Tax: %@ Coupon: %@ Affl: %@ Products: %lu", + self.revenue, + self.currency, + self.transactionID, + self.shipping, + self.tax, + self.coupon, + self.affiliation, + (unsigned long) self.products.count]; +} + +@end + +#pragma mark - BranchCommerceEventRequest + +@interface BranchCommerceEventRequest () +@property (strong) NSDictionary *commerceDictionary; +@property (strong) NSDictionary *metadata; +@property (copy) void (^completion)(NSDictionary* response, NSError* error); +@end + +@implementation BranchCommerceEventRequest + +- (instancetype) initWithCommerceEvent:(BNCCommerceEvent*)commerceEvent + metadata:(NSDictionary*)metadata + completion:(void (^)(NSDictionary* response, NSError* error))completion { + self = [super init]; + if (!self) return self; + + if ([commerceEvent.revenue isEqualToNumber:[NSDecimalNumber numberWithDouble:0.0]]) { + BNCLogWarning(@"Sending a commerce event with zero revenue."); + } + + self.commerceDictionary = [commerceEvent dictionary]; + self.metadata = metadata; + self.completion = completion; + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface + key:(NSString *)key callback:(BNCServerCallback)callback { + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + + NSMutableDictionary *params = [NSMutableDictionary dictionary]; + params[BRANCH_REQUEST_KEY_ACTION] = @"purchase"; + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + + if (self.metadata) + params[@"metadata"] = self.metadata; + if (self.commerceDictionary) + params[@"commerce_data"] = self.commerceDictionary; + if (preferenceHelper.limitFacebookTracking) + params[@"limit_facebook_tracking"] = (__bridge NSNumber*) kCFBooleanTrue; + + NSString *URL = [preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_USER_COMPLETED_ACTION]; + [serverInterface postRequest:params + url:URL + key:key + callback:callback]; +} + +- (void)processResponse:(BNCServerResponse*)response + error:(NSError*)error { + + NSDictionary *dictionary = + ([response.data isKindOfClass:[NSDictionary class]]) + ? (NSDictionary*) response.data + : nil; + + if (self.completion) + self.completion(dictionary, error); +} + +#pragma mark BranchCommerceEventRequest NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) return self; + self.commerceDictionary = [decoder decodeObjectOfClass:NSDictionary.class forKey:@"commerceDictionary"]; + self.metadata = [decoder decodeObjectOfClass:NSDictionary.class forKey:@"metaData"]; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.commerceDictionary forKey:@"commerceDictionary"]; + [coder encodeObject:self.metadata forKey:@"metadata"]; +} + ++ (BOOL) supportsSecureCoding { + return YES; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCConfig.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCConfig.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCConfig.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCConfig.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCConfig.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCConfig.m new file mode 100644 index 00000000000..5fe0d05d8f5 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCConfig.m @@ -0,0 +1,15 @@ +// +// BNCConfig.c +// Branch-SDK +// +// Created by Edward Smith on 12/12/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#include "BNCConfig.h" + +NSString * const BNC_API_BASE_URL = @"https://api2.branch.io"; +BOOL const BNC_API_PINNED = NO; +NSString * const BNC_API_VERSION = @"v1"; +NSString * const BNC_LINK_URL = @"https://bnc.lt"; +NSString * const BNC_SDK_VERSION = @"0.25.11"; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCContentDiscoveryManager.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCContentDiscoveryManager.h new file mode 100644 index 00000000000..ac696ca4de9 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCContentDiscoveryManager.h @@ -0,0 +1,62 @@ +// +// BNCContentDiscoveryManager.h +// Branch-TestBed +// +// Created by Graham Mueller on 7/17/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +#import "BNCCallbacks.h" + +@interface BNCContentDiscoveryManager : NSObject + +- (NSString *)spotlightIdentifierFromActivity:(NSUserActivity *)userActivity; +- (NSString *)standardSpotlightIdentifierFromActivity:(NSUserActivity *)userActivity; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description callback:(callbackWithUrl)callback; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable callback:(callbackWithUrl)callback; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable type:(NSString *)type callback:(callbackWithUrl)callback; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable type:(NSString *)type thumbnailUrl:(NSURL *)thumbnailUrl callback:(callbackWithUrl)callback; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable type:(NSString *)type thumbnailUrl:(NSURL *)thumbnailUrl keywords:(NSSet *)keywords callback:(callbackWithUrl)callback; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable type:(NSString *)type thumbnailUrl:(NSURL *)thumbnailUrl keywords:(NSSet *)keywords; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable type:(NSString *)type thumbnailUrl:(NSURL *)thumbnailUrl keywords:(NSSet *)keywords userInfo:(NSDictionary *)userInfo; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable thumbnailUrl:(NSURL *)thumbnailUrl userInfo:(NSDictionary *)userInfo; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable thumbnailUrl:(NSURL *)thumbnailUrl keywords:(NSSet *)keywords userInfo:(NSDictionary *)userInfo; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable type:(NSString *)type thumbnailUrl:(NSURL *)thumbnailUrl keywords:(NSSet *)keywords userInfo:(NSDictionary *)userInfo callback:(callbackWithUrl)callback; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable type:(NSString *)type thumbnailUrl:(NSURL *)thumbnailUrl keywords:(NSSet *)keywords userInfo:(NSDictionary *)userInfo expirationDate:(NSDate *)expirationDate callback:(callbackWithUrl)callback; + +- (void)indexContentWithTitle:(NSString *)title description:(NSString *)description canonicalId:(NSString *)canonicalId publiclyIndexable:(BOOL)publiclyIndexable type:(NSString *)type thumbnailUrl:(NSURL *)thumbnailUrl keywords:(NSSet *)keywords userInfo:(NSDictionary *)userInfo expirationDate:(NSDate *)expirationDate callback:(callbackWithUrl)callback; + + +/* This one has a different callback, which includes the spotlightIdentifier, and requires a different signature + It cannot be part of the stack of method signatures above, because of the different callback type.*/ +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + canonicalId:(NSString *)canonicalId + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + userInfo:(NSDictionary *)userInfo + expirationDate:(NSDate *)expirationDate + callback:(callbackWithUrl)callback + spotlightCallback:(callbackWithUrlAndSpotlightIdentifier)spotlightCallback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCContentDiscoveryManager.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCContentDiscoveryManager.m new file mode 100644 index 00000000000..01f5181a09e --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCContentDiscoveryManager.m @@ -0,0 +1,406 @@ +// +// BNCContentDiscoveryManager.m +// Branch-TestBed +// +// Created by Graham Mueller on 7/17/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#import "BNCContentDiscoveryManager.h" +#import "Branch.h" +#import "BranchConstants.h" +#import "BNCSpotlightService.h" + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 + #if __has_feature(modules) + @import MobileCoreServices; + #else + #import + #endif +#endif + +static NSString* const kUTTypeGeneric = @"public.content"; + +#ifndef CSSearchableItemActivityIdentifier +#define CSSearchableItemActivityIdentifier @"kCSSearchableItemActivityIdentifier" +#endif + +@interface BNCContentDiscoveryManager (){ + dispatch_queue_t _workQueue; +} + +@property (strong, readonly) dispatch_queue_t workQueue; +@property (strong, atomic) BNCSpotlightService* spotlight; + +@end + +@implementation BNCContentDiscoveryManager + + +- (id) init { + self = [super init]; + + if (self) { + self.spotlight = [[BNCSpotlightService alloc] init]; + } + return self; +} + +#pragma mark - Launch handling + +- (NSString *)spotlightIdentifierFromActivity:(NSUserActivity *)userActivity { +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 + // If it has our prefix, then the link identifier is just the last piece of the identifier. + NSString *activityIdentifier = userActivity.userInfo[CSSearchableItemActivityIdentifier]; + BOOL isBranchIdentifier = [activityIdentifier hasPrefix:BRANCH_SPOTLIGHT_PREFIX]; + if (isBranchIdentifier) { + return activityIdentifier; + } +#endif + + return nil; +} + +- (NSString *)standardSpotlightIdentifierFromActivity:(NSUserActivity *)userActivity { +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 + if (userActivity.userInfo[CSSearchableItemActivityIdentifier]) { + return userActivity.userInfo[CSSearchableItemActivityIdentifier]; + } +#endif + + return nil; +} + +#pragma mark - Content Indexing + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description { + [self indexContentWithTitle:title + description:description + publiclyIndexable:NO + type:(NSString *)kUTTypeGeneric + thumbnailUrl:nil + keywords:nil + userInfo:nil + expirationDate:nil + callback:NULL]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + callback:(callbackWithUrl)callback { + [self indexContentWithTitle:title + description:description + publiclyIndexable:NO + type:(NSString *)kUTTypeGeneric + thumbnailUrl:nil + keywords:nil + userInfo:nil + expirationDate:nil + callback:callback]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + callback:(callbackWithUrl)callback { + [self indexContentWithTitle:title + description:description + publiclyIndexable:publiclyIndexable + type:(NSString *)kUTTypeGeneric + thumbnailUrl:nil + keywords:nil userInfo:nil + expirationDate:nil + callback:callback]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + callback:(callbackWithUrl)callback { + [self indexContentWithTitle:title + description:description + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:nil + keywords:nil + userInfo:nil + expirationDate:nil + callback: callback]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + callback:(callbackWithUrl)callback { + [self indexContentWithTitle:title + description:description + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:thumbnailUrl + keywords:nil + userInfo:nil + expirationDate:nil + callback:callback]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + callback:(callbackWithUrl)callback { + [self indexContentWithTitle:title + description:description + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:thumbnailUrl + keywords:keywords + userInfo:nil + expirationDate:nil + callback:callback]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords { + [self indexContentWithTitle:title + description:description + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:thumbnailUrl + keywords:keywords + userInfo:nil + expirationDate:nil + callback:NULL]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + userInfo:(NSDictionary *)userInfo { + [self indexContentWithTitle:title + description:description + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:thumbnailUrl + keywords:keywords + userInfo:userInfo + expirationDate:nil + callback:NULL]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + thumbnailUrl:(NSURL *)thumbnailUrl + userInfo:(NSDictionary *)userInfo { + [self indexContentWithTitle:title + description:description + publiclyIndexable:publiclyIndexable + type:kUTTypeGeneric + thumbnailUrl:thumbnailUrl + keywords:nil + userInfo:userInfo + expirationDate:nil + callback:NULL]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + userInfo:(NSDictionary *)userInfo { + [self indexContentWithTitle:title + description:description + publiclyIndexable:publiclyIndexable + type:kUTTypeGeneric + thumbnailUrl:thumbnailUrl + keywords:keywords + userInfo:userInfo + expirationDate:nil + callback:NULL]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + userInfo:(NSDictionary *)userInfo + expirationDate:(NSDate*)expirationDate + callback:(callbackWithUrl)callback { + [self indexContentWithTitle:title + description:description + canonicalId:nil + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:thumbnailUrl + keywords:keywords + userInfo:userInfo + expirationDate:nil + callback:callback + spotlightCallback:nil]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + canonicalId:(NSString *)canonicalId + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + userInfo:(NSDictionary *)userInfo + expirationDate:(NSDate*)expirationDate + callback:(callbackWithUrl)callback { + [self indexContentWithTitle:title + description:description + canonicalId:canonicalId + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:thumbnailUrl + keywords:keywords + userInfo:userInfo + expirationDate:nil + callback:callback + spotlightCallback:nil]; +} + + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + userInfo:(NSDictionary *)userInfo + callback:(callbackWithUrl)callback { + [self indexContentWithTitle:title + description:description + canonicalId:nil + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:thumbnailUrl + keywords:keywords + userInfo:userInfo + expirationDate:nil + callback:callback + spotlightCallback:nil]; +} + +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + userInfo:(NSDictionary *)userInfo + spotlightCallback:(callbackWithUrlAndSpotlightIdentifier)spotlightCallback { + [self indexContentWithTitle:title + description:description + canonicalId:nil + publiclyIndexable:publiclyIndexable + type:type + thumbnailUrl:thumbnailUrl + keywords:keywords + userInfo:userInfo + expirationDate:nil + callback:nil + spotlightCallback:spotlightCallback]; +} + +-(void) indexObject:(BranchUniversalObject *)universalObject + onCompletion:(void (^)(BranchUniversalObject *, NSString*, NSError *))completion { + + [self indexContentWithTitle:universalObject.title + description:universalObject.description + canonicalId:universalObject.canonicalUrl + publiclyIndexable:universalObject.locallyIndex + type:universalObject.contentMetadata.contentSchema + thumbnailUrl:[NSURL URLWithString: universalObject.imageUrl] + keywords:[NSSet setWithArray:universalObject.keywords] + userInfo:universalObject.contentMetadata.customMetadata expirationDate:nil + callback:nil + spotlightCallback:^(NSString * _Nullable url, NSString * _Nullable spotlightIdentifier, NSError * _Nullable error) { + + if (error) { + completion(universalObject,url,error); + } else { + completion(universalObject,url,error); + } + }]; +} + +//This is the final one, which figures out which callback to use, if any +// The simpler callbackWithURL overrides spotlightCallback, so don't send both +- (void)indexContentWithTitle:(NSString *)title + description:(NSString *)description + canonicalId:(NSString *)canonicalId + publiclyIndexable:(BOOL)publiclyIndexable + type:(NSString *)type + thumbnailUrl:(NSURL *)thumbnailUrl + keywords:(NSSet *)keywords + userInfo:(NSDictionary *)userInfo + expirationDate:(NSDate *)expirationDate + callback:(callbackWithUrl)callback + spotlightCallback:(callbackWithUrlAndSpotlightIdentifier)spotlightCallback { + + if (!userInfo) userInfo = @{}; + NSMutableDictionary *customData = [NSMutableDictionary dictionaryWithDictionary:userInfo]; + if (!customData) customData = [NSMutableDictionary dictionaryWithDictionary:@{}]; + + BNCSpotlightService* spotlightService = [[BNCSpotlightService alloc] init]; + + BranchUniversalObject *universalObject = [[BranchUniversalObject alloc] initWithTitle:title]; + [universalObject setContentDescription:description]; + [universalObject setCanonicalUrl:canonicalId]; + [universalObject setLocallyIndex:publiclyIndexable]; + [universalObject.contentMetadata setContentSchema:type]; + [universalObject setImageUrl:[thumbnailUrl absoluteString]]; + [universalObject setKeywords:[keywords allObjects]]; + [universalObject.contentMetadata setCustomMetadata:customData]; + [universalObject setExpirationDate:expirationDate]; + + if(publiclyIndexable) { + [spotlightService indexWithBranchUniversalObject:universalObject + linkProperties:nil + callback:^(BranchUniversalObject * _Nullable universalObject, + NSString * _Nullable url, + NSError * _Nullable error) { + if (callback) { + callback(url, error); + } + else if (spotlightCallback) { + spotlightCallback(url, url, error); + } + }]; + + } else { + [spotlightService indexWithBranchUniversalObject:universalObject + linkProperties:nil + callback:^(BranchUniversalObject * _Nullable universalObject, + NSString * _Nullable url, + NSError * _Nullable error) { + if (callback) { + callback(url, error); + } + else if (spotlightCallback) { + spotlightCallback(url, url, error); + } + }]; + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCrashlyticsWrapper.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCrashlyticsWrapper.h new file mode 100644 index 00000000000..c5fc8245a27 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCrashlyticsWrapper.h @@ -0,0 +1,34 @@ +// +// BNCCrashlyticsWrapper.h +// Branch.framework +// +// Created by Jimmy Dee on 7/18/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +/** + * Convenience class to dynamically wrap the Crashlytics SDK + * if present. If it is not present, everything here is a no-op. + */ +@interface BNCCrashlyticsWrapper : NSObject + +/// Reference to the Crashlytics.sharedInstance or nil. +@property (nonatomic, nullable, readonly) id crashlytics; + +/// Convenience method to create new instances ++ (instancetype _Nonnull)wrapper; + +/** + * Use these methods to set key values in a Crashlytics report. + */ +- (void)setObjectValue:(id _Nullable)value forKey:(NSString * _Nonnull)key; +- (void)setIntValue:(int)value forKey:(NSString * _Nonnull)key; +- (void)setBoolValue:(BOOL)value forKey:(NSString * _Nonnull)key; +- (void)setFloatValue:(float)value forKey:(NSString * _Nonnull)key; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCrashlyticsWrapper.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCrashlyticsWrapper.m new file mode 100644 index 00000000000..7046ba6ec25 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCCrashlyticsWrapper.m @@ -0,0 +1,71 @@ +// +// BNCCrashlyticsReportingHelper.m +// Branch.framework +// +// Created by Jimmy Dee on 7/18/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#import "BNCCrashlyticsWrapper.h" + +@interface BNCCrashlyticsWrapper() +@property (nonatomic, nullable) id crashlytics; +@end + +@implementation BNCCrashlyticsWrapper + ++ (id)sharedInstance +{ + // This just exists so that sharedInstance is not an unknown selector. + return nil; +} + ++ (instancetype)wrapper +{ + return [[self alloc] init]; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + // Dynamically obtain Crashlytics.sharedInstance if the Crashlytics SDK is linked. + Class Crashlytics = NSClassFromString(@"Crashlytics"); + if ([Crashlytics respondsToSelector:@selector(sharedInstance)]) { + id crashlyticsInstance = [Crashlytics sharedInstance]; + if ([crashlyticsInstance isKindOfClass:Crashlytics] && + [crashlyticsInstance respondsToSelector:@selector(setObjectValue:forKey:)] && + [crashlyticsInstance respondsToSelector:@selector(setBoolValue:forKey:)] && + [crashlyticsInstance respondsToSelector:@selector(setFloatValue:forKey:)] && + [crashlyticsInstance respondsToSelector:@selector(setIntValue:forKey:)]) + _crashlytics = crashlyticsInstance; + } + } + return self; +} + +- (void)setObjectValue:(id)value forKey:(NSString *)key +{ + if (!self.crashlytics) return; + [self.crashlytics setObjectValue:value forKey:key]; +} + +- (void)setIntValue:(int)value forKey:(NSString *)key +{ + if (!self.crashlytics) return; + [self.crashlytics setIntValue:value forKey:key]; +} + +- (void)setFloatValue:(float)value forKey:(NSString *)key +{ + if (!self.crashlytics) return; + [self.crashlytics setFloatValue:value forKey:key]; +} + +- (void)setBoolValue:(BOOL)value forKey:(NSString *)key +{ + if (!self.crashlytics) return; + [self.crashlytics setBoolValue:value forKey:key]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCDebug.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDebug.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCDebug.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDebug.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDebug.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDebug.m new file mode 100644 index 00000000000..6a8732c8ed2 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDebug.m @@ -0,0 +1,225 @@ +/** + @file BNCDebug.m + @package Branch-SDK + @brief Debugging Support. + + @author Edward Smith + @date October 2016 + @copyright Copyright © 2016 Branch. All rights reserved. +*/ + +#import "BNCDebug.h" +#if __has_feature(modules) +@import Darwin.sys.sysctl; +@import ObjectiveC.runtime; +#else +#import +#import +#endif + +BOOL BNCDebuggerIsAttached() { + // From an Apple tech note that I've lost --EB Smith + + // Returns true if the current process is being debugged (either + // running under the debugger or has a debugger attached post facto). + + int junk; + int mib[4]; + struct kinfo_proc info; + size_t size; + + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + + info.kp_proc.p_flag = 0; + + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + // Call sysctl. + + size = sizeof(info); + junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + if (junk != 0) { + NSLog(@"Program error in BNCDebuggerIsAttached. Junk != 0!"); + } + + // We're being debugged if the P_TRACED flag is set. + + return ( (info.kp_proc.p_flag & P_TRACED) != 0 ); +} + +NSString * _Nonnull BNCDebugStringFromObject(id _Nullable instance) { + // Dump the class -- + + if (!instance) return @"Object is nil.\n"; + + const char* superclassname = ""; + Class class = object_getClass(instance); + Class superclass = class_getSuperclass(class); + if (superclass) superclassname = class_getName(superclass); + if (!superclassname) superclassname = ""; + + NSMutableString *result = [NSMutableString stringWithCapacity:512]; + if (class_isMetaClass(class)) { + [result appendFormat:@"\nClass %p is class '%s' of class '%s':\n", + (void*)instance, class_getName(class), superclassname]; + class = instance; + } else { + [result appendFormat:@"\nInstance %p is of class '%s' of class '%s':\n", + (void*)instance, class_getName(class), superclassname]; + } + + // Ivars -- + + #define isTypeOf(encoding, type) \ + (strncmp(encoding, @encode(type), strlen(encoding)) == 0) + + #define AppendValueOfType(type, format) \ + if (isTypeOf(encoding, type)) { \ + if (ivarPtr) { \ + [result appendFormat:@"\tIvar '%s' type '%s' value '"format"'.\n", \ + ivarName, #type, *((type*)ivarPtr)]; \ + } else { \ + [result appendFormat:@"\tIvar '%s' type '%s'.\n", \ + ivarName, #type]; \ + } \ + } else + + uint count = 0; + Ivar *ivars = class_copyIvarList(class, &count); + for (uint i = 0; ivars && i < count; ++i) { + const char* encoding = ivar_getTypeEncoding(ivars[i]); + const char* ivarName = ivar_getName(ivars[i]); + const void* ivarPtr = nil; + if (class == instance) { + // instance is a class, so there aren't any ivar values. + } else if (encoding[0] == '@' || encoding[0] == '#') { + ivarPtr = (__bridge void*) object_getIvar(instance, ivars[i]); + } else { + ivarPtr = (void*) (((__bridge void*)instance) + ivar_getOffset(ivars[i])); + } + + if (encoding[0] == '@') { + if (ivarPtr) + [result appendFormat:@"\tIvar '%s' type '%@' value '%@'.\n", + ivarName, NSStringFromClass(((__bridge id)ivarPtr).class), ivarPtr]; + else { + NSString *className = [NSString stringWithFormat:@"%s", encoding]; + if ([className hasPrefix:@"@\""]) + className = [className substringFromIndex:2]; + if ([className hasSuffix:@"\""]) + className = [className substringToIndex:className.length-1]; + [result appendFormat:@"\tIvar '%s' type '%@'.\n", ivarName, className]; + } + } else if (isTypeOf(encoding, Class)) { + if (ivarPtr) + [result appendFormat:@"\tIvar '%s' type 'class' value '%@'.\n", + ivarName, NSStringFromClass((__bridge Class _Nonnull)(ivarPtr))]; + else + [result appendFormat:@"\tIvar '%s' type 'class'.\n", + ivarName]; + } else if (isTypeOf(encoding, char*)) { + if (ivarPtr) + [result appendFormat:@"\tIvar '%s' type 'char*' value '%s'.\n", + ivarName, *(char**)ivarPtr]; + else + [result appendFormat:@"\tIvar '%s' type 'char*'.\n", + ivarName]; + } else if (isTypeOf(encoding, BOOL)) { + if (ivarPtr) + [result appendFormat:@"\tIvar '%s' type 'BOOL' value '%s'.\n", + ivarName, (*(BOOL*)ivarPtr)?"YES":"NO"]; + else + [result appendFormat:@"\tIvar '%s' type 'BOOL'.\n", + ivarName]; + } else if (isTypeOf(encoding, int)) { + if (ivarPtr) + [result appendFormat:@"\tIvar '%s' type '%s' value '%d'.\n", + ivarName, "int", *((int*)ivarPtr)]; + else + [result appendFormat:@"\tIvar '%s' type '%s'.\n", + ivarName, "int"]; + } else + // clang-format off + AppendValueOfType(float, "%f") + AppendValueOfType(double, "%f") + AppendValueOfType(long double, "%Lf") + AppendValueOfType(char, "%c") + AppendValueOfType(int, "%d") + AppendValueOfType(short, "%hd") + AppendValueOfType(long, "%ld") + AppendValueOfType(long long, "%lld") + AppendValueOfType(unsigned char, "%c") + AppendValueOfType(unsigned int, "%u") + AppendValueOfType(unsigned short, "%hu") + AppendValueOfType(unsigned long, "%lu") + AppendValueOfType(unsigned long long, "%llu") + [result appendFormat:@"\tIvar '%s' type '%s' (un-handled type).\n", + ivarName, + encoding]; + // clang-format off + } + if (ivars) free(ivars); + + #undef AppendValueOfType + #undef isTypeOf + + // Properties -- + + count = 0; + objc_property_t *properties = class_copyPropertyList(class, &count); + for (int i = 0; properties && i < count; ++i) + [result appendFormat:@"\tProperty name: '%s'.\n", property_getName(properties[i])]; + if (properties) free(properties); + + // Class methods -- + + count = 0; + Method *methods = class_copyMethodList(object_getClass(class), &count); + for (int i = 0; i < count; ++i) + [result appendFormat:@"\tClass method name: '%s'.\n", + sel_getName(method_getName(methods[i]))]; + if (methods) free(methods); + + // Instance methods -- + + count = 0; + methods = class_copyMethodList(class, &count); + for (int i = 0; i < count; ++i) + [result appendFormat:@"\tMethod name: '%s'.\n", + sel_getName(method_getName(methods[i]))]; + if (methods) free(methods); + + return result; +} + +NSArray * _Nonnull BNCDebugArrayOfReqisteredClasses() { + // Add all loaded classes to the NSString array: + + int numClasses = 0; + Class * classes = NULL; + + numClasses = objc_getClassList(NULL, 0); + if (numClasses <= 0) return @[]; + + classes = (__unsafe_unretained Class*) malloc(sizeof(Class) * numClasses); + numClasses = objc_getClassList(classes, numClasses); + + NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:numClasses]; + + Class *class = classes; + for (int i = 0; i < numClasses; ++i) { + NSString* s = NSStringFromClass(*class); + if (s) [result addObject:s]; + ++class; + } + + free(classes); + return result; +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeepLinkViewControllerInstance.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeepLinkViewControllerInstance.h new file mode 100644 index 00000000000..d0e82aff0f6 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeepLinkViewControllerInstance.h @@ -0,0 +1,14 @@ +// +// BNCDeepLinkViewControllerInstance.h +// Branch-SDK +// +// Created by Parth Kalavadia on 5/15/17. +// Copyright © 2017 Parth Kalavadia. All rights reserved. +// + +#import "BranchDeepLinkingController.h" + +@interface BNCDeepLinkViewControllerInstance : NSObject +@property (strong, nonatomic)UIViewController* viewController; +@property (assign)BNCViewControllerPresentationOption option; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeepLinkViewControllerInstance.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeepLinkViewControllerInstance.m new file mode 100644 index 00000000000..1f13676e71f --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeepLinkViewControllerInstance.m @@ -0,0 +1,12 @@ +// +// BNCDeepLinkViewControllerInstance.m +// Branch-SDK +// +// Created by Parth Kalavadia on 5/15/17. +// Copyright © 2017 Parth Kalavadia. All rights reserved. +// + +#import "BNCDeepLinkViewControllerInstance.h" + +@implementation BNCDeepLinkViewControllerInstance +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeviceInfo.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeviceInfo.h new file mode 100644 index 00000000000..15942560f76 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeviceInfo.h @@ -0,0 +1,50 @@ +// +// BNCDeviceInfo.h +// Branch-TestBed +// +// Created by Sojan P.R. on 3/22/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +@import CoreGraphics; +#else +#import +#import +#endif + +@interface BNCDeviceInfo : NSObject + +//---------Properties-------------// +@property (atomic, copy, readonly) NSString *hardwareId; +@property (atomic, copy, readonly) NSString *hardwareIdType; +@property (atomic, readonly) BOOL isRealHardwareId; +@property (atomic, copy, readonly) NSString *vendorId; //!< VendorId can be nil initially and non-nil later. +@property (atomic, copy, readonly) NSString *brandName; +@property (atomic, copy, readonly) NSString *modelName; +@property (atomic, copy, readonly) NSString *osName; +@property (atomic, copy, readonly) NSString *osVersion; +@property (atomic, copy, readonly) NSNumber *screenWidth; +@property (atomic, copy, readonly) NSNumber *screenHeight; +@property (atomic, readonly) BOOL isAdTrackingEnabled; + +@property (atomic, copy, readonly) NSString *extensionType; +@property (atomic, copy, readonly) NSString *branchSDKVersion; +@property (atomic, copy, readonly) NSString *applicationVersion; +@property (atomic, assign, readonly) CGFloat screenScale; +@property (atomic, copy, readonly) NSString* adId; +@property (atomic, assign, readonly) BOOL unidentifiedDevice; + +@property (atomic, copy, readonly) NSString* country; //!< The iso2 Country name (us, in,etc). +@property (atomic, copy, readonly) NSString* language; //!< The iso2 language code (en, ml). +@property (atomic, copy, readonly) NSString* browserUserAgent; //!< Simple user agent string. +@property (atomic, copy, readonly) NSString* localIPAddress; //!< The current local IP address. +@property (atomic, copy, readonly) NSArray *allIPAddresses; //!< All local IP addresses. +//----------Methods----------------// ++ (BNCDeviceInfo *)getInstance; ++ (NSString*) userAgentString; // Warning: Has an implied lock on main thread on first call. ++ (NSString*) systemBuildVersion; + +- (NSDictionary*) v2dictionary; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeviceInfo.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeviceInfo.m new file mode 100644 index 00000000000..07fb12bb5c2 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCDeviceInfo.m @@ -0,0 +1,473 @@ +// +// BNCDeviceInfo.m +// Branch-TestBed +// +// Created by Sojan P.R. on 3/22/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import "BNCDeviceInfo.h" +#import "BNCPreferenceHelper.h" +#import "BNCSystemObserver.h" +#import "BNCAvailability.h" +#import "BNCLog.h" +#import "BNCConfig.h" +#import "BNCPreferenceHelper.h" + +#if __has_feature(modules) +@import UIKit; +#else +#import +#endif +#import // @import not available in Xcode 7 +#import +#import +#import +#import + +// Forward declare this for older versions of iOS +@interface NSLocale (BranchAvailability) +- (NSString*) countryCode; +- (NSString*) languageCode; +@end + +#pragma mark BRNNetworkInfo + +typedef NS_ENUM(NSInteger, BNCNetworkAddressType) { + BNCNetworkAddressTypeUnknown = 0, + BNCNetworkAddressTypeIPv4, + BNCNetworkAddressTypeIPv6 +}; + +@interface BNCNetworkInterface : NSObject + ++ (NSArray*) currentInterfaces; + +@property (nonatomic, strong) NSString *interfaceName; +@property (nonatomic, assign) BNCNetworkAddressType addressType; +@property (nonatomic, strong) NSString *address; +@end + +@implementation BNCNetworkInterface + ++ (NSArray*) currentInterfaces { + + struct ifaddrs *interfaces = NULL; + NSMutableArray *currentInterfaces = [NSMutableArray arrayWithCapacity:8]; + + // Retrieve the current interfaces - returns 0 on success + + if (getifaddrs(&interfaces) != 0) { + int e = errno; + BNCLogError(@"Can't read ip address: (%d): %s.", e, strerror(e)); + goto exit; + } + + // Loop through linked list of interfaces -- + + struct ifaddrs *interface = NULL; + for(interface=interfaces; interface; interface=interface->ifa_next) { + // BNCLogDebugSDK(@"Found %s: %x.", interface->ifa_name, interface->ifa_flags); + + // Check the state: IFF_RUNNING, IFF_UP, IFF_LOOPBACK, etc. + if ((interface->ifa_flags & IFF_UP) && + (interface->ifa_flags & IFF_RUNNING) && + !(interface->ifa_flags & IFF_LOOPBACK)) { + } else { + continue; + } + + // TODO: Check ifdata too. + // struct if_data *ifdata = interface->ifa_data; + + const struct sockaddr_in *addr = (const struct sockaddr_in*)interface->ifa_addr; + if (!addr) continue; + + BNCNetworkAddressType type = BNCNetworkAddressTypeUnknown; + char addrBuf[ MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN) ]; + + if (addr->sin_family == AF_INET) { + if (inet_ntop(AF_INET, &addr->sin_addr, addrBuf, INET_ADDRSTRLEN)) + type = BNCNetworkAddressTypeIPv4; + } + else + if (addr->sin_family == AF_INET6) { + const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6*)interface->ifa_addr; + if (inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf, INET6_ADDRSTRLEN)) + type = BNCNetworkAddressTypeIPv6; + } + else { + continue; + } + + NSString *name = [NSString stringWithUTF8String:interface->ifa_name]; + if (name && type != BNCNetworkAddressTypeUnknown) { + BNCNetworkInterface *interface = [BNCNetworkInterface new]; + interface.interfaceName = name; + interface.addressType = type; + interface.address = [NSString stringWithUTF8String:addrBuf]; + [currentInterfaces addObject:interface]; + } + } + +exit: + if (interfaces) freeifaddrs(interfaces); + return currentInterfaces; +} + +- (NSString*) description { + return [NSString stringWithFormat:@"<%@ %p %@ %@>", + NSStringFromClass(self.class), + self, + self.interfaceName, + self.address + ]; +} + +@end + +#pragma mark - BNCDeviceInfo + +@interface BNCDeviceInfo() +@end + + +@implementation BNCDeviceInfo { + NSString *_vendorId; + NSString *_localIPAddress; +} + ++ (BNCDeviceInfo *)getInstance { + static BNCDeviceInfo *bnc_deviceInfo = 0; + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^{ + bnc_deviceInfo = [[BNCDeviceInfo alloc] init]; + }); + return bnc_deviceInfo; +} + +- (id)init { + self = [super init]; + if (!self) return self; + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + BOOL isRealHardwareId; + NSString *hardwareIdType; + NSString *hardwareId = + [BNCSystemObserver getUniqueHardwareId:&isRealHardwareId + isDebug:preferenceHelper.isDebug + andType:&hardwareIdType]; + if (hardwareId) { + _hardwareId = hardwareId.copy; + _isRealHardwareId = isRealHardwareId; + _hardwareIdType = hardwareIdType.copy; + } + + _brandName = [BNCSystemObserver getBrand].copy; + _modelName = [BNCSystemObserver getModel].copy; + _osName = [BNCSystemObserver getOS].copy; + _osVersion = [BNCSystemObserver getOSVersion].copy; + _screenWidth = [BNCSystemObserver getScreenWidth].copy; + _screenHeight = [BNCSystemObserver getScreenHeight].copy; + _isAdTrackingEnabled = [BNCSystemObserver adTrackingSafe]; + + _country = [BNCDeviceInfo bnc_country].copy; + _language = [BNCDeviceInfo bnc_language].copy; + _browserUserAgent = [BNCDeviceInfo userAgentString].copy; + _extensionType = self.class.extensionType.copy; + _branchSDKVersion = [NSString stringWithFormat:@"ios%@", BNC_SDK_VERSION]; + _applicationVersion = [NSBundle mainBundle].infoDictionary[@"CFBundleShortVersionString"]; + if (!_applicationVersion.length) + _applicationVersion = [NSBundle mainBundle].infoDictionary[@"CFBundleVersionKey"]; + _screenScale = [UIScreen mainScreen].scale; + _adId = [BNCSystemObserver getAdId]; + + return self; +} + ++ (NSString*_Nonnull) extensionType { + NSString *result = @"FULL_APP"; + NSString *extensionType = [NSBundle mainBundle].infoDictionary[@"NSExtension"][@"NSExtensionPointIdentifier"]; + if ([extensionType isEqualToString:@"com.apple.identitylookup.message-filter"]) { + result = @"IMESSAGE_APP"; + } + return result; +} + +- (NSString *)vendorId { + @synchronized (self) { + if (_vendorId) return _vendorId; + /* + * https://developer.apple.com/documentation/uikit/uidevice/1620059-identifierforvendor + * BNCSystemObserver.getVendorId is based on UIDevice.identifierForVendor. Note from the + * docs above: + * + * If the value is nil, wait and get the value again later. This happens, for example, + * after the device has been restarted but before the user has unlocked the device. + * + * It's not clear if that specific example scenario would apply to opening Branch links, + * but this lazy initialization is probably safer. + */ + _vendorId = [BNCSystemObserver getVendorId].copy; + return _vendorId; + } +} + +- (BOOL) unidentifiedDevice { + return (self.vendorId == nil && self.adId == nil); +} + +- (NSString*) localIPAddress { // For 'local_ip' server field. + @synchronized (self) { + NSArray*interfaces = [BNCNetworkInterface currentInterfaces]; + for (BNCNetworkInterface *interface in interfaces) { + if (interface.addressType == BNCNetworkAddressTypeIPv4) + return interface.address; + } + return nil; + } +} + +- (NSArray*) allIPAddresses { + NSMutableArray *array = [NSMutableArray new]; + for (BNCNetworkInterface *inf in [BNCNetworkInterface currentInterfaces]) { + [array addObject:inf.description]; + } + return array; +} + ++ (NSString*) bnc_country { + + NSString *country = nil; + #define returnIfValidCountry() \ + if ([country isKindOfClass:[NSString class]] && country.length) { \ + return country; \ + } else { \ + country = nil; \ + } + + // Should work on iOS 10 + NSLocale *currentLocale = [NSLocale currentLocale]; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wpartial-availability" + if ([currentLocale respondsToSelector:@selector(countryCode)]) { + country = [currentLocale countryCode]; + } + #pragma clang diagnostic pop + returnIfValidCountry(); + + // Should work on iOS 9 + NSString *rawLanguage = [[NSLocale preferredLanguages] firstObject]; + NSDictionary *languageDictionary = [NSLocale componentsFromLocaleIdentifier:rawLanguage]; + country = [languageDictionary objectForKey:@"kCFLocaleCountryCodeKey"]; + returnIfValidCountry(); + + // Should work on iOS 8 and below. + //NSString* language = [[NSLocale preferredLanguages] firstObject]; + NSString *rawLocale = currentLocale.localeIdentifier; + NSRange range = [rawLocale rangeOfString:@"_"]; + if (range.location != NSNotFound) { + range = NSMakeRange(range.location+1, rawLocale.length-range.location-1); + country = [rawLocale substringWithRange:range]; + } + returnIfValidCountry(); + + #undef returnIfValidCountry + + return nil; +} + ++ (NSString*) bnc_language { + + NSString *language = nil; + #define returnIfValidLanguage() \ + if ([language isKindOfClass:[NSString class]] && language.length) { \ + return language; \ + } else { \ + language = nil; \ + } \ + + // Should work on iOS 10 + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wpartial-availability" + NSLocale *currentLocale = [NSLocale currentLocale]; + if ([currentLocale respondsToSelector:@selector(languageCode)]) { + language = [currentLocale languageCode]; + } + #pragma clang diagnostic pop + returnIfValidLanguage(); + + // Should work on iOS 9 + NSString *rawLanguage = [[NSLocale preferredLanguages] firstObject]; + NSDictionary *languageDictionary = [NSLocale componentsFromLocaleIdentifier:rawLanguage]; + language = [languageDictionary objectForKey:@"kCFLocaleLanguageCodeKey"]; + returnIfValidLanguage(); + + // Should work on iOS 8 and below. + language = [[NSLocale preferredLanguages] firstObject]; + returnIfValidLanguage(); + + #undef returnIfValidLanguage + + return nil; +} + ++ (NSString*) systemBuildVersion { + int mib[2] = { CTL_KERN, KERN_OSVERSION }; + u_int namelen = sizeof(mib) / sizeof(mib[0]); + + // Get the size for the buffer -- + + size_t bufferSize = 0; + sysctl(mib, namelen, NULL, &bufferSize, NULL, 0); + if (bufferSize <= 0) return nil; + + u_char buildBuffer[bufferSize]; + int result = sysctl(mib, namelen, buildBuffer, &bufferSize, NULL, 0); + + NSString *version = nil; + if (result >= 0) { + version = [[NSString alloc] + initWithBytes:buildBuffer + length:bufferSize-1 + encoding:NSUTF8StringEncoding]; + } + return version; +} + ++ (NSString*) userAgentString { + + static NSString* brn_browserUserAgentString = nil; + + void (^setBrowserUserAgent)(void) = ^() { + @synchronized (self) { + if (!brn_browserUserAgentString) { + brn_browserUserAgentString = + [[[UIWebView alloc] + initWithFrame:CGRectZero] + stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"]; + BNCPreferenceHelper *preferences = [BNCPreferenceHelper preferenceHelper]; + preferences.browserUserAgentString = brn_browserUserAgentString; + preferences.lastSystemBuildVersion = self.systemBuildVersion; + BNCLogDebugSDK(@"userAgentString: '%@'.", brn_browserUserAgentString); + } + } + }; + + NSString* (^browserUserAgent)(void) = ^ NSString* () { + @synchronized (self) { + return brn_browserUserAgentString; + } + }; + + @synchronized (self) { + // We only get the string once per app run: + + if (brn_browserUserAgentString) + return brn_browserUserAgentString; + + // Did we cache it? + + BNCPreferenceHelper *preferences = [BNCPreferenceHelper preferenceHelper]; + if (preferences.browserUserAgentString && + preferences.lastSystemBuildVersion && + [preferences.lastSystemBuildVersion isEqualToString:self.systemBuildVersion]) { + brn_browserUserAgentString = [preferences.browserUserAgentString copy]; + return brn_browserUserAgentString; + } + + // Make sure this executes on the main thread. + // Uses an implied lock through dispatch_queues: This can deadlock if mis-used! + + if (NSThread.isMainThread) { + setBrowserUserAgent(); + return brn_browserUserAgentString; + } + + } + + // Different case for iOS 7.0: + if ([UIDevice currentDevice].systemVersion.doubleValue < 8.0) { + BNCLogDebugSDK(@"Getting iOS 7 UserAgent."); + dispatch_sync(dispatch_get_main_queue(), ^ { + setBrowserUserAgent(); + }); + BNCLogDebugSDK(@"Got iOS 7 UserAgent."); + return browserUserAgent(); + } + + // Wait and yield to prevent deadlock: + int retries = 10; + int64_t timeoutDelta = (dispatch_time_t)((long double)NSEC_PER_SEC * (long double)0.100); + while (!browserUserAgent() && retries > 0) { + + dispatch_block_t agentBlock = dispatch_block_create_with_qos_class( + DISPATCH_BLOCK_DETACHED | DISPATCH_BLOCK_ENFORCE_QOS_CLASS, + QOS_CLASS_USER_INTERACTIVE, + 0, ^ { + BNCLogDebugSDK(@"Will set userAgent."); + setBrowserUserAgent(); + BNCLogDebugSDK(@"Did set userAgent."); + }); + dispatch_async(dispatch_get_main_queue(), agentBlock); + + dispatch_time_t timeoutTime = dispatch_time(DISPATCH_TIME_NOW, timeoutDelta); + dispatch_block_wait(agentBlock, timeoutTime); + retries--; + } + BNCLogDebugSDK(@"Retries: %d", 10-retries); + + return browserUserAgent(); +} + +- (NSDictionary*) v2dictionary { + NSMutableDictionary *dictionary = [NSMutableDictionary new]; + + #define BNCFieldDefinesDictionaryFromSelf + #include "BNCFieldDefines.h" + + addString(osName, os); + addString(osVersion, os_version); + addString(extensionType, environment); + addString(vendorId, idfv); + if ([BNCPreferenceHelper preferenceHelper].isDebug) { + dictionary[@"unidentified_device"] = (__bridge NSNumber*) kCFBooleanTrue; + } else { + addString(adId, idfa); + } + addString(browserUserAgent, user_agent); + addString(country, country); + addString(language, language); + addString(brandName, brand); + addString(applicationVersion, app_version); + addString(modelName, model); + addDouble(screenScale, screen_dpi); + addNumber(screenHeight, screen_height); + addNumber(screenWidth, screen_width); + addBoolean(unidentifiedDevice, unidentified_device); + addString(localIPAddress, local_ip); + + #include "BNCFieldDefines.h" + + if (!self.isAdTrackingEnabled) + dictionary[@"limit_ad_tracking"] = (__bridge NSNumber*) kCFBooleanTrue; + + NSString *s = nil; + BNCPreferenceHelper *preferences = [BNCPreferenceHelper preferenceHelper]; + + s = preferences.userIdentity; + if (s.length) dictionary[@"developer_identity"] = s; + + s = preferences.deviceFingerprintID; + if (s.length) dictionary[@"device_fingerprint_id"] = s; + + if (preferences.limitFacebookTracking) + dictionary[@"limit_facebook_tracking"] = (__bridge NSNumber*) kCFBooleanTrue; + + dictionary[@"sdk"] = @"ios"; + dictionary[@"sdk_version"] = BNC_SDK_VERSION; + + return dictionary; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCEncodingUtils.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCEncodingUtils.h new file mode 100644 index 00000000000..34ecf5b5e8a --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCEncodingUtils.h @@ -0,0 +1,67 @@ +// +// BNCEncodingUtils.h +// Branch +// +// Created by Graham Mueller on 3/31/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +#pragma mark BNCWireFormat + +extern NSDate* BNCDateFromWireFormat(id object); +extern NSNumber* BNCWireFormatFromDate(NSDate *date); +extern NSNumber* BNCWireFormatFromBool(BOOL b); + +extern NSString* BNCStringFromWireFormat(id object); +extern NSString* BNCWireFormatFromString(NSString *string); + +#pragma mark - BNCKeyValue + +@interface BNCKeyValue : NSObject + ++ (BNCKeyValue*) key:(NSString*)key value:(NSString*)value; +- (NSString*) description; + +@property (nonatomic, strong) NSString* key; +@property (nonatomic, strong) NSString* value; + +@end + +#pragma mark - BNCEncodingUtils + +@interface BNCEncodingUtils : NSObject + ++ (NSString *)base64EncodeStringToString:(NSString *)strData; ++ (NSString *)base64DecodeStringToString:(NSString *)strData; ++ (NSString *)base64EncodeData:(NSData *)objData; ++ (NSData *)base64DecodeString:(NSString *)strBase64; + ++ (NSString *)md5Encode:(NSString *)input; + ++ (NSString *)urlEncodedString:(NSString *)string; ++ (NSString *)encodeArrayToJsonString:(NSArray *)dictionary; ++ (NSString *)encodeDictionaryToJsonString:(NSDictionary *)dictionary; ++ (NSData *)encodeDictionaryToJsonData:(NSDictionary *)dictionary; + ++ (NSString*) stringByPercentDecodingString:(NSString*)string; ++ (NSString*) stringByPercentEncodingStringForQuery:(NSString *)string; + ++ (NSString *)sanitizedStringFromString:(NSString *)dirtyString; ++ (NSDictionary *)decodeJsonDataToDictionary:(NSData *)jsonData; ++ (NSDictionary *)decodeJsonStringToDictionary:(NSString *)jsonString; + ++ (NSDictionary *)decodeQueryStringToDictionary:(NSString *)queryString; ++ (NSString *)encodeDictionaryToQueryString:(NSDictionary *)dictionary; + ++ (NSString *) hexStringFromData:(NSData*)data; ++ (NSData *) dataFromHexString:(NSString*)string; + ++ (NSArray*) queryItems:(NSURL*)URL; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCEncodingUtils.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCEncodingUtils.m new file mode 100644 index 00000000000..98fd978b378 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCEncodingUtils.m @@ -0,0 +1,522 @@ +// +// BNCEncodingUtils.m +// Branch +// +// Created by Graham Mueller on 3/31/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCEncodingUtils.h" +#import "BNCPreferenceHelper.h" +#import "BNCLog.h" +#import + +#pragma mark BNCWireFormat + +NSDate* BNCDateFromWireFormat(id object) { + NSDate *date = nil; + if ([object respondsToSelector:@selector(doubleValue)]) { + NSTimeInterval t = [object doubleValue]; + date = [NSDate dateWithTimeIntervalSince1970:t/1000.0]; + } + return date; +} + +NSNumber* BNCWireFormatFromDate(NSDate *date) { + NSNumber *number = nil; + NSTimeInterval t = [date timeIntervalSince1970]; + if (date && t != 0.0 ) { + number = [NSNumber numberWithLongLong:(long long)(t*1000.0)]; + } + return number; +} + +NSNumber* BNCWireFormatFromBool(BOOL b) { + return (b) ? (__bridge NSNumber*) kCFBooleanTrue : nil; +} + +NSString* BNCStringFromWireFormat(id object) { + if ([object isKindOfClass:NSString.class]) + return object; + else + if ([object respondsToSelector:@selector(stringValue)]) + return [object stringValue]; + else + if ([object respondsToSelector:@selector(description)]) + return [object description]; + return nil; +} + +NSString* BNCWireFormatFromString(NSString *string) { + return string; +} + +#pragma mark - BNCKeyValue + +@implementation BNCKeyValue + ++ (BNCKeyValue*) key:(NSString*)key value:(NSString*)value { + BNCKeyValue *kv = [[BNCKeyValue alloc] init]; + kv.key = key; + kv.value = value; + return kv; +} + +- (NSString*) description { + return [NSString stringWithFormat:@"<%@, %@>", self.key, self.value]; +} + +- (BOOL) isEqual:(id)rawObject { + BNCKeyValue *object = rawObject; + return + [object isKindOfClass:[BNCKeyValue class]] && + [self.key isEqualToString:object.key] && + [self.value isEqualToString:object.value] + ; +} + +@end + +#pragma mark - BNCEncodingUtils + +@implementation BNCEncodingUtils + +#pragma mark - Base 64 Encoding + ++ (NSString *)base64EncodeStringToString:(NSString *)strData { + return [self base64EncodeData:[strData dataUsingEncoding:NSUTF8StringEncoding]]; +} + ++ (NSString *)base64DecodeStringToString:(NSString *)strData { + NSData* data =[BNCEncodingUtils base64DecodeString:strData]; + if (data == nil) { + return nil; + } + return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; +} + ++ (NSString *)base64EncodeData:(NSData *)data { + if (data) return [data base64EncodedStringWithOptions:0]; + return @""; +} + ++ (NSData *)base64DecodeString:(NSString *)strBase64 { + if (strBase64) return [[NSData alloc] initWithBase64EncodedString:strBase64 options:0]; + return nil; +} + +#pragma mark - MD5 methods + ++ (NSString *)md5Encode:(NSString *)input { + if (!input) { + return @""; + } + + const char *cStr = [input UTF8String]; + unsigned char digest[CC_MD5_DIGEST_LENGTH]; + CC_MD5(cStr, (CC_LONG)strlen(cStr), digest); + + NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2]; + + for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) { + [output appendFormat:@"%02x", digest[i]]; + } + + return output; +} + + +#pragma mark - Param Encoding methods + ++ (NSString *)iso8601StringFromDate:(NSDate *)date { + static NSDateFormatter *dateFormatter; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + [dateFormatter setLocale:[NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]]; // POSIX to avoid weird issues + [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZZZZZ"]; + }); + + return [dateFormatter stringFromDate:date]; +} + ++ (NSString *)sanitizedStringFromString:(NSString *)dirtyString { + NSString *dirtyCopy = [dirtyString copy]; // dirtyString seems to get dealloc'ed sometimes. Make a copy. + NSString *cleanString = [[[[[[[[dirtyCopy + stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"] + stringByReplacingOccurrencesOfString:@"\b" withString:@"\\b"] + stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"] + stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"] + stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"] + stringByReplacingOccurrencesOfString:@"\t" withString:@"\\t"] + stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""] + stringByReplacingOccurrencesOfString:@"`" withString:@"'"]; + return cleanString; +} + ++ (NSData *)encodeDictionaryToJsonData:(NSDictionary *)dictionary { + NSString *jsonString = [BNCEncodingUtils encodeDictionaryToJsonString:dictionary]; + NSUInteger length = [jsonString lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + return [NSData dataWithBytes:[jsonString UTF8String] length:length]; +} + ++ (NSString *)encodeDictionaryToJsonString:(NSDictionary *)dictionary { + NSMutableString *encodedDictionary = [[NSMutableString alloc] initWithString:@"{"]; + for (NSString *key in dictionary) { + NSString *value = nil; + BOOL string = YES; + + id obj = dictionary[key]; + if ([obj isKindOfClass:[NSString class]]) { + value = [BNCEncodingUtils sanitizedStringFromString:obj]; + } + else if ([obj isKindOfClass:[NSURL class]]) { + value = [obj absoluteString]; + } + else if ([obj isKindOfClass:[NSDate class]]) { + value = [BNCEncodingUtils iso8601StringFromDate:obj]; + } + else if ([obj isKindOfClass:[NSArray class]]) { + value = [BNCEncodingUtils encodeArrayToJsonString:obj]; + string = NO; + } + else if ([obj isKindOfClass:[NSDictionary class]] || [obj isKindOfClass:[NSMutableDictionary class]]) { + value = [BNCEncodingUtils encodeDictionaryToJsonString:obj]; + string = NO; + } + else if ([obj isKindOfClass:[NSNumber class]]) { + string = NO; + if (obj == (id)kCFBooleanFalse) + value = @"false"; + else + if (obj == (id)kCFBooleanTrue) + value = @"true"; + else + value = [obj stringValue]; + } + else if ([obj isKindOfClass:[NSNull class]]) { + value = @"null"; + string = NO; + } + else { + // If this type is not a known type, don't attempt to encode it. + BNCLogError(@"Cannot encode value for key %@. The value is not an accepted type.", key); + continue; + } + + [encodedDictionary appendFormat:@"\"%@\":", [BNCEncodingUtils sanitizedStringFromString:key]]; + + // If this is a "string" object, wrap it in quotes + if (string) { + [encodedDictionary appendFormat:@"\"%@\",", value]; + } + // Otherwise, just add the raw value after the colon + else { + [encodedDictionary appendFormat:@"%@,", value]; + } + } + + if (encodedDictionary.length > 1) { + [encodedDictionary deleteCharactersInRange:NSMakeRange([encodedDictionary length] - 1, 1)]; + } + + [encodedDictionary appendString:@"}"]; + + BNCLogDebugSDK(@"Encoded dictionary: %@.", encodedDictionary); + return encodedDictionary; +} + ++ (NSString *)encodeArrayToJsonString:(NSArray *)array { + // Empty array + if (![array count]) { + return @"[]"; + } + + NSMutableString *encodedArray = [[NSMutableString alloc] initWithString:@"["]; + for (id obj in array) { + NSString *value = nil; + BOOL string = YES; + + if ([obj isKindOfClass:[NSString class]]) { + value = [BNCEncodingUtils sanitizedStringFromString:obj]; + } + else if ([obj isKindOfClass:[NSURL class]]) { + value = [obj absoluteString]; + } + else if ([obj isKindOfClass:[NSDate class]]) { + value = [BNCEncodingUtils iso8601StringFromDate:obj]; + } + else if ([obj isKindOfClass:[NSArray class]]) { + value = [BNCEncodingUtils encodeArrayToJsonString:obj]; + string = NO; + } + else if ([obj isKindOfClass:[NSDictionary class]] || [obj isKindOfClass:[NSMutableDictionary class]]) { + value = [BNCEncodingUtils encodeDictionaryToJsonString:obj]; + string = NO; + } + else if ([obj isKindOfClass:[NSNumber class]]) { + value = [obj stringValue]; + string = NO; + } + else if ([obj isKindOfClass:[NSNull class]]) { + value = @"null"; + string = NO; + } + else { + // If this type is not a known type, don't attempt to encode it. + BNCLogError(@"Cannot encode value %@. The value is not an accepted type.", obj); + continue; + } + + // If this is a "string" object, wrap it in quotes + if (string) { + [encodedArray appendFormat:@"\"%@\",", value]; + } + // Otherwise, just add the raw value after the colon + else { + [encodedArray appendFormat:@"%@,", value]; + } + } + + // Delete the trailing comma + [encodedArray deleteCharactersInRange:NSMakeRange([encodedArray length] - 1, 1)]; + [encodedArray appendString:@"]"]; + + BNCLogDebugSDK(@"Encoded array: %@.", encodedArray); + return encodedArray; +} + ++ (NSString *)urlEncodedString:(NSString *)string { + NSMutableCharacterSet *charSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy]; + [charSet removeCharactersInString:@"!*'\"();:@&=+$,/?%#[]% "]; + return [string stringByAddingPercentEncodingWithAllowedCharacters:charSet]; +} + ++ (NSString *)encodeDictionaryToQueryString:(NSDictionary *)dictionary { + NSMutableString *queryString = [[NSMutableString alloc] initWithString:@"?"]; + + for (NSString *key in [dictionary allKeys]) { + // No empty keys, please. + if (key.length) { + id obj = dictionary[key]; + NSString *value; + + if ([obj isKindOfClass:[NSString class]]) { + value = [BNCEncodingUtils urlEncodedString:obj]; + } + else if ([obj isKindOfClass:[NSURL class]]) { + value = [BNCEncodingUtils urlEncodedString:[obj absoluteString]]; + } + else if ([obj isKindOfClass:[NSDate class]]) { + value = [BNCEncodingUtils iso8601StringFromDate:obj]; + } + else if ([obj isKindOfClass:[NSNumber class]]) { + value = [obj stringValue]; + } + else { + // If this type is not a known type, don't attempt to encode it. + BNCLogError(@"Cannot encode value %@. The value is not an accepted type.", obj); + continue; + } + + [queryString appendFormat:@"%@=%@&", [BNCEncodingUtils urlEncodedString:key], value]; + } + } + + // Delete last character (either trailing & or ? if no params present) + [queryString deleteCharactersInRange:NSMakeRange(queryString.length - 1, 1)]; + + return queryString; +} + ++ (NSString*) stringByPercentDecodingString:(NSString *)string { + return [string stringByRemovingPercentEncoding]; +} + ++ (NSString*) stringByPercentEncodingStringForQuery:(NSString *)string { + return [string stringByAddingPercentEncodingWithAllowedCharacters: + [NSCharacterSet URLQueryAllowedCharacterSet]]; +} + +#pragma mark - Param Decoding Methods + ++ (NSDictionary *)decodeJsonDataToDictionary:(NSData *)jsonData { + NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; + + return [BNCEncodingUtils decodeJsonStringToDictionary:jsonString]; +} + ++ (NSDictionary *)decodeJsonStringToDictionary:(NSString *)jsonString { + // Just a basic decode, easy enough + NSData *tempData = [jsonString dataUsingEncoding:NSUTF8StringEncoding]; + if (!tempData) { + return @{}; + } + + NSDictionary *plainDecodedDictionary = [NSJSONSerialization JSONObjectWithData:tempData options:NSJSONReadingMutableContainers error:nil]; + if (plainDecodedDictionary) { + return plainDecodedDictionary; + } + + // If the first decode failed, it could be because the data was encoded. Try decoding first. + NSString *decodedVersion = [BNCEncodingUtils base64DecodeStringToString:jsonString]; + tempData = [decodedVersion dataUsingEncoding:NSUTF8StringEncoding]; + if (!tempData) { + return @{}; + } + + NSDictionary *base64DecodedDictionary = [NSJSONSerialization JSONObjectWithData:tempData options:NSJSONReadingMutableContainers error:nil]; + if (base64DecodedDictionary) { + return base64DecodedDictionary; + } + + // Apparently this data was not parsible into a dictionary, so we'll just return an empty one + return @{}; +} + ++ (NSDictionary *)decodeQueryStringToDictionary:(NSString *)queryString { + NSArray *pairs = [queryString componentsSeparatedByString:@"&"]; + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + + for (NSString *pair in pairs) { + NSArray *kv = [pair componentsSeparatedByString:@"="]; + if (kv.count > 1) { // If this key has a value (so, not foo&bar=...) + NSString *key = kv[0]; + NSString *val; + + //Pre iOS 7, stringByReplacingPercentEscapesUsingEncoding was deprecated in iOS 9 + if (NSFoundationVersionNumber < NSFoundationVersionNumber_iOS_7_0) { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + val = [kv[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + #pragma clang diagnostic pop + + } else { //iOS 7 and later + val = [kv[1] stringByRemovingPercentEncoding]; // uses the default UTF-8 encoding, introduced in iOS 7 + } + + // Don't add empty items + if (val.length) { + params[key] = val; + } + } + } + + return params; +} + +#pragma mark - Hex Strings + ++ (NSString *) hexStringFromData:(NSData*)data { + + NSUInteger bytesCount = data.length; + if (bytesCount <= 0) return @""; + + const char *hexChars = "0123456789ABCDEF"; + const char *dataBuffer = data.bytes; + char *chars = malloc(sizeof(char) * (bytesCount * 2 + 1)); + if (!chars) return @""; + char *s = chars; + for (unsigned i = 0; i < bytesCount; ++i) { + *s++ = hexChars[((*dataBuffer & 0xF0) >> 4)]; + *s++ = hexChars[(*dataBuffer & 0x0F)]; + dataBuffer++; + } + *s = '\0'; + + NSString *hexString = [NSString stringWithUTF8String:chars]; + if (chars) free(chars); + return hexString; +} + ++ (NSData *) dataFromHexString:(NSString*)string { + if (!string) return nil; + + NSData *data = nil; + NSData *inputData = [string dataUsingEncoding:NSUTF8StringEncoding]; + + size_t length = (inputData.length+1)/2; + uint8_t *bytes = malloc(length); + uint8_t *b = bytes; + if (!bytes) goto exit; + + int highValue = -1; + const uint8_t *p = (const uint8_t*) [inputData bytes]; + for (NSUInteger i = 0; i < inputData.length; ++i) { + int value; + if (*p >= '0' && *p <= '9') + value = *p - '0'; + else + if (*p >= 'A' && *p <= 'F') + value = *p - 'A' + 10; + else + if (*p >= 'a' && *p <= 'f') + value = *p - 'a' + 10; + else + if (isspace(*p)) { + p++; + continue; + } else { + goto exit; // Invalid character. + } + + if (highValue == -1) { + highValue = value; + } else { + *b++ = (highValue << 4) | value; + highValue = -1; + } + p++; + } + + // If highValue != -1 then we got an odd number of hex values, which is an error. + if (highValue == -1) + data = [NSData dataWithBytes:bytes length:b-bytes]; + +exit: + if (bytes) { + BNCLogAssert((size_t)(b-bytes)<=length); + free(bytes); + } + return data; +} + +#pragma mark - URL QueryItems + ++ (NSArray*) queryItems:(NSURL*)URL { + NSMutableArray* keyValues = [NSMutableArray new]; + if (!URL) return keyValues; + + NSArray *queryItems = [[URL query] componentsSeparatedByString:@"&"]; + for (NSString* itemPair in queryItems) { + + BNCKeyValue *keyValue = [BNCKeyValue new]; + NSRange range = [itemPair rangeOfString:@"="]; + if (range.location == NSNotFound) { + if (itemPair.length) + keyValue.key = itemPair; + } else { + keyValue.key = [itemPair substringWithRange:NSMakeRange(0, range.location)]; + NSRange r = NSMakeRange(range.location+1, itemPair.length-range.location-1); + if (r.length > 0) + keyValue.value = [itemPair substringWithRange:r]; + } + + keyValue.key = [BNCEncodingUtils stringByPercentDecodingString:keyValue.key]; + keyValue.key = [keyValue.key stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + keyValue.value = [BNCEncodingUtils stringByPercentDecodingString:keyValue.value]; + keyValue.value = [keyValue.value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + + if (keyValue.key.length || keyValue.value.length) { + if (keyValue.key == nil) keyValue.key = @""; + if (keyValue.value == nil) keyValue.value = @""; + [keyValues addObject:keyValue]; + } + } + + return keyValues; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCError.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCError.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCError.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCError.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCError.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCError.m new file mode 100644 index 00000000000..703f1302f75 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCError.m @@ -0,0 +1,126 @@ +/** + @file BNCError.m + @package Branch-SDK + @brief Branch errors. + + @author Qinwei Gong + @date November 2014 + @copyright Copyright © 2014 Branch. All rights reserved. +*/ + +#import "BNCError.h" +#import "BNCLocalization.h" + +NSString * const BNCErrorDomain = @"io.branch.sdk.error"; + +__attribute__((constructor)) void BNCForceNSErrorCategoryToLoad() { + // Nothing here, but forces linker to load the category. +} + +@implementation NSError (Branch) + ++ (NSString*) messageForCode:(BNCErrorCode)code { + + // The order is important! + + static NSString* const messages[] = { + + // BNCInitError + @"The Branch user session has not been initialized.", + + // BNCDuplicateResourceError + @"A resource with this identifier already exists.", + + // BNCRedeemCreditsError + @"You're trying to redeem more credits than are available. Have you loaded rewards?", + + // BNCBadRequestError + @"The network request was invalid.", + + // BNCServerProblemError + @"Trouble reaching the Branch servers, please try again shortly.", + + // BNCNilLogError + @"Can't log error messages because the logger is set to nil.", + + // BNCVersionError + @"Incompatible version.", + + // BNCNetworkServiceInterfaceError + @"The underlying network service does not conform to the BNCNetworkOperationProtocol.", + + // BNCInvalidNetworkPublicKeyError + @"Public key is not an SecKeyRef type.", + + // BNCContentIdentifierError + @"A canonical identifier or title are required to uniquely identify content.", + + // BNCSpotlightNotAvailableError + @"The Core Spotlight indexing service is not available on this device.", + + // BNCSpotlightTitleError + @"Spotlight indexing requires a title.", + + // BNCRedeemZeroCreditsError + @"Can't redeem zero credits.", + + // BNCSpotlightIdentifierError + @"The Spotlight identifier is required to remove indexing from spotlight.", + + //BNCSpotlightPublicIndexError + @"Spotlight cannot remove publicly indexed content.", + + //BNCTrackingDisabledError + @"User tracking is disabled." + }; + + #define _countof(array) (sizeof(array)/sizeof(array[0])) + + // Sanity check + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wunreachable-code" + if (_countof(messages) != (BNCHighestError - BNCInitError)) { + [NSException raise:NSInternalInconsistencyException format:@"Branch error message count is wrong."]; + return @"Branch encountered an error."; + } + #pragma clang diagnostic pop + + if (code < BNCInitError || code >= BNCHighestError) + return @"Branch encountered an error."; + + return messages[code - BNCInitError]; +} + ++ (NSError*_Nonnull) branchErrorWithCode:(BNCErrorCode)errorCode + error:(NSError*)error + localizedMessage:(NSString*_Nullable)message { + + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + + NSString *localizedString = BNCLocalizedString([self messageForCode:errorCode]); + if (localizedString) userInfo[NSLocalizedDescriptionKey] = localizedString; + if (message) { + userInfo[NSLocalizedFailureReasonErrorKey] = message; + } + if (error) { + userInfo[NSUnderlyingErrorKey] = error; + if (!userInfo[NSLocalizedFailureReasonErrorKey] && error.localizedDescription) + userInfo[NSLocalizedFailureReasonErrorKey] = error.localizedDescription; + } + + return [NSError errorWithDomain:BNCErrorDomain code:errorCode userInfo:userInfo]; +} + ++ (NSError*_Nonnull) branchErrorWithCode:(BNCErrorCode)errorCode { + return [NSError branchErrorWithCode:errorCode error:nil localizedMessage:nil]; +} + ++ (NSError*_Nonnull) branchErrorWithCode:(BNCErrorCode)errorCode error:(NSError*_Nullable)error { + return [NSError branchErrorWithCode:errorCode error:error localizedMessage:nil]; +} + ++ (NSError*_Nonnull) branchErrorWithCode:(BNCErrorCode)errorCode localizedMessage:(NSString*_Nullable)message { + return [NSError branchErrorWithCode:errorCode error:nil localizedMessage:message]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFabricAnswers.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFabricAnswers.h new file mode 100644 index 00000000000..c1e88e3305f --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFabricAnswers.h @@ -0,0 +1,20 @@ +// +// BNCFabricAnswers.h +// Branch-TestBed +// +// Created by Ahmed Nawar on 6/2/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +@interface BNCFabricAnswers : NSObject + ++ (NSDictionary*) branchConfigurationDictionary; ++ (void)sendEventWithName:(NSString*)name andAttributes:(NSDictionary*)attributes; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFabricAnswers.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFabricAnswers.m new file mode 100644 index 00000000000..63076828535 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFabricAnswers.m @@ -0,0 +1,71 @@ +// +// BNCFabricAnswers.m +// Branch-TestBed +// +// Created by Ahmed Nawar on 6/2/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import "Branch.h" +#import "BNCFabricAnswers.h" +#import "BNCPreferenceHelper.h" +#import "../Fabric/Answers.h" +#import "../Fabric/Fabric+FABKits.h" + +@implementation BNCFabricAnswers + ++ (void)sendEventWithName:(NSString *)name andAttributes:(NSDictionary *)attributes { + ANSLogCustomEvent(name, [self prepareBranchDataForAnswers:attributes]); +} + ++ (NSDictionary *)prepareBranchDataForAnswers:(NSDictionary *)dictionary { + NSMutableDictionary *temp = [[NSMutableDictionary alloc] init]; + + for (NSString *key in dictionary.allKeys) { + if ([key hasPrefix:@"+"] || ([key hasPrefix:@"$"] && ![key isEqualToString:@"$identity_id"])) { + // ignore because this data is not found on the ShareSheet + continue; + } else if ([dictionary[key] isKindOfClass:[NSArray class]]) { + // flatten arrays, special treatement for ~tags + NSString *aKey; + if ([key hasPrefix:@"~"]) + aKey = [key substringFromIndex:1]; + else + aKey = key; + NSArray *valuesArray = dictionary[key]; + for (NSUInteger i = 0; i < valuesArray.count; ++i) { + temp[[NSString stringWithFormat:@"%@.%lu", aKey, (unsigned long)i]] = valuesArray[i]; + } + } else if ([key hasPrefix:@"~"]) { + // strip tildes ~ + temp[[key substringFromIndex:1]] = dictionary[key]; + } else if ([key isEqualToString:@"$identity_id"]) { + temp[@"referring_branch_identity"] = dictionary[key]; + } + } + + [[BNCPreferenceHelper preferenceHelper] synchronize]; + NSString *identity = [[BNCPreferenceHelper preferenceHelper].identityID copy]; + if (identity) { + temp[@"branch_identity"] = identity; + } + + return temp; +} + ++ (NSDictionary*) branchConfigurationDictionary { + Class fabric = NSClassFromString(@"Fabric"); + if ([fabric respondsToSelector:@selector(configurationDictionaryForKitClass:)]) { + + // The name of this key was specified in the account-creation API integration + NSString * const BNC_BRANCH_FABRIC_APP_KEY_KEY = @"branch_key"; + + NSDictionary *configDictionary = [fabric configurationDictionaryForKitClass:[Branch class]]; + NSDictionary *dictionary = [configDictionary objectForKey:BNC_BRANCH_FABRIC_APP_KEY_KEY]; + + return dictionary; + } + return nil; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFieldDefines.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFieldDefines.h new file mode 100644 index 00000000000..50f7eea8514 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCFieldDefines.h @@ -0,0 +1,186 @@ +// +// BNCFieldDefines.h +// Branch-TestBed +// +// Created by edward on 8/17/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#if defined(addString) // -------------------------------------------------------------------------- + + // Already defined so undefine them: + + #undef addString + #undef addDate + #undef addDouble + #undef addBoolean + #undef addDecimal + #undef addNumber + #undef addInteger + #undef addStringifiedDictionary + #undef addStringArray + #undef addDictionary + #undef BNCFieldDefinesObjectFromDictionary + #undef BNCFieldDefinesDictionaryFromSelf + +#elif defined(BNCFieldDefinesObjectFromDictionary) // ---------------------------------------------- + + #define addString(field, name) { \ + NSString *string = dictionary[@#name]; \ + if ([string isKindOfClass:[NSString class]]) { \ + object.field = string; \ + } \ + } + + #define addDate(field, name) { \ + NSNumber *number = dictionary[@#name]; \ + if ([number isKindOfClass:[NSNumber class]] || \ + [number isKindOfClass:[NSString class]]) { \ + NSTimeInterval t = [number doubleValue]; \ + if (t) object.field = [NSDate dateWithTimeIntervalSince1970:t/1000.0]; \ + } \ + } + + #define addDouble(field, name) { \ + NSNumber *number = dictionary[@#name]; \ + if ([number isKindOfClass:[NSNumber class]] || \ + [number isKindOfClass:[NSString class]]) { \ + object.field = [number doubleValue]; \ + } \ + } + + #define addBoolean(field, name) { \ + NSNumber *number = dictionary[@#name]; \ + if ([number isKindOfClass:[NSNumber class]] || \ + [number isKindOfClass:[NSString class]]) { \ + object.field = [number boolValue]; \ + } \ + } + + #define addDecimal(field, name) { \ + NSString *string = dictionary[@#name]; \ + if ([string isKindOfClass:[NSNumber class]]) \ + string = [string description]; \ + if ([string isKindOfClass:[NSString class]]) { \ + object.field = [NSDecimalNumber decimalNumberWithString:string]; \ + } \ + } + + #define addNumber(field, name) { \ + NSNumber *number = dictionary[@#name]; \ + if ([number isKindOfClass:[NSString class]]) \ + number = [number doubleValue]; \ + if ([number isKindOfClass:[NSNumber class]]) { \ + object.field = number; \ + } \ + } + + #define addInteger(field, name) { \ + NSNumber *number = dictionary[@#name]; \ + if ([number isKindOfClass:[NSNumber class]] || \ + [number isKindOfClass:[NSString class]]) { \ + object.field = [number integerValue]; \ + } \ + } + + #define addStringifiedDictionary(field, name) { \ + NSString *string = dictionary[@#name]; \ + if ([string isKindOfClass:[NSString class]]) { \ + NSDictionary *d = [BNCEncodingUtils decodeJsonStringToDictionary:string]; \ + object.field = [NSMutableDictionary dictionaryWithDictionary:d]; \ + } \ + } + + #define addStringArray(field, name) { \ + NSArray *a = dictionary[@#name]; \ + if ([a isKindOfClass:[NSArray class]]) { \ + NSMutableArray *newArray = [NSMutableArray array]; \ + for (NSString *s in a) { \ + if ([s isKindOfClass:[NSString class]]) { \ + [newArray addObject:s]; \ + } \ + } \ + object.field = newArray; \ + } else if ([a isKindOfClass:[NSString class]]) { \ + object.field = [NSMutableArray arrayWithObject:a]; \ + } else { \ + object.field = (id) [NSMutableArray new]; \ + } \ + } + + #define addDictionary(field, name) { \ + NSDictionary *d = dictionary[@#name]; \ + if ([d isKindOfClass:[NSDictionary class]]) { \ + object.field = [NSMutableDictionary dictionaryWithDictionary:d]; \ + } \ + } + +#elif defined(BNCFieldDefinesDictionaryFromSelf) // ---------------------------------------------- + + #define addString(field, name) { \ + if (self.field.length) { \ + dictionary[@#name] = self.field; \ + } \ + } + + #define addDate(field, name) { \ + if (self.field) { \ + NSTimeInterval t = self.field.timeIntervalSince1970; \ + dictionary[@#name] = [NSNumber numberWithLongLong:(long long)(t*1000.0)]; \ + } \ + } + + #define addDouble(field, name) { \ + if (self.field != 0.0) { \ + dictionary[@#name] = [NSNumber numberWithDouble:self.field]; \ + } \ + } + + #define addBoolean(field, name) { \ + if (self.field) { \ + dictionary[@#name] = (__bridge NSNumber*) kCFBooleanTrue; \ + } \ + } + + #define addDecimal(field, name) { \ + if (self.field) { \ + dictionary[@#name] = self.field; \ + } \ + } + + #define addNumber(field, name) { \ + if (self.field != nil) { \ + dictionary[@#name] = self.field; \ + } \ + } + + #define addInteger(field, name) { \ + if (self.field != 0) { \ + dictionary[@#name] = [NSNumber numberWithInteger:self.field]; \ + } \ + } + + #define addStringifiedDictionary(field, name) { \ + if (self.field.count) { \ + NSString *string = [BNCEncodingUtils encodeDictionaryToJsonString:self.field]; \ + dictionary[@#name] = string; \ + } \ + } + + #define addStringArray(field, name) { \ + if (self.field.count) { \ + dictionary[@#name] = self.field; \ + } \ + } + + #define addDictionary(field, name) { \ + if (self.field.count) { \ + dictionary[@#name] = self.field; \ + } \ + } + +#else + +//#error "Define either 'BNCFieldDefinesDictionaryFromObject' or 'BNCFieldDefinesObjectFromDictionary'. + +#endif diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCKeyChain.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCKeyChain.h new file mode 100644 index 00000000000..fe14d76d133 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCKeyChain.h @@ -0,0 +1,64 @@ +/** + @file BNCKeyChain.h + @package Branch-SDK + @brief Simple access routines for secure keychain storage. + + @author Edward Smith + @date January 8, 2018 + @copyright Copyright © 2018 Branch. All rights reserved. +*/ + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +@interface BNCKeyChain : NSObject + +/** + @brief Remove a value for a service and key. Optionally removes all keys and values for a service. + + @param service The name of the service under which to store the key. + @param key The key to remove the value from. If `nil` is passed, all keys and values are removed for that service. + @return Returns an `NSError` if an error occurs. +*/ ++ (NSError*_Nullable) removeValuesForService:(NSString*_Nullable)service + key:(NSString*_Nullable)key; + +/** + @brief Returns a value for the passed service and key. + + @param service The name of the service that the value is stored under. + @param key The key that the value is stored under. + @param error If an error occurs, and `error` is a pointer to an error pointer, the error is returned here. + @return Returns the value stored under `service` and `key`, or `nil` if none found. +*/ ++ (id _Nullable) retrieveValueForService:(NSString*_Nonnull)service + key:(NSString*_Nonnull)key + error:(NSError*_Nullable __autoreleasing *_Nullable)error; + +/** + @brief Returns an array of all items found in the keychain for an app. + + @param error If an error occurs, the error is returned in `error` if it is not `NULL`. + @return Returns an array of the items stored in the keychain or `nil`. +*/ ++ (NSArray*_Nullable) retieveAllValuesWithError:(NSError*_Nullable __autoreleasing *_Nullable)error; + +/** + @brief Stores an item in the keychain. + + @param service The service name to store the item under. + @param key The key to store the item under. + @param accessGroup The iCloud security access group for sharing the item. Specify `nil` if item should not be shared. + @return Returns an error if an error occurs. + */ ++ (NSError*_Nullable) storeValue:(id _Nonnull)value + forService:(NSString*_Nonnull)service + key:(NSString*_Nonnull)key + cloudAccessGroup:(NSString*_Nullable)accessGroup; + +/// The default security access group for the app. ++ (NSString*_Nullable) securityAccessGroup; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCKeyChain.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCKeyChain.m new file mode 100644 index 00000000000..44a3f651887 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCKeyChain.m @@ -0,0 +1,250 @@ +/** + @file BNCKeyChain.m + @package Branch-SDK + @brief Simple access routines for secure keychain storage. + + @author Edward Smith + @date January 8, 2018 + @copyright Copyright © 2018 Branch. All rights reserved. +*/ + +#import "BNCKeyChain.h" +#import "BNCLog.h" + +// Apple Keychain Reference: +// https://developer.apple.com/library/content/documentation/Conceptual/ +// keychainServConcepts/02concepts/concepts.html#//apple_ref/doc/uid/TP30000897-CH204-SW1 +// +// To translate security errors to text from the command line use: `security error -34018` + +#pragma mark SecCopyErrorMessageString + +#if TARGET_OS_OSX + +//#pragma clang link undefined _SecCopyErrorMessageString // -Wl,-U,_SecCopyErrorMessageString +extern CFStringRef SecCopyErrorMessageString(OSStatus status, void *reserved) + __attribute__((weak_import)); + +#else + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" +CFStringRef SecCopyErrorMessageString(OSStatus status, void *reserved) { + return CFSTR("Sec OSStatus error."); +} +#pragma clang diagnostic pop + +#endif + +#pragma mark - BNCKeyChain + +@implementation BNCKeyChain + ++ (NSError*) errorWithKey:(NSString*)key OSStatus:(OSStatus)status { + // Security errors are defined in Security/SecBase.h + if (status == errSecSuccess) return nil; + NSString *reason = nil; + NSString *description = + [NSString stringWithFormat:@"Security error with key '%@': code %ld.", key, (long) status]; + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wtautological-compare" + #pragma clang diagnostic ignored "-Wpartial-availability" + if (SecCopyErrorMessageString != NULL) + reason = (__bridge_transfer NSString*) SecCopyErrorMessageString(status, NULL); + #pragma clang diagnostic pop + + if (!reason) + reason = @"Sec OSStatus error."; + + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:@{ + NSLocalizedDescriptionKey: description, + NSLocalizedFailureReasonErrorKey: reason + }]; + return error; +} + ++ (NSArray*) retieveAllValuesWithError:(NSError**)error { + if (error) *error = nil; + NSDictionary* dictionary = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue, + (__bridge id)kSecAttrSynchronizable: (__bridge id)kSecAttrSynchronizableAny, + (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll + }; + CFTypeRef valueData = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dictionary, &valueData); + if (status == errSecItemNotFound) status = 0; + if (status) { + NSError *localError = [self errorWithKey:@"" OSStatus:status]; + BNCLogDebugSDK(@"Can't retrieve key: %@.", localError); + if (error) *error = localError; + if (valueData) CFRelease(valueData); + return nil; + } + NSMutableArray *array = nil; + if ([((__bridge NSArray*)valueData) isKindOfClass:[NSArray class]]) { + NSArray *dataArray = (__bridge NSArray*) valueData; + array = [NSMutableArray new]; + for (NSData* data in dataArray) { + id value = nil; + @try { + value = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + } + @catch (id) { + value = nil; + } + if (value) + [array addObject:value]; + else + if (data) + [array addObject:data]; + } + } + if (valueData) + CFRelease(valueData); + return array; +} + ++ (id) retrieveValueForService:(NSString*)service key:(NSString*)key error:(NSError**)error { + if (error) *error = nil; + if (service == nil || key == nil) { + NSError *localError = [self errorWithKey:key OSStatus:errSecParam]; + if (error) *error = localError; + return nil; + } + + NSDictionary* dictionary = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService: service, + (__bridge id)kSecAttrAccount: key, + (__bridge id)kSecReturnData: (__bridge id)kCFBooleanTrue, + (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne, + (__bridge id)kSecAttrSynchronizable: (__bridge id)kSecAttrSynchronizableAny + }; + CFDataRef valueData = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dictionary, (CFTypeRef *)&valueData); + if (status != errSecSuccess) { + NSError *localError = [self errorWithKey:key OSStatus:status]; + BNCLogDebugSDK(@"Can't retrieve key: %@.", localError); + if (error) *error = localError; + if (valueData) CFRelease(valueData); + return nil; + } + id value = nil; + if (valueData) { + @try { + value = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData*)valueData]; + } + @catch (id) { + value = nil; + NSError *localError = [self errorWithKey:key OSStatus:errSecDecode]; + if (error) *error = localError; + } + CFRelease(valueData); + } + return value; +} + ++ (NSError*) storeValue:(id)value + forService:(NSString*)service + key:(NSString*)key + cloudAccessGroup:(NSString*)accessGroup { + + if (value == nil || service == nil || key == nil) + return [self errorWithKey:key OSStatus:errSecParam]; + + NSData* valueData = nil; + @try { + valueData = [NSKeyedArchiver archivedDataWithRootObject:value]; + } + @catch(id) { + valueData = nil; + } + if (!valueData) { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSPropertyListWriteStreamError userInfo:nil]; + return error; + } + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithDictionary:@{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService: service, + (__bridge id)kSecAttrAccount: key, + (__bridge id)kSecAttrSynchronizable: (__bridge id)kSecAttrSynchronizableAny + }]; + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dictionary); + if (status != errSecSuccess && status != errSecItemNotFound) { + NSError *error = [self errorWithKey:key OSStatus:status]; + BNCLogDebugSDK(@"Can't clear to store key: %@.", error); + } + + dictionary[(__bridge id)kSecValueData] = valueData; + dictionary[(__bridge id)kSecAttrIsInvisible] = (__bridge id)kCFBooleanTrue; + dictionary[(__bridge id)kSecAttrAccessible] = (__bridge id)kSecAttrAccessibleAfterFirstUnlock; + + if (accessGroup.length) { + dictionary[(__bridge id)kSecAttrAccessGroup] = accessGroup; + dictionary[(__bridge id)kSecAttrSynchronizable] = (__bridge id) kCFBooleanTrue; + } else { + dictionary[(__bridge id)kSecAttrSynchronizable] = (__bridge id) kCFBooleanFalse; + } + status = SecItemAdd((__bridge CFDictionaryRef)dictionary, NULL); + if (status) { + NSError *error = [self errorWithKey:key OSStatus:status]; + BNCLogDebugSDK(@"Can't store key: %@.", error); + return error; + } + return nil; +} + ++ (NSError*) removeValuesForService:(NSString*)service key:(NSString*)key { + NSMutableDictionary* dictionary = [NSMutableDictionary dictionaryWithDictionary:@{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrSynchronizable: (__bridge id)kSecAttrSynchronizableAny + }]; + if (service) dictionary[(__bridge id)kSecAttrService] = service; + if (key) dictionary[(__bridge id)kSecAttrAccount] = key; + + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)dictionary); + if (status == errSecItemNotFound) status = errSecSuccess; + if (status) { + NSError *error = [self errorWithKey:key OSStatus:status]; + BNCLogDebugSDK(@"Can't remove key: %@.", error); + return error; + } + return nil; +} + ++ (NSString*_Nullable) securityAccessGroup { + // https://stackoverflow.com/questions/11726672/access-app-identifier-prefix-programmatically + @synchronized(self) { + static NSString*_securityAccessGroup = nil; + if (_securityAccessGroup) return _securityAccessGroup; + + // First store a value: + NSError*error = [self storeValue:@"Value" forService:@"BranchKeychainService" key:@"Temp" cloudAccessGroup:nil]; + if (error) BNCLogDebugSDK(@"Error storing temp value: %@.", error); + + NSDictionary* dictionary = @{ + (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword, + (__bridge id)kSecReturnAttributes: (__bridge id)kCFBooleanTrue, + (__bridge id)kSecAttrSynchronizable: (__bridge id)kSecAttrSynchronizableAny, + (__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitOne + }; + CFDictionaryRef resultDictionary = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)dictionary, (CFTypeRef*)&resultDictionary); + if (status == errSecItemNotFound) return nil; + if (status != errSecSuccess) { + BNCLogDebugSDK(@"Get securityAccessGroup returned(%ld): %@.", + (long) status, [self errorWithKey:nil OSStatus:status]); + return nil; + } + NSString*group = + [(__bridge NSDictionary *)resultDictionary objectForKey:(__bridge NSString *)kSecAttrAccessGroup]; + if (group.length > 0) _securityAccessGroup = [group copy]; + CFRelease(resultDictionary); + return _securityAccessGroup; + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCLinkCache.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkCache.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCLinkCache.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkCache.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkCache.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkCache.m new file mode 100644 index 00000000000..252b5781979 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkCache.m @@ -0,0 +1,45 @@ +// +// BNCLinkCache.m +// Branch-SDK +// +// Created by Qinwei Gong on 1/23/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + + +#import "BNCLinkCache.h" + + +@interface BNCLinkCache () +@property (nonatomic, strong) NSMutableDictionary *cache; +@end + + +@implementation BNCLinkCache + +- (id)init { + if ((self = [super init])) { + self.cache = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)setObject:(NSString *)anObject forKey:(BNCLinkData *)aKey { + @synchronized (self) { + self.cache[@([aKey hash])] = anObject; + } +} + +- (NSString *)objectForKey:(BNCLinkData *)aKey { + @synchronized (self) { + return self.cache[@([aKey hash])]; + } +} + +- (void) clear { + @synchronized (self) { + [self.cache removeAllObjects]; + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCLinkData.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkData.h similarity index 94% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCLinkData.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkData.h index d2488702c80..c092409bc33 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCLinkData.h +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkData.h @@ -17,7 +17,7 @@ typedef NS_ENUM(NSUInteger, BranchLinkType) { BranchLinkTypeOneTimeUse = 1 }; -@interface BNCLinkData : NSObject +@interface BNCLinkData : NSObject @property (strong, nonatomic) NSMutableDictionary *data; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkData.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkData.m new file mode 100644 index 00000000000..0a37194cdcf --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLinkData.m @@ -0,0 +1,193 @@ +// +// BNCLinkData.m +// Branch-SDK +// +// Created by Qinwei Gong on 1/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + + +#import "BNCLinkData.h" +#import "BNCEncodingUtils.h" +#import "BranchConstants.h" + + +@interface BNCLinkData () +@property (strong, nonatomic) NSArray *tags; +@property (strong, nonatomic) NSString *alias; +@property (strong, nonatomic) NSString *channel; +@property (strong, nonatomic) NSString *feature; +@property (strong, nonatomic) NSString *stage; +@property (strong, nonatomic) NSString *campaign; +@property (strong, nonatomic) NSDictionary *params; +@property (strong, nonatomic) NSString *ignoreUAString; +@property (assign, nonatomic) BranchLinkType type; +@property (assign, nonatomic) NSUInteger duration; +@end + + +@implementation BNCLinkData + +- (id)init { + if ((self = [super init])) { + self.data = [[NSMutableDictionary alloc] init]; + self.data[@"source"] = @"ios"; + } + return self; +} + +- (void)setupTags:(NSArray *)tags { + if (tags) { + _tags = tags; + + self.data[BRANCH_REQUEST_KEY_URL_TAGS] = tags; + } +} + +- (void)setupAlias:(NSString *)alias { + if (alias) { + _alias = alias; + + self.data[BRANCH_REQUEST_KEY_URL_ALIAS] = alias; + } +} + +- (void)setupType:(BranchLinkType)type { + if (type) { + _type = type; + + self.data[BRANCH_REQUEST_KEY_URL_LINK_TYPE] = @(type); + } +} + +- (void)setupMatchDuration:(NSUInteger)duration { + if (duration > 0) { + _duration = duration; + + self.data[BRANCH_REQUEST_KEY_URL_DURATION] = @(duration); + } +} + +- (void)setupChannel:(NSString *)channel { + if (channel) { + _channel = channel; + + self.data[BRANCH_REQUEST_KEY_URL_CHANNEL] = channel; + } +} + +- (void)setupFeature:(NSString *)feature { + if (feature) { + _feature = feature; + + self.data[BRANCH_REQUEST_KEY_URL_FEATURE] = feature; + } +} + +- (void)setupStage:(NSString *)stage { + if (stage) { + _stage = stage; + + self.data[BRANCH_REQUEST_KEY_URL_STAGE] = stage; + } +} + +- (void)setupCampaign:(NSString *)campaign { + if (campaign) { + _campaign = campaign; + + self.data[BRANCH_REQUEST_KEY_URL_CAMPAIGN] = campaign; + } +} + +- (void)setupIgnoreUAString:(NSString *)ignoreUAString { + if (ignoreUAString) { + _ignoreUAString = ignoreUAString; + + self.data[BRANCH_REQUEST_KEY_URL_IGNORE_UA_STRING] = ignoreUAString; + } +} + +- (void)setupParams:(NSDictionary *)params { + if (params) { + _params = params; + + self.data[BRANCH_REQUEST_KEY_URL_DATA] = params; + } +} + +- (NSUInteger)hash { + NSUInteger result = 1; + NSUInteger prime = 19; + + NSString *encodedParams = [BNCEncodingUtils encodeDictionaryToJsonString:self.params]; + result = prime * result + self.type; + result = prime * result + [[BNCEncodingUtils md5Encode:self.alias] hash]; + result = prime * result + [[BNCEncodingUtils md5Encode:self.channel] hash]; + result = prime * result + [[BNCEncodingUtils md5Encode:self.feature] hash]; + result = prime * result + [[BNCEncodingUtils md5Encode:self.stage] hash]; + result = prime * result + [[BNCEncodingUtils md5Encode:self.campaign] hash]; + result = prime * result + [[BNCEncodingUtils md5Encode:encodedParams] hash]; + result = prime * result + self.duration; + + for (NSString *tag in self.tags) { + result = prime * result + [[BNCEncodingUtils md5Encode:tag] hash]; + } + + return result; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + if (self.tags) { + [coder encodeObject:self.tags forKey:BRANCH_REQUEST_KEY_URL_TAGS]; + } + if (self.alias) { + [coder encodeObject:self.alias forKey:BRANCH_REQUEST_KEY_URL_ALIAS]; + } + if (self.type) { + [coder encodeObject:@(self.type) forKey:BRANCH_REQUEST_KEY_URL_LINK_TYPE]; + } + if (self.channel) { + [coder encodeObject:self.channel forKey:BRANCH_REQUEST_KEY_URL_CHANNEL]; + } + if (self.feature) { + [coder encodeObject:self.feature forKey:BRANCH_REQUEST_KEY_URL_FEATURE]; + } + if (self.stage) { + [coder encodeObject:self.stage forKey:BRANCH_REQUEST_KEY_URL_STAGE]; + } + if (self.campaign) { + [coder encodeObject:self.campaign forKey:BRANCH_REQUEST_KEY_URL_CAMPAIGN]; + } + if (self.params) { + NSString *encodedParams = [BNCEncodingUtils encodeDictionaryToJsonString:self.params]; + [coder encodeObject:encodedParams forKey:BRANCH_REQUEST_KEY_URL_DATA]; + } + if (self.duration > 0) { + [coder encodeObject:@(self.duration) forKey:BRANCH_REQUEST_KEY_URL_DURATION]; + } +} + +- (id)initWithCoder:(NSCoder *)coder { + if ((self = [super init])) { + self.tags = [coder decodeObjectOfClass:NSArray.class forKey:BRANCH_REQUEST_KEY_URL_TAGS]; + self.alias = [coder decodeObjectOfClass:NSString.class forKey:BRANCH_REQUEST_KEY_URL_ALIAS]; + self.type = [[coder decodeObjectOfClass:NSNumber.class forKey:BRANCH_REQUEST_KEY_URL_LINK_TYPE] integerValue]; + self.channel = [coder decodeObjectOfClass:NSString.class forKey:BRANCH_REQUEST_KEY_URL_CHANNEL]; + self.feature = [coder decodeObjectOfClass:NSString.class forKey:BRANCH_REQUEST_KEY_URL_FEATURE]; + self.stage = [coder decodeObjectOfClass:NSString.class forKey:BRANCH_REQUEST_KEY_URL_STAGE]; + self.campaign = [coder decodeObjectOfClass:NSString.class forKey:BRANCH_REQUEST_KEY_URL_CAMPAIGN]; + self.duration = [[coder decodeObjectOfClass:NSNumber.class forKey:BRANCH_REQUEST_KEY_URL_DURATION] integerValue]; + + NSString *encodedParams = [coder decodeObjectOfClass:NSString.class forKey:BRANCH_REQUEST_KEY_URL_DATA]; + self.params = [BNCEncodingUtils decodeJsonStringToDictionary:encodedParams]; + } + + return self; +} + ++ (BOOL) supportsSecureCoding { + return YES; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLocalization.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLocalization.h new file mode 100644 index 00000000000..35c5dbf3d68 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLocalization.h @@ -0,0 +1,50 @@ +/** + @file BNCLocalization.h + @package Branch-SDK + @brief Branch string localizations. + + @author Parth Kalavadia + @date July 2017 + @copyright Copyright © 2017 Branch. All rights reserved. +*/ + +/** + @discusion + + # BNCLocalization + + Since the Branch SDK can be shipped as a static library, it can't use the standard Apple + string localization mechanism. + + Use this class to localize the few user-facing string resources the Branch SDK has. +*/ + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface BNCLocalization : NSObject + ++ (instancetype) shared; ++ (NSString*) applicationLanguage; ++ (NSDictionary*) languageDictionaries; +- (NSString*) localizeString:(NSString*)string; + +/// Set to an empty string or nil to reset the current language. +@property (copy, atomic) NSString* currentLanguage; +@property (strong, atomic, readonly) NSDictionary *currentLanguageDictionary; +@end + +#pragma mark Convenience Functions + +static inline NSString* BNCLocalizedString(NSString*const string) { + return [[BNCLocalization shared] localizeString:string]; +} + +extern NSString* BNCLocalizedFormattedString(NSString*const format, ...) NS_FORMAT_FUNCTION(1,2); + +NS_ASSUME_NONNULL_END diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLocalization.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLocalization.m new file mode 100644 index 00000000000..cb38170faaa --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLocalization.m @@ -0,0 +1,295 @@ +/** + @file BNCLocalization.m + @package Branch-SDK + @brief Branch string localizations. + + @author Parth Kalavadia + @date July 2017 + @copyright Copyright © 2017 Branch. All rights reserved. +*/ + +#import "BNCLocalization.h" +#import "BNCLog.h" + +#pragma mark Convenience Functions + +NSString* _Nonnull BNCLocalizedFormattedString(NSString* _Nonnull const format, ...) { + NSString *string = @""; + if (format) { + va_list args; + va_start(args, format); + string = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + } + return string; +} + +#pragma mark BNCLocalization + +@interface BNCLocalization () { + NSString *_currentLanguage; +} +@end + +@implementation BNCLocalization + ++ (instancetype) shared { + static BNCLocalization *bnc_shared = 0; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + bnc_shared = [[BNCLocalization alloc] init]; + }); + return bnc_shared; +} + ++ (NSString*_Nonnull) applicationLanguage { + NSString* lang = [[[NSBundle mainBundle] preferredLocalizations] firstObject]; + return lang ? lang : @""; +} + +- (instancetype) init { + self = [super init]; + if (!self) return self; + self.currentLanguage = [self.class applicationLanguage]; + return self; +} + +- (NSString*_Nonnull) currentLanguage { + @synchronized (self) { + return _currentLanguage; + } +} + +- (void) setCurrentLanguage:(NSString*)language { + @synchronized (self) { + + if (!language.length) { + language = [self.class applicationLanguage]; + } + + NSRange range = + [language rangeOfCharacterFromSet: + [[NSCharacterSet letterCharacterSet] invertedSet]]; + if (range.location != NSNotFound) + language = [language substringToIndex:range.location]; + language = [language lowercaseString]; + + _currentLanguageDictionary = self.class.languageDictionaries[language]; + if (_currentLanguageDictionary) { + _currentLanguage = language.copy; + } else { + _currentLanguage = @"en"; + _currentLanguageDictionary = [BNCLocalization en_localized]; + } + } +} + +- (NSString*_Nonnull) localizeString:(NSString *)string { + if (!string) return @""; + + NSString *localized = self.currentLanguageDictionary[string]; + if (localized) return localized; + + BNCLogWarning( + @"Branch is missing the localization missing for language '%@' string '%@'.", + self.currentLanguage, string); + + localized = self.class.languageDictionaries[@"en"][string]; + if (localized) return localized; + + return string; +} + ++(NSDictionary*_Nonnull) languageDictionaries { + NSDictionary* languages = @{ + @"en": [BNCLocalization en_localized], + @"ru": [BNCLocalization ru_localized], + }; + return languages; +} + ++ (NSDictionary*_Nonnull) en_localized { + NSDictionary* en_dict = @{ + + // BNCInitError + @"The Branch user session has not been initialized.": + @"The Branch user session has not been initialized.", + + // BNCDuplicateResourceError + @"A resource with this identifier already exists.": + @"A resource with this identifier already exists.", + + // BNCRedeemCreditsError + @"You're trying to redeem more credits than are available. Have you loaded rewards?": + @"You're trying to redeem more credits than are available. Have you loaded rewards?", + + // BNCBadRequestError + @"The network request was invalid.": + @"The network request was invalid.", + + // BNCServerProblemError + @"Trouble reaching the Branch servers, please try again shortly.": + @"Trouble reaching the Branch servers, please try again shortly.", + + // BNCNilLogError + @"Can't log error messages because the logger is set to nil.": + @"Can't log error messages because the logger is set to nil.", + + // BNCVersionError + @"Incompatible version.": + @"Incompatible version.", + + // BNCNetworkServiceInterfaceError + @"The underlying network service does not conform to the BNCNetworkOperationProtocol.": + @"The underlying network service does not conform to the BNCNetworkOperationProtocol.", + + // BNCInvalidNetworkPublicKeyError + @"Public key is not an SecKeyRef type.": + @"Public key is not an SecKeyRef type.", + + // BNCContentIdentifierError + @"A canonical identifier or title are required to uniquely identify content.": + @"A canonical identifier or title are required to uniquely identify content.", + + // BNCSpotlightNotAvailableError + @"The Core Spotlight indexing service is not available on this device.": + @"The Core Spotlight indexing service is not available on this device.", + + // BNCSpotlightTitleError + @"Spotlight indexing requires a title.": + @"Spotlight indexing requires a title.", + + // BNCRedeemZeroCreditsError + @"Can't redeem zero credits.": + @"Can't redeem zero credits.", + + // Unknown error + @"Branch encountered an error.": + @"Branch encountered an error.", + + // Network provider error messages + @"A network operation instance is expected to be returned by the networkOperationWithURLRequest:completion: method.": + @"A network operation instance is expected to be returned by the networkOperationWithURLRequest:completion: method.", + + @"Network operation of class '%@' does not conform to the BNCNetworkOperationProtocol.": + @"Network operation of class '%@' does not conform to the BNCNetworkOperationProtocol.", + + @"The network operation start date is not set. The Branch SDK expects the network operation start date to be set by the network provider.": + @"The network operation start date is not set. The Branch SDK expects the network operation start date to be set by the network provider.", + + @"The network operation timeout date is not set. The Branch SDK expects the network operation timeout date to be set by the network provider.": + @"The network operation timeout date is not set. The Branch SDK expects the network operation timeout date to be set by the network provider.", + + @"The network operation request is not set. The Branch SDK expects the network operation request to be set by the network provider.": + @"The network operation request is not set. The Branch SDK expects the network operation request to be set by the network provider.", + + // Other errors + @"The request was invalid.": + @"The request was invalid.", + + @"Could not register view.": + @"Could not register view.", + + @"Could not generate a URL.": + @"Could not generate a URL.", + + @"User tracking is disabled.": + @"User tracking is disabled." + }; + return en_dict; +} + ++ (NSDictionary*_Nonnull) ru_localized { + + NSDictionary* ru_dict = @{ + + // BNCInitError + @"The Branch user session has not been initialized.": + @"Сессия Branch не была инициализирована.", + + // BNCDuplicateResourceError + @"A resource with this identifier already exists.": + @"Ресурс с таким идентификатором уже существует.", + + // BNCRedeemCreditsError + @"You're trying to redeem more credits than are available. Have you loaded rewards?": + @"Вы пытаетесь применить больше кредитов, чем доступно. Были ли загружены награды?", + + // BNCBadRequestError + @"The network request was invalid.": + @"Неправильный сетевой запрос.", + + // BNCServerProblemError + @"Trouble reaching the Branch servers, please try again shortly.": + @"Проблемы с соединением с серверами Branch, попробуйте повторить операцию позже.", + + // BNCNilLogError + @"Can't log error messages because the logger is set to nil.": + @"Невозможно записать ошибку, так как значение логгера nil.", + + // BNCVersionError + @"Incompatible version.": + @"Несовместимая версия.", + + // BNCNetworkServiceInterfaceError + @"The underlying network service does not conform to the BNCNetworkOperationProtocol.": + @"Используемый сетевой сервис не соблюдает протокол BNCNetworkOperationProtocol.", + + // BNCInvalidNetworkPublicKeyError + @"Public key is not an SecKeyRef type.": + @"Публичный ключ неверного типа. Ожидаемый тип: SecKeyRef.", + + // BNCContentIdentifierError + @"A canonical identifier or title are required to uniquely identify content.": + @"Канонический идентификатор или название необходимы для идентификации контента.", + + // BNCSpotlightNotAvailableError + @"The Core Spotlight indexing service is not available on this device.": + @"Сервис Core Spotlight Indexing не доступен на этом устройстве.", + + // BNCSpotlightTitleError + @"Spotlight indexing requires a title.": + @"Название – необходимый параметр для Spotlight Indexing", + + // BNCRedeemZeroCreditsError + @"Can't redeem zero credits.": + @"Нельзя применить ноль кредитов.", + + // Unknown error + @"Branch encountered an error.": + @"Неизвестная ошибка в Branch.", + + // Network provider error messages + @"A network operation instance is expected to be returned by the networkOperationWithURLRequest:completion: method.": + @"Объект сетевого оператора должен быть возвращен после запроса к методу networkOperationWithURLRequest:completion:.", + + @"Network operation of class '%@' does not conform to the BNCNetworkOperationProtocol.": + @"Сетевая операция класса '%@' не соответствует протоколу BNCNetworkOperationProtocol.", + + @"The network operation start date is not set. The Branch SDK expects the network operation start date to be set by the network provider.": + @"Дата старта сетевого запроса не была установлена. SDK Branch ожидает, что дату старта сетевого запроса назначит сетевой провайдер.", + + @"The network operation timeout date is not set. The Branch SDK expects the network operation timeout date to be set by the network provider.": + @"Дата тайм-аута сетевого запроса не была назначена. SDK Branch ожидает, что дату тайм-аута сетевого запроса назначит сетевой провайдер.", + + @"The network operation request is not set. The Branch SDK expects the network operation request to be set by the network provider.": + @"Сетевой запрос не был назначен. SDK Branch ожидает, что сетевой запрос назначит сетевой провайдер.", + + // Other errors + @"The request was invalid.": + @"Неверный запрос.", + + @"Could not register view.": + @"Не получилось зарегистрировать view.", + + @"Could not generate a URL.": + @"Не получилось сгенерировать URL.", + + @"User tracking is disbabled.": + @"Трекинг пользователя отключен.", + }; + + return ru_dict; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCLog.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLog.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCLog.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLog.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLog.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLog.m new file mode 100644 index 00000000000..ab3905c2184 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCLog.m @@ -0,0 +1,673 @@ +/** + @file BNCLog.m + @package Branch-SDK + @brief Simple logging functions. + + @author Edward Smith + @date October 2016 + @copyright Copyright © 2016 Branch. All rights reserved. +*/ + +#import "BNCLog.h" +#import // import not available in Xcode 7 + +#define _countof(array) (sizeof(array)/sizeof(array[0])) +static NSNumber *bnc_LogIsInitialized = nil; + +// All log synchronization and globals are coordinated through the bnc_LogQueue. +static dispatch_queue_t bnc_LogQueue = nil; + +static off_t bnc_LogOffset = 0; +static off_t bnc_LogOffsetMax = 100; +static off_t bnc_LogRecordSize = 1024; +static BNCLogOutputFunctionPtr bnc_LoggingFunction = nil; // Default to just NSLog output. +static BNCLogFlushFunctionPtr bnc_LogFlushFunction = nil; +static NSDateFormatter *bnc_LogDateFormatter = nil; + +// A fallback attempt at logging if an error occurs in BNCLog. +// BNCLog can't log itself, but if an error occurs it uses this simple define: +extern void BNCLogInternalErrorFunction(int linenumber, NSString*format, ...); +void BNCLogInternalErrorFunction(int linenumber, NSString*format, ...) { + + va_list args; + va_start(args, format); + NSString* message = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + NSLog(@"[branch.io] BNCLog.m (%d) Log error: %@", linenumber, message); +} + +#define BNCLogInternalError(...) \ + BNCLogInternalErrorFunction(__LINE__, __VA_ARGS__) + + +inline static void BNCLogInitializeClient_Internal() { + BNCLogInitialize(); + BNCLogClientInitializeFunctionPtr initFunction = BNCLogSetClientInitializeFunction(NULL); + if (initFunction) { + initFunction(); + } +} + +#pragma mark - Default Output Functions + +static int bnc_LogDescriptor = -1; + +void BNCLogFunctionOutputToStdOut( + NSDate*_Nonnull timestamp, + BNCLogLevel level, + NSString *_Nullable message + ) { + NSData *data = [message dataUsingEncoding:NSNEXTSTEPStringEncoding]; + if (!data) data = [@"" dataUsingEncoding:NSNEXTSTEPStringEncoding]; + long n = write(STDOUT_FILENO, data.bytes, data.length); + if (n < 0) { + int e = errno; + BNCLogInternalError(@"Can't write log message (%d): %s.", e, strerror(e)); + } + write(STDOUT_FILENO, "\n ", sizeof('\n')); +} + +void BNCLogFunctionOutputToStdErr( + NSDate*_Nonnull timestamp, + BNCLogLevel level, + NSString *_Nullable message + ) { + NSData *data = [message dataUsingEncoding:NSNEXTSTEPStringEncoding]; + if (!data) data = [@"" dataUsingEncoding:NSNEXTSTEPStringEncoding]; + long n = write(STDERR_FILENO, data.bytes, data.length); + if (n < 0) { + int e = errno; + BNCLogInternalError(@"Can't write log message (%d): %s.", e, strerror(e)); + } + write(STDERR_FILENO, "\n ", sizeof('\n')); +} + +void BNCLogFunctionOutputToFileDescriptor( + NSDate*_Nonnull timestamp, + BNCLogLevel level, + NSString *_Nullable message + ) { + // Pad length to even characters + if (!message) message = @""; + NSString *string = [NSString stringWithFormat:@"%@\n", message]; + NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; + if ((data.length & 1) == 1) { + string = [NSString stringWithFormat:@"%@ \n", message]; + data = [string dataUsingEncoding:NSUTF8StringEncoding]; + } + if ((data.length & 1) != 0) { + BNCLogInternalError(@"Writing un-even bytes!"); + } + long n = write(bnc_LogDescriptor, data.bytes, data.length); + if (n < 0) { + int e = errno; + BNCLogInternalError(@"Can't write log message (%d): %s.", e, strerror(e)); + } +} + +void BNCLogFlushFileDescriptor() { + if (bnc_LogDescriptor >= 0) { + fsync(bnc_LogDescriptor); + } +} + +void BNCLogSetOutputToURL_Interal(NSURL *_Nullable url) { + if (url == nil) return; + bnc_LogDescriptor = open( + url.path.UTF8String, + O_RDWR|O_CREAT|O_APPEND, + S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP + ); + if (bnc_LogDescriptor < 0) { + int e = errno; + BNCLogInternalError(@"Can't open log file '%@'.", url); + BNCLogInternalError(@"Can't open log file (%d): %s.", e, strerror(e)); + return; + } + bnc_LoggingFunction = BNCLogFunctionOutputToFileDescriptor; + bnc_LogFlushFunction = BNCLogFlushFileDescriptor; +} + +void BNCLogSetOutputToURL(NSURL *_Nullable url) { + dispatch_sync(bnc_LogQueue, ^{ + if (bnc_LogFlushFunction) + bnc_LogFlushFunction(); + if (bnc_LogDescriptor >= 0) { + close(bnc_LogDescriptor); + bnc_LogDescriptor = -1; + } + BNCLogSetOutputToURL_Interal(url); + }); +} + +#pragma mark - Record Wrap Output File Functions + +void BNCLogRecordWrapWrite(NSDate*_Nonnull timestamp, BNCLogLevel level, NSString *_Nullable message) { + + NSString * string = [NSString stringWithFormat:@"%@ %ld %@", + [bnc_LogDateFormatter stringFromDate:timestamp], (long) level, message]; + NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding]; + + char buffer[bnc_LogRecordSize]; + memset(buffer, ' ', sizeof(buffer)); + buffer[sizeof(buffer)-1] = '\n'; + long len = MIN(stringData.length, sizeof(buffer)-1); + memcpy(buffer, stringData.bytes, len); + + off_t n = write(bnc_LogDescriptor, buffer, sizeof(buffer)); + if (n < 0) { + int e = errno; + BNCLogInternalError(@"Can't write log message (%d): %s.", e, strerror(e)); + } + bnc_LogOffset++; + if (bnc_LogOffset >= bnc_LogOffsetMax) { + bnc_LogOffset = 0; + n = lseek(bnc_LogDescriptor, 0, SEEK_SET); + if (n < 0) { + int e = errno; + BNCLogInternalError(@"Can't seek in log (%d): %s.", e, strerror(e)); + } + } +} + +void BNCLogRecordWrapFlush() { + if (bnc_LogDescriptor >= 0) { + fsync(bnc_LogDescriptor); + } +} + +BOOL BNCLogRecordWrapOpenURL_Internal(NSURL *url, long maxRecords, long recordSize) { + if (url == nil) return NO; + bnc_LogOffsetMax = MAX(1, maxRecords); + bnc_LogRecordSize = MAX(64, recordSize); + if ((bnc_LogRecordSize & 1) != 0) { + // Can't have odd-length records. + bnc_LogRecordSize++; + } + bnc_LogDescriptor = open( + url.path.UTF8String, + O_RDWR|O_CREAT, + S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP + ); + if (bnc_LogDescriptor < 0) { + int e = errno; + BNCLogInternalError(@"Can't open log file '%@'.", url); + BNCLogInternalError(@"Can't open log file (%d): %s.", e, strerror(e)); + return NO; + } + bnc_LoggingFunction = BNCLogRecordWrapWrite; + bnc_LogFlushFunction = BNCLogRecordWrapFlush; + + // Truncate the file if the file size > max file size. + + off_t fileOffset = 0; + off_t maxSz = bnc_LogOffsetMax * bnc_LogRecordSize; + off_t sz = lseek(bnc_LogDescriptor, 0, SEEK_END); + if (sz < 0) { + int e = errno; + BNCLogInternalError(@"Can't seek in log (%d): %s.", e, strerror(e)); + } else if (sz > maxSz) { + fileOffset = ftruncate(bnc_LogDescriptor, maxSz); + if (fileOffset < 0) { + int e = errno; + BNCLogInternalError(@"Can't truncate log (%d): %s.", e, strerror(e)); + } + } + lseek(bnc_LogDescriptor, 0, SEEK_SET); + + // Read the records until the oldest record is found -- + + off_t oldestOffset = 0; + NSDate * oldestDate = [NSDate distantFuture]; + NSDate * lastDate = [NSDate distantFuture]; + + off_t offset = 0; + char buffer[bnc_LogRecordSize]; + ssize_t bytesRead = read(bnc_LogDescriptor, &buffer, sizeof(buffer)); + while ((unsigned long) bytesRead == sizeof(buffer)) { + NSString *dateString = + [[NSString alloc] initWithBytes:buffer length:27 encoding:NSUTF8StringEncoding]; + NSDate *date = [bnc_LogDateFormatter dateFromString:dateString]; + if ([date compare:oldestDate] < 0 || + [date compare:lastDate] < 0) { + oldestOffset = offset; + oldestDate = date; + } + offset++; + if (date) lastDate = date; + bytesRead = read(bnc_LogDescriptor, &buffer, sizeof(buffer)); + } + if (offset < bnc_LogOffsetMax) + bnc_LogOffset = offset; + else + if (oldestOffset >= bnc_LogOffsetMax) + bnc_LogOffset = 0; + else + bnc_LogOffset = oldestOffset; + sz = lseek(bnc_LogDescriptor, bnc_LogOffset*bnc_LogRecordSize, SEEK_SET); + if (sz < 0) { + int e = errno; + BNCLogInternalError(@"Can't seek in log (%d): %s.", e, strerror(e)); + } + return YES; +} + +BOOL BNCLogRecordWrapOpenURL(NSURL *url, long maxRecords, long recordSize) { + __block BOOL result = NO; + dispatch_sync(bnc_LogQueue, ^{ + if (bnc_LogFlushFunction) + bnc_LogFlushFunction(); + if (bnc_LogDescriptor >= 0) { + close(bnc_LogDescriptor); + bnc_LogDescriptor = -1; + } + bnc_LoggingFunction = NULL; + bnc_LogFlushFunction = NULL; + result = BNCLogRecordWrapOpenURL_Internal(url, maxRecords, recordSize); + }); + return result; +} + +void BNCLogSetOutputToURLRecordWrapSize(NSURL *_Nullable url, long maxRecords, long recordSize) { + BNCLogRecordWrapOpenURL(url, maxRecords, recordSize); +} + +void BNCLogSetOutputToURLRecordWrap(NSURL *_Nullable url, long maxRecords) { + BNCLogSetOutputToURLRecordWrapSize(url, maxRecords, 1024); +} + +#pragma mark - Byte Wrap Output File Functions + +void BNCLogByteWrapWrite(NSDate*_Nonnull timestamp, BNCLogLevel level, NSString *_Nullable message) { + + NSString * string = [NSString stringWithFormat:@"%@ %ld %@\n", + [bnc_LogDateFormatter stringFromDate:timestamp], (long) level, message]; + NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding]; + + if ((stringData.length & 1) != 0) { + string = [NSString stringWithFormat:@"%@ %ld %@ \n", + [bnc_LogDateFormatter stringFromDate:timestamp], (long) level, message]; + stringData = [string dataUsingEncoding:NSUTF8StringEncoding]; + } + + // Truncate the file if the file size > max file size. + + if ((bnc_LogOffset + stringData.length) > bnc_LogOffsetMax) { + long n = ftruncate(bnc_LogDescriptor, bnc_LogOffset); + if (n < 0) { + int e = errno; + BNCLogInternalError(@"Can't truncate log (%d): %s.", e, strerror(e)); + } + lseek(bnc_LogDescriptor, 0, SEEK_SET); + bnc_LogOffset = 0; + } + + long n = write(bnc_LogDescriptor, stringData.bytes, stringData.length); + if (n < 0) { + int e = errno; + BNCLogInternalError(@"Can't write log message (%d): %s.", e, strerror(e)); + } else { + bnc_LogOffset += n; + } +} + +void BNCLogByteWrapFlush() { + if (bnc_LogDescriptor >= 0) { + fsync(bnc_LogDescriptor); + } +} + +NSString *BNCLogByteWrapReadNextRecord() { + + char *buffer = NULL; + long bufferSize = 0; + off_t originalOffset = lseek(bnc_LogDescriptor, 0, SEEK_CUR); + if (originalOffset < 0) { + int e = errno; + BNCLogInternalError(@"Can't find offset in log file (%d): %s.", e, strerror(e)); + goto error_exit; + } + + do { + + bufferSize += 1024; + if (buffer) free(buffer); + buffer = malloc(bufferSize); + if (!buffer) { + BNCLogInternalError(@"Can't allocate a buffer of %ld bytes.", bufferSize); + goto error_exit; + } + + off_t newOffset = lseek(bnc_LogDescriptor, originalOffset, SEEK_SET); + if (newOffset < 0) { + int e = errno; + BNCLogInternalError(@"Can't seek in log file (%d): %s.", e, strerror(e)); + goto error_exit; + } + ssize_t bytesRead = read(bnc_LogDescriptor, buffer, bufferSize); + if (bytesRead == 0) { + goto error_exit; + } else if (bytesRead < 0) { + int e = errno; + if (e != EOF) { + BNCLogInternalError(@"Can't read log message (%d): %s.", e, strerror(e)); + } + goto error_exit; + } + + char* p = buffer; + intptr_t endByte = bytesRead; + while ( (p-buffer) < endByte && *p != '\n') { + p++; + } + if ((p-buffer) < endByte && *p == '\n') { + intptr_t offset = (p-buffer)+1; + NSString *result = + [[NSString alloc] + initWithBytes:buffer length:offset encoding:NSUTF8StringEncoding]; + bnc_LogOffset = originalOffset + offset; + newOffset = lseek(bnc_LogDescriptor, bnc_LogOffset, SEEK_SET); + if (newOffset < 0) { + int e = errno; + BNCLogInternalError(@"Can't seek in log file (%d): %s.", e, strerror(e)); + } + free(buffer); + return result; + } + + } while (bufferSize < 1024*20); + +error_exit: + if (buffer) free(buffer); + return nil; +} + +BOOL BNCLogByteWrapOpenURL_Internal(NSURL *url, long maxBytes) { + if (url == nil) return NO; + bnc_LogOffsetMax = MAX(256, maxBytes); + bnc_LogDescriptor = open( + url.path.UTF8String, + O_RDWR|O_CREAT, + S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP + ); + if (bnc_LogDescriptor < 0) { + int e = errno; + BNCLogInternalError(@"Can't open log file '%@'.", url); + BNCLogInternalError(@"Can't open log file (%d): %s.", e, strerror(e)); + return NO; + } + bnc_LoggingFunction = BNCLogByteWrapWrite; + bnc_LogFlushFunction = BNCLogByteWrapFlush; + + // Truncate the file if the file size > max file size. + + off_t newOffset = 0; + off_t maxSz = bnc_LogOffsetMax; + off_t sz = lseek(bnc_LogDescriptor, 0, SEEK_END); + if (sz < 0) { + int e = errno; + BNCLogInternalError(@"Can't seek in log (%d): %s.", e, strerror(e)); + } else if (sz > maxSz) { + newOffset = ftruncate(bnc_LogDescriptor, maxSz); + if (newOffset < 0) { + int e = errno; + BNCLogInternalError(@"Can't truncate log (%d): %s.", e, strerror(e)); + } + } + bnc_LogOffset = 0; + lseek(bnc_LogDescriptor, bnc_LogOffset, SEEK_SET); + + // Read the records until the oldest record is found -- + + BOOL logDidWrap = NO; + off_t wrapOffset = 0; + + off_t lastOffset = 0; + NSDate *lastDate = [NSDate distantPast]; + + NSString *record = BNCLogByteWrapReadNextRecord(); + while (record) { + NSDate *date = nil; + if (record.length >= 27) { + NSString *dateString = [record substringWithRange:NSMakeRange(0, 27)]; + date = [bnc_LogDateFormatter dateFromString:dateString]; + } + if (!date || [date compare:lastDate] < 0) { + wrapOffset = lastOffset; + logDidWrap = YES; + } + lastDate = date ?: [NSDate distantPast]; + lastOffset = bnc_LogOffset; + record = BNCLogByteWrapReadNextRecord(); + } + if (logDidWrap) { + bnc_LogOffset = wrapOffset; + } else if (bnc_LogOffset >= bnc_LogOffsetMax) + bnc_LogOffset = 0; + newOffset = lseek(bnc_LogDescriptor, bnc_LogOffset, SEEK_SET); + if (newOffset < 0) { + int e = errno; + BNCLogInternalError(@"Can't seek in log (%d): %s.", e, strerror(e)); + } + return YES; +} + +void BNCLogSetOutputToURLByteWrap(NSURL *_Nullable URL, long maxBytes) { + __block BOOL result = NO; + dispatch_sync(bnc_LogQueue, ^{ + if (bnc_LogFlushFunction) + bnc_LogFlushFunction(); + if (bnc_LogDescriptor >= 0) { + close(bnc_LogDescriptor); + bnc_LogDescriptor = -1; + } + result = BNCLogByteWrapOpenURL_Internal(URL, maxBytes); + }); +} + +#pragma mark - Log Message Severity + +static BNCLogLevel bnc_LogDisplayLevel = BNCLogLevelWarning; + +BNCLogLevel BNCLogDisplayLevel() { + BNCLogInitializeClient_Internal(); + __block BNCLogLevel level = BNCLogLevelAll; + dispatch_sync(bnc_LogQueue, ^{ + level = bnc_LogDisplayLevel; + }); + return level; +} + +void BNCLogSetDisplayLevel(BNCLogLevel level) { + BNCLogInitializeClient_Internal(); + dispatch_async(bnc_LogQueue, ^{ + bnc_LogDisplayLevel = level; + }); +} + +static NSString*const bnc_logLevelStrings[] = { + @"BNCLogLevelAll", + @"BNCLogLevelBreakPoint", + @"BNCLogLevelDebug", + @"BNCLogLevelWarning", + @"BNCLogLevelError", + @"BNCLogLevelAssert", + @"BNCLogLevelLog", + @"BNCLogLevelNone", + @"BNCLogLevelMax" +}; + +NSString* BNCLogStringFromLogLevel(BNCLogLevel level) { + level = MAX(MIN(level, BNCLogLevelMax), 0); + return bnc_logLevelStrings[level]; +} + +BNCLogLevel BNCLogLevelFromString(NSString*string) { + if (!string) return BNCLogLevelNone; + for (NSUInteger i = 0; i < _countof(bnc_logLevelStrings); ++i) { + if ([bnc_logLevelStrings[i] isEqualToString:string]) { + return i; + } + } + if ([string isEqualToString:@"BNCLogLevelDebugSDK"]) { + return BNCLogLevelDebugSDK; + } + return BNCLogLevelNone; +} + +#pragma mark - Client Initialization Function + +static _Atomic(BNCLogClientInitializeFunctionPtr) bnc_LogClientInitializeFunctionPtr = (BNCLogClientInitializeFunctionPtr) 0; + +extern BNCLogClientInitializeFunctionPtr _Null_unspecified BNCLogSetClientInitializeFunction( + BNCLogClientInitializeFunctionPtr _Nullable clientInitializationFunction + ) { + BNCLogClientInitializeFunctionPtr lastPtr = + atomic_exchange(&bnc_LogClientInitializeFunctionPtr, clientInitializationFunction); + return lastPtr; +} + +#pragma mark - Break Points + +static BOOL bnc_LogBreakPointsAreEnabled = NO; + +BOOL BNCLogBreakPointsAreEnabled() { + __block BOOL enabled = NO; + dispatch_sync(bnc_LogQueue, ^{ + enabled = bnc_LogBreakPointsAreEnabled; + }); + return enabled; +} + +void BNCLogSetBreakPointsEnabled(BOOL enabled) { + dispatch_async(bnc_LogQueue, ^{ + bnc_LogBreakPointsAreEnabled = enabled; + }); +} + +#pragma mark - Log Functions + +BNCLogOutputFunctionPtr _Nullable BNCLogOutputFunction() { + __block BNCLogOutputFunctionPtr ptr = NULL; + dispatch_sync(bnc_LogQueue, ^{ + ptr = bnc_LoggingFunction; + }); + return ptr; +} + +void BNCLogCloseLogFile() { + dispatch_sync(bnc_LogQueue, ^{ + if (bnc_LogFlushFunction) + bnc_LogFlushFunction(); + if (bnc_LogDescriptor >= 0) { + close(bnc_LogDescriptor); + bnc_LogDescriptor = -1; + } + bnc_LogFlushFunction = NULL; + bnc_LoggingFunction = NULL; + }); +} + +void BNCLogSetOutputFunction(BNCLogOutputFunctionPtr _Nullable logFunction) { + dispatch_async(bnc_LogQueue, ^{ + bnc_LoggingFunction = logFunction; + }); +} + +BNCLogFlushFunctionPtr BNCLogFlushFunction() { + __block BNCLogFlushFunctionPtr ptr = NULL; + dispatch_sync(bnc_LogQueue, ^{ + ptr = bnc_LogFlushFunction; + }); + return ptr; +} + +void BNCLogSetFlushFunction(BNCLogFlushFunctionPtr flushFunction) { + dispatch_async(bnc_LogQueue, ^{ + bnc_LogFlushFunction = flushFunction; + }); +} + +#pragma mark - BNCLogInternal + +void BNCLogWriteMessageFormat( + BNCLogLevel logLevel, + const char *_Nullable file, + int32_t lineNumber, + NSString *_Nullable message, + ... + ) { + BNCLogInitializeClient_Internal(); + if (!file) file = ""; + if (!message) message = @""; + if (![message isKindOfClass:[NSString class]]) { + message = [NSString stringWithFormat:@"0x%016llx <%@> %@", + (uint64_t) message, message.class, message.description]; + } + + NSString* filename = + [[NSString stringWithCString:file encoding:NSMacOSRomanStringEncoding] + lastPathComponent]; + + NSString * const logLevels[BNCLogLevelMax] = { + @"DebugSDK", + @"Break", + @"Debug", + @"Warning", + @"Error", + @"Assert", + @"Log", + @"None", + }; + + logLevel = MAX(MIN(logLevel, BNCLogLevelMax-1), 0); + NSString *levelString = logLevels[logLevel]; + + va_list args; + va_start(args, message); + NSString* m = [[NSString alloc] initWithFormat:message arguments:args]; + NSString* s = [NSString stringWithFormat: + @"[branch.io] %@(%d) %@: %@", filename, lineNumber, levelString, m]; + va_end(args); + + dispatch_async(bnc_LogQueue, ^{ + if (logLevel >= bnc_LogDisplayLevel) { + NSLog(@"%@", s); // Upgrade this to unified logging when we can. + } + if (bnc_LoggingFunction) + bnc_LoggingFunction([NSDate date], logLevel, s); + }); +} + +void BNCLogWriteMessage( + BNCLogLevel logLevel, + NSString *_Nonnull file, + int32_t lineNumber, + NSString *_Nonnull message + ) { + BNCLogWriteMessageFormat(logLevel, file.UTF8String, lineNumber, @"%@", message); +} + +void BNCLogFlushMessages() { + dispatch_sync(bnc_LogQueue, ^{ + if (bnc_LogFlushFunction) + bnc_LogFlushFunction(); + }); +} + +#pragma mark - BNCLogInitialize + +__attribute__((constructor)) void BNCLogInitialize(void) { + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^ { + bnc_LogQueue = dispatch_queue_create("io.branch.sdk.log", DISPATCH_QUEUE_SERIAL); + + bnc_LogDateFormatter = [[NSDateFormatter alloc] init]; + bnc_LogDateFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + bnc_LogDateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSSSSX"; + bnc_LogDateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + + bnc_LogIsInitialized = @(YES); + }); +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCPreferenceHelper.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCPreferenceHelper.h similarity index 99% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCPreferenceHelper.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCPreferenceHelper.h index 949cde7cb4e..215aec636e3 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCPreferenceHelper.h +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCPreferenceHelper.h @@ -94,5 +94,6 @@ NSURL* /* _Nonnull */ BNCURLForBranchDirectory(void); - (NSMutableString*) sanitizedMutableBaseURL:(NSString*)baseUrl; - (void) synchronize; // Flushes preference queue to persistence. ++ (void) clearAll; @end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCPreferenceHelper.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCPreferenceHelper.m new file mode 100644 index 00000000000..83b6786a4f7 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCPreferenceHelper.m @@ -0,0 +1,959 @@ +// +// BNCPreferenceHelper.m +// Branch-SDK +// +// Created by Alex Austin on 6/6/14. +// Copyright (c) 2014 Branch Metrics. All rights reserved. +// + +#import "BNCPreferenceHelper.h" +#import "BNCEncodingUtils.h" +#import "BNCConfig.h" +#import "Branch.h" +#import "BNCLog.h" +#import "BNCFabricAnswers.h" +#import "BranchConstants.h" +#import "NSString+Branch.h" + +static const NSTimeInterval DEFAULT_TIMEOUT = 5.5; +static const NSTimeInterval DEFAULT_RETRY_INTERVAL = 0; +static const NSInteger DEFAULT_RETRY_COUNT = 3; + +static NSString * const BRANCH_PREFS_FILE = @"BNCPreferences"; + +static NSString * const BRANCH_PREFS_KEY_APP_VERSION = @"bnc_app_version"; +static NSString * const BRANCH_PREFS_KEY_LAST_RUN_BRANCH_KEY = @"bnc_last_run_branch_key"; +static NSString * const BRANCH_PREFS_KEY_LAST_STRONG_MATCH_DATE = @"bnc_strong_match_created_date"; +static NSString * const BRANCH_PREFS_KEY_DEVICE_FINGERPRINT_ID = @"bnc_device_fingerprint_id"; +static NSString * const BRANCH_PREFS_KEY_SESSION_ID = @"bnc_session_id"; +static NSString * const BRANCH_PREFS_KEY_IDENTITY_ID = @"bnc_identity_id"; +static NSString * const BRANCH_PREFS_KEY_IDENTITY = @"bnc_identity"; +static NSString * const BRANCH_PREFS_KEY_CHECKED_FACEBOOK_APP_LINKS = @"bnc_checked_fb_app_links"; +static NSString * const BRANCH_PREFS_KEY_CHECKED_APPLE_SEARCH_ADS = @"bnc_checked_apple_search_ads"; +static NSString * const BRANCH_PREFS_KEY_APPLE_SEARCH_ADS_INFO = @"bnc_apple_search_ads_info"; +static NSString * const BRANCH_PREFS_KEY_LINK_CLICK_IDENTIFIER = @"bnc_link_click_identifier"; +static NSString * const BRANCH_PREFS_KEY_SPOTLIGHT_IDENTIFIER = @"bnc_spotlight_identifier"; +static NSString * const BRANCH_PREFS_KEY_UNIVERSAL_LINK_URL = @"bnc_universal_link_url"; +static NSString * const BRANCH_PREFS_KEY_SESSION_PARAMS = @"bnc_session_params"; +static NSString * const BRANCH_PREFS_KEY_INSTALL_PARAMS = @"bnc_install_params"; +static NSString * const BRANCH_PREFS_KEY_USER_URL = @"bnc_user_url"; +static NSString * const BRANCH_PREFS_KEY_BRANCH_UNIVERSAL_LINK_DOMAINS = @"branch_universal_link_domains"; + +static NSString * const BRANCH_PREFS_KEY_CREDITS = @"bnc_credits"; +static NSString * const BRANCH_PREFS_KEY_CREDIT_BASE = @"bnc_credit_base_"; + +static NSString * const BRANCH_PREFS_KEY_BRANCH_VIEW_USAGE_CNT = @"bnc_branch_view_usage_cnt_"; +static NSString * const BRANCH_PREFS_KEY_ANALYTICAL_DATA = @"bnc_branch_analytical_data"; +static NSString * const BRANCH_PREFS_KEY_ANALYTICS_MANIFEST = @"bnc_branch_analytics_manifest"; + +@interface BNCPreferenceHelper () { + NSOperationQueue *_persistPrefsQueue; + NSString *_lastSystemBuildVersion; + NSString *_browserUserAgentString; + NSString *_branchAPIURL; + NSString *_referringURL; +} + +@property (strong, nonatomic) NSMutableDictionary *persistenceDict; +@property (strong, nonatomic) NSMutableDictionary *creditsDictionary; +@property (strong, nonatomic) NSMutableDictionary *requestMetadataDictionary; +@property (strong, nonatomic) NSMutableDictionary *instrumentationDictionary; +@end + +@implementation BNCPreferenceHelper + +@synthesize + lastRunBranchKey = _lastRunBranchKey, + appVersion = _appVersion, + deviceFingerprintID = _deviceFingerprintID, + sessionID = _sessionID, + spotlightIdentifier = _spotlightIdentifier, + identityID = _identityID, + linkClickIdentifier = _linkClickIdentifier, + userUrl = _userUrl, + userIdentity = _userIdentity, + sessionParams = _sessionParams, + installParams = _installParams, + universalLinkUrl = _universalLinkUrl, + externalIntentURI = _externalIntentURI, + isDebug = _isDebug, + shouldWaitForInit = _shouldWaitForInit, + retryCount = _retryCount, + retryInterval = _retryInterval, + timeout = _timeout, + lastStrongMatchDate = _lastStrongMatchDate, + checkedFacebookAppLinks = _checkedFacebookAppLinks, + checkedAppleSearchAdAttribution = _checkedAppleSearchAdAttribution, + appleSearchAdDetails = _appleSearchAdDetails, + requestMetadataDictionary = _requestMetadataDictionary, + instrumentationDictionary = _instrumentationDictionary; + ++ (BNCPreferenceHelper *)preferenceHelper { + static BNCPreferenceHelper *preferenceHelper; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + preferenceHelper = [[BNCPreferenceHelper alloc] init]; + }); + + return preferenceHelper; +} + +- (id)init { + self = [super init]; + if (!self) return self; + + _timeout = DEFAULT_TIMEOUT; + _retryCount = DEFAULT_RETRY_COUNT; + _retryInterval = DEFAULT_RETRY_INTERVAL; + _isDebug = NO; + _persistPrefsQueue = [[NSOperationQueue alloc] init]; + _persistPrefsQueue.maxConcurrentOperationCount = 1; + + return self; +} + ++ (BNCPreferenceHelper *)getInstance { + static BNCPreferenceHelper *preferenceHelper; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + preferenceHelper = [[BNCPreferenceHelper alloc] init]; + }); + + return preferenceHelper; +} + +- (void) synchronize { + [_persistPrefsQueue waitUntilAllOperationsAreFinished]; +} + +- (void) dealloc { + [self synchronize]; +} + +#pragma mark - API methods + +- (void) setBranchAPIURL:(NSString*)branchAPIURL_ { + @synchronized (self) { + _branchAPIURL = [branchAPIURL_ copy]; + } +} + +- (NSString*) branchAPIURL { + @synchronized (self) { + if (!_branchAPIURL) { + _branchAPIURL = [BNC_API_BASE_URL copy]; + } + return _branchAPIURL; + } +} + +- (NSString *)getAPIBaseURL { + @synchronized (self) { + return [NSString stringWithFormat:@"%@/%@/", self.branchAPIURL, BNC_API_VERSION]; + } +} + +- (NSString *)getAPIURL:(NSString *) endpoint { + return [[self getAPIBaseURL] stringByAppendingString:endpoint]; +} + +- (NSString *)getEndpointFromURL:(NSString *)url { + NSString *APIBase = self.branchAPIURL; + if ([url hasPrefix:APIBase]) { + NSUInteger index = APIBase.length; + return [url substringFromIndex:index]; + } + return @""; +} + +#pragma mark - Preference Storage + +- (NSString *)lastRunBranchKey { + if (!_lastRunBranchKey) { + _lastRunBranchKey = [self readStringFromDefaults:BRANCH_PREFS_KEY_LAST_RUN_BRANCH_KEY]; + } + return _lastRunBranchKey; +} + +- (void)setLastRunBranchKey:(NSString *)lastRunBranchKey { + if (![_lastRunBranchKey isEqualToString:lastRunBranchKey]) { + _lastRunBranchKey = lastRunBranchKey; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_LAST_RUN_BRANCH_KEY value:lastRunBranchKey]; + } +} + +- (NSDate *)lastStrongMatchDate { + if (!_lastStrongMatchDate) { + _lastStrongMatchDate = (NSDate *)[self readObjectFromDefaults:BRANCH_PREFS_KEY_LAST_STRONG_MATCH_DATE]; + } + return _lastStrongMatchDate; +} + +- (void)setLastStrongMatchDate:(NSDate *)lastStrongMatchDate { + if (lastStrongMatchDate == nil || ![_lastStrongMatchDate isEqualToDate:lastStrongMatchDate]) { + _lastStrongMatchDate = lastStrongMatchDate; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_LAST_STRONG_MATCH_DATE value:lastStrongMatchDate]; + } +} + +- (NSString *)appVersion { + if (!_appVersion) { + _appVersion = [self readStringFromDefaults:BRANCH_PREFS_KEY_APP_VERSION]; + } + return _appVersion; +} + +- (void)setAppVersion:(NSString *)appVersion { + if (![_appVersion isEqualToString:appVersion]) { + _appVersion = appVersion; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_APP_VERSION value:appVersion]; + } +} + +- (NSString *)deviceFingerprintID { + if (!_deviceFingerprintID) { + _deviceFingerprintID = [self readStringFromDefaults:BRANCH_PREFS_KEY_DEVICE_FINGERPRINT_ID]; + } + + return _deviceFingerprintID; +} + +- (void)setDeviceFingerprintID:(NSString *)deviceFingerprintID { + if (deviceFingerprintID == nil || ![_deviceFingerprintID isEqualToString:deviceFingerprintID]) { + _deviceFingerprintID = deviceFingerprintID; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_DEVICE_FINGERPRINT_ID value:deviceFingerprintID]; + } +} + +- (NSString *)sessionID { + if (!_sessionID) { + _sessionID = [self readStringFromDefaults:BRANCH_PREFS_KEY_SESSION_ID]; + } + + return _sessionID; +} + +- (void)setSessionID:(NSString *)sessionID { + if (sessionID == nil || ![_sessionID isEqualToString:sessionID]) { + _sessionID = sessionID; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_SESSION_ID value:sessionID]; + } +} + +- (NSString *)identityID { + return [self readStringFromDefaults:BRANCH_PREFS_KEY_IDENTITY_ID]; +} + +- (void)setIdentityID:(NSString *)identityID { + [self writeObjectToDefaults:BRANCH_PREFS_KEY_IDENTITY_ID value:identityID]; +} + +- (NSString *)userIdentity { + return [self readStringFromDefaults:BRANCH_PREFS_KEY_IDENTITY]; +} + +- (void)setUserIdentity:(NSString *)userIdentity { + [self writeObjectToDefaults:BRANCH_PREFS_KEY_IDENTITY value:userIdentity]; +} + +- (NSString *)linkClickIdentifier { + return [self readStringFromDefaults:BRANCH_PREFS_KEY_LINK_CLICK_IDENTIFIER]; +} + +- (void)setLinkClickIdentifier:(NSString *)linkClickIdentifier { + [self writeObjectToDefaults:BRANCH_PREFS_KEY_LINK_CLICK_IDENTIFIER value:linkClickIdentifier]; +} + +- (NSString *)spotlightIdentifier { + return [self readStringFromDefaults:BRANCH_PREFS_KEY_SPOTLIGHT_IDENTIFIER]; +} + +- (void)setSpotlightIdentifier:(NSString *)spotlightIdentifier { + [self writeObjectToDefaults:BRANCH_PREFS_KEY_SPOTLIGHT_IDENTIFIER value:spotlightIdentifier]; +} + +- (NSString *)externalIntentURI { + @synchronized(self) { + if (!_externalIntentURI) { + _externalIntentURI = [self readStringFromDefaults:BRANCH_REQUEST_KEY_EXTERNAL_INTENT_URI]; + } + return _externalIntentURI; + } +} + +- (void)setExternalIntentURI:(NSString *)externalIntentURI { + @synchronized(self) { + if (externalIntentURI == nil || ![_externalIntentURI isEqualToString:externalIntentURI]) { + _externalIntentURI = externalIntentURI; + [self writeObjectToDefaults:BRANCH_REQUEST_KEY_EXTERNAL_INTENT_URI value:externalIntentURI]; + } + } +} + +- (NSString*) referringURL { + @synchronized (self) { + if (!_referringURL) _referringURL = [self readStringFromDefaults:@"referringURL"]; + return _referringURL; + } +} + +- (void) setReferringURL:(NSString *)referringURL { + @synchronized (self) { + _referringURL = [referringURL copy]; + [self writeObjectToDefaults:@"referringURL" value:_referringURL]; + } +} + +- (NSString *)universalLinkUrl { + return [self readStringFromDefaults:BRANCH_PREFS_KEY_UNIVERSAL_LINK_URL]; +} + +- (void)setUniversalLinkUrl:(NSString *)universalLinkUrl { + [self writeObjectToDefaults:BRANCH_PREFS_KEY_UNIVERSAL_LINK_URL value:universalLinkUrl]; +} + +- (NSString *)sessionParams { + @synchronized (self) { + if (!_sessionParams) { + _sessionParams = [self readStringFromDefaults:BRANCH_PREFS_KEY_SESSION_PARAMS]; + } + return _sessionParams; + } +} + +- (void)setSessionParams:(NSString *)sessionParams { + @synchronized (self) { + if (sessionParams == nil || ![_sessionParams isEqualToString:sessionParams]) { + _sessionParams = sessionParams; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_SESSION_PARAMS value:sessionParams]; + } + } +} + +- (NSString *)installParams { + @synchronized(self) { + if (!_installParams) { + id installParamsFromCache = [self readStringFromDefaults:BRANCH_PREFS_KEY_INSTALL_PARAMS]; + if ([installParamsFromCache isKindOfClass:[NSString class]]) { + _installParams = [self readStringFromDefaults:BRANCH_PREFS_KEY_INSTALL_PARAMS]; + } + else if ([installParamsFromCache isKindOfClass:[NSDictionary class]]) { + [self writeObjectToDefaults:BRANCH_PREFS_KEY_INSTALL_PARAMS value:nil]; + } + } + return _installParams; + } +} + +- (void)setInstallParams:(NSString *)installParams { + @synchronized(self) { + if ([installParams isKindOfClass:[NSDictionary class]]) { + _installParams = [BNCEncodingUtils encodeDictionaryToJsonString:(NSDictionary *)installParams]; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_INSTALL_PARAMS value:_installParams]; + return; + } + if (installParams == nil || ![_installParams isEqualToString:installParams]) { + _installParams = installParams; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_INSTALL_PARAMS value:installParams]; + } + } +} + +- (void) setAppleSearchAdDetails:(NSDictionary*)details { + if (details == nil || [details isKindOfClass:[NSDictionary class]]) { + _appleSearchAdDetails = details; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_APPLE_SEARCH_ADS_INFO value:details]; + } +} + +- (NSDictionary*) appleSearchAdDetails { + if (!_appleSearchAdDetails) { + _appleSearchAdDetails = (NSDictionary *) [self readObjectFromDefaults:BRANCH_PREFS_KEY_APPLE_SEARCH_ADS_INFO]; + } + return [_appleSearchAdDetails isKindOfClass:[NSDictionary class]] ? _appleSearchAdDetails : nil; +} + +- (void) setAppleSearchAdNeedsSend:(BOOL)appleSearchAdNeedsSend { + [self writeBoolToDefaults:@"_appleSearchAdNeedsSend" value:appleSearchAdNeedsSend]; +} + +- (BOOL) appleSearchAdNeedsSend { + return [self readBoolFromDefaults:@"_appleSearchAdNeedsSend"]; +} + +- (NSString*) lastSystemBuildVersion { + if (!_lastSystemBuildVersion) { + _lastSystemBuildVersion = [self readStringFromDefaults:@"_lastSystemBuildVersion"]; + } + return _lastSystemBuildVersion; +} + +- (void) setLastSystemBuildVersion:(NSString *)lastSystemBuildVersion { + if (![_lastSystemBuildVersion isEqualToString:lastSystemBuildVersion]) { + _lastSystemBuildVersion = lastSystemBuildVersion; + [self writeObjectToDefaults:@"_lastSystemBuildVersion" value:_lastSystemBuildVersion]; + } +} + +- (NSString*) browserUserAgentString { + if (!_browserUserAgentString) { + _browserUserAgentString = [self readStringFromDefaults:@"_browserUserAgentString"]; + } + return _browserUserAgentString; +} + +- (void) setBrowserUserAgentString:(NSString *)browserUserAgentString { + if (![_browserUserAgentString isEqualToString:browserUserAgentString]) { + _browserUserAgentString = browserUserAgentString; + [self writeObjectToDefaults:@"_browserUserAgentString" value:_browserUserAgentString]; + } +} + +- (NSString *)userUrl { + if (!_userUrl) { + _userUrl = [self readStringFromDefaults:BRANCH_PREFS_KEY_USER_URL]; + } + + return _userUrl; +} + +- (void)setUserUrl:(NSString *)userUrl { + if (![_userUrl isEqualToString:userUrl]) { + _userUrl = userUrl; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_USER_URL value:userUrl]; + } +} + +- (NSMutableString*) sanitizedMutableBaseURL:(NSString*)baseUrl_ { + NSMutableString *baseUrl = [baseUrl_ mutableCopy]; + if (self.trackingDisabled) { + NSString *id_string = [NSString stringWithFormat:@"%%24identity_id=%@", self.identityID]; + NSRange range = [baseUrl rangeOfString:id_string]; + if (range.location != NSNotFound) [baseUrl replaceCharactersInRange:range withString:@""]; + } else + if ([baseUrl hasSuffix:@"&"] || [baseUrl hasSuffix:@"?"]) { + } else + if ([baseUrl bnc_containsString:@"?"]) { + [baseUrl appendString:@"&"]; + } + else { + [baseUrl appendString:@"?"]; + } + return baseUrl; +} + +- (BOOL)checkedAppleSearchAdAttribution { + _checkedAppleSearchAdAttribution = [self readBoolFromDefaults:BRANCH_PREFS_KEY_CHECKED_APPLE_SEARCH_ADS]; + return _checkedAppleSearchAdAttribution; +} + +- (void)setCheckedAppleSearchAdAttribution:(BOOL)checked { + _checkedAppleSearchAdAttribution = checked; + [self writeBoolToDefaults:BRANCH_PREFS_KEY_CHECKED_APPLE_SEARCH_ADS value:checked]; +} + + +- (BOOL)checkedFacebookAppLinks { + _checkedFacebookAppLinks = [self readBoolFromDefaults:BRANCH_PREFS_KEY_CHECKED_FACEBOOK_APP_LINKS]; + return _checkedFacebookAppLinks; +} + +- (void)setCheckedFacebookAppLinks:(BOOL)checked { + _checkedFacebookAppLinks = checked; + [self writeBoolToDefaults:BRANCH_PREFS_KEY_CHECKED_FACEBOOK_APP_LINKS value:checked]; +} + +- (void)clearUserCreditsAndCounts { + self.creditsDictionary = [[NSMutableDictionary alloc] init]; +} + +- (id)getBranchUniversalLinkDomains { + return [[[NSBundle mainBundle] infoDictionary] objectForKey:BRANCH_PREFS_KEY_BRANCH_UNIVERSAL_LINK_DOMAINS]; +} + +- (NSMutableDictionary *)requestMetadataDictionary { + if (!_requestMetadataDictionary) { + _requestMetadataDictionary = [NSMutableDictionary dictionary]; + } + return _requestMetadataDictionary; +} + +- (void)setRequestMetadataKey:(NSString *)key value:(NSObject *)value { + if (!key) { + return; + } + if ([self.requestMetadataDictionary objectForKey:key] && !value) { + [self.requestMetadataDictionary removeObjectForKey:key]; + } + else if (value) { + [self.requestMetadataDictionary setObject:value forKey:key]; + } +} + +- (NSMutableDictionary *)instrumentationDictionary { + @synchronized (self) { + if (!_instrumentationDictionary) { + _instrumentationDictionary = [NSMutableDictionary dictionary]; + } + return _instrumentationDictionary; + } +} + +- (void)addInstrumentationDictionaryKey:(NSString *)key value:(NSString *)value { + @synchronized (self) { + if (key && value) { + [self.instrumentationDictionary setObject:value forKey:key]; + } + } +} + +- (void)clearInstrumentationDictionary { + @synchronized (self) { + NSArray *keys = [_instrumentationDictionary allKeys]; + for (NSUInteger i = 0 ; i < [keys count]; i++) { + [_instrumentationDictionary removeObjectForKey:keys[i]]; + } + } +} + +- (BOOL) limitFacebookTracking { + @synchronized (self) { + return [self readBoolFromDefaults:@"_limitFacebookTracking"]; + } +} + +- (void) setLimitFacebookTracking:(BOOL)limitFacebookTracking { + @synchronized (self) { + [self writeBoolToDefaults:@"_limitFacebookTracking" value:limitFacebookTracking]; + } +} + +- (NSDate*) previousAppBuildDate { + @synchronized (self) { + NSDate *date = (NSDate*) [self readObjectFromDefaults:@"_previousAppBuildDate"]; + if ([date isKindOfClass:[NSDate class]]) return date; + return nil; + } +} + +- (void) setPreviousAppBuildDate:(NSDate*)date { + @synchronized (self) { + if (date == nil || [date isKindOfClass:[NSDate class]]) + [self writeObjectToDefaults:@"_previousAppBuildDate" value:date]; + } +} + +- (NSArray*) URLBlackList { + @synchronized(self) { + id a = [self readObjectFromDefaults:@"URLBlackList"]; + if ([a isKindOfClass:NSArray.class]) return a; + return nil; + } +} + +- (void) setURLBlackList:(NSArray *)URLBlackList { + @synchronized(self) { + [self writeObjectToDefaults:@"URLBlackList" value:URLBlackList]; + } +} + +- (NSInteger) URLBlackListVersion { + @synchronized(self) { + return [self readIntegerFromDefaults:@"URLBlackListVersion"]; + } +} + +- (void) setURLBlackListVersion:(NSInteger)URLBlackListVersion { + @synchronized(self) { + [self writeIntegerToDefaults:@"URLBlackListVersion" value:URLBlackListVersion]; + } +} + +- (BOOL) blacklistURLOpen { + @synchronized(self) { + return [self readBoolFromDefaults:@"blacklistURLOpen"]; + } +} + +- (void) setBlacklistURLOpen:(BOOL)value { + @synchronized(self) { + [self writeBoolToDefaults:@"blacklistURLOpen" value:value]; + } +} + + +- (BOOL) trackingDisabled { + @synchronized(self) { + NSNumber *b = (id) [self readObjectFromDefaults:@"trackingDisabled"]; + if ([b isKindOfClass:NSNumber.class]) return [b boolValue]; + return false; + } +} + +- (void) setTrackingDisabled:(BOOL)disabled { + @synchronized(self) { + NSNumber *b = [NSNumber numberWithBool:disabled]; + [self writeObjectToDefaults:@"trackingDisabled" value:b]; + if (disabled) [self clearTrackingInformation]; + } +} + +- (void) clearTrackingInformation { + @synchronized(self) { + /* Don't clear these: + self.deviceFingerprintID = nil; + self.userIdentity = nil; + self.identityID = nil; + */ + self.sessionID = nil; + self.linkClickIdentifier = nil; + self.spotlightIdentifier = nil; + self.referringURL = nil; + self.universalLinkUrl = nil; + self.installParams = nil; + self.appleSearchAdDetails = nil; + self.appleSearchAdNeedsSend = NO; + self.sessionParams = nil; + self.externalIntentURI = nil; + self.savedAnalyticsData = nil; + self.previousAppBuildDate = nil; + self.requestMetadataDictionary = nil; + self.lastStrongMatchDate = nil; + } +} + +#pragma mark - Credit Storage + +- (NSMutableDictionary *)creditsDictionary { + if (!_creditsDictionary) { + _creditsDictionary = [[self readObjectFromDefaults:BRANCH_PREFS_KEY_CREDITS] mutableCopy]; + + if (!_creditsDictionary) { + _creditsDictionary = [[NSMutableDictionary alloc] init]; + } + } + + return _creditsDictionary; +} + +- (void)setCreditCount:(NSInteger)count { + [self setCreditCount:count forBucket:@"default"]; +} + +- (void)setCreditCount:(NSInteger)count forBucket:(NSString *)bucket { + self.creditsDictionary[[BRANCH_PREFS_KEY_CREDIT_BASE stringByAppendingString:bucket]] = @(count); + + [self writeObjectToDefaults:BRANCH_PREFS_KEY_CREDITS value:self.creditsDictionary]; +} + +- (void)removeCreditCountForBucket:(NSString *)bucket { + NSMutableDictionary *dictToWrite = self.creditsDictionary; + [dictToWrite removeObjectForKey:[BRANCH_PREFS_KEY_CREDIT_BASE stringByAppendingString:bucket]]; + + [self writeObjectToDefaults:BRANCH_PREFS_KEY_CREDITS value:self.creditsDictionary]; +} + +- (NSDictionary *)getCreditDictionary { + NSMutableDictionary *returnDictionary = [[NSMutableDictionary alloc] init]; + for(NSString *key in self.creditsDictionary) { + NSString *cleanKey = [key stringByReplacingOccurrencesOfString:BRANCH_PREFS_KEY_CREDIT_BASE + withString:@""]; + returnDictionary[cleanKey] = self.creditsDictionary[key]; + } + return returnDictionary; +} + +- (NSInteger)getCreditCount { + return [self getCreditCountForBucket:@"default"]; +} + +- (NSInteger)getCreditCountForBucket:(NSString *)bucket { + return [self.creditsDictionary[[BRANCH_PREFS_KEY_CREDIT_BASE stringByAppendingString:bucket]] integerValue]; +} + +- (void)clearUserCredits { + self.creditsDictionary = [[NSMutableDictionary alloc] init]; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_CREDITS value:self.creditsDictionary]; +} + +#pragma mark - Count Storage + +- (void)updateBranchViewCount:(NSString *)branchViewID { + NSInteger currentCount = [self getBranchViewCount:branchViewID] + 1; + [self writeObjectToDefaults:[BRANCH_PREFS_KEY_BRANCH_VIEW_USAGE_CNT stringByAppendingString:branchViewID] value:@(currentCount)]; +} + +- (NSInteger)getBranchViewCount:(NSString *)branchViewID { + NSInteger count = [self readIntegerFromDefaults:[BRANCH_PREFS_KEY_BRANCH_VIEW_USAGE_CNT stringByAppendingString:branchViewID]]; + if (count == NSNotFound){ + count = 0; + } + return count; +} + +- (void)saveBranchAnalyticsData:(NSDictionary *)analyticsData { + if (_sessionID) { + if (!_savedAnalyticsData) { + _savedAnalyticsData = [self getBranchAnalyticsData]; + } + NSMutableArray *viewDataArray = [_savedAnalyticsData objectForKey:_sessionID]; + if (!viewDataArray) { + viewDataArray = [[NSMutableArray alloc] init]; + [_savedAnalyticsData setObject:viewDataArray forKey:_sessionID]; + } + [viewDataArray addObject:analyticsData]; + [self writeObjectToDefaults:BRANCH_PREFS_KEY_ANALYTICAL_DATA value:_savedAnalyticsData]; + } +} + +- (void)clearBranchAnalyticsData { + [self writeObjectToDefaults:BRANCH_PREFS_KEY_ANALYTICAL_DATA value:nil]; + _savedAnalyticsData = nil; +} + +- (NSMutableDictionary *)getBranchAnalyticsData { + NSMutableDictionary *analyticsDataObj = _savedAnalyticsData; + if (!analyticsDataObj) { + analyticsDataObj = (NSMutableDictionary *)[self readObjectFromDefaults:BRANCH_PREFS_KEY_ANALYTICAL_DATA]; + if (!analyticsDataObj) { + analyticsDataObj = [[NSMutableDictionary alloc] init]; + } + } + return analyticsDataObj; +} + +- (void)saveContentAnalyticsManifest:(NSDictionary *)cdManifest { + [self writeObjectToDefaults:BRANCH_PREFS_KEY_ANALYTICS_MANIFEST value:cdManifest]; +} + +- (NSDictionary *)getContentAnalyticsManifest { + return (NSDictionary *)[self readObjectFromDefaults:BRANCH_PREFS_KEY_ANALYTICS_MANIFEST]; +} + +#pragma mark - Writing To Persistence + +- (void)writeIntegerToDefaults:(NSString *)key value:(NSInteger)value { + [self writeObjectToDefaults:key value:@(value)]; +} + +- (void)writeBoolToDefaults:(NSString *)key value:(BOOL)value { + [self writeObjectToDefaults:key value:@(value)]; +} + +- (void)writeObjectToDefaults:(NSString *)key value:(NSObject *)value { + @synchronized (self) { + if (value) { + self.persistenceDict[key] = value; + } + else { + [self.persistenceDict removeObjectForKey:key]; + } + [self persistPrefsToDisk]; + } +} + +- (void)persistPrefsToDisk { + @synchronized (self) { + if (!self.persistenceDict) return; + NSData *data = nil; + @try { + data = [NSKeyedArchiver archivedDataWithRootObject:self.persistenceDict]; + } + @catch (id exception) { + data = nil; + BNCLogWarning(@"Exception creating preferences data: %@.", exception); + } + if (!data) { + BNCLogWarning(@"Can't create preferences data."); + return; + } + NSURL *prefsURL = [self.class.URLForPrefsFile copy]; + NSBlockOperation *newPersistOp = [NSBlockOperation blockOperationWithBlock:^ { + NSError *error = nil; + [data writeToURL:prefsURL options:NSDataWritingAtomic error:&error]; + if (error) { + BNCLogWarning(@"Failed to persist preferences: %@.", error); + } + }]; + [_persistPrefsQueue addOperation:newPersistOp]; + } +} + ++ (void) clearAll { + NSURL *prefsURL = [self.URLForPrefsFile copy]; + if (prefsURL) [[NSFileManager defaultManager] removeItemAtURL:prefsURL error:nil]; +} + +#pragma mark - Reading From Persistence + +- (NSMutableDictionary *)persistenceDict { + @synchronized(self) { + if (!_persistenceDict) { + NSDictionary *persistenceDict = nil; + @try { + NSError *error = nil; + NSData *data = [NSData dataWithContentsOfURL:self.class.URLForPrefsFile + options:0 error:&error]; + if (!error && data) + persistenceDict = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + } + @catch (NSException*) { + BNCLogWarning(@"Failed to load preferences from storage."); + } + + if ([persistenceDict isKindOfClass:[NSDictionary class]]) + _persistenceDict = [persistenceDict mutableCopy]; + else + _persistenceDict = [[NSMutableDictionary alloc] init]; + } + return _persistenceDict; + } +} + +- (NSObject *)readObjectFromDefaults:(NSString *)key { + @synchronized(self) { + NSObject *obj = self.persistenceDict[key]; + return obj; + } +} + +- (NSString *)readStringFromDefaults:(NSString *)key { + @synchronized(self) { + id str = self.persistenceDict[key]; + if ([str isKindOfClass:[NSNumber class]]) { + str = [str stringValue]; + } + return str; + } +} + +- (BOOL)readBoolFromDefaults:(NSString *)key { + @synchronized(self) { + BOOL boo = [self.persistenceDict[key] boolValue]; + return boo; + } +} + +- (NSInteger)readIntegerFromDefaults:(NSString *)key { + @synchronized(self) { + NSNumber *number = self.persistenceDict[key]; + if (number != nil) { + return [number integerValue]; + } + return NSNotFound; + } +} + +#pragma mark - Preferences File URL + ++ (NSString *)prefsFile_deprecated { + NSString * path = + [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) + firstObject] + stringByAppendingPathComponent:BRANCH_PREFS_FILE]; + return path; +} + ++ (NSURL* _Nonnull) URLForPrefsFile { + NSURL *URL = BNCURLForBranchDirectory(); + URL = [URL URLByAppendingPathComponent:BRANCH_PREFS_FILE isDirectory:NO]; + return URL; +} + ++ (void) moveOldPrefsFile { + NSString* oldPath = self.prefsFile_deprecated; + NSURL *oldURL = (oldPath) ? [NSURL fileURLWithPath:self.prefsFile_deprecated] : nil; + NSURL *newURL = [self URLForPrefsFile]; + + if (!oldURL || !newURL) { return; } + + NSError *error = nil; + [[NSFileManager defaultManager] + moveItemAtURL:oldURL + toURL:newURL + error:&error]; + + if (error && error.code != NSFileNoSuchFileError) { + if (error.code == NSFileWriteFileExistsError) { + [[NSFileManager defaultManager] + removeItemAtURL:oldURL + error:&error]; + } else { + BNCLogError(@"Can't move prefs file: %@.", error); + } + } +} + ++ (void) initialize { + if (self == [BNCPreferenceHelper self]) { + [self moveOldPrefsFile]; + } +} + +@end + +#pragma mark - BNCURLForBranchDirectory + +NSURL* _Null_unspecified BNCCreateDirectoryForBranchURLWithSearchPath_Unthreaded(NSSearchPathDirectory directory) { + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSArray *URLs = [fileManager URLsForDirectory:directory inDomains:NSUserDomainMask | NSLocalDomainMask]; + + for (NSURL *URL in URLs) { + NSError *error = nil; + NSURL *branchURL = [[NSURL alloc] initWithString:@"io.branch" relativeToURL:URL]; + BOOL success = + [fileManager + createDirectoryAtURL:branchURL + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (success) { + return branchURL; + } else { + NSLog(@"[branch.io] Info: CreateBranchURL failed: %@ URL: %@.", error, branchURL); + } + } + return nil; +} + +NSURL* _Nonnull BNCURLForBranchDirectory_Unthreaded() { + NSArray *kSearchDirectories = @[ + @(NSApplicationSupportDirectory), + @(NSLibraryDirectory), + @(NSCachesDirectory), + @(NSDocumentDirectory), + ]; + + for (NSNumber *directory in kSearchDirectories) { + NSSearchPathDirectory directoryValue = [directory unsignedLongValue]; + NSURL *URL = BNCCreateDirectoryForBranchURLWithSearchPath_Unthreaded(directoryValue); + if (URL) return URL; + } + + // Worst case backup plan: + NSString *path = [@"~/Library/io.branch" stringByExpandingTildeInPath]; + NSURL *branchURL = [NSURL fileURLWithPath:path isDirectory:YES]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSError *error = nil; + BOOL success = + [fileManager + createDirectoryAtURL:branchURL + withIntermediateDirectories:YES + attributes:nil + error:&error]; + if (!success) { + NSLog(@"[io.branch] Error: Worst case CreateBranchURL error was: %@ URL: %@.", error, branchURL); + } + return branchURL; +} + +NSURL* _Nonnull BNCURLForBranchDirectory() { + static NSURL *urlForBranchDirectory = nil; + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^ { + urlForBranchDirectory = BNCURLForBranchDirectory_Unthreaded(); + }); + return urlForBranchDirectory; +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSpotlightService.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSpotlightService.h new file mode 100644 index 00000000000..be3cac3515f --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSpotlightService.h @@ -0,0 +1,38 @@ +// +// BNCSpotlightService.h +// Branch-SDK +// +// Created by Parth Kalavadia on 8/10/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif +@class BranchUniversalObject, BranchLinkProperties; + +@interface BNCSpotlightService : NSObject + +//Indexing API +//indexPublicaly +- (void)indexWithBranchUniversalObject:(BranchUniversalObject* _Nonnull)universalObject + linkProperties:(BranchLinkProperties* _Nullable)linkProperties + callback:(void (^_Nullable)(BranchUniversalObject * _Nullable universalObject, + NSString* _Nullable url, + NSError * _Nullable error))completion; +//indexWithBranchShareLink +- (void)indexPrivatelyWithBranchUniversalObjects:(NSArray* _Nonnull)universalObjects + completion:(void (^_Nullable) (NSArray* _Nullable, + NSError* _Nullable))completion; + +//Remove indexing API +- (void)removeSearchableItemsWithIdentifier:(NSString * _Nonnull)identifier + callback:(void (^_Nullable)(NSError * _Nullable error))completion; + +- (void)removeSearchableItemsWithIdentifiers:(NSArray *_Nonnull)identifiers + callback:(void (^_Nullable)(NSError * _Nullable error))completion; + +- (void)removeAllBranchSearchableItemsWithCallback:(void (^_Nullable)(NSError * _Nullable error))completion; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSpotlightService.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSpotlightService.m new file mode 100644 index 00000000000..3ae6826cd3a --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSpotlightService.m @@ -0,0 +1,510 @@ +// +// BNCSpotlightService.m +// Branch-SDK +// +// Created by Parth Kalavadia on 8/10/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#import "BNCSpotlightService.h" +#import "Branch.h" +#import "BNCSystemObserver.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + +#pragma mark Defensive Declarations + +#ifndef CSSearchableItemActivityIdentifier +#define CSSearchableItemActivityIdentifier @"kCSSearchableItemActivityIdentifier" +#endif + +@interface CSSearchableItemAttributeSetDummyClass : NSObject +- (void) setKeywords:(NSArray*)keywords; +- (void) setWeakRelatedUniqueIdentifier:(NSString*)uniqueIdentifier; +@end + +static NSString* const kUTTypeGeneric = @"public.content"; +static NSString* const kDomainIdentifier = @"io.branch.sdk.spotlight"; + +#pragma mark - BNCSpotlightService + +@interface BNCSpotlightService() { + dispatch_queue_t _workQueue; +} +@property (strong, nonatomic) NSMutableDictionary *userInfo; +@property (strong, readonly) dispatch_queue_t workQueue; +@end + +#pragma mark - BNCSpotlightService + +@implementation BNCSpotlightService + +- (void)indexWithBranchUniversalObject:(BranchUniversalObject* _Nonnull)universalObject + linkProperties:(BranchLinkProperties* _Nullable)linkProperties + callback:(void (^_Nullable)(BranchUniversalObject * _Nullable universalObject, + NSString* _Nullable url, + NSError * _Nullable error))completion { + if ([BNCSystemObserver getOSVersion].floatValue < 9.0) { + NSError *error = [NSError branchErrorWithCode:BNCSpotlightNotAvailableError]; + if (completion) { + completion(universalObject,nil,error); + } + return; + } + if (!universalObject.title) { + NSError *error = [NSError branchErrorWithCode:BNCSpotlightTitleError]; + if (completion) { + completion(universalObject,[BNCPreferenceHelper preferenceHelper].userUrl, error); + } + return; + } + + BranchLinkProperties* spotlightLinkProperties; + if (linkProperties == nil) { + spotlightLinkProperties = [[BranchLinkProperties alloc] init]; + } + [spotlightLinkProperties setFeature:BNCSpotlightFeature]; + + NSURL* thumbnailUrl = [NSURL URLWithString:universalObject.imageUrl]; + BOOL thumbnailIsRemote = thumbnailUrl && ![thumbnailUrl isFileURL]; + + if (thumbnailIsRemote) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *thumbnailData = [NSData dataWithContentsOfURL:thumbnailUrl]; + dispatch_async(dispatch_get_main_queue(), ^{ + + [self indexContentWithBranchUniversalObject:universalObject + linkProperty:linkProperties + thumbnailUrl:thumbnailUrl + thumbnailData:thumbnailData + callback:^(NSString * _Nullable url, NSError * _Nullable error) { + if (completion) + completion(universalObject,url,error); + }]; + }); + }); + } + else { + [self indexContentWithBranchUniversalObject:universalObject + linkProperty:linkProperties + thumbnailUrl:thumbnailUrl + thumbnailData:nil + callback:^(NSString * _Nullable url, NSError * _Nullable error) { + if (completion) + completion(universalObject,url,error); + }]; + } +} + +- (void)indexContentWithBranchUniversalObject:(BranchUniversalObject*)universalObject + linkProperty:(BranchLinkProperties *)linkProperty + thumbnailUrl:(NSURL *)thumbnailUrl + thumbnailData:(NSData *)thumbnailData + callback:(void (^_Nullable)(NSString* _Nullable url, NSError * _Nullable error))completion { + + if (!linkProperty) { + linkProperty = [[BranchLinkProperties alloc] init]; + linkProperty.channel = @"Spotlight Search"; + } + + if (universalObject.locallyIndex) { + NSString *dynamicUrl = [universalObject getLongUrlWithChannel:nil + andTags:nil + andFeature:BNCSpotlightFeature + andStage:nil + andAlias:nil]; + + id attributes = [self attributeSetWithUniversalObject:universalObject + thumbnail:thumbnailData + url:dynamicUrl]; + NSDictionary *indexingParams = @{@"title": universalObject.title, + @"url": dynamicUrl, + @"spotlightId": dynamicUrl, + @"userInfo": [universalObject.contentMetadata.customMetadata mutableCopy], + @"keywords": [NSSet setWithArray:universalObject.keywords], + @"attributeSet": attributes + }; + + [self indexUsingSearchableItem:indexingParams + thumbnailData:thumbnailData + callback:^(NSString * _Nullable url, NSError * _Nullable error) { + if (completion) + completion(url,error); + }]; + + } else { + + __weak __typeof(self) weakSelf = self; + [universalObject getShortUrlWithLinkProperties:linkProperty + andCallback:^(NSString * _Nullable url, NSError * _Nullable error) { + __strong __typeof(self) strongSelf = weakSelf; + if (!strongSelf || !url || error) { + if (completion) completion([BNCPreferenceHelper preferenceHelper].userUrl, error); + return; + } + id attributes = + [strongSelf attributeSetWithUniversalObject:universalObject thumbnail:thumbnailData url:url]; + NSMutableDictionary *indexingParams = [NSMutableDictionary new]; + indexingParams[@"title"] = universalObject.title; + indexingParams[@"url"] = url; + indexingParams[@"spotlightId"] = url; + indexingParams[@"userInfo"] = [universalObject.contentMetadata.customMetadata mutableCopy]; + indexingParams[@"keywords"] = [NSSet setWithArray:universalObject.keywords]; + indexingParams[@"attributeSet"] = attributes; + [self indexUsingNSUserActivity:indexingParams]; + if (completion) completion(url, nil); + } + ]; + + } +} + +- (id)attributeSetWithUniversalObject:(BranchUniversalObject*)universalObject + thumbnail:(NSData*)thumbnailData + url:(NSString*)url { + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 + NSString *type = universalObject.contentMetadata.contentSchema ?: (NSString *)kUTTypeGeneric; + + id CSSearchableItemAttributeSetClass = NSClassFromString(@"CSSearchableItemAttributeSet"); + if (!CSSearchableItemAttributeSetClass) + return nil; + + id attributes = [CSSearchableItemAttributeSetClass alloc]; + if (!attributes || ![attributes respondsToSelector:@selector(initWithItemContentType:)]) + return nil; + attributes = [attributes initWithItemContentType:type]; + + #define safePerformSelector(_selector, parameter) { \ + if (parameter != nil && [attributes respondsToSelector:@selector(_selector)]) { \ + [attributes _selector parameter]; \ + } \ + } + + safePerformSelector(setTitle:, universalObject.title); + safePerformSelector(setContentDescription:, universalObject.contentDescription); + NSURL* thumbnailUrl = [NSURL URLWithString:universalObject.imageUrl]; + BOOL thumbnailIsRemote = thumbnailUrl && ![thumbnailUrl isFileURL]; + if (!thumbnailIsRemote) { + safePerformSelector(setThumbnailURL:, thumbnailUrl); + } + safePerformSelector(setThumbnailData:, thumbnailData); + safePerformSelector(setContentURL:, [NSURL URLWithString:url]); + if (universalObject.keywords && [attributes respondsToSelector:@selector(setKeywords:)]) + [((CSSearchableItemAttributeSetDummyClass*)attributes) setKeywords:universalObject.keywords]; + safePerformSelector(setWeakRelatedUniqueIdentifier:, universalObject.canonicalIdentifier); + + #undef safePerformSelector + + return attributes; + +#else + + return nil; + +#endif +} + +- (void)indexPrivatelyWithBranchUniversalObjects:(NSArray* _Nonnull)universalObjects + completion:(void (^_Nullable) (NSArray * _Nullable universalObjects, + NSError * _Nullable error))completion { + BOOL isIndexingAvailable = NO; + Class CSSearchableIndexClass = NSClassFromString(@"CSSearchableIndex"); + Class CSSearchableItemClass = NSClassFromString(@"CSSearchableItem"); + SEL isIndexingAvailableSelector = NSSelectorFromString(@"isIndexingAvailable"); + isIndexingAvailable = + ((BOOL (*)(id, SEL))[CSSearchableIndexClass methodForSelector:isIndexingAvailableSelector]) + (CSSearchableIndexClass, isIndexingAvailableSelector); + + #define IndexingNotAvailable() { \ + NSError *error = [NSError branchErrorWithCode:BNCSpotlightNotAvailableError];\ + if (completion) {\ + completion(nil,error);\ + }\ + return;\ + } + + if (!isIndexingAvailable || + !CSSearchableIndexClass || + ![CSSearchableIndexClass respondsToSelector:@selector(defaultSearchableIndex)] || + !CSSearchableItemClass) { + IndexingNotAvailable(); + } + dispatch_group_t workGroup = dispatch_group_create(); + NSMutableArray *searchableItems = [[NSMutableArray alloc] init]; + NSMutableDictionary *mapSpotlightIdentifier = + [[NSMutableDictionary alloc] init]; + + for (BranchUniversalObject* universalObject in universalObjects) { + dispatch_group_enter(workGroup); + NSString* dynamicUrl = [universalObject getLongUrlWithChannel:nil + andTags:nil + andFeature:BNCSpotlightFeature + andStage:nil andAlias:nil]; + if (!dynamicUrl) continue; + mapSpotlightIdentifier[dynamicUrl] = universalObject; + NSURL* thumbnailUrl = [NSURL URLWithString:universalObject.imageUrl]; + BOOL thumbnailIsRemote = thumbnailUrl && ![thumbnailUrl isFileURL]; + if (thumbnailIsRemote) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *thumbnailData = [NSData dataWithContentsOfURL:thumbnailUrl]; + dispatch_async(dispatch_get_main_queue(), ^{ + + id attributes = [self attributeSetWithUniversalObject:universalObject + thumbnail:thumbnailData + url:dynamicUrl]; + + id item = [CSSearchableItemClass alloc]; + item = [item initWithUniqueIdentifier:dynamicUrl + domainIdentifier:kDomainIdentifier + attributeSet:attributes]; + + [searchableItems addObject:item]; + dispatch_group_leave(workGroup); + }); + }); + } + else { + id attributes = [self attributeSetWithUniversalObject:universalObject + thumbnail:nil + url:dynamicUrl]; + id item = [CSSearchableItemClass alloc]; + item = [item initWithUniqueIdentifier:dynamicUrl + domainIdentifier:kDomainIdentifier + attributeSet:attributes]; + + [searchableItems addObject:item]; + dispatch_group_leave(workGroup); + } + } + + dispatch_group_notify(workGroup, dispatch_get_main_queue(), ^{ + id index = [CSSearchableIndexClass defaultSearchableIndex]; + + if (![index respondsToSelector:@selector(indexSearchableItems:completionHandler:)]) { + IndexingNotAvailable(); + } + + [index indexSearchableItems:searchableItems completionHandler:^(NSError * _Nullable error) { + if (!error) { + if (completion) + completion(universalObjects,nil); + } + else { + if (completion) + completion(nil,error); + } + + }]; + }); + #undef IndexingNotAvailable +} + +- (dispatch_queue_t) workQueue { + @synchronized (self) { + if (!_workQueue) + _workQueue = dispatch_queue_create("io.branch.sdk.spotlight.indexing", DISPATCH_QUEUE_CONCURRENT); + return _workQueue; + } +} + +- (void)indexUsingNSUserActivity:(NSDictionary *)params { + self.userInfo = params[@"userInfo"]; + self.userInfo[CSSearchableItemActivityIdentifier] = params[@"spotlightId"]; + UIViewController *activeViewController = [UIViewController bnc_currentViewController]; + if (!activeViewController) { + // if no view controller, don't index. Current use case: iMessage extensions + return; + } + NSString *uniqueIdentifier = [NSString stringWithFormat:@"io.branch.%@", [[NSBundle mainBundle] bundleIdentifier]]; + // Can't create any weak references here to the userActivity, otherwise it will not index. + activeViewController.userActivity = [[NSUserActivity alloc] initWithActivityType:uniqueIdentifier]; + activeViewController.userActivity.delegate = self; + activeViewController.userActivity.title = params[@"title"]; + activeViewController.userActivity.webpageURL = [NSURL URLWithString:params[@"url"]]; + activeViewController.userActivity.eligibleForSearch = YES; + activeViewController.userActivity.eligibleForPublicIndexing = YES; // TODO: Update with the new indexPublically. + activeViewController.userActivity.userInfo = self.userInfo; // This alone doesn't pass userInfo through + activeViewController.userActivity.requiredUserInfoKeys = [NSSet setWithArray:self.userInfo.allKeys]; // This along with the delegate method userActivityWillSave, however, seem to force the userInfo to come through. + activeViewController.userActivity.keywords = params[@"keywords"]; + SEL setContentAttributeSetSelector = NSSelectorFromString(@"setContentAttributeSet:"); + ((void (*)(id, SEL, id))[activeViewController.userActivity methodForSelector:setContentAttributeSetSelector])(activeViewController.userActivity, setContentAttributeSetSelector, params[@"attributeSet"]); + + [activeViewController.userActivity becomeCurrent]; + +} + +- (void)indexUsingSearchableItem:(NSDictionary*)indexingParam + thumbnailData:(NSData*)thumbnailData + callback:(void (^_Nullable)(NSString* _Nullable url, NSError * _Nullable error))completion { + + BOOL isIndexingAvailable = NO; + Class CSSearchableIndexClass = NSClassFromString(@"CSSearchableIndex"); + SEL isIndexingAvailableSelector = NSSelectorFromString(@"isIndexingAvailable"); + isIndexingAvailable = + ((BOOL (*)(id, SEL))[CSSearchableIndexClass methodForSelector:isIndexingAvailableSelector]) + (CSSearchableIndexClass, isIndexingAvailableSelector); + + #define IndexingNotAvalable() { \ + NSError *error = [NSError branchErrorWithCode:BNCSpotlightNotAvailableError];\ + if (completion) {\ + completion(nil,error);\ + }\ + return;\ + } + + if (!isIndexingAvailable) { + IndexingNotAvalable(); + } + + NSString *dynamicUrl = indexingParam[@"url"]; + Class CSSearchableItemClass = NSClassFromString(@"CSSearchableItem"); + + if (!CSSearchableItemClass) { + IndexingNotAvalable(); + } + + id item = [CSSearchableItemClass alloc]; + + if (!item || + ![item respondsToSelector:@selector(initWithUniqueIdentifier:domainIdentifier:attributeSet:)]) { + IndexingNotAvalable(); + } + + item = [item initWithUniqueIdentifier:indexingParam[@"url"] + domainIdentifier:kDomainIdentifier + attributeSet:indexingParam[@"attributeSet"]]; + + if (CSSearchableIndexClass && + [CSSearchableIndexClass respondsToSelector:@selector(defaultSearchableIndex)]){ + id defaultSearchableIndex = [CSSearchableIndexClass defaultSearchableIndex]; + if ([defaultSearchableIndex respondsToSelector:@selector(indexSearchableItems:completionHandler:)]) { + [defaultSearchableIndex indexSearchableItems:@[item] + completionHandler: ^(NSError * __nullable error) { + NSString *url = error == nil?dynamicUrl:nil; + if (completion) { + completion(url, error); + } + }]; + } + else { + IndexingNotAvalable(); + } + } + else { + IndexingNotAvalable(); + } + #undef IndexingNotAvalable +} + +#pragma mark - UserActivity Delegate Methods + +- (void)userActivityWillSave:(NSUserActivity *)userActivity { + [userActivity addUserInfoEntriesFromDictionary:self.userInfo]; +} + +#pragma remove private content from spotlight indexed using Searchable Item + +- (void)removeSearchableItemsWithIdentifier:(NSString * _Nonnull)identifier + callback:(void (^_Nullable)(NSError * _Nullable error))completion { + if (identifier == nil) { + NSError *error = [NSError branchErrorWithCode:BNCSpotlightIdentifierError + localizedMessage:@"Spotlight indentifier not available"]; + if (completion) completion(error); + return; + } + [self removeSearchableItemsWithIdentifiers:@[identifier] callback:^(NSError * _Nullable error) { + if (completion) + completion(error); + }]; +} + +- (void)removeSearchableItemsWithIdentifiers:(NSArray *_Nonnull)identifiers + callback:(void (^_Nullable)(NSError * _Nullable error))completion { + + BOOL isIndexingAvailable = NO; + Class CSSearchableIndexClass = NSClassFromString(@"CSSearchableIndex"); + SEL isIndexingAvailableSelector = NSSelectorFromString(@"isIndexingAvailable"); + isIndexingAvailable = + ((BOOL (*)(id, SEL))[CSSearchableIndexClass methodForSelector:isIndexingAvailableSelector]) + (CSSearchableIndexClass, isIndexingAvailableSelector); + + #define IndexingNotAvalable() { \ + NSError *error = [NSError branchErrorWithCode:BNCSpotlightNotAvailableError];\ + if (completion) {\ + completion(error);\ + }\ + return;\ + } + + if (!isIndexingAvailable) { + IndexingNotAvalable(); + } + else { + if (CSSearchableIndexClass && + [CSSearchableIndexClass respondsToSelector:@selector(defaultSearchableIndex)]){ + id defaultSearchableIndex = [CSSearchableIndexClass defaultSearchableIndex]; + if ([defaultSearchableIndex respondsToSelector:@selector(deleteSearchableItemsWithIdentifiers:completionHandler:)]) { + [defaultSearchableIndex deleteSearchableItemsWithIdentifiers:identifiers + completionHandler:^(NSError * _Nullable error) { + if (completion) + completion(error); + }]; + } + else { + IndexingNotAvalable(); + } + } + else { + IndexingNotAvalable(); + } + } + + #undef IndexingNotAvalable +} + +- (void)removeAllBranchSearchableItemsWithCallback:(void (^_Nullable)(NSError * _Nullable error))completion { + + BOOL isIndexingAvailable = NO; + Class CSSearchableIndexClass = NSClassFromString(@"CSSearchableIndex"); + SEL isIndexingAvailableSelector = NSSelectorFromString(@"isIndexingAvailable"); + isIndexingAvailable = + ((BOOL (*)(id, SEL))[CSSearchableIndexClass methodForSelector:isIndexingAvailableSelector]) + (CSSearchableIndexClass, isIndexingAvailableSelector); + + #define IndexingNotAvalable() { \ + NSError *error = [NSError branchErrorWithCode:BNCSpotlightNotAvailableError];\ + if (completion) {\ + completion(error);\ + }\ + return;\ + } + + if (!isIndexingAvailable) { + IndexingNotAvalable(); + } + else { + id CSSearchableIndexClass = NSClassFromString(@"CSSearchableIndex"); + if (CSSearchableIndexClass && + [CSSearchableIndexClass respondsToSelector:@selector(defaultSearchableIndex)]){ + id defaultSearchableIndex = [CSSearchableIndexClass defaultSearchableIndex]; + if ([defaultSearchableIndex respondsToSelector: + @selector(deleteSearchableItemsWithDomainIdentifiers:completionHandler:)]) { + [defaultSearchableIndex deleteSearchableItemsWithDomainIdentifiers:@[kDomainIdentifier] + completionHandler:^(NSError * _Nullable error) { + if (completion) + completion(error); + }]; + } + else { + IndexingNotAvalable(); + } + } + else { + IndexingNotAvalable(); + } + } + #undef IndexingNotAvalable +} +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCStrongMatchHelper.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCStrongMatchHelper.h new file mode 100644 index 00000000000..74be6c7fff6 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCStrongMatchHelper.h @@ -0,0 +1,23 @@ +// +// BNCStrongMatchHelper.h +// Branch-TestBed +// +// Created by Derrick Staten on 8/26/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +@interface BNCStrongMatchHelper : NSObject + ++ (BNCStrongMatchHelper *)strongMatchHelper; +- (void)createStrongMatchWithBranchKey:(NSString *)branchKey; ++ (NSURL *)getUrlForCookieBasedMatchingWithBranchKey:(NSString *)branchKey + redirectUrl:(NSString *)redirectUrl; + +@property (atomic, assign, readonly) BOOL shouldDelayInstallRequest; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCStrongMatchHelper.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCStrongMatchHelper.m new file mode 100644 index 00000000000..63f77e8c3e9 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCStrongMatchHelper.m @@ -0,0 +1,359 @@ +// +// BNCStrongMatchHelper.m +// Branch-TestBed +// +// Created by Derrick Staten on 8/26/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + + +#import "BNCStrongMatchHelper.h" +#import "BNCConfig.h" +#import "BNCPreferenceHelper.h" +#import "BNCSystemObserver.h" +#import "BranchConstants.h" +#import "BNCLog.h" +#import "UIViewController+Branch.h" +#import + + +#pragma mark BNCStrongMatchHelper iOS 8.0 + + +// This is a stub the class for older Xcode versions. These methods don't do anything. +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < 90000 // iOS < 9.0 + +@implementation BNCStrongMatchHelper + ++ (BNCStrongMatchHelper *)strongMatchHelper { + return nil; +} + +- (void)createStrongMatchWithBranchKey:(NSString *)branchKey { +} + +- (BOOL)shouldDelayInstallRequest { + return NO; +} + ++ (NSURL *)getUrlForCookieBasedMatchingWithBranchKey:(NSString *)branchKey + redirectUrl:(NSString *)redirectUrl { + return nil; +} + +@end + + +#else // ------------------------------------------------------------------------------ iOS >= 9.0 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + +#if __has_feature(modules) +@import SafariServices; +#else +#import +#endif + + +#pragma mark - BNCMatchView + + +@interface BNCMatchView : UIView +@end + + +@implementation BNCMatchView + +- (instancetype) initWithFrame:(CGRect)frame { + self = [super initWithFrame:frame]; + self.alpha = 0.0; + return self; +} + +- (void) setAlpha:(CGFloat)alpha { + [super setAlpha:0.0]; +} + +- (CGFloat) alpha { + return 1.0; +} + +@end + + +#pragma mark - BNCMatchViewController + + +// This is a class interface that will be dynamically subclassed from SFSafariViewController +@interface BNCMatchViewController : UIViewController +@end + + +@implementation BNCMatchViewController + +- (instancetype) initWithURL:(NSURL*)URL { + self = [super init]; + return self; +} + +- (BOOL) canBecomeFirstResponder { + return NO; +} + +- (BOOL) becomeFirstResponder { + return NO; +} + +- (UIResponder*) nextResponder { + return nil; +} + +- (void) setDelegate:(id)delegate { +} + +@end + + +#pragma mark - BNCStrongMatchHelper iOS 9.0 + + +@interface BNCStrongMatchHelper () +@property (assign, nonatomic) BOOL requestInProgress; +@property (readwrite) BOOL shouldDelayInstallRequest; +@property (strong, nonatomic) UIWindow *primaryWindow; +@property (strong, nonatomic) BNCMatchView *matchView; +@property (strong, nonatomic) BNCMatchViewController *matchViewController; +@end + + +@implementation BNCStrongMatchHelper + ++ (BNCStrongMatchHelper *)strongMatchHelper { + static BNCStrongMatchHelper *strongMatchHelper; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + strongMatchHelper = [[BNCStrongMatchHelper alloc] init]; + }); + + return strongMatchHelper; +} + ++ (NSURL *)getUrlForCookieBasedMatchingWithBranchKey:(NSString *)branchKey + redirectUrl:(NSString *)redirectUrl { + if (!branchKey || !self.cookiesAvailableInOS) { + return nil; + } + + NSString *appDomainLinkURL; + id ret = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"branch_app_domain"]; + if (ret) { + if ([ret isKindOfClass:[NSString class]]) { + appDomainLinkURL = ret; + if (!([appDomainLinkURL hasPrefix:@"https:"] || [appDomainLinkURL hasPrefix:@"http:"])) + appDomainLinkURL = [NSString stringWithFormat:@"https://%@", ret]; + } + } else { + appDomainLinkURL = BNC_LINK_URL; + } + NSMutableString *urlString = + [[NSMutableString alloc] initWithFormat:@"%@/_strong_match?os=%@", + appDomainLinkURL, [BNCSystemObserver getOS]]; + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + BOOL isRealHardwareId; + NSString *hardwareIdType; + NSString *hardwareId = + [BNCSystemObserver getUniqueHardwareId:&isRealHardwareId + isDebug:preferenceHelper.isDebug andType:&hardwareIdType]; + if (!hardwareId || !isRealHardwareId) { + BNCLogWarning(@"Cannot use cookie-based matching while setDebug is enabled."); + return nil; + } + + [urlString appendFormat:@"&%@=%@", BRANCH_REQUEST_KEY_HARDWARE_ID, hardwareId]; + + if (preferenceHelper.deviceFingerprintID) { + [urlString appendFormat:@"&%@=%@", + BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID, + preferenceHelper.deviceFingerprintID]; + } + + if ([BNCSystemObserver getAppVersion]) { + [urlString appendFormat:@"&%@=%@", + BRANCH_REQUEST_KEY_APP_VERSION, + [BNCSystemObserver getAppVersion]]; + } + + [urlString appendFormat:@"&branch_key=%@", branchKey]; + [urlString appendFormat:@"&sdk=ios%@", BNC_SDK_VERSION]; + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + + if (redirectUrl) { + [urlString appendFormat:@"&redirect_url=%@", + [redirectUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; + } + + NSString *escapedURL = + [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + return [NSURL URLWithString:escapedURL]; + + #pragma clang diagnostic pop +} + ++ (BOOL)cookiesAvailableInOS { + return [UIDevice currentDevice].systemVersion.doubleValue < 11.0; +} + +- (void)createStrongMatchWithBranchKey:(NSString *)branchKey { + if (!self.class.cookiesAvailableInOS) return; + + @synchronized (self) { + if (self.requestInProgress) return; + + NSInteger const ABOUT_30_DAYS_TIME_IN_SECONDS = 60 * 60 * 24 * 30; + NSDate *thirtyDaysAgo = [NSDate dateWithTimeIntervalSinceNow:-ABOUT_30_DAYS_TIME_IN_SECONDS]; + NSDate *lastCheck = [BNCPreferenceHelper preferenceHelper].lastStrongMatchDate; + if ([lastCheck compare:thirtyDaysAgo] == NSOrderedDescending) return; + + NSURL *strongMatchUrl = + [BNCStrongMatchHelper + getUrlForCookieBasedMatchingWithBranchKey:branchKey + redirectUrl:nil]; + if (!strongMatchUrl) return; + + self.requestInProgress = YES; + self.shouldDelayInstallRequest = YES; + + // Must be on next run loop to avoid a warning + dispatch_async(dispatch_get_main_queue(), ^{ + + if (![self willLoadViewControllerWithURL:strongMatchUrl]) { + self.shouldDelayInstallRequest = NO; + self.requestInProgress = NO; + return; + } + + // Give enough time for Safari to load the request (optimized for 3G) + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), + dispatch_get_main_queue(), + ^{ [self unloadViewController]; } + ); + }); + } +} + +- (BOOL) subclass:(Class)subclass selector:(SEL)selector { + Class templateClass = objc_getClass("BNCMatchViewController"); + if (!templateClass) return NO; + + Method method = class_getInstanceMethod(templateClass, selector); + if (!method) return NO; + + const char * typeEncoding = method_getTypeEncoding(method); + if (!typeEncoding) return NO; + + IMP implementation = class_getMethodImplementation(templateClass, selector); + if (!implementation) return NO; + + class_addMethod(subclass, selector, implementation, typeEncoding); + return YES; +} + +- (BOOL) willLoadViewControllerWithURL:(NSURL*)matchURL { + if (self.primaryWindow) return NO; + if (!([matchURL.scheme isEqualToString:@"https"] || [matchURL.scheme isEqualToString:@"http"])) + return NO; + + // Dynamically subclass the SFSafariViewController if available. + // This allows us to compile and link to an app that doesn't + // include SafariServices, but is also able to compile and link + // when it is. + + Class SFSafariViewControllerClass = NSClassFromString(@"SFSafariViewController"); + if (!SFSafariViewControllerClass) { + BNCLogWarning(@"cookieBasedMatching is enabled but SafariServices framework is not available."); + return NO; + } + + Class BNCMatchViewControllerSubclass = NSClassFromString(@"BNCMatchViewController_Safari"); + if (!BNCMatchViewControllerSubclass) { + // The class isn't registered. Create it: + + BNCMatchViewControllerSubclass = + objc_allocateClassPair(SFSafariViewControllerClass, "BNCMatchViewController_Safari", 0); + if (!BNCMatchViewControllerSubclass) return NO; + + BOOL fail = NO; + fail |= ![self subclass:BNCMatchViewControllerSubclass selector:@selector(becomeFirstResponder)]; + fail |= ![self subclass:BNCMatchViewControllerSubclass selector:@selector(canBecomeFirstResponder)]; + fail |= ![self subclass:BNCMatchViewControllerSubclass selector:@selector(nextResponder)]; + if (fail) { + objc_disposeClassPair(BNCMatchViewControllerSubclass); + return NO; + } + objc_registerClassPair(BNCMatchViewControllerSubclass); + } + + BNCLogDebugSDK(@"Safari is initializing."); + self.primaryWindow = [UIViewController bnc_currentWindow]; + + @try { + self.matchViewController = [[BNCMatchViewControllerSubclass alloc] initWithURL:matchURL]; + } + @catch (id e) { + BNCLogError(@"Can't create BNCMatchViewController with URL '%@': %@.", matchURL, e); + self.matchViewController = nil; + } + if (!self.matchViewController) return NO; + + self.matchViewController.delegate = self; + self.matchViewController.view.frame = self.primaryWindow.bounds; + + self.matchView = [[BNCMatchView alloc] initWithFrame:self.primaryWindow.bounds]; + self.matchView.alpha = 1.0; + [self.matchView addSubview:self.matchViewController.view]; + + UIViewController *rootViewController = [self.primaryWindow.rootViewController bnc_currentViewController]; + + [rootViewController addChildViewController:self.matchViewController]; + UIView *parentView = rootViewController.view ?: self.primaryWindow; + [parentView insertSubview:self.matchView atIndex:0]; + + [self.matchViewController didMoveToParentViewController:rootViewController]; + + return YES; +} + +- (void) unloadViewController { + BNCLogDebugSDK(@"Safari unloadViewController called."); + + [self.matchViewController willMoveToParentViewController:nil]; + [self.matchViewController.view removeFromSuperview]; + [self.matchViewController removeFromParentViewController]; + self.matchViewController.delegate = nil; + self.matchViewController = nil; + + [self.matchView removeFromSuperview]; + self.matchView = nil; + + self.primaryWindow = nil; + + [BNCPreferenceHelper preferenceHelper].lastStrongMatchDate = [NSDate date]; + self.shouldDelayInstallRequest = NO; + self.requestInProgress = NO; +} + +- (void)safariViewController:(SFSafariViewController *)controller + didCompleteInitialLoad:(BOOL)didLoadSuccessfully { + BNCLogDebugSDK(@"Safari did load. Success: %d.", didLoadSuccessfully); + [self unloadViewController]; +} + +@end + +#pragma clang diagnostic pop +#endif // ------------------------------------------------------------------------------ iOS >= 9.0 diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSystemObserver.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSystemObserver.h new file mode 100644 index 00000000000..8fe1097db65 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSystemObserver.h @@ -0,0 +1,35 @@ +// +// BNCSystemObserver.h +// Branch-SDK +// +// Created by Alex Austin on 6/5/14. +// Copyright (c) 2014 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +@interface BNCSystemObserver : NSObject + ++ (NSString *)getUniqueHardwareId:(BOOL *)isReal + isDebug:(BOOL)debug + andType:(NSString *__autoreleasing*)type; ++ (NSString *)getVendorId; ++ (NSString *)getDefaultUriScheme; ++ (NSString *)getAppVersion; ++ (NSString *)getBundleID; ++ (NSString *)getTeamIdentifier; ++ (NSString *)getBrand; ++ (NSString *)getModel; ++ (NSString *)getOS; ++ (NSString *)getOSVersion; ++ (NSNumber *)getScreenWidth; ++ (NSNumber *)getScreenHeight; ++ (BOOL)isSimulator; ++ (BOOL)adTrackingSafe; ++ (NSString*) getAdId; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSystemObserver.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSystemObserver.m new file mode 100644 index 00000000000..655e8672549 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCSystemObserver.m @@ -0,0 +1,172 @@ +// +// BNCSystemObserver.m +// Branch-SDK +// +// Created by Alex Austin on 6/5/14. +// Copyright (c) 2014 Branch Metrics. All rights reserved. +// + +#import "BNCPreferenceHelper.h" +#import "BNCSystemObserver.h" +#import "BNCLog.h" +#if __has_feature(modules) +@import UIKit; +@import SystemConfiguration; +@import Darwin.POSIX.sys.utsname; +#else +#import +#import +#import +#endif + +@implementation BNCSystemObserver + ++ (NSString *)getUniqueHardwareId:(BOOL *)isReal + isDebug:(BOOL)debug + andType:(NSString *__autoreleasing*)type { + NSString *uid = nil; + *isReal = YES; + + if (!debug) { + uid = [self getAdId]; + *type = @"idfa"; + } + + if (!uid && NSClassFromString(@"UIDevice") && !debug) { + uid = [[UIDevice currentDevice].identifierForVendor UUIDString]; + *type = @"vendor_id"; + } + + if (!uid) { + uid = [[NSUUID UUID] UUIDString]; + *type = @"random"; + *isReal = NO; + } + + return uid; +} + ++ (NSString*) getAdId { + NSString *uid = nil; + + Class ASIdentifierManagerClass = NSClassFromString(@"ASIdentifierManager"); + if (ASIdentifierManagerClass) { + SEL sharedManagerSelector = NSSelectorFromString(@"sharedManager"); + id sharedManager = + ((id (*)(id, SEL))[ASIdentifierManagerClass methodForSelector:sharedManagerSelector]) + (ASIdentifierManagerClass, sharedManagerSelector); + SEL advertisingIdentifierSelector = NSSelectorFromString(@"advertisingIdentifier"); + NSUUID *uuid = + ((NSUUID* (*)(id, SEL))[sharedManager methodForSelector:advertisingIdentifierSelector]) + (sharedManager, advertisingIdentifierSelector); + uid = [uuid UUIDString]; + // limit ad tracking is enabled. iOS 10+ + if ([uid isEqualToString:@"00000000-0000-0000-0000-000000000000"]) { + uid = nil; + } + } + + return uid; +} + ++ (NSString *)getVendorId { + NSString *vendorId = nil; + + if (NSClassFromString(@"UIDevice")) { + vendorId = [[UIDevice currentDevice].identifierForVendor UUIDString]; + } + + return vendorId; +} + ++ (BOOL)adTrackingSafe { + Class ASIdentifierManagerClass = NSClassFromString(@"ASIdentifierManager"); + if (ASIdentifierManagerClass) { + SEL sharedManagerSelector = NSSelectorFromString(@"sharedManager"); + id sharedManager = ((id (*)(id, SEL))[ASIdentifierManagerClass methodForSelector:sharedManagerSelector])(ASIdentifierManagerClass, sharedManagerSelector); + SEL advertisingEnabledSelector = NSSelectorFromString(@"isAdvertisingTrackingEnabled"); + BOOL enabled = ((BOOL (*)(id, SEL))[sharedManager methodForSelector:advertisingEnabledSelector])(sharedManager, advertisingEnabledSelector); + return enabled; + } + return YES; +} + ++ (NSString *)getDefaultUriScheme { + NSArray *urlTypes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleURLTypes"]; + + for (NSDictionary *urlType in urlTypes) { + NSArray *urlSchemes = [urlType objectForKey:@"CFBundleURLSchemes"]; + for (NSString *uriScheme in urlSchemes) { + if ([uriScheme hasPrefix:@"fb"]) continue; // Facebook + if ([uriScheme hasPrefix:@"db"]) continue; // DB? + if ([uriScheme hasPrefix:@"twitterkit-"]) continue; // Twitter + if ([uriScheme hasPrefix:@"pdk"]) continue; // Pinterest + if ([uriScheme hasPrefix:@"pin"]) continue; // Pinterest + if ([uriScheme hasPrefix:@"com.googleusercontent.apps"]) continue; // Google + + // Otherwise this must be it! + return uriScheme; + } + } + return nil; +} + ++ (NSString *)getAppVersion { + return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; +} + ++ (NSString *)getBundleID { + return [[NSBundle mainBundle] bundleIdentifier]; +} + ++ (NSString *)getTeamIdentifier { + NSString *teamWithDot = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"AppIdentifierPrefix"]; + if (teamWithDot.length) { + return [teamWithDot substringToIndex:([teamWithDot length] - 1)]; + } + return nil; +} + ++ (NSString *)getBrand { + return @"Apple"; +} + ++ (NSString *)getModel { + struct utsname systemInfo; + uname(&systemInfo); + + return [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding]; +} + ++ (BOOL)isSimulator { + #if (TARGET_OS_SIMULATOR) + return YES; + #else + return NO; + #endif +} + ++ (NSString *)getOS { + return @"iOS"; +} + ++ (NSString *)getOSVersion { + UIDevice *device = [UIDevice currentDevice]; + return [device systemVersion]; +} + ++ (NSNumber *)getScreenWidth { + UIScreen *mainScreen = [UIScreen mainScreen]; + CGFloat scaleFactor = mainScreen.scale; + CGFloat width = mainScreen.bounds.size.width * scaleFactor; + return [NSNumber numberWithInteger:(NSInteger)width]; +} + ++ (NSNumber *)getScreenHeight { + UIScreen *mainScreen = [UIScreen mainScreen]; + CGFloat scaleFactor = mainScreen.scale; + CGFloat height = mainScreen.bounds.size.height * scaleFactor; + return [NSNumber numberWithInteger:(NSInteger)height]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCThreads.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCThreads.h new file mode 100644 index 00000000000..e164f631b93 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCThreads.h @@ -0,0 +1,43 @@ +/** + @file BNCThreads.h + @package Branch-SDK + @brief Utilities for working with threads, queues, and blocks. + + @author Edward Smith + @date May 2018 + @copyright Copyright © 2018 Branch. All rights reserved. +*/ + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +///@group Blocks and Threads +#pragma mark - Blocks and Threads + +static inline dispatch_time_t BNCDispatchTimeFromSeconds(NSTimeInterval seconds) { + return dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC); +} + +static inline void BNCAfterSecondsPerformBlockOnMainThread(NSTimeInterval seconds, dispatch_block_t block) { + dispatch_after(BNCDispatchTimeFromSeconds(seconds), dispatch_get_main_queue(), block); +} + +static inline void BNCPerformBlockOnMainThreadAsync(dispatch_block_t block) { + dispatch_async(dispatch_get_main_queue(), block); +} + +static inline uint64_t BNCNanoSecondsFromTimeInterval(NSTimeInterval interval) { + return interval * ((NSTimeInterval) NSEC_PER_SEC); +} + +static inline void BNCSleepForTimeInterval(NSTimeInterval seconds) { + double secPart = trunc(seconds); + double nanoPart = trunc((seconds - secPart) * ((double)NSEC_PER_SEC)); + struct timespec sleepTime; + sleepTime.tv_sec = (__typeof(sleepTime.tv_sec)) secPart; + sleepTime.tv_nsec = (__typeof(sleepTime.tv_nsec)) nanoPart; + nanosleep(&sleepTime, NULL); +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCThreads.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCThreads.m new file mode 100644 index 00000000000..53d0aae5b77 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCThreads.m @@ -0,0 +1,11 @@ +/** + @file BNCThreads.m + @package Branch-SDK + @brief Utilities for working with threads, queues, and blocks. + + @author Edward Smith + @date May 2018 + @copyright Copyright © 2018 Branch. All rights reserved. +*/ + +#import "BNCThreads.h" diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCURLBlackList.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCURLBlackList.h new file mode 100644 index 00000000000..1c148d706ab --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCURLBlackList.h @@ -0,0 +1,41 @@ +/** + @file BNCURLBlackList.h + @package Branch-SDK + @brief Manages a list of sensitive URLs such as login data that should not be handled by Branch. + + @author Edward Smith + @date February 14, 2018 + @copyright Copyright © 2018 Branch. All rights reserved. +*/ + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +@interface BNCURLBlackList : NSObject + +/** + @brief Checks if a given URL should be ignored (blacklisted). + + @param url The URL to be checked. + @return Returns true if the provided URL should be ignored. +*/ +- (BOOL) isBlackListedURL:(NSURL*_Nullable)url; + +/** + @brief Returns the pattern that matches a URL, if any. + + @param url The URL to be checked. + @return Returns the pattern matching the URL or `nil` if no patterns match. +*/ +- (NSString*_Nullable) blackListPatternMatchingURL:(NSURL*_Nullable)url; + +/// Refreshes the list of ignored URLs from the server. +- (void) refreshBlackListFromServerWithCompletion:(void (^_Nullable) (NSError*_Nullable error, NSArray*_Nullable list))completion; + +/// Is YES if the listed has already been updated from the server. +@property (assign, readonly) BOOL hasRefreshedBlackListFromServer; +@property (strong) NSArray*_Nullable blackList; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCURLBlackList.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCURLBlackList.m new file mode 100644 index 00000000000..2f703726942 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BNCURLBlackList.m @@ -0,0 +1,182 @@ +/** + @file BNCURLBlackList.m + @package Branch-SDK + @brief Manages a list of URLs that we should ignore. + + @author Edward Smith + @date February 14, 2018 + @copyright Copyright © 2018 Branch. All rights reserved. +*/ + +#import "BNCURLBlackList.h" +#import "Branch.h" + +@interface BNCURLBlackList () { + NSArray*_blackList; +} +@property (strong) NSArray *blackListRegex; +@property (assign) NSInteger blackListVersion; +@property (strong) id networkService; +@property (assign) BOOL hasRefreshedBlackListFromServer; +@property (strong) NSError *error; +@property (strong) NSURL *blackListJSONURL; +@end + +@implementation BNCURLBlackList + +- (instancetype) init { + self = [super init]; + if (!self) return self; + + self.blackList = @[ + @"^fb\\d+:", + @"^li\\d+:", + @"^pdk\\d+:", + @"^twitterkit-.*:", + @"^com\\.googleusercontent\\.apps\\.\\d+-.*:\\/oauth", + @"^(?i)(?!(http|https):).*(:|:.*\\b)(password|o?auth|o?auth.?token|access|access.?token)\\b", + @"^(?i)((http|https):\\/\\/).*[\\/|?|#].*\\b(password|o?auth|o?auth.?token|access|access.?token)\\b", + ]; + self.blackListVersion = -1; // First time always refresh the list version, version 0. + + NSArray *storedList = [BNCPreferenceHelper preferenceHelper].URLBlackList; + if (storedList.count > 0) { + self.blackList = storedList; + self.blackListVersion = [BNCPreferenceHelper preferenceHelper].URLBlackListVersion; + } + + NSError *error = nil; + _blackListRegex = [self.class compileRegexArray:self.blackList error:&error]; + self.error = error; + + return self; +} + +- (void) dealloc { + [self.networkService cancelAllOperations]; + self.networkService = nil; +} + +- (void) setBlackList:(NSArray *)blackList { + @synchronized (self) { + _blackList = blackList; + _blackListRegex = [self.class compileRegexArray:_blackList error:nil]; + } +} + +- (NSArray*) blackList { + @synchronized (self) { + return _blackList; + } +} + ++ (NSArray*) compileRegexArray:(NSArray*)blacklist + error:(NSError*_Nullable __autoreleasing *_Nullable)error_ { + if (error_) *error_ = nil; + NSMutableArray *array = [NSMutableArray new]; + for (NSString *pattern in blacklist) { + NSError *error = nil; + NSRegularExpression *regex = + [NSRegularExpression regularExpressionWithPattern:pattern + options: NSRegularExpressionAnchorsMatchLines | NSRegularExpressionUseUnicodeWordBoundaries + error:&error]; + if (error || !regex) { + BNCLogError(@"Invalid regular expression '%@': %@.", pattern, error); + if (error_ && !*error_) *error_ = error; + } else { + [array addObject:regex]; + } + } + return array; +} + +- (NSString*_Nullable) blackListPatternMatchingURL:(NSURL*_Nullable)url { + NSString *urlString = url.absoluteString; + if (urlString == nil || urlString.length <= 0) return nil; + NSRange range = NSMakeRange(0, urlString.length); + + for (NSRegularExpression* regex in self.blackListRegex) { + NSUInteger matches = [regex numberOfMatchesInString:urlString options:0 range:range]; + if (matches > 0) return regex.pattern; + } + + return nil; +} + +- (BOOL) isBlackListedURL:(NSURL *)url { + return ([self blackListPatternMatchingURL:url]) ? YES : NO; +} + +- (void) refreshBlackListFromServer { + [self refreshBlackListFromServerWithCompletion:nil]; +} + +- (void) refreshBlackListFromServerWithCompletion:(void (^) (NSError*error, NSArray*list))completion { + @synchronized(self) { + if (self.hasRefreshedBlackListFromServer) { + if (completion) completion(self.error, self.blackList); + return; + } + self.hasRefreshedBlackListFromServer = YES; + } + + self.error = nil; + NSString *urlString = [self.blackListJSONURL absoluteString]; + if (!urlString) { + urlString = [NSString stringWithFormat:@"https://cdn.branch.io/sdk/uriskiplist_v%ld.json", + (long) self.blackListVersion+1]; + } + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString] + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:30.0]; + + self.networkService = [[Branch networkServiceClass] new]; + id operation = + [self.networkService networkOperationWithURLRequest:request completion: + ^(id operation) { + [self processServerOperation:operation]; + if (completion) completion(self.error, self.blackList); + [self.networkService cancelAllOperations]; + self.networkService = nil; + } + ]; + [operation start]; +} + +- (void) processServerOperation:(id)operation { + NSError *error = nil; + NSString *responseString = nil; + if (operation.responseData) + responseString = [[NSString alloc] initWithData:operation.responseData encoding:NSUTF8StringEncoding]; + if (operation.response.statusCode == 404) { + BNCLogDebugSDK(@"No new BlackList refresh found."); + } else { + BNCLogDebugSDK(@"BlackList refresh result. Error: %@ status: %ld body:\n%@.", + operation.error, (long)operation.response.statusCode, responseString); + } + if (operation.error || operation.responseData == nil || operation.response.statusCode != 200) { + self.error = operation.error; + return; + } + + NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:operation.responseData options:0 error:&error]; + if (error) { + self.error = error; + BNCLogError(@"Can't parse JSON: %@.", error); + return; + } + + NSArray *blackListURLs = dictionary[@"uri_skip_list"]; + if (![blackListURLs isKindOfClass:NSArray.class]) return; + + NSNumber *blackListVersion = dictionary[@"version"]; + if (![blackListVersion isKindOfClass:NSNumber.class]) return; + + self.blackList = blackListURLs; + self.blackListVersion = [blackListVersion longValue]; + [BNCPreferenceHelper preferenceHelper].URLBlackList = self.blackList; + [BNCPreferenceHelper preferenceHelper].URLBlackListVersion = self.blackListVersion; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch+Validator.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch+Validator.h new file mode 100644 index 00000000000..742e0f0a367 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch+Validator.h @@ -0,0 +1,17 @@ +// +// Branch+Validator.h +// Branch +// +// Created by agrim on 12/18/17. +// Copyright © 2017 Branch, Inc. All rights reserved. +// + +#import "Branch.h" + +@interface Branch (Validator) +- (void) validateSDKIntegrationCore; +- (void) validateDeeplinkRouting:(NSDictionary *)params; ++ (NSString *) returnNonUniversalLink:(NSString *) referringLink; +@end + +void BNCForceBranchValidatorCategoryToLoad(void); diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch+Validator.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch+Validator.m new file mode 100644 index 00000000000..855a5ae1925 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch+Validator.m @@ -0,0 +1,308 @@ +// +// Branch+Validator.m +// Branch +// +// Created by agrim on 12/18/17. +// Copyright © 2017 Branch, Inc. All rights reserved. +// + +#import "Branch+Validator.h" +#import "BNCSystemObserver.h" +#import "BranchConstants.h" +#import "BNCApplication.h" +#import "BNCEncodingUtils.h" + +void BNCForceBranchValidatorCategoryToLoad(void) { + // Empty body but forces loader to load the category. +} + +static inline void BNCPerformBlockOnMainThreadAsync(dispatch_block_t block) { + dispatch_async(dispatch_get_main_queue(), block); +} + +static inline dispatch_time_t BNCDispatchTimeFromSeconds(NSTimeInterval seconds) { + return dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC); +} + +static inline void BNCAfterSecondsPerformBlockOnMainThread(NSTimeInterval seconds, dispatch_block_t block) { + dispatch_after(BNCDispatchTimeFromSeconds(seconds), dispatch_get_main_queue(), block); +} + +#pragma mark - Branch (Validator) + +@implementation Branch (Validator) + +- (void)validateSDKIntegrationCore { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + [self startValidation]; + }); +} + +- (void) startValidation { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSString *endpoint = + [BRANCH_REQUEST_ENDPOINT_APP_LINK_SETTINGS stringByAppendingPathComponent:preferenceHelper.lastRunBranchKey]; + [[[BNCServerInterface alloc] init] + getRequest:nil + url:[preferenceHelper getAPIURL:endpoint] + key:nil + callback:^ (BNCServerResponse *response, NSError *error) { + if (error) { + [self showAlertWithTitle:@"Error" message:error.localizedDescription]; + } else { + [self validateIntegrationWithServerResponse:response]; + } + }]; +} + +- (void) validateIntegrationWithServerResponse:(BNCServerResponse*)response { + NSString*passString = @"PASS"; + NSString*errorString = @"ERROR"; + + // Decode the server message: + NSString*serverUriScheme = BNCStringFromWireFormat(response.data[@"ios_uri_scheme"]) ?: @""; + NSString*serverBundleID = BNCStringFromWireFormat(response.data[@"ios_bundle_id"]) ?: @""; + NSString*serverTeamID = BNCStringFromWireFormat(response.data[@"ios_team_id"]) ?: @""; + + // Verify: + NSLog(@"** Initiating Branch integration verification **"); + NSLog(@"-------------------------------------------------"); + + NSLog(@"------ Checking for URI scheme correctness ------"); + NSString *clientUriScheme = [NSString stringWithFormat:@"%@%@", [BNCSystemObserver getDefaultUriScheme], @"://"]; + NSString *uriScheme = [serverUriScheme isEqualToString:clientUriScheme] ? passString : errorString; + NSString *uriSchemeMessage = + [NSString stringWithFormat:@"%@: Dashboard Link Settings page '%@' compared to client side '%@'", + uriScheme, serverUriScheme, clientUriScheme]; + NSLog(@"%@",uriSchemeMessage); + NSLog(@"-------------------------------------------------"); + + NSLog(@"-- Checking for bundle identifier correctness ---"); + NSString *clientBundleIdentifier = [[NSBundle mainBundle] bundleIdentifier] ?: @""; + NSString *bundleIdentifier = [serverBundleID isEqualToString:clientBundleIdentifier] ? passString : errorString; + NSString *bundleIdentifierMessage = + [NSString stringWithFormat:@"%@: Dashboard Link Settings page '%@' compared to client side '%@'", + bundleIdentifier, serverBundleID, clientBundleIdentifier]; + NSLog(@"%@",bundleIdentifierMessage); + NSLog(@"-------------------------------------------------"); + + NSLog(@"----- Checking for iOS Team ID correctness ------"); + NSString *clientTeamId = [BNCApplication currentApplication].teamID ?: @""; + NSString *teamID = [serverTeamID isEqualToString:clientTeamId] ? passString : errorString; + NSString *teamIDMessage = + [NSString stringWithFormat:@"%@: Dashboard Link Settings page '%@' compared to client side '%@'", + teamID, serverTeamID, clientTeamId]; + NSLog(@"%@",teamIDMessage); + NSLog(@"-------------------------------------------------"); + + if ([teamID isEqualToString:errorString] || + [bundleIdentifier isEqualToString:errorString] || + [uriScheme isEqualToString:errorString]) { + NSLog(@"%@: server side '%@' compared to client side '%@'.", errorString, serverTeamID, clientTeamId); + NSLog(@"To fix your Dashboard settings head over to https://branch.app.link/link-settings-page"); + NSLog(@"If you see a null value on the client side, please temporarily add the following key-value pair to your plist: \n\tAppIdentifierPrefix$(AppIdentifierPrefix)\n-> then re-run this test."); + NSLog(@"-------------------------------------------------"); + } + + NSLog(@"-------------------------------------------------------------------------------------------------------------------"); + NSLog(@"-----To test your deeplink routing append ?bnc_validate=true to any branch link and click it on your mobile device-----"); + NSLog(@"-------------------------------------------------------------------------------------------------------------------"); + + BOOL testsFailed = NO; + NSString *kPassMark = @"✅\t"; + NSString *kFailMark = @"❌\t"; + + // Build an alert string: + NSString *alertString = @"\n"; + if (serverUriScheme.length && [serverUriScheme isEqualToString:clientUriScheme]) { + alertString = [alertString stringByAppendingFormat:@"%@URI Scheme matches:\n\t'%@'\n", + kPassMark, serverUriScheme]; + } else { + testsFailed = YES; + alertString = [alertString stringByAppendingFormat:@"%@URI Scheme mismatch:\n\t'%@'\n", + kFailMark, serverUriScheme]; + } + + if ([serverBundleID isEqualToString:clientBundleIdentifier]) { + alertString = [alertString stringByAppendingFormat:@"%@App Bundle ID matches:\n\t'%@'\n", + kPassMark, serverBundleID]; + } else { + testsFailed = YES; + alertString = [alertString stringByAppendingFormat:@"%@App Bundle ID mismatch:\n\t'%@'\n", + kFailMark, serverBundleID]; + } + + if ([serverTeamID isEqualToString:clientTeamId]) { + alertString = [alertString stringByAppendingFormat:@"%@Team ID matches:\n\t'%@'\n", + kPassMark, serverTeamID]; + } else { + testsFailed = YES; + alertString = [alertString stringByAppendingFormat:@"%@Team ID mismatch:\n\t'%@'\n", + kFailMark, serverTeamID]; + } + + if (testsFailed) { + alertString = [alertString stringByAppendingString:@"\nFailed!\nCheck the log for details."]; + } else { + alertString = [alertString stringByAppendingString:@"\nPassed!"]; + } + + NSMutableParagraphStyle *ps = [NSMutableParagraphStyle new]; + ps.alignment = NSTextAlignmentLeft; + NSAttributedString *styledAlertString = + [[NSAttributedString alloc] + initWithString:alertString + attributes:@{ + NSParagraphStyleAttributeName: ps + }]; + + BNCPerformBlockOnMainThreadAsync(^{ + UIAlertController *alertController = + [UIAlertController alertControllerWithTitle:@"Branch Integration" + message:alertString + preferredStyle:UIAlertControllerStyleAlert]; + if (testsFailed) { + [alertController + addAction:[UIAlertAction actionWithTitle:@"Bummer" + style:UIAlertActionStyleDefault + handler:nil]]; + } else { + [alertController + addAction:[UIAlertAction actionWithTitle:@"Next Step" + style:UIAlertActionStyleDefault + handler:^ (UIAlertAction *action) { [self showNextStep]; }]]; + } + [alertController setValue:styledAlertString forKey:@"attributedMessage"]; + [[UIViewController bnc_currentViewController] + presentViewController:alertController + animated:YES + completion:nil]; + }); +} + +- (void) showNextStep { + NSString *message = + @"\nGreat! Remove the 'validateSDKIntegration' line in your app.\n\n" + "Next check your deep link routing.\n\n" + "Append '?bnc_validate=true' to any of your app's Branch links and " + "click on it on your mobile device (not the Simulator!) to start the test.\n\n" + "For instance, to validate a link like:\n" + "https://.app.link/NdJ6nFzRbK\n\n" + "click on:\n" + "https://.app.link/NdJ6nFzRbK?bnc_validate=true"; + NSLog( + @"\n----------------------------------------------------------------------------" + "\nBranch Integration Next Steps\n" + "\n" + "%@" + "\n----------------------------------------------------------------------------", message); + [self showAlertWithTitle:@"Next Step" message:message]; +} + +- (void) showAlertWithTitle:(NSString*)title message:(NSString*)message { + BNCPerformBlockOnMainThreadAsync(^{ + UIAlertController *alertController = + [UIAlertController alertControllerWithTitle:title + message:message + preferredStyle:UIAlertControllerStyleAlert]; + [alertController + addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault handler:nil]]; + [[UIViewController bnc_currentViewController] + presentViewController:alertController + animated:YES + completion:nil]; + }); +} + +- (void)returnToBrowserBasedOnReferringLink:(NSString *)referringLink + currentTest:(NSString *)currentTest + newTestVal:(NSString *)val { + // TODO: handling for missing ~referring_link + // TODO: test with short url where, say, t1=b is set in deep link data. + // If this logic fails then we'll need to generate a new short URL, which is sucky. + referringLink = [self.class returnNonUniversalLink:referringLink]; + NSURLComponents *comp = [NSURLComponents componentsWithURL:[NSURL URLWithString:referringLink] + resolvingAgainstBaseURL:NO]; // TODO: Check iOS 8 support + NSArray *queryParams = [comp queryItems]; + NSMutableArray *newQueryParams = [NSMutableArray array]; + for (NSURLQueryItem *queryParam in queryParams) { + if (![queryParam.name isEqualToString:currentTest]) { + [newQueryParams addObject:queryParam]; + } + } + [newQueryParams addObject:[NSURLQueryItem queryItemWithName:currentTest value:val]]; + [newQueryParams addObject:[NSURLQueryItem queryItemWithName:@"validate" value:@"true"]]; + comp.queryItems = newQueryParams; + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + Class applicationClass = NSClassFromString(@"UIApplication"); + id sharedApplication = [applicationClass performSelector:@selector(sharedApplication)]; + SEL openURL = @selector(openURL:); + if ([sharedApplication respondsToSelector:openURL]) + [sharedApplication performSelector:openURL withObject:comp.URL]; + #pragma clang diagnostic pop +} + +- (void)validateDeeplinkRouting:(NSDictionary *)params { + BNCAfterSecondsPerformBlockOnMainThread(0.30, ^{ + UIAlertController *alertController = + [UIAlertController + alertControllerWithTitle:@"Branch Deeplink Routing Support" + message:nil + preferredStyle:UIAlertControllerStyleAlert]; + + if ([params[@"+clicked_branch_link"] isEqualToNumber:@YES]) { + alertController.message = + @"Good news - we got link data. Now a question for you, astute developer: " + "did the app deep link to the specific piece of content you expected to see?"; + // yes + [alertController addAction:[UIAlertAction + actionWithTitle:@"Yes" style:UIAlertActionStyleDefault + handler:^(UIAlertAction * _Nonnull action) { + [self returnToBrowserBasedOnReferringLink:params[@"~referring_link"] + currentTest:params[@"ct"] newTestVal:@"g"]; + }]]; + // no + [alertController addAction:[UIAlertAction + actionWithTitle:@"No" style:UIAlertActionStyleDestructive + handler:^(UIAlertAction * _Nonnull action) { + [self returnToBrowserBasedOnReferringLink:params[@"~referring_link"] + currentTest:params[@"ct"] newTestVal:@"r"]; + }]]; + // cancel + [alertController addAction:[UIAlertAction + actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]]; + } + else { + alertController.message = + @"Bummer. It seems like +clicked_branch_link is false - we didn't deep link. " + "Double check that the link you're clicking has the same branch_key that is being " + "used in your .plist file. Return to Safari when you're ready to test again."; + [alertController addAction:[UIAlertAction + actionWithTitle:@"Got it" style:UIAlertActionStyleDefault handler:nil]]; + } + [[UIViewController bnc_currentViewController] + presentViewController:alertController animated:YES completion:nil]; + }); +} + ++ (NSString *) returnNonUniversalLink:(NSString *) referringLink { + // Appending /e/ to not treat this link as a Universal link + NSArray *lines = [referringLink componentsSeparatedByString: @"/"]; + referringLink = @""; + for (int i = 0 ; i < [lines count]; i++) { + if(i != 2) { + referringLink = [referringLink stringByAppendingString:lines[i]]; + referringLink = [referringLink stringByAppendingString:@"/"]; + } else { + referringLink = [referringLink stringByAppendingString:lines[i]]; + referringLink = [referringLink stringByAppendingString:@"/e/"]; + } + } + referringLink = [referringLink stringByReplacingOccurrencesOfString:@"-alternate" withString:@""]; + return referringLink; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/Branch.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch.h similarity index 97% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/Branch.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch.h index 48d40628df3..add20b72673 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/Branch.h +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch.h @@ -27,6 +27,7 @@ #import "BNCAvailability.h" #import "BranchActivityItemProvider.h" #import "BranchConstants.h" +#import "BranchCSSearchableItemAttributeSet.h" #import "BranchDeepLinkingController.h" #import "BranchEvent.h" #import "BranchLinkProperties.h" @@ -66,7 +67,7 @@ Indicates this link is being used to trigger a deal, like a discounted rate. `BRANCH_FEATURE_TAG_GIFT` - Indicates this link is being used to sned a gift to another user. + Indicates this link is being used to send a gift to another user. */ extern NSString * const BRANCH_FEATURE_TAG_SHARE; extern NSString * const BRANCH_FEATURE_TAG_REFERRAL; @@ -224,9 +225,24 @@ typedef NS_ENUM(NSUInteger, BranchCreditHistoryOrder) { You can only set the Branch key once per app run. @param branchKey The Branch key to use. + @param error NSError will be set if Branch encounters a key error. */ ++ (void) setBranchKey:(NSString*)branchKey error:(NSError **)error; + +/** + Directly sets the Branch key to be used. Branch usually reads the Branch key from your app's + Info.plist file which is recommended and more convenient. But the Branch key can also be set + with this method. See the documentation at + https://dev.branch.io/getting-started/sdk-integration-guide/guide/ios/#configure-xcode-project + for information about configuring your app with Branch keys. + + You can only set the Branch key once per app run. Any errors are logged. + + @param branchKey The Branch key to use. + */ + (void) setBranchKey:(NSString*)branchKey; + /// @return Returns the current Branch key. + (NSString*) branchKey; @@ -461,7 +477,11 @@ typedef NS_ENUM(NSUInteger, BranchCreditHistoryOrder) { /** Call this method from inside your app delegate's `application:openURL:sourceApplication:annotation:` - method with the so that Branch can open the passed URL. + method so that Branch can open the passed URL. This method is for pre-iOS 9 compatibility: If you don't need + pre-iOS 9 compatibility, override your app delegate's `application:openURL:options:` method instead and use + the Branch `application:openURL:options:` to open the URL. + + @warning Pre-iOS 9 compatibility only. @param application The application that was passed to your app delegate. @param url The URL that was passed to your app delegate. @@ -475,12 +495,10 @@ typedef NS_ENUM(NSUInteger, BranchCreditHistoryOrder) { annotation:(id)annotation; /** - Call this method from inside your app delegate's `application:openURL:options:` - method with the so that Branch can open the passed URL. + Call this method from inside your app delegate's `application:openURL:options:` method so that Branch can + open the passed URL. - This method is functionally the same as calling the Branch method - `application:openURL:sourceApplication:annotation:`. This method matches the new Apple appDelegate - method for convenience. + This is the preferred Branch method to call inside your `application:openURL:options:` method. @param application The application that was passed to your app delegate. @param url The URL that was passed to your app delegate. @@ -1631,7 +1649,23 @@ typedef NS_ENUM(NSUInteger, BranchCreditHistoryOrder) { - (void) sendServerRequest:(BNCServerRequest*)request; - (void) sendServerRequestWithoutSession:(BNCServerRequest*)request; +/** + This is the block that is called each time a new Branch session is started. It is automatically set + when Branch is initialized with `initSessionWithLaunchOptions:andRegisterDeepLinkHandler`. + */ +@property (copy, nonatomic) void(^ sessionInitWithParamsCallback) (NSDictionary * params, NSError * error); + +/** + This is the block that is called each time a new Branch session is started. It is automatically set + when Branch is initialized with `initSessionWithLaunchOptions:andRegisterDeepLinkHandlerUsingBranchUniversalObject`. + + The difference with this callback from `sessionInitWithParamsCallback` is that it is called with a + BranchUniversalObject. + */ +@property (copy, nonatomic) void (^ sessionInitWithBranchUniversalObjectCallback) + (BranchUniversalObject * universalObject, BranchLinkProperties * linkProperties, NSError * error); + // Read-only property exposed for unit testing. -@property (strong, readonly) BNCServerInterface *serverInterface; +@property (strong, readonly) BNCServerInterface* serverInterface; - (void) clearNetworkQueue; @end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch.m new file mode 100644 index 00000000000..3da67fd55a4 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Branch.m @@ -0,0 +1,2531 @@ +// +// Branch_SDK.m +// Branch-SDK +// +// Created by Alex Austin on 6/5/14. +// Copyright (c) 2014 Branch Metrics. All rights reserved. +// + +#import "Branch.h" +#import "BNCContentDiscoveryManager.h" +#import "BNCCrashlyticsWrapper.h" +#import "BNCDeepLinkViewControllerInstance.h" +#import "BNCEncodingUtils.h" +#import "BNCFabricAnswers.h" +#import "BNCLinkData.h" +#import "BNCNetworkService.h" +#import "BNCPreferenceHelper.h" +#import "BNCServerRequest.h" +#import "BNCServerRequestQueue.h" +#import "BNCServerResponse.h" +#import "BNCStrongMatchHelper.h" +#import "BNCSystemObserver.h" +#import "BranchCloseRequest.h" +#import "BranchConstants.h" +#import "BranchContentDiscoverer.h" +#import "BranchCreditHistoryRequest.h" +#import "BranchInstallRequest.h" +#import "BranchLoadRewardsRequest.h" +#import "BranchLogoutRequest.h" +#import "BranchOpenRequest.h" +#import "BranchRedeemRewardsRequest.h" +#import "BranchSetIdentityRequest.h" +#import "BranchShortUrlRequest.h" +#import "BranchShortUrlSyncRequest.h" +#import "BranchSpotlightUrlRequest.h" +#import "BranchUniversalObject.h" +#import "BranchUserCompletedActionRequest.h" +#import "NSMutableDictionary+Branch.h" +#import "NSString+Branch.h" +#import "Branch+Validator.h" +#import "BNCSpotlightService.h" +#import "../Fabric/FABKitProtocol.h" // Fabric +#import "BNCApplication.h" +#import "BNCURLBlackList.h" +#import + +NSString * const BRANCH_FEATURE_TAG_SHARE = @"share"; +NSString * const BRANCH_FEATURE_TAG_REFERRAL = @"referral"; +NSString * const BRANCH_FEATURE_TAG_INVITE = @"invite"; +NSString * const BRANCH_FEATURE_TAG_DEAL = @"deal"; +NSString * const BRANCH_FEATURE_TAG_GIFT = @"gift"; + +NSString * const BRANCH_INIT_KEY_CHANNEL = @"~channel"; +NSString * const BRANCH_INIT_KEY_FEATURE = @"~feature"; +NSString * const BRANCH_INIT_KEY_TAGS = @"~tags"; +NSString * const BRANCH_INIT_KEY_CAMPAIGN = @"~campaign"; +NSString * const BRANCH_INIT_KEY_STAGE = @"~stage"; +NSString * const BRANCH_INIT_KEY_CREATION_SOURCE = @"~creation_source"; +NSString * const BRANCH_INIT_KEY_REFERRER = @"+referrer"; +NSString * const BRANCH_INIT_KEY_PHONE_NUMBER = @"+phone_number"; +NSString * const BRANCH_INIT_KEY_IS_FIRST_SESSION = @"+is_first_session"; +NSString * const BRANCH_INIT_KEY_CLICKED_BRANCH_LINK = @"+clicked_branch_link"; +static NSString * const BRANCH_PUSH_NOTIFICATION_PAYLOAD_KEY = @"branch"; + +NSString * const BNCCanonicalIdList = @"$canonical_identifier_list"; +NSString * const BNCPurchaseAmount = @"$amount"; +NSString * const BNCPurchaseCurrency = @"$currency"; +NSString * const BNCRegisterViewEvent = @"View"; +NSString * const BNCAddToWishlistEvent = @"Add to Wishlist"; +NSString * const BNCAddToCartEvent = @"Add to Cart"; +NSString * const BNCPurchaseInitiatedEvent = @"Purchase Started"; +NSString * const BNCPurchasedEvent = @"Purchased"; +NSString * const BNCShareInitiatedEvent = @"Share Started"; +NSString * const BNCShareCompletedEvent = @"Share Completed"; + +static NSString * const BNCLogLevelKey = @"io.branch.sdk.BNCLogLevel"; +NSString * const BNCSpotlightFeature = @"spotlight"; + +#ifndef CSSearchableItemActivityIdentifier +#define CSSearchableItemActivityIdentifier @"kCSSearchableItemActivityIdentifier" +#endif + +static inline dispatch_time_t BNCDispatchTimeFromSeconds(NSTimeInterval seconds) { + return dispatch_time(DISPATCH_TIME_NOW, seconds * NSEC_PER_SEC); +} + +static inline void BNCAfterSecondsPerformBlock(NSTimeInterval seconds, dispatch_block_t block) { + dispatch_after(BNCDispatchTimeFromSeconds(seconds), dispatch_get_main_queue(), block); +} + +#pragma mark - Load Categories + +void ForceCategoriesToLoad(void); +void ForceCategoriesToLoad(void) { + BNCForceNSErrorCategoryToLoad(); + BNCForceNSStringCategoryToLoad(); + BNCForceNSMutableDictionaryCategoryToLoad(); + BNCForceBranchValidatorCategoryToLoad(); + BNCForceUIViewControllerCategoryToLoad(); +} + +#pragma mark - BranchLink + +@implementation BranchLink + ++ (BranchLink*) linkWithUniversalObject:(BranchUniversalObject*)universalObject + properties:(BranchLinkProperties*)linkProperties { + BranchLink *link = [[BranchLink alloc] init]; + link.universalObject = universalObject; + link.linkProperties = linkProperties; + return link; +} + +@end + +#pragma mark - Branch + +typedef NS_ENUM(NSInteger, BNCInitStatus) { + BNCInitStatusUninitialized = 0, + BNCInitStatusInitializing, + BNCInitStatusInitialized +}; + +@interface Branch() { + NSInteger _networkCount; + BNCURLBlackList *_userURLBlackList; +} + +@property (strong, nonatomic) BNCServerInterface *serverInterface; +@property (strong, nonatomic) BNCServerRequestQueue *requestQueue; +@property (strong, nonatomic) dispatch_semaphore_t processing_sema; +@property (assign, atomic) NSInteger networkCount; +@property (assign, nonatomic) NSInteger asyncRequestCount; +@property (assign, nonatomic) BNCInitStatus initializationStatus; +@property (assign, nonatomic) BOOL shouldCallSessionInitCallback; +@property (assign, nonatomic) BOOL shouldAutomaticallyDeepLink; +@property (strong, nonatomic) BNCLinkCache *linkCache; +@property (strong, nonatomic) BNCPreferenceHelper *preferenceHelper; +@property (strong, nonatomic) BNCContentDiscoveryManager *contentDiscoveryManager; +@property (strong, nonatomic) NSMutableDictionary *deepLinkControllers; +@property (weak, nonatomic) UIViewController *deepLinkPresentingController; +@property (assign, nonatomic) BOOL useCookieBasedMatching; +@property (strong, nonatomic) NSDictionary *deepLinkDebugParams; +@property (assign, nonatomic) BOOL accountForFacebookSDK; +@property (strong, nonatomic) id FBSDKAppLinkUtility; +@property (assign, nonatomic) BOOL delayForAppleAds; +@property (assign, nonatomic) BOOL searchAdsDebugMode; +@property (strong, nonatomic) NSMutableArray *whiteListedSchemeList; +@property (strong, nonatomic) BNCURLBlackList *URLBlackList; +@end + +@implementation Branch + +#pragma mark - Public methods + +#pragma mark - GetInstance methods + +static NSURL* bnc_logURL = nil; + ++ (void) openLog { + // Initialize the log + @synchronized (self) { + if (bnc_logURL) { + #if defined(BNCKeepLogfiles) + BNCLogSetOutputToURLByteWrap(bnc_logURL, 102400); + #else + BNCLogSetOutputFunction(NULL); + #endif + } else { + BNCLogInitialize(); + BNCLogSetDisplayLevel(BNCLogLevelAll); + bnc_logURL = BNCURLForBranchDirectory(); + bnc_logURL = [[NSURL alloc] initWithString:@"Branch.log" relativeToURL:bnc_logURL]; + #if defined(BNCKeepLogfiles) + BNCLogSetOutputToURLByteWrap(bnc_logURL, 102400); + #else + BNCLogSetOutputFunction(NULL); + if (bnc_logURL) + [[NSFileManager defaultManager] removeItemAtURL:bnc_logURL error:nil]; + #endif + BNCLogSetDisplayLevel(BNCLogLevelWarning); // Default + + // Try loading from the Info.plist + NSString *logLevelString = [[NSBundle mainBundle] infoDictionary][@"BranchLogLevel"]; + if ([logLevelString isKindOfClass:[NSString class]]) { + BNCLogLevel logLevel = BNCLogLevelFromString(logLevelString); + BNCLogSetDisplayLevel(logLevel); + } + + // Try loading from user defaults + NSNumber *logLevel = [[NSUserDefaults standardUserDefaults] objectForKey:BNCLogLevelKey]; + if ([logLevel isKindOfClass:[NSNumber class]]) { + BNCLogSetDisplayLevel([logLevel integerValue]); + } + + BNCLog(@"Branch version %@ started at %@.", BNC_SDK_VERSION, [NSDate date]); + } + } +} + ++ (void) closeLog { + BNCLogCloseLogFile(); +} + +void BranchClassInitializeLog(void); +void BranchClassInitializeLog(void) { + [Branch openLog]; +} + ++ (void) load { + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^{ + BNCLogSetClientInitializeFunction(BranchClassInitializeLog); + }); +} + ++ (Branch *) getTestInstance { + Branch.useTestBranchKey = YES; + return Branch.getInstance; +} + ++ (Branch *)getInstance { + return [Branch getInstanceInternal:self.class.branchKey returnNilIfNoCurrentInstance:NO]; +} + ++ (Branch *)getInstance:(NSString *)branchKey { + self.branchKey = branchKey; + return [Branch getInstanceInternal:self.branchKey returnNilIfNoCurrentInstance:NO]; +} + +- (id)initWithInterface:(BNCServerInterface *)interface + queue:(BNCServerRequestQueue *)queue + cache:(BNCLinkCache *)cache + preferenceHelper:(BNCPreferenceHelper *)preferenceHelper + key:(NSString *)key { + + self = [super init]; + if (!self) return self; + + // Initialize instance variables + + _serverInterface = interface; + _serverInterface.preferenceHelper = preferenceHelper; + _requestQueue = queue; + _linkCache = cache; + _preferenceHelper = preferenceHelper; + + _contentDiscoveryManager = [[BNCContentDiscoveryManager alloc] init]; + _initializationStatus = BNCInitStatusUninitialized; + _shouldCallSessionInitCallback = YES; + _processing_sema = dispatch_semaphore_create(1); + _networkCount = 0; + _asyncRequestCount = 0; + _deepLinkControllers = [[NSMutableDictionary alloc] init]; + _whiteListedSchemeList = [[NSMutableArray alloc] init]; + _useCookieBasedMatching = YES; + self.class.branchKey = key; + self.URLBlackList = [BNCURLBlackList new]; + [BranchOpenRequest setWaitNeededForOpenResponseLock]; + + // Register for notifications + + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter + addObserver:self + selector:@selector(applicationWillResignActive) + name:UIApplicationWillResignActiveNotification + object:nil]; + + [notificationCenter + addObserver:self + selector:@selector(applicationDidBecomeActive) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + + return self; +} + +static Class bnc_networkServiceClass = NULL; + ++ (void) setNetworkServiceClass:(Class)networkServiceClass { + @synchronized ([Branch class]) { + if (bnc_networkServiceClass) { + BNCLogError(@"The Branch network service class is already set. It can be set only once."); + return; + } + if (![networkServiceClass conformsToProtocol:@protocol(BNCNetworkServiceProtocol)]) { + BNCLogError(@"Class '%@' doesn't conform to protocol '%@'.", + NSStringFromClass(networkServiceClass), + NSStringFromProtocol(@protocol(BNCNetworkServiceProtocol)) + ); + return; + } + bnc_networkServiceClass = networkServiceClass; + } +} + ++ (Class) networkServiceClass { + @synchronized ([Branch class]) { + if (!bnc_networkServiceClass) bnc_networkServiceClass = [BNCNetworkService class]; + return bnc_networkServiceClass; + } +} + +#pragma mark - BrachActivityItemProvider methods + ++ (BranchActivityItemProvider *)getBranchActivityItemWithParams:(NSDictionary *)params { + return [[BranchActivityItemProvider alloc] initWithParams:params tags:nil feature:nil stage:nil campaign:nil alias:nil delegate:nil]; +} + ++ (BranchActivityItemProvider *)getBranchActivityItemWithParams:(NSDictionary *)params feature:(NSString *)feature { + return [[BranchActivityItemProvider alloc] initWithParams:params tags:nil feature:feature stage:nil campaign:nil alias:nil delegate:nil]; +} + ++ (BranchActivityItemProvider *)getBranchActivityItemWithParams:(NSDictionary *)params feature:(NSString *)feature stage:(NSString *)stage { + return [[BranchActivityItemProvider alloc] initWithParams:params tags:nil feature:feature stage:stage campaign:nil alias:nil delegate:nil]; +} + ++ (BranchActivityItemProvider *)getBranchActivityItemWithParams:(NSDictionary *)params feature:(NSString *)feature stage:(NSString *)stage tags:(NSArray *)tags { + return [[BranchActivityItemProvider alloc] initWithParams:params tags:tags feature:feature stage:stage campaign:nil alias:nil delegate:nil]; +} + ++ (BranchActivityItemProvider *)getBranchActivityItemWithParams:(NSDictionary *)params feature:(NSString *)feature stage:(NSString *)stage campaign:(NSString *)campaign tags:(NSArray *)tags alias:(NSString *)alias { + return [[BranchActivityItemProvider alloc] initWithParams:params tags:tags feature:feature stage:stage campaign:campaign alias:alias delegate:nil]; +} + ++ (BranchActivityItemProvider *)getBranchActivityItemWithParams:(NSDictionary *)params feature:(NSString *)feature stage:(NSString *)stage tags:(NSArray *)tags alias:(NSString *)alias { + return [[BranchActivityItemProvider alloc] initWithParams:params tags:tags feature:feature stage:stage campaign:nil alias:alias delegate:nil]; +} + ++ (BranchActivityItemProvider *)getBranchActivityItemWithParams:(NSDictionary *)params feature:(NSString *)feature stage:(NSString *)stage tags:(NSArray *)tags alias:(NSString *)alias delegate:(id )delegate { + return [[BranchActivityItemProvider alloc] initWithParams:params tags:tags feature:feature stage:stage campaign:nil alias:alias delegate:delegate]; +} + + +#pragma mark - Configuration methods + +static BOOL bnc_useTestBranchKey = NO; +static NSString *bnc_branchKey = nil; +static BOOL bnc_enableFingerprintIDInCrashlyticsReports = YES; + ++ (void) resetBranchKey { + bnc_branchKey = nil; +} + ++ (void) setUseTestBranchKey:(BOOL)useTestKey { + @synchronized (self) { + if (bnc_branchKey && !!useTestKey != !!bnc_useTestBranchKey) { + BNCLogError(@"Can't switch the Branch key once it's in use."); + return; + } + bnc_useTestBranchKey = useTestKey; + } +} + ++ (BOOL) useTestBranchKey { + @synchronized (self) { + return bnc_useTestBranchKey; + } +} + ++ (void)setBranchKey:(NSString *)branchKey { + NSError *error; + [self setBranchKey:branchKey error:&error]; + + if (error) { + BNCLogError(@"Branch init error: %@", error.localizedDescription); + } +} + ++ (void)setBranchKey:(NSString*)branchKey error:(NSError **)error { + @synchronized (self) { + if (bnc_branchKey) { + if (branchKey && + [branchKey isKindOfClass:[NSString class]] && + [branchKey isEqualToString:bnc_branchKey]) { + return; + } + + NSString *errorMessage = [NSString stringWithFormat:@"Branch key can only be set once."]; + *error = [NSError branchErrorWithCode:BNCInitError localizedMessage:errorMessage]; + return; + } + + if (![branchKey isKindOfClass:[NSString class]]) { + NSString *typeName = (branchKey) ? NSStringFromClass(branchKey.class) : @""; + + NSString *errorMessage = [NSString stringWithFormat:@"Invalid Branch key of type '%@'.", typeName]; + *error = [NSError branchErrorWithCode:BNCInitError localizedMessage:errorMessage]; + return; + } + + if ([branchKey hasPrefix:@"key_test"]) { + bnc_useTestBranchKey = YES; + BNCLogWarning( + @"You are using your test app's Branch Key. " + "Remember to change it to live Branch Key for production deployment." + ); + + } else if ([branchKey hasPrefix:@"key_live"]) { + bnc_useTestBranchKey = NO; + + } else { + NSString *errorMessage = [NSString stringWithFormat:@"Invalid Branch key format. Did you add your Branch key to your Info.plist? Passed key is '%@'.", branchKey]; + *error = [NSError branchErrorWithCode:BNCInitError localizedMessage:errorMessage]; + return; + } + + bnc_branchKey = branchKey; + } +} + ++ (NSString*) branchKey { + @synchronized (self) { + if (bnc_branchKey) return bnc_branchKey; + + // The name of this key was specified in the Fabric account-creation API integration + NSString * const BNC_BRANCH_FABRIC_APP_KEY_KEY = @"branch_key"; + + NSDictionary *branchDictionary = + [[[NSBundle mainBundle] infoDictionary] objectForKey:BNC_BRANCH_FABRIC_APP_KEY_KEY]; + + if (!branchDictionary) { + branchDictionary = [BNCFabricAnswers branchConfigurationDictionary]; + } + + NSString *branchKey = nil; + if ([branchDictionary isKindOfClass:[NSString class]]) { + branchKey = (NSString*) branchDictionary; + } else + if ([branchDictionary isKindOfClass:[NSDictionary class]]) { + branchKey = + (self.useTestBranchKey) ? branchDictionary[@"test"] : branchDictionary[@"live"]; + } + + self.branchKey = branchKey; + if (!bnc_branchKey) { + BNCLogError(@"Your Branch key is not set in your Info.plist file. See " + "https://dev.branch.io/getting-started/sdk-integration-guide/guide/ios/#configure-xcode-project" + " for configuration instructions."); + } + return bnc_branchKey; + } +} + ++ (BOOL) branchKeyIsSet { + @synchronized (self) { + return (bnc_branchKey.length) ? YES : NO; + } +} + ++ (void)setEnableFingerprintIDInCrashlyticsReports:(BOOL)enabled +{ + @synchronized(self) { + bnc_enableFingerprintIDInCrashlyticsReports = enabled; + } +} + ++ (BOOL) enableFingerprintIDInCrashlyticsReports +{ + @synchronized (self) { + return bnc_enableFingerprintIDInCrashlyticsReports; + } +} + +- (void)setDebug { + self.preferenceHelper.isDebug = YES; + if (BNCLogDisplayLevel() > BNCLogLevelDebug) + BNCLogSetDisplayLevel(BNCLogLevelDebug); +} + +- (void)validateSDKIntegration { + [self validateSDKIntegrationCore]; +} + +- (void)resetUserSession { + self.initializationStatus = BNCInitStatusUninitialized; +} + +- (BOOL)isUserIdentified { + return self.preferenceHelper.userIdentity != nil; +} + +- (void)setNetworkTimeout:(NSTimeInterval)timeout { + self.preferenceHelper.timeout = timeout; +} + +- (void)setMaxRetries:(NSInteger)maxRetries { + self.preferenceHelper.retryCount = maxRetries; +} + +- (void)setRetryInterval:(NSTimeInterval)retryInterval { + self.preferenceHelper.retryInterval = retryInterval; +} + +- (void)disableCookieBasedMatching { + self.useCookieBasedMatching = NO; +} + +- (void)accountForFacebookSDKPreventingAppLaunch { + self.accountForFacebookSDK = YES; +} + +- (void)suppressWarningLogs { + BNCLogSetDisplayLevel(BNCLogLevelError); +} + +- (void)setRequestMetadataKey:(NSString *)key value:(NSObject *)value { + [self.preferenceHelper setRequestMetadataKey:key value:value]; +} + +- (void)enableDelayedInit { + self.preferenceHelper.shouldWaitForInit = YES; + + self.useCookieBasedMatching = NO; // Developers delaying init should implement their own SFSafariViewController +} + +- (void)disableDelayedInit { + self.preferenceHelper.shouldWaitForInit = NO; +} + +- (NSURL *)getUrlForOnboardingWithRedirectUrl:(NSString *)redirectUrl { + return [BNCStrongMatchHelper getUrlForCookieBasedMatchingWithBranchKey:self.class.branchKey redirectUrl:redirectUrl]; +} + +- (void)resumeInit { + self.preferenceHelper.shouldWaitForInit = NO; + if (self.initializationStatus == BNCInitStatusInitialized) { + BNCLogError(@"User session has already been initialized, so resumeInit is aborting."); + } + else if (![self.requestQueue containsInstallOrOpen]) { + BNCLogError(@"No install or open request, so resumeInit is aborting."); + } + else { + [self processNextQueueItem]; + } +} + +- (void)setInstallRequestDelay:(NSInteger)installRequestDelay { + self.preferenceHelper.installRequestDelay = installRequestDelay; +} + ++ (BOOL) trackingDisabled { + @synchronized(self) { + return [BNCPreferenceHelper preferenceHelper].trackingDisabled; + } +} + ++ (void) setTrackingDisabled:(BOOL)disabled { + @synchronized(self) { + BOOL currentSetting = self.trackingDisabled; + if (!!currentSetting == !!disabled) + return; + if (disabled) { + // Set the flag (which also clears the settings): + [BNCPreferenceHelper preferenceHelper].trackingDisabled = YES; + Branch *branch = Branch.getInstance; + [branch clearNetworkQueue]; + branch.initializationStatus = BNCInitStatusUninitialized; + [branch.linkCache clear]; + // Release the lock in case it's locked: + [BranchOpenRequest releaseOpenResponseLock]; + } else { + // Set the flag: + [BNCPreferenceHelper preferenceHelper].trackingDisabled = NO; + // Initialize a Branch session: + [Branch.getInstance initUserSessionAndCallCallback:NO]; + } + } +} + +#pragma mark - InitSession Permutation methods + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options { + [self initSessionWithLaunchOptions:options + isReferrable:YES + explicitlyRequestedReferrable:NO + automaticallyDisplayController:NO + registerDeepLinkHandler:nil]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options andRegisterDeepLinkHandler:(callbackWithParams)callback { + [self initSessionWithLaunchOptions:options isReferrable:YES explicitlyRequestedReferrable:NO automaticallyDisplayController:NO registerDeepLinkHandler:callback]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options andRegisterDeepLinkHandlerUsingBranchUniversalObject:(callbackWithBranchUniversalObject)callback { + [self initSessionWithLaunchOptions:options isReferrable:YES explicitlyRequestedReferrable:NO automaticallyDisplayController:NO registerDeepLinkHandlerUsingBranchUniversalObject:callback]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options isReferrable:(BOOL)isReferrable { + [self initSessionWithLaunchOptions:options isReferrable:isReferrable explicitlyRequestedReferrable:YES automaticallyDisplayController:NO registerDeepLinkHandler:nil]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options automaticallyDisplayDeepLinkController:(BOOL)automaticallyDisplayController { + [self initSessionWithLaunchOptions:options isReferrable:YES explicitlyRequestedReferrable:NO automaticallyDisplayController:automaticallyDisplayController registerDeepLinkHandler:nil]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options isReferrable:(BOOL)isReferrable andRegisterDeepLinkHandler:(callbackWithParams)callback { + [self initSessionWithLaunchOptions:options isReferrable:isReferrable explicitlyRequestedReferrable:YES automaticallyDisplayController:NO registerDeepLinkHandler:callback]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options automaticallyDisplayDeepLinkController:(BOOL)automaticallyDisplayController deepLinkHandler:(callbackWithParams)callback { + [self initSessionWithLaunchOptions:options isReferrable:YES explicitlyRequestedReferrable:NO automaticallyDisplayController:automaticallyDisplayController registerDeepLinkHandler:callback]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options isReferrable:(BOOL)isReferrable automaticallyDisplayDeepLinkController:(BOOL)automaticallyDisplayController { + [self initSessionWithLaunchOptions:options isReferrable:isReferrable explicitlyRequestedReferrable:YES automaticallyDisplayController:automaticallyDisplayController registerDeepLinkHandler:nil]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options automaticallyDisplayDeepLinkController:(BOOL)automaticallyDisplayController isReferrable:(BOOL)isReferrable deepLinkHandler:(callbackWithParams)callback { + [self initSessionWithLaunchOptions:options isReferrable:isReferrable explicitlyRequestedReferrable:YES automaticallyDisplayController:automaticallyDisplayController registerDeepLinkHandler:callback]; +} + + +#pragma mark - Actual Init Session + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options isReferrable:(BOOL)isReferrable explicitlyRequestedReferrable:(BOOL)explicitlyRequestedReferrable automaticallyDisplayController:(BOOL)automaticallyDisplayController registerDeepLinkHandlerUsingBranchUniversalObject:(callbackWithBranchUniversalObject)callback { + self.sessionInitWithBranchUniversalObjectCallback = callback; + [self initSessionWithLaunchOptions:options isReferrable:isReferrable explicitlyRequestedReferrable:explicitlyRequestedReferrable automaticallyDisplayController:automaticallyDisplayController]; +} + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options isReferrable:(BOOL)isReferrable explicitlyRequestedReferrable:(BOOL)explicitlyRequestedReferrable automaticallyDisplayController:(BOOL)automaticallyDisplayController registerDeepLinkHandler:(callbackWithParams)callback { + self.sessionInitWithParamsCallback = callback; + [self initSessionWithLaunchOptions:options isReferrable:isReferrable explicitlyRequestedReferrable:explicitlyRequestedReferrable automaticallyDisplayController:automaticallyDisplayController]; +} + + +- (void)initSessionWithLaunchOptions:(NSDictionary *)options + isReferrable:(BOOL)isReferrable + explicitlyRequestedReferrable:(BOOL)explicitlyRequestedReferrable + automaticallyDisplayController:(BOOL)automaticallyDisplayController { + // Log this early. Not consistently showing up when logged from [Branch initialize]. + [self.class addBranchSDKVersionToCrashlyticsReport]; + + self.shouldAutomaticallyDeepLink = automaticallyDisplayController; + + // If the SDK is already initialized, this means that initSession is being called later in the app lifecycle + // and that the developer is expecting to receive deep link parameters via the callback block immediately + if (self.initializationStatus == BNCInitStatusInitialized) { + [self initUserSessionAndCallCallback:YES]; + } + + // The rest of this function assumes that initSession is being called BEFORE continueUserActivity and openUrl + // in the application life cycle, and that the SDK is not yet initialized. + + // Handle push notification on app launch + if ([options objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]) { + id branchUrlFromPush = + [options objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey][BRANCH_PUSH_NOTIFICATION_PAYLOAD_KEY]; + if ([branchUrlFromPush isKindOfClass:[NSString class]]) { + self.preferenceHelper.universalLinkUrl = branchUrlFromPush; + self.preferenceHelper.referringURL = branchUrlFromPush; + } + } + + if ([BNCSystemObserver getOSVersion].integerValue >= 8) { + + // Handle case where there's no URI scheme or Universal Link + if (![options.allKeys containsObject:UIApplicationLaunchOptionsURLKey] && + ![options.allKeys containsObject:UIApplicationLaunchOptionsUserActivityDictionaryKey]) { + + self.asyncRequestCount = 0; + + // These methods will increment self.asyncRequestCount if they make an async call: + + // If Facebook SDK is present, call deferred app link check here which will later on call initUserSession + [self checkFacebookAppLinks]; + // If developer opted in, call deferred apple search attribution API here which will later on call initUserSession + [self checkAppleSearchAdsAttribution]; + + if (self.asyncRequestCount == 0) { + // If we're not looking for App Links or Apple Search Ads, initialize + [self initUserSessionAndCallCallback:YES]; + } + } + // Handle case where there is Universal Link present + else if ([options.allKeys containsObject:UIApplicationLaunchOptionsUserActivityDictionaryKey]) { + // Optional flag for the developer if they're letting Facebook return the boolean on didFinishLaunchingWithOptions + // Note that this is no longer a recommended path, and that we tell developers to just return YES + if (self.accountForFacebookSDK) { + // does not work in Swift, because Objective-C to Swift interop is bad + id activity = + [[options objectForKey:UIApplicationLaunchOptionsUserActivityDictionaryKey] + objectForKey:@"UIApplicationLaunchOptionsUserActivityKey"]; + if (activity && [activity isKindOfClass:[NSUserActivity class]]) { + [self continueUserActivity:activity]; + return; + } + } + // Wait for continueUserActivity Branch AppDelegate call to come through + self.preferenceHelper.shouldWaitForInit = YES; + } + } + else if (![options.allKeys containsObject:UIApplicationLaunchOptionsURLKey]) { + [self initUserSessionAndCallCallback:YES]; + } +} + + + +//these params will be added +- (void) setDeepLinkDebugMode:(NSDictionary *)debugParams { + self.deepLinkDebugParams = debugParams; +} + +-(void)setWhiteListedSchemes:(NSArray *)schemes { + self.whiteListedSchemeList = [schemes mutableCopy]; +} + +-(void)addWhiteListedScheme:(NSString *)scheme { + [self.whiteListedSchemeList addObject:scheme]; +} + +- (void) setBlackListURLRegex:(NSArray*)blackListURLs { + @synchronized (self) { + _userURLBlackList = [[BNCURLBlackList alloc] init]; + _userURLBlackList.blackList = blackListURLs; + } +} + +- (NSArray*) blackListURLRegex { + @synchronized (self) { + return _userURLBlackList.blackList; + } +} + +- (BOOL)handleDeepLink:(NSURL *)url { + return [self handleDeepLink:url fromSelf:YES]; +} + +-(BOOL)handleDeepLinkWithNewSession:(NSURL *)url{ + return [self handleDeepLink:url fromSelf:YES]; +} + +- (BOOL)handleDeepLink:(NSURL *)url fromSelf:(BOOL)isFromSelf { + NSString *blackListPattern = nil; + blackListPattern = [self.URLBlackList blackListPatternMatchingURL:url]; + if (!blackListPattern) { + blackListPattern = [_userURLBlackList blackListPatternMatchingURL:url]; + } + if (blackListPattern) { + self.preferenceHelper.blacklistURLOpen = YES; + self.preferenceHelper.externalIntentURI = blackListPattern; + self.preferenceHelper.referringURL = blackListPattern; + if (isFromSelf) [self resetUserSession]; + [self initUserSessionAndCallCallback:(self.initializationStatus != BNCInitStatusInitialized)]; + return NO; + } + + NSString *scheme = [url scheme]; + if ([scheme isEqualToString:@"http"] || [scheme isEqualToString:@"https"]) { + return [self handleUniversalDeepLink_private:url.absoluteString fromSelf:isFromSelf]; + } else { + return [self handleSchemeDeepLink_private:url fromSelf:isFromSelf]; + } +} + +- (BOOL)handleSchemeDeepLink_private:(NSURL*)url fromSelf:(BOOL)isFromSelf { + BOOL handled = NO; + self.preferenceHelper.referringURL = nil; + if (url && ![url isEqual:[NSNull null]]) { + + NSString *urlScheme = [url scheme]; + + // save the incoming url in the preferenceHelper in the externalIntentURI field + if ([self.whiteListedSchemeList count]) { + for (NSString *scheme in self.whiteListedSchemeList) { + if (urlScheme && [scheme isEqualToString:urlScheme]) { + self.preferenceHelper.externalIntentURI = [url absoluteString]; + self.preferenceHelper.referringURL = [url absoluteString]; + break; + } + } + } else { + self.preferenceHelper.externalIntentURI = [url absoluteString]; + self.preferenceHelper.referringURL = [url absoluteString]; + } + + NSString *query = [url fragment]; + if (!query) { + query = [url query]; + } + + NSDictionary *params = [BNCEncodingUtils decodeQueryStringToDictionary:query]; + if (params[@"link_click_id"]) { + handled = YES; + if (isFromSelf) { + [self resetUserSession]; + } + self.preferenceHelper.linkClickIdentifier = params[@"link_click_id"]; + } + } + [self initUserSessionAndCallCallback:(self.initializationStatus != BNCInitStatusInitialized)]; + return handled; +} + + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + sourceApplication:(NSString *)sourceApplication + annotation:(id)annotation { + + BOOL fromSelf = NO; + NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier]; + if (bundleID && sourceApplication) { + fromSelf = [bundleID isEqualToString:sourceApplication]; + } + if (!fromSelf) [self resetUserSession]; + return [self handleDeepLink:url fromSelf:fromSelf]; +} + +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options { + + NSString *source = nil; + NSString *annotation = nil; + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wpartial-availability" + if (UIApplicationOpenURLOptionsSourceApplicationKey && + UIApplicationOpenURLOptionsAnnotationKey) { + source = options[UIApplicationOpenURLOptionsSourceApplicationKey]; + annotation = options[UIApplicationOpenURLOptionsAnnotationKey]; + } + #pragma clang diagnostic pop + return [self application:application openURL:url sourceApplication:source annotation:annotation]; +} + +- (BOOL)handleUniversalDeepLink_private:(NSString*)urlString fromSelf:(BOOL)isFromSelf { + if (isFromSelf) { + [self resetUserSession]; + } + + if (urlString.length) { + self.preferenceHelper.universalLinkUrl = urlString; + self.preferenceHelper.referringURL = urlString; + } + self.preferenceHelper.shouldWaitForInit = NO; + [self initUserSessionAndCallCallback:YES]; + + id branchUniversalLinkDomains = [self.preferenceHelper getBranchUniversalLinkDomains]; + if ([branchUniversalLinkDomains isKindOfClass:[NSString class]] && + [urlString bnc_containsString:branchUniversalLinkDomains]) { + return YES; + } + else if ([branchUniversalLinkDomains isKindOfClass:[NSArray class]]) { + for (id oneDomain in branchUniversalLinkDomains) { + if ([oneDomain isKindOfClass:[NSString class]] && [urlString bnc_containsString:oneDomain]) { + return YES; + } + } + } + + NSString *userActivityURL = urlString; + NSArray *branchDomains = [NSArray arrayWithObjects:@"bnc.lt", @"app.link", @"test-app.link", nil]; + for (NSString* domain in branchDomains) { + if ([userActivityURL bnc_containsString:domain]) + return YES; + } + + return NO; +} + +- (BOOL)continueUserActivity:(NSUserActivity *)userActivity { + BNCLogDebugSDK(@"continueUserActivity:"); + + // Check to see if a browser activity needs to be handled + if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + // If we're already in-progress cancel the last open and do this one. + [self removeInstallOrOpen]; + return [self handleDeepLink:userActivity.webpageURL fromSelf:YES]; + } + + // Check to see if a spotlight activity needs to be handled + NSString *spotlightIdentifier = + [self.contentDiscoveryManager spotlightIdentifierFromActivity:userActivity]; + NSURL *webURL = userActivity.webpageURL; + + + if ([self isBranchLink:userActivity.userInfo[CSSearchableItemActivityIdentifier]]) { + return [self handleDeepLinkWithNewSession:[NSURL URLWithString:userActivity.userInfo[CSSearchableItemActivityIdentifier]]]; + } + else if (webURL != nil && [self isBranchLink:[webURL absoluteString]]) { + return [self handleDeepLinkWithNewSession:webURL]; + } + else if (spotlightIdentifier) { + self.preferenceHelper.spotlightIdentifier = spotlightIdentifier; + } + else { + NSString *nonBranchSpotlightIdentifier = + [self.contentDiscoveryManager standardSpotlightIdentifierFromActivity:userActivity]; + if (nonBranchSpotlightIdentifier) { + self.preferenceHelper.spotlightIdentifier = nonBranchSpotlightIdentifier; + } + } + self.preferenceHelper.shouldWaitForInit = NO; + [self initUserSessionAndCallCallback:YES]; + + return spotlightIdentifier != nil; +} + +- (BOOL)isBranchLink: (NSString*)urlString { + id branchUniversalLinkDomains = [self.preferenceHelper getBranchUniversalLinkDomains]; + if ([branchUniversalLinkDomains isKindOfClass:[NSString class]] && + [urlString containsString:branchUniversalLinkDomains]) { + return YES; + } + else if ([branchUniversalLinkDomains isKindOfClass:[NSArray class]]) { + for (id oneDomain in branchUniversalLinkDomains) { + if ([oneDomain isKindOfClass:[NSString class]] && [urlString containsString:oneDomain]) { + return YES; + } + } + } + + NSString *userActivityURL = urlString; + NSArray *branchDomains = [NSArray arrayWithObjects:@"bnc.lt", @"app.link", @"test-app.link", nil]; + for (NSString* domain in branchDomains) { + if ([userActivityURL containsString:domain]) + return YES; + } + return NO; +} + +#pragma mark - Push Notification support + +- (void)handlePushNotification:(NSDictionary *)userInfo { + // look for a branch shortlink in the payload (shortlink because iOS7 only supports 256 bytes) + NSString *urlStr = [userInfo objectForKey:BRANCH_PUSH_NOTIFICATION_PAYLOAD_KEY]; + if (urlStr.length) { + // reusing this field, so as not to create yet another url slot on prefshelper + self.preferenceHelper.universalLinkUrl = urlStr; + self.preferenceHelper.referringURL = urlStr; + } + + // If app is active, then close out the session and start a new one. + // Else the URL will be handled by `applicationDidBecomeActive`. + + Class UIApplicationClass = NSClassFromString(@"UIApplication"); + if (urlStr && [[UIApplicationClass sharedApplication] applicationState] == UIApplicationStateActive) { + NSURL *url = [NSURL URLWithString:urlStr]; + if (url) [self handleDeepLink:url fromSelf:YES]; + } +} + +#pragma mark - Apple Search Ad Check + +- (void)delayInitToCheckForSearchAds { + self.delayForAppleAds = YES; +} + +- (void)setAppleSearchAdsDebugMode { + self.searchAdsDebugMode = YES; +} + +- (BOOL)checkAppleSearchAdsAttribution { + if (!self.delayForAppleAds) + return NO; + + NSNumber *hasAppleSearchAdAttribution = self.preferenceHelper.appleSearchAdDetails[@"iad-attribution"]; + if ([hasAppleSearchAdAttribution boolValue]) + return NO; + + NSDate *installDatePlus30 = + [[BNCApplication currentApplication].currentInstallDate + dateByAddingTimeInterval:(30.0*24.0*60.0*60.0)]; + if ([installDatePlus30 compare:[NSDate date]] < 0) { + return NO; + } + + Class ADClientClass = NSClassFromString(@"ADClient"); + SEL sharedClient = NSSelectorFromString(@"sharedClient"); + SEL requestAttribution = NSSelectorFromString(@"requestAttributionDetailsWithBlock:"); + + BOOL ADClientIsAvailable = + ADClientClass && + [ADClientClass instancesRespondToSelector:requestAttribution] && + [ADClientClass methodForSelector:sharedClient]; + + if (!ADClientIsAvailable) { + BNCLogWarning(@"`delayForAppleAds` is true but ADClient is not available. Is the iAD.framework included and iOS 10?"); + return NO; + } + + id sharedClientInstance = ((id (*)(id, SEL))[ADClientClass methodForSelector:sharedClient])(ADClientClass, sharedClient); + + self.preferenceHelper.shouldWaitForInit = YES; + self.preferenceHelper.checkedAppleSearchAdAttribution = YES; + self.asyncRequestCount++; + + NSDate *startDate = [NSDate date]; + _Atomic __block BOOL hasBeenCalled = NO; + void (^__nullable completionBlock)(NSDictionary *attrDetails, NSError *error) = + ^ void(NSDictionary *__nullable attrDetails, NSError *__nullable error) { + @synchronized(self) { + NSTimeInterval elapsedSeconds = - [startDate timeIntervalSinceNow]; + BNCLogDebugSDK(@"Elapsed Apple Search Ad callback time: %1.3fs.", elapsedSeconds); + + if (attrDetails.count > 0 && !error) { + [self.preferenceHelper addInstrumentationDictionaryKey:@"apple_search_ad" + value:[[NSNumber numberWithInteger:elapsedSeconds*1000] stringValue]]; + } + if (self.searchAdsDebugMode) { + // If searchAdsDebugMode is on then force the result to a set value for testing. + NSTimeInterval const kOneHour = (60.0*60.0*1.0); // Round down to one day for testing. + NSTimeInterval t = trunc([[NSDate date] timeIntervalSince1970] / kOneHour) * kOneHour; + NSDate *date = [NSDate dateWithTimeIntervalSince1970:t]; + attrDetails = @{ + @"Version3.1": @{ + @"iad-adgroup-id": @1234567890, + @"iad-adgroup-name": @"AdGroupName", + @"iad-attribution": (id)kCFBooleanTrue, + @"iad-campaign-id": @1234567890, + @"iad-campaign-name": @"CampaignName", + @"iad-click-date": date, + @"iad-conversion-date": date, + @"iad-creative-id": @1234567890, + @"iad-creative-name": @"CreativeName", + @"iad-keyword": @"Keyword", + @"iad-lineitem-id": @1234567890, + @"iad-lineitem-name": @"LineName", + @"iad-org-name": @"OrgName" + } + }; + } + if (!error) { + if (attrDetails == nil) + attrDetails = @{}; + if (self.preferenceHelper.appleSearchAdDetails == nil) + self.preferenceHelper.appleSearchAdDetails = @{}; + if (![self.preferenceHelper.appleSearchAdDetails isEqualToDictionary:attrDetails]) { + self.preferenceHelper.appleSearchAdDetails = attrDetails; + self.preferenceHelper.appleSearchAdNeedsSend = YES; + } + } + // Only continue with initialization once: + BOOL localHasBeenCalled = atomic_exchange(&hasBeenCalled, YES); + if (localHasBeenCalled) return; + + if (error) { + BNCLogError(@"Error while getting Apple Search Ad attribution: %@.", error); + } + + self.asyncRequestCount--; + // If there's another async attribution check in flight, don't continue with init: + if (self.asyncRequestCount > 0) return; + + self.preferenceHelper.shouldWaitForInit = NO; + dispatch_async(dispatch_get_main_queue(), ^{ + [self initUserSessionAndCallCallback:(self.initializationStatus != BNCInitStatusInitialized)]; + }); + } + }; + + // Set a expiration timer in case we don't get a call back (I'm looking at you, iPad): + // This may be a temporary quick fix until we figure out a better wat to do search ads. + BNCAfterSecondsPerformBlock(10.0, ^ { + completionBlock(nil, [NSError errorWithDomain:NSNetServicesErrorDomain code:NSNetServicesTimeoutError userInfo:nil]); + }); + + ((void (*)(id, SEL, void (^ __nullable)(NSDictionary *__nullable attrDetails, NSError * __nullable error))) + [sharedClientInstance methodForSelector:requestAttribution]) + (sharedClientInstance, requestAttribution, completionBlock); + + return YES; +} + +#pragma mark - Facebook App Link Check + +- (void)registerFacebookDeepLinkingClass:(id)FBSDKAppLinkUtility { + self.FBSDKAppLinkUtility = FBSDKAppLinkUtility; +} + +- (BOOL)checkFacebookAppLinks { + if (self.FBSDKAppLinkUtility) { + + SEL fetchDeferredAppLink = NSSelectorFromString(@"fetchDeferredAppLink:"); + + if ([self.FBSDKAppLinkUtility methodForSelector:fetchDeferredAppLink]) { + void (^__nullable completionBlock)(NSURL *appLink, NSError *error) = ^void(NSURL *__nullable appLink, NSError *__nullable error) { + self.asyncRequestCount--; + + // if there's another async attribution check in flight, don't continue with init + if (self.asyncRequestCount > 0) { return; } + + self.preferenceHelper.shouldWaitForInit = NO; + + [self handleDeepLink:appLink]; + }; + + self.asyncRequestCount++; + self.preferenceHelper.checkedFacebookAppLinks = YES; + self.preferenceHelper.shouldWaitForInit = YES; + + ((void (*)(id, SEL, void (^ __nullable)(NSURL *__nullable appLink, NSError * __nullable error)))[self.FBSDKAppLinkUtility methodForSelector:fetchDeferredAppLink])(self.FBSDKAppLinkUtility, fetchDeferredAppLink, completionBlock); + + return YES; + } + } + + return NO; +} + +#pragma mark - Deep Link Controller methods + +- (void)registerDeepLinkController:(UIViewController *)controller forKey:(NSString *)key { + self.deepLinkControllers[key] = controller; +} + +- (void)registerDeepLinkController:(UIViewController *)controller forKey:(NSString *)key withPresentation:(BNCViewControllerPresentationOption)option{ + + BNCDeepLinkViewControllerInstance* deepLinkModal = [[BNCDeepLinkViewControllerInstance alloc] init]; + + deepLinkModal.viewController = controller; + deepLinkModal.option = option; + + self.deepLinkControllers[key] = deepLinkModal; +} + + +#pragma mark - Identity methods + +- (void)setIdentity:(NSString *)userId { + [self setIdentity:userId withCallback:NULL]; +} + +- (void)setIdentity:(NSString *)userId withCallback:(callbackWithParams)callback { + if (!userId || [self.preferenceHelper.userIdentity isEqualToString:userId]) { + if (callback) { + callback([self getFirstReferringParams], nil); + } + return; + } + [self initSessionIfNeededAndNotInProgress]; + BranchSetIdentityRequest *req = [[BranchSetIdentityRequest alloc] initWithUserId:userId callback:callback]; + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; +} + +- (void)logout { + [self logoutWithCallback:nil]; +} + + +- (void)logoutWithCallback:(callbackWithStatus)callback { + if (self.initializationStatus == BNCInitStatusUninitialized) { + NSError *error = + (Branch.trackingDisabled) + ? [NSError branchErrorWithCode:BNCTrackingDisabledError] + : [NSError branchErrorWithCode:BNCInitError]; + BNCLogError(@"Branch is not initialized, cannot logout."); + if (callback) {callback(NO, error);} + return; + } + + BranchLogoutRequest *req = + [[BranchLogoutRequest alloc] initWithCallback:^(BOOL success, NSError *error) { + if (success) { + // Clear cached links + self.linkCache = [[BNCLinkCache alloc] init]; + + if (callback) { + callback(YES, nil); + } + BNCLogDebug(@"Logout success."); + } else /*failure*/ { + if (callback) { + callback(NO, error); + } + BNCLogDebug(@"Logout failure."); + } + }]; + + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; +} + + + +#pragma mark - User Action methods + + +- (void)userCompletedAction:(NSString *)action { + [self userCompletedAction:action withState:nil withDelegate:nil]; +} + +- (void)userCompletedAction:(NSString *)action withState:(NSDictionary *)state { + [self userCompletedAction:action withState:state withDelegate:nil]; +} + +- (void)userCompletedAction:(NSString *)action withState:(NSDictionary *)state withDelegate:(id)branchViewCallback { + if (!action) { + return; + } + + [self initSessionIfNeededAndNotInProgress]; + + BranchUserCompletedActionRequest *req = [[BranchUserCompletedActionRequest alloc] initWithAction:action state:state withBranchViewCallback:branchViewCallback]; + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; +} + + +- (void) sendServerRequest:(BNCServerRequest*)request { + [self initSessionIfNeededAndNotInProgress]; + [self.requestQueue enqueue:request]; + [self processNextQueueItem]; +} + +- (void) sendServerRequestWithoutSession:(BNCServerRequest*)request { + [self.requestQueue enqueue:request]; + [self processNextQueueItem]; +} + +- (void) sendCommerceEvent:(BNCCommerceEvent *)commerceEvent + metadata:(NSDictionary*)metadata + withCompletion:(void (^)(NSDictionary *, NSError *))completion { + + [self initSessionIfNeededAndNotInProgress]; + BranchCommerceEventRequest *request = + [[BranchCommerceEventRequest alloc] + initWithCommerceEvent:commerceEvent + metadata:metadata + completion:completion]; + [self.requestQueue enqueue:request]; + [self processNextQueueItem]; +} + +#pragma mark - Credit methods + +- (void)loadRewardsWithCallback:(callbackWithStatus)callback { + [self initSessionIfNeededAndNotInProgress]; + + BranchLoadRewardsRequest *req = [[BranchLoadRewardsRequest alloc] initWithCallback:callback]; + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; +} + +- (NSInteger)getCredits { + return [self.preferenceHelper getCreditCount]; +} + +- (void)redeemRewards:(NSInteger)count { + [self redeemRewards:count forBucket:@"default" callback:NULL]; +} + +- (void)redeemRewards:(NSInteger)count callback:(callbackWithStatus)callback { + [self redeemRewards:count forBucket:@"default" callback:callback]; +} + +- (NSInteger)getCreditsForBucket:(NSString *)bucket { + return [self.preferenceHelper getCreditCountForBucket:bucket]; +} + +- (void)redeemRewards:(NSInteger)count forBucket:(NSString *)bucket { + [self redeemRewards:count forBucket:bucket callback:NULL]; +} + +- (void)redeemRewards:(NSInteger)count forBucket:(NSString *)bucket callback:(callbackWithStatus)callback { + if (count == 0) { + if (callback) { + callback(false, [NSError branchErrorWithCode:BNCRedeemZeroCreditsError]); + } + else { + BNCLogWarning(@"Cannot redeem zero credits."); + } + return; + } + + NSInteger totalAvailableCredits = [self.preferenceHelper getCreditCountForBucket:bucket]; + if (count > totalAvailableCredits) { + if (callback) { + callback(false, [NSError branchErrorWithCode:BNCRedeemCreditsError]); + } + else { + BNCLogWarning(@"You're trying to redeem more credits than are available. Have you loaded rewards?"); + } + return; + } + + [self initSessionIfNeededAndNotInProgress]; + + BranchRedeemRewardsRequest *req = [[BranchRedeemRewardsRequest alloc] initWithAmount:count bucket:bucket callback:callback]; + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; +} + +- (void)getCreditHistoryWithCallback:(callbackWithList)callback { + [self getCreditHistoryForBucket:nil after:nil number:100 order:BranchMostRecentFirst andCallback:callback]; +} + +- (void)getCreditHistoryForBucket:(NSString *)bucket andCallback:(callbackWithList)callback { + [self getCreditHistoryForBucket:bucket after:nil number:100 order:BranchMostRecentFirst andCallback:callback]; +} + +- (void)getCreditHistoryAfter:(NSString *)creditTransactionId number:(NSInteger)length order:(BranchCreditHistoryOrder)order andCallback:(callbackWithList)callback { + [self getCreditHistoryForBucket:nil after:creditTransactionId number:length order:order andCallback:callback]; +} + +- (void)getCreditHistoryForBucket:(NSString *)bucket after:(NSString *)creditTransactionId number:(NSInteger)length order:(BranchCreditHistoryOrder)order andCallback:(callbackWithList)callback { + [self initSessionIfNeededAndNotInProgress]; + + BranchCreditHistoryRequest *req = [[BranchCreditHistoryRequest alloc] initWithBucket:bucket creditTransactionId:creditTransactionId length:length order:order callback:callback]; + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; +} + +- (BranchUniversalObject *)getFirstReferringBranchUniversalObject { + NSDictionary *params = [self getFirstReferringParams]; + if ([[params objectForKey:BRANCH_INIT_KEY_CLICKED_BRANCH_LINK] isEqual:@1]) { + return [BranchUniversalObject objectWithDictionary:params]; + } + return nil; +} + +- (BranchLinkProperties *)getFirstReferringBranchLinkProperties { + NSDictionary *params = [self getFirstReferringParams]; + if ([[params objectForKey:BRANCH_INIT_KEY_CLICKED_BRANCH_LINK] isEqual:@1]) { + return [BranchLinkProperties getBranchLinkPropertiesFromDictionary:params]; + } + return nil; +} + +- (NSDictionary *)getFirstReferringParams { + NSDictionary *origInstallParams = [BNCEncodingUtils decodeJsonStringToDictionary:self.preferenceHelper.installParams]; + + if (self.deepLinkDebugParams) { + NSMutableDictionary* debugInstallParams = + [[BNCEncodingUtils decodeJsonStringToDictionary:self.preferenceHelper.sessionParams] + mutableCopy]; + [debugInstallParams addEntriesFromDictionary:self.deepLinkDebugParams]; + return debugInstallParams; + } + return origInstallParams; +} + +- (NSDictionary *)getLatestReferringParams { + NSDictionary *origSessionParams = [BNCEncodingUtils decodeJsonStringToDictionary:self.preferenceHelper.sessionParams]; + + if (self.deepLinkDebugParams) { + NSMutableDictionary* debugSessionParams = [origSessionParams mutableCopy]; + [debugSessionParams addEntriesFromDictionary:self.deepLinkDebugParams]; + return debugSessionParams; + } + return origSessionParams; +} + +- (NSDictionary*) getLatestReferringParamsSynchronous { + [BranchOpenRequest waitForOpenResponseLock]; + NSDictionary *result = [self getLatestReferringParams]; + [BranchOpenRequest releaseOpenResponseLock]; + return result; +} + +- (BranchUniversalObject *)getLatestReferringBranchUniversalObject { + NSDictionary *params = [self getLatestReferringParams]; + if ([[params objectForKey:BRANCH_INIT_KEY_CLICKED_BRANCH_LINK] isEqual:@1]) { + return [BranchUniversalObject objectWithDictionary:params]; + } + return nil; +} + +- (BranchLinkProperties *)getLatestReferringBranchLinkProperties { + NSDictionary *params = [self getLatestReferringParams]; + if ([[params objectForKey:BRANCH_INIT_KEY_CLICKED_BRANCH_LINK] boolValue]) { + return [BranchLinkProperties getBranchLinkPropertiesFromDictionary:params]; + } + return nil; +} + +#pragma mark - ShortUrl methods + +- (NSString *)getShortURL { + return [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:nil andFeature:nil andStage:nil andCampaign:nil andParams:nil ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params { + return [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:nil andFeature:nil andStage:nil andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage { + return [self generateShortUrl:tags andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andAlias:(NSString *)alias { + return [self generateShortUrl:tags andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andAlias:(NSString *)alias ignoreUAString:(NSString *)ignoreUAString { + return [self generateShortUrl:tags andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:ignoreUAString forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andCampaign:(NSString *)campaign andAlias:(NSString *)alias ignoreUAString:(NSString *)ignoreUAString forceLinkCreation:(BOOL)forceLinkCreation { + return [self generateShortUrl:tags andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:campaign andParams:params ignoreUAString:ignoreUAString forceLinkCreation:forceLinkCreation]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andType:(BranchLinkType)type { + return [self generateShortUrl:tags andAlias:nil andType:type andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andMatchDuration:(NSUInteger)duration { + return [self generateShortUrl:tags andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:duration andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage { + return [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andAlias:(NSString *)alias { + return [self generateShortUrl:nil andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andType:(BranchLinkType)type { + return [self generateShortUrl:nil andAlias:nil andType:type andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andMatchDuration:(NSUInteger)duration { + return [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:duration andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature { + return [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:nil andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortUrlWithParams:(NSDictionary *)params andTags:(NSArray *)tags andAlias:(NSString *)alias andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andCampaign:(NSString *)campaign andMatchDuration:(NSUInteger)duration { + return [self generateShortUrl:tags andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:duration andChannel:channel andFeature:feature andStage:stage andCampaign:campaign andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (NSString *)getShortUrlWithParams:(NSDictionary *)params andTags:(NSArray *)tags andAlias:(NSString *)alias andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andMatchDuration:(NSUInteger)duration { + return [self generateShortUrl:tags andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:duration andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params ignoreUAString:nil forceLinkCreation:YES]; +} + +- (void)getShortURLWithCallback:(callbackWithUrl)callback { + [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:nil andFeature:nil andStage:nil andCampaign:nil andParams:nil andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andCallback:(callbackWithUrl)callback { + [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:nil andFeature:nil andStage:nil andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andCallback:(callbackWithUrl)callback { + [self generateShortUrl:tags andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andAlias:(NSString *)alias andCallback:(callbackWithUrl)callback { + [self generateShortUrl:tags andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andType:(BranchLinkType)type andCallback:(callbackWithUrl)callback { + [self generateShortUrl:tags andAlias:nil andType:type andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andTags:(NSArray *)tags andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andMatchDuration:(NSUInteger)duration andCallback:(callbackWithUrl)callback { + [self generateShortUrl:tags andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:duration andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andCallback:(callbackWithUrl)callback { + [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andAlias:(NSString *)alias andCallback:(callbackWithUrl)callback { + [self generateShortUrl:nil andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andType:(BranchLinkType)type andCallback:(callbackWithUrl)callback { + [self generateShortUrl:nil andAlias:nil andType:type andMatchDuration:0 andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andMatchDuration:(NSUInteger)duration andCallback:(callbackWithUrl)callback { + [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:duration andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andFeature:(NSString *)feature andCallback:(callbackWithUrl)callback { + [self generateShortUrl:nil andAlias:nil andType:BranchLinkTypeUnlimitedUse andMatchDuration:0 andChannel:channel andFeature:feature andStage:nil andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getShortUrlWithParams:(NSDictionary *)params andTags:(NSArray *)tags andAlias:(NSString *)alias andMatchDuration:(NSUInteger)duration andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andCampaign:campaign andCallback:(callbackWithUrl)callback { + [self generateShortUrl:tags andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:duration andChannel:channel andFeature:feature andStage:stage andCampaign:campaign andParams:params andCallback:callback]; +} + +- (void)getShortUrlWithParams:(NSDictionary *)params andTags:(NSArray *)tags andAlias:(NSString *)alias andMatchDuration:(NSUInteger)duration andChannel:(NSString *)channel andFeature:(NSString *)feature andStage:(NSString *)stage andCallback:(callbackWithUrl)callback { + [self generateShortUrl:tags andAlias:alias andType:BranchLinkTypeUnlimitedUse andMatchDuration:duration andChannel:channel andFeature:feature andStage:stage andCampaign:nil andParams:params andCallback:callback]; +} + +- (void)getSpotlightUrlWithParams:(NSDictionary *)params callback:(callbackWithParams)callback { + [self initSessionIfNeededAndNotInProgress]; + + BranchSpotlightUrlRequest *req = [[BranchSpotlightUrlRequest alloc] initWithParams:params callback:callback]; + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; +} + +#pragma mark - LongUrl methods +- (NSString *)getLongURLWithParams:(NSDictionary *)params andChannel:(NSString *)channel andTags:(NSArray *)tags andFeature:(NSString *)feature andStage:(NSString *)stage andAlias:(NSString *)alias { + return [self generateLongURLWithParams:params andChannel:channel andTags:tags andFeature:feature andStage:stage andAlias:alias]; +} + +- (NSString *)getLongURLWithParams:(NSDictionary *)params { + return [self generateLongURLWithParams:params andChannel:nil andTags:nil andFeature:nil andStage:nil andAlias:nil]; +} + +- (NSString *)getLongURLWithParams:(NSDictionary *)params andFeature:(NSString *)feature { + return [self generateLongURLWithParams:params andChannel:nil andTags:nil andFeature:feature andStage:nil andAlias:nil]; +} + +- (NSString *)getLongURLWithParams:(NSDictionary *)params andFeature:(NSString *)feature andStage:(NSString *)stage { + return [self generateLongURLWithParams:params andChannel:nil andTags:nil andFeature:feature andStage:stage andAlias:nil]; +} + +- (NSString *)getLongURLWithParams:(NSDictionary *)params andFeature:(NSString *)feature andStage:(NSString *)stage andTags:(NSArray *)tags { + return [self generateLongURLWithParams:params andChannel:nil andTags:tags andFeature:feature andStage:stage andAlias:nil]; +} + +- (NSString *)getLongURLWithParams:(NSDictionary *)params andFeature:(NSString *)feature andStage:(NSString *)stage andAlias:(NSString *)alias { + return [self generateLongURLWithParams:params andChannel:nil andTags:nil andFeature:feature andStage:stage andAlias:alias]; +} + +#pragma mark - Discoverable content methods + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description { + [self.contentDiscoveryManager indexContentWithTitle:title description:description]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description callback:(callbackWithUrl)callback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description callback:callback]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description publiclyIndexable:(BOOL)publiclyIndexable callback:(callbackWithUrl)callback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable callback:callback]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable callback:(callbackWithUrl)callback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable type:type callback:callback]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable callback:(callbackWithUrl)callback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl callback:callback]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords callback:(callbackWithUrl)callback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl keywords:keywords callback:callback]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl linkParams:(NSDictionary *)linkParams publiclyIndexable:(BOOL)publiclyIndexable { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable thumbnailUrl:thumbnailUrl userInfo:linkParams]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl linkParams:(NSDictionary *)linkParams publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable thumbnailUrl:thumbnailUrl keywords:keywords userInfo:linkParams]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl linkParams:(NSDictionary *)linkParams type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl keywords:keywords userInfo:linkParams]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl keywords:keywords]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl linkParams:(NSDictionary *)linkParams type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords callback:(callbackWithUrl)callback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl keywords:keywords userInfo:linkParams callback:callback]; +} +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl linkParams:(NSDictionary *)linkParams type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords expirationDate:(NSDate *)expirationDate callback:(callbackWithUrl)callback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl keywords:keywords userInfo:linkParams expirationDate:expirationDate callback:callback]; +} +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl canonicalId:canonicalId linkParams:(NSDictionary *)linkParams type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords expirationDate:(NSDate *)expirationDate callback:(callbackWithUrl)callback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description canonicalId:canonicalId publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl keywords:keywords userInfo:linkParams expirationDate:expirationDate callback:callback]; +} + +// Use this with iOS 9+ only +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl linkParams:(NSDictionary *)linkParams type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords expirationDate:(NSDate *)expirationDate spotlightCallback:(callbackWithUrlAndSpotlightIdentifier)spotlightCallback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description canonicalId:nil publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl keywords:keywords userInfo:linkParams expirationDate:expirationDate callback:nil spotlightCallback:spotlightCallback]; +} + +- (void)createDiscoverableContentWithTitle:(NSString *)title description:(NSString *)description thumbnailUrl:(NSURL *)thumbnailUrl canonicalId:(NSString *)canonicalId linkParams:(NSDictionary *)linkParams type:(NSString *)type publiclyIndexable:(BOOL)publiclyIndexable keywords:(NSSet *)keywords expirationDate:(NSDate *)expirationDate spotlightCallback:(callbackWithUrlAndSpotlightIdentifier)spotlightCallback { + [self.contentDiscoveryManager indexContentWithTitle:title description:description canonicalId:canonicalId publiclyIndexable:publiclyIndexable type:type thumbnailUrl:thumbnailUrl keywords:keywords userInfo:linkParams expirationDate:expirationDate callback:nil spotlightCallback:spotlightCallback]; +} + +- (void)indexOnSpotlightWithBranchUniversalObject:(BranchUniversalObject*)universalObject + linkProperties:(BranchLinkProperties*)linkProperties + completion:(void (^) (BranchUniversalObject *universalObject, NSString * url,NSError *error))completion { + BNCSpotlightService *spotlightService = [[BNCSpotlightService alloc] init]; + + if (!universalObject) { + NSError* error = [NSError branchErrorWithCode:BNCInitError localizedMessage:@"Branch Universal Object is nil"]; + if (completion) completion(universalObject,nil,error); + return; + } else { + [spotlightService indexWithBranchUniversalObject:universalObject + linkProperties:linkProperties + callback:^(BranchUniversalObject * _Nullable universalObject, + NSString * _Nullable url, + NSError * _Nullable error) { + if (completion) completion(universalObject,url,error); + }]; + } +} + +/* Indexing of multiple BUOs + * Content privately indexed irrestive of the value of contentIndexMode + */ + + +- (void)indexOnSpotlightUsingSearchableItems:(NSArray* )universalObjects + completion:(void (^) (NSArray* universalObjects, + NSError* error))completion { + + BNCSpotlightService *spotlight = [[BNCSpotlightService alloc] init]; + [spotlight indexPrivatelyWithBranchUniversalObjects:universalObjects + completion:^(NSArray * _Nullable universalObjects, + NSError * _Nullable error) { + if (completion) completion(universalObjects,error); + }]; +} + +- (void)removeSearchableItemWithBranchUniversalObject:(BranchUniversalObject *)universalObject + callback:(void (^_Nullable)(NSError * _Nullable error))completion { + BNCSpotlightService *spotlight = [[BNCSpotlightService alloc] init]; + + NSString *dynamicUrl = [universalObject getLongUrlWithChannel:nil + andTags:nil + andFeature:BNCSpotlightFeature + andStage:nil + andAlias:nil]; + [spotlight removeSearchableItemsWithIdentifier:dynamicUrl + callback:^(NSError * _Nullable error) { + if (completion) completion(error); + }]; +} + + +/* Only removes the indexing of BUOs indexed through CSSearchable item + */ +- (void)removeSearchableItemsWithBranchUniversalObjects:(NSArray *)universalObjects + callback:(void (^)(NSError * error))completion { + BNCSpotlightService *spotlight = [[BNCSpotlightService alloc] init]; + NSMutableArray *identifiers = [[NSMutableArray alloc] init]; + for (BranchUniversalObject* universalObject in universalObjects) { + NSString *dynamicUrl = [universalObject getLongUrlWithChannel:nil + andTags:nil + andFeature:BNCSpotlightFeature + andStage:nil andAlias:nil]; + if (dynamicUrl) [identifiers addObject:dynamicUrl]; + } + + [spotlight removeSearchableItemsWithIdentifiers:identifiers + callback:^(NSError * error) { + if (completion) + completion(error); + }]; +} + +/* Removes all content from spotlight indexed through CSSearchable item and has set the Domain identifier = "com.branch.io" + */ + +- (void)removeAllPrivateContentFromSpotLightWithCallback:(void (^)(NSError * error))completion { + BNCSpotlightService *spotlight = [[BNCSpotlightService alloc] init]; + [spotlight removeAllBranchSearchableItemsWithCallback:^(NSError * _Nullable error) { + completion(error); + }]; +} + +#pragma mark - Private methods + ++ (Branch *)getInstanceInternal:(NSString *)key returnNilIfNoCurrentInstance:(BOOL)returnNilIfNoCurrentInstance { + + static Branch *branch = nil; + @synchronized (self) { + if (!branch && returnNilIfNoCurrentInstance) { + return nil; + } + + static dispatch_once_t onceToken = 0; + dispatch_once(&onceToken, ^{ + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + + // If there was stored key and it isn't the same as the currently used (or doesn't exist), we need to clean up + // Note: Link Click Identifier is not cleared because of the potential for that to mess up a deep link + if (preferenceHelper.lastRunBranchKey && ![key isEqualToString:preferenceHelper.lastRunBranchKey]) { + BNCLogWarning(@"The Branch Key has changed, clearing relevant items."); + + preferenceHelper.appVersion = nil; + preferenceHelper.deviceFingerprintID = nil; + preferenceHelper.sessionID = nil; + preferenceHelper.identityID = nil; + preferenceHelper.userUrl = nil; + preferenceHelper.installParams = nil; + preferenceHelper.sessionParams = nil; + + [[BNCServerRequestQueue getInstance] clearQueue]; + } + + if (self.enableFingerprintIDInCrashlyticsReports) { + BNCCrashlyticsWrapper *crashlytics = [BNCCrashlyticsWrapper wrapper]; + // may be nil + [crashlytics setObjectValue:preferenceHelper.deviceFingerprintID forKey:BRANCH_CRASHLYTICS_FINGERPRINT_ID_KEY]; + } + + preferenceHelper.lastRunBranchKey = key; + branch = + [[Branch alloc] initWithInterface:[[BNCServerInterface alloc] init] + queue:[BNCServerRequestQueue getInstance] + cache:[[BNCLinkCache alloc] init] + preferenceHelper:preferenceHelper + key:key]; + }); + return branch; + } +} + + +#pragma mark - URL Generation methods + +- (void)generateShortUrl:(NSArray *)tags + andAlias:(NSString *)alias + andType:(BranchLinkType)type + andMatchDuration:(NSUInteger)duration + andChannel:(NSString *)channel + andFeature:(NSString *)feature + andStage:(NSString *)stage + andCampaign:campaign andParams:(NSDictionary *)params + andCallback:(callbackWithUrl)callback { + [self initSessionIfNeededAndNotInProgress]; + + BNCLinkData *linkData = [self prepareLinkDataFor:tags + andAlias:alias + andType:type + andMatchDuration:duration + andChannel:channel + andFeature:feature + andStage:stage + andCampaign:campaign + andParams:params + ignoreUAString:nil]; + + if ([self.linkCache objectForKey:linkData]) { + if (callback) { + callback([self.linkCache objectForKey:linkData], nil); + } + return; + } + + BranchShortUrlRequest *req = [[BranchShortUrlRequest alloc] initWithTags:tags + alias:alias + type:type + matchDuration:duration + channel:channel + feature:feature + stage:stage + campaign:campaign + params:params + linkData:linkData + linkCache:self.linkCache + callback:callback]; + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; +} + +- (NSString *)generateShortUrl:(NSArray *)tags + andAlias:(NSString *)alias + andType:(BranchLinkType)type + andMatchDuration:(NSUInteger)duration + andChannel:(NSString *)channel + andFeature:(NSString *)feature + andStage:(NSString *)stage + andCampaign:(NSString *)campaign + andParams:(NSDictionary *)params + ignoreUAString:(NSString *)ignoreUAString + forceLinkCreation:(BOOL)forceLinkCreation { + + NSString *shortURL = nil; + + BNCLinkData *linkData = + [self prepareLinkDataFor:tags + andAlias:alias + andType:type + andMatchDuration:duration + andChannel:channel + andFeature:feature + andStage:stage + andCampaign:campaign + andParams:params + ignoreUAString:ignoreUAString]; + + // If an ignore UA string is present, we always get a new url. + // Otherwise, if we've already seen this request, use the cached version. + if (!ignoreUAString && [self.linkCache objectForKey:linkData]) { + shortURL = [self.linkCache objectForKey:linkData]; + } else { + BranchShortUrlSyncRequest *req = + [[BranchShortUrlSyncRequest alloc] + initWithTags:tags + alias:alias + type:type + matchDuration:duration + channel:channel + feature:feature + stage:stage + campaign:campaign + params:params + linkData:linkData + linkCache:self.linkCache]; + + if ((self.initializationStatus == BNCInitStatusInitialized) && !self.preferenceHelper.trackingDisabled) { + BNCLogDebug(@"Creating a custom URL synchronously."); + BNCServerResponse *serverResponse = [req makeRequest:self.serverInterface key:self.class.branchKey]; + shortURL = [req processResponse:serverResponse]; + + // cache the link + if (shortURL) { + [self.linkCache setObject:shortURL forKey:linkData]; + } + } else { + if (forceLinkCreation) { + if (self.class.branchKey) { + return [BranchShortUrlSyncRequest createLinkFromBranchKey:self.class.branchKey + tags:tags alias:alias type:type matchDuration:duration + channel:channel feature:feature stage:stage params:params]; + } + } + BNCLogError(@"Making a Branch request before init has succeeded!"); + } + } + + return shortURL; +} + +- (NSString *)generateLongURLWithParams:(NSDictionary *)params + andChannel:(NSString *)channel + andTags:(NSArray *)tags + andFeature:(NSString *)feature + andStage:(NSString *)stage + andAlias:(NSString *)alias { + + NSString *baseLongUrl = [NSString stringWithFormat:@"%@/a/%@", BNC_LINK_URL, self.class.branchKey]; + + return [self longUrlWithBaseUrl:baseLongUrl params:params tags:tags feature:feature + channel:nil stage:stage alias:alias duration:0 type:BranchLinkTypeUnlimitedUse]; +} + +- (NSString *)longUrlWithBaseUrl:(NSString *)baseUrl + params:(NSDictionary *)params + tags:(NSArray *)tags + feature:(NSString *)feature + channel:(NSString *)channel + stage:(NSString *)stage + alias:(NSString *)alias + duration:(NSUInteger)duration + type:(BranchLinkType)type { + + NSMutableString *longUrl = [self.preferenceHelper sanitizedMutableBaseURL:baseUrl]; + for (NSString *tag in tags) { + [longUrl appendFormat:@"tags=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:tag]]; + } + + if ([alias length]) { + [longUrl appendFormat:@"alias=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:alias]]; + } + + if ([channel length]) { + [longUrl appendFormat:@"channel=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:channel]]; + } + + if ([feature length]) { + [longUrl appendFormat:@"feature=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:feature]]; + } + + if ([stage length]) { + [longUrl appendFormat:@"stage=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:stage]]; + } + if (type) { + [longUrl appendFormat:@"type=%ld&", (long)type]; + } + if (duration) { + [longUrl appendFormat:@"matchDuration=%ld&", (long)duration]; + } + + NSData *jsonData = [BNCEncodingUtils encodeDictionaryToJsonData:params]; + NSString *base64EncodedParams = [BNCEncodingUtils base64EncodeData:jsonData]; + [longUrl appendFormat:@"source=ios&data=%@", base64EncodedParams]; + + return longUrl; +} + +- (BNCLinkData *)prepareLinkDataFor:(NSArray *)tags + andAlias:(NSString *)alias + andType:(BranchLinkType)type + andMatchDuration:(NSUInteger)duration + andChannel:(NSString *)channel + andFeature:(NSString *)feature + andStage:(NSString *)stage + andCampaign:(NSString *)campaign + andParams:(NSDictionary *)params + ignoreUAString:(NSString *)ignoreUAString { + + BNCLinkData *post = [[BNCLinkData alloc] init]; + + [post setupType:type]; + [post setupTags:tags]; + [post setupChannel:channel]; + [post setupFeature:feature]; + [post setupStage:stage]; + [post setupCampaign:campaign]; + [post setupAlias:alias]; + [post setupMatchDuration:duration]; + [post setupIgnoreUAString:ignoreUAString]; + [post setupParams:params]; + + return post; +} + +#pragma mark - BranchUniversalObject methods + +- (void)registerViewWithParams:(NSDictionary *)params andCallback:(callbackWithParams)callback { + [self initSessionIfNeededAndNotInProgress]; + BranchUniversalObject *buo = [[BranchUniversalObject alloc] init]; + buo.contentMetadata.customMetadata = (id) params; + [[BranchEvent standardEvent:BranchStandardEventViewItem withContentItem:buo] logEvent]; + if (callback) callback(@{}, nil); +} + +#pragma mark - Application State Change methods + +- (void)applicationDidBecomeActive { + if (!Branch.trackingDisabled) { + if ((self.initializationStatus != BNCInitStatusInitialized) && + !self.preferenceHelper.shouldWaitForInit && + ![self.requestQueue containsInstallOrOpen]) { + [self initUserSessionAndCallCallback:YES]; + } + } +} + +- (void)applicationWillResignActive { + if (!Branch.trackingDisabled) { + [self callClose]; + [self.requestQueue persistImmediately]; + [BranchOpenRequest setWaitNeededForOpenResponseLock]; + BNCLogDebugSDK(@"Application resigned active."); + [self.class closeLog]; + [self.class openLog]; + } +} + +- (void)callClose { + if (self.initializationStatus != BNCInitStatusUninitialized) { + self.initializationStatus = BNCInitStatusUninitialized; + + BranchContentDiscoverer *contentDiscoverer = [BranchContentDiscoverer getInstance]; + if (contentDiscoverer) [contentDiscoverer stopDiscoveryTask]; + + if (self.preferenceHelper.sessionID && ![self.requestQueue containsClose]) { + BranchCloseRequest *req = [[BranchCloseRequest alloc] init]; + [self.requestQueue enqueue:req]; + [self processNextQueueItem]; + } + } +} + +#pragma mark - Queue management + +- (NSInteger) networkCount { + @synchronized (self) { + return _networkCount; + } +} + +- (void) setNetworkCount:(NSInteger)networkCount { + @synchronized (self) { + _networkCount = networkCount; + } +} + +- (void)insertRequestAtFront:(BNCServerRequest *)req { + if (self.networkCount == 0) { + [self.requestQueue insert:req at:0]; + } + else { + [self.requestQueue insert:req at:1]; + } +} + +static inline void BNCPerformBlockOnMainThreadSync(dispatch_block_t block) { + if (block) { + if ([NSThread isMainThread]) { + block(); + } else { + dispatch_sync(dispatch_get_main_queue(), block); + } + } +} + +//static inline void BNCPerformBlockOnMainThreadAsync(dispatch_block_t block) { +// dispatch_async(dispatch_get_main_queue(), block); +//} + +- (void) processRequest:(BNCServerRequest*)req + response:(BNCServerResponse*)response + error:(NSError*)error { + + // If the request was successful, or was a bad user request, continue processing. + if (!error || + error.code == BNCTrackingDisabledError || + error.code == BNCBadRequestError || + error.code == BNCDuplicateResourceError) { + + BNCPerformBlockOnMainThreadSync(^{ [req processResponse:response error:error]; }); + + [self.requestQueue dequeue]; + self.networkCount = 0; + [self processNextQueueItem]; + } + // On network problems, or Branch down, call the other callbacks and stop processing. + else { + BNCLogDebugSDK(@"Network error: failing queued requests."); + + // First, gather all the requests to fail + NSMutableArray *requestsToFail = [[NSMutableArray alloc] init]; + for (int i = 0; i < self.requestQueue.queueDepth; i++) { + BNCServerRequest *request = [self.requestQueue peekAt:i]; + if (request) { + [requestsToFail addObject:request]; + } + } + + // Next, remove all the requests that should not be replayed. Note, we do this before + // calling callbacks, in case any of the callbacks try to kick off another request, which + // could potentially start another request (and call these callbacks again) + + NSSet *replayableRequests = [[NSSet alloc] initWithArray:@[ + BranchEventRequest.class, + BranchUserCompletedActionRequest.class, + BranchSetIdentityRequest.class, + BranchCommerceEventRequest.class, + ]]; + + for (BNCServerRequest *request in requestsToFail) { + if (Branch.trackingDisabled || ![replayableRequests containsObject:request.class]) { + [self.requestQueue remove:request]; + } + } + + // Then, set the network count to zero, indicating that requests can be started again + self.networkCount = 0; + + // Finally, call all the requests callbacks with the error + for (BNCServerRequest *request in requestsToFail) { + BNCPerformBlockOnMainThreadSync(^ { [request processResponse:nil error:error]; }); + } + } +} + +- (void)processNextQueueItem { + dispatch_semaphore_wait(self.processing_sema, DISPATCH_TIME_FOREVER); + + if (self.networkCount == 0 && + self.requestQueue.queueDepth > 0 && + !self.preferenceHelper.shouldWaitForInit) { + + self.networkCount = 1; + dispatch_semaphore_signal(self.processing_sema); + BNCServerRequest *req = [self.requestQueue peek]; + + if (req) { + + if (Branch.trackingDisabled) { + // If tracking is disabled keep failing everything in the queue. + } + else if (![req isKindOfClass:[BranchInstallRequest class]] && !self.preferenceHelper.identityID) { + BNCLogError(@"User session has not been initialized!"); + BNCPerformBlockOnMainThreadSync(^{ + [req processResponse:nil error:[NSError branchErrorWithCode:BNCInitError]]; + }); + return; + } + else if (![req isKindOfClass:[BranchOpenRequest class]] && + (!self.preferenceHelper.deviceFingerprintID || !self.preferenceHelper.sessionID)) { + BNCLogError(@"Missing session items!"); + BNCPerformBlockOnMainThreadSync(^{ + [req processResponse:nil error:[NSError branchErrorWithCode:BNCInitError]]; + }); + return; + } + + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); + dispatch_async(queue, ^ { + [req makeRequest:self.serverInterface key:self.class.branchKey callback: + ^(BNCServerResponse* response, NSError* error) { + [self processRequest:req response:response error:error]; + }]; + }); + } + } + else { + dispatch_semaphore_signal(self.processing_sema); + } +} + +- (void) clearNetworkQueue { + dispatch_semaphore_wait(self.processing_sema, DISPATCH_TIME_FOREVER); + self.networkCount = 0; + [[BNCServerRequestQueue getInstance] clearQueue]; + dispatch_semaphore_signal(self.processing_sema); +} + +#pragma mark - Session Initialization + +- (void)initSessionIfNeededAndNotInProgress { + if (self.initializationStatus == BNCInitStatusUninitialized && + !self.preferenceHelper.shouldWaitForInit && + ![self.requestQueue containsInstallOrOpen]) { + [self initUserSessionAndCallCallback:NO]; + } +} + +- (void)initUserSessionAndCallCallback:(BOOL)callCallback { + self.shouldCallSessionInitCallback = callCallback; + + NSString *urlstring = nil; + if (self.preferenceHelper.universalLinkUrl.length) + urlstring = self.preferenceHelper.universalLinkUrl; + else + if (self.preferenceHelper.externalIntentURI.length) + urlstring = self.preferenceHelper.externalIntentURI; + + if (urlstring.length) { + NSArray *queryItems = [BNCEncodingUtils queryItems:[NSURL URLWithString:urlstring]]; + for (BNCKeyValue*item in queryItems) { + if ([item.key isEqualToString:@"BranchLogLevel"]) { + BNCLogLevel logLevel = BNCLogLevelFromString(item.value); + [[NSUserDefaults standardUserDefaults] + setObject:[NSNumber numberWithInteger:logLevel] + forKey:BNCLogLevelKey]; + BNCLogSetDisplayLevel(logLevel); + NSLog(@"[io.branch.sdk] BNCLogLevel set to %ld.", (long) logLevel); + } + } + } + // If the session is not yet initialized + if (self.initializationStatus == BNCInitStatusUninitialized) { + [self initializeSession]; + } + // If the session was initialized, but callCallback was specified, do so. + else if (callCallback && self.initializationStatus == BNCInitStatusInitialized) { + if (self.sessionInitWithParamsCallback) { + self.sessionInitWithParamsCallback([self getLatestReferringParams], nil); + } + else if (self.sessionInitWithBranchUniversalObjectCallback) { + self.sessionInitWithBranchUniversalObjectCallback( + [self getLatestReferringBranchUniversalObject], + [self getLatestReferringBranchLinkProperties], + nil + ); + } + } +} + +- (void)initializeSession { + Class clazz = [BranchInstallRequest class]; + if (self.preferenceHelper.identityID) { + clazz = [BranchOpenRequest class]; + } + + callbackWithStatus initSessionCallback = ^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^ { + if (error) { + [self handleInitFailure:error]; + } else { + [self handleInitSuccess]; + } + }); + }; + + if ([BNCSystemObserver getOSVersion].integerValue >= 9 && self.useCookieBasedMatching) { + [[BNCStrongMatchHelper strongMatchHelper] createStrongMatchWithBranchKey:self.class.branchKey]; + } + + // Notify everyone -- + + NSURL *URL = + (self.preferenceHelper.referringURL.length) + ? [NSURL URLWithString:self.preferenceHelper.referringURL] + : nil; + + if ([self.delegate respondsToSelector:@selector(branch:willStartSessionWithURL:)]) + [self.delegate branch:self willStartSessionWithURL:URL]; + + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + userInfo[BranchURLKey] = URL; + [[NSNotificationCenter defaultCenter] + postNotificationName:BranchWillStartSessionNotification + object:self + userInfo:userInfo]; + + // Fix the queue order and open -- + + @synchronized (self) { + [self removeInstallOrOpen]; + [BranchOpenRequest setWaitNeededForOpenResponseLock]; + BranchOpenRequest *req = [[clazz alloc] initWithCallback:initSessionCallback]; + [self insertRequestAtFront:req]; + self.initializationStatus = BNCInitStatusInitializing; + [self processNextQueueItem]; + } +} + +- (BOOL) removeInstallOrOpen { + @synchronized (self) { + if ([self.requestQueue removeInstallOrOpen]) { + self.networkCount = 0; + return YES; + } + return NO; + } +} + +- (void)handleInitSuccess { + + self.initializationStatus = BNCInitStatusInitialized; + NSDictionary *latestReferringParams = [self getLatestReferringParams]; + + if ([latestReferringParams[@"_branch_validate"] isEqualToString:@"060514"]) { + [self validateDeeplinkRouting:latestReferringParams]; + } + else if (([latestReferringParams[@"bnc_validate"] isEqualToString:@"true"])) { + NSString* referringLink = [self.class returnNonUniversalLink:latestReferringParams[@"~referring_link"] ]; + NSURLComponents *comp = [NSURLComponents componentsWithURL:[NSURL URLWithString:referringLink] + resolvingAgainstBaseURL:NO]; + + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Warc-performSelector-leaks" + Class applicationClass = NSClassFromString(@"UIApplication"); + id sharedApplication = [applicationClass performSelector:@selector(sharedApplication)]; + SEL openURL = @selector(openURL:); + if ([sharedApplication respondsToSelector:openURL]) + [sharedApplication performSelector:openURL withObject:comp.URL]; + #pragma clang diagnostic pop + } + + if (self.shouldCallSessionInitCallback) { + if (self.sessionInitWithParamsCallback) { + self.sessionInitWithParamsCallback(latestReferringParams, nil); + } + else if (self.sessionInitWithBranchUniversalObjectCallback) { + self.sessionInitWithBranchUniversalObjectCallback( + [self getLatestReferringBranchUniversalObject], + [self getLatestReferringBranchLinkProperties], + nil + ); + } + } + [self sendOpenNotificationWithLinkParameters:latestReferringParams error:nil]; + + if (!self.URLBlackList.hasRefreshedBlackListFromServer) + [self.URLBlackList refreshBlackListFromServerWithCompletion:nil]; + + if (self.shouldAutomaticallyDeepLink) { + // Find any matched keys, then launch any controllers that match + // TODO which one to launch if more than one match? + NSMutableSet *keysInParams = [NSMutableSet setWithArray:[latestReferringParams allKeys]]; + NSSet *desiredKeysSet = [NSSet setWithArray:[self.deepLinkControllers allKeys]]; + [keysInParams intersectSet:desiredKeysSet]; + + // If we find a matching key, configure and show the controller + if ([keysInParams count]) { + NSString *key = [[keysInParams allObjects] firstObject]; + UIViewController *branchSharingController = self.deepLinkControllers[key]; + if ([branchSharingController respondsToSelector:@selector(configureControlWithData:)]) { + [branchSharingController configureControlWithData:latestReferringParams]; + } + else { + BNCLogWarning(@"The automatic deeplink view controller '%@' for key '%@' does not implement 'configureControlWithData:'.", + branchSharingController, key); + } + + self.deepLinkPresentingController = [UIViewController bnc_currentViewController]; + if([self.deepLinkControllers[key] isKindOfClass:[BNCDeepLinkViewControllerInstance class]]) { + BNCDeepLinkViewControllerInstance* deepLinkInstance = self.deepLinkControllers[key]; + UIViewController *branchSharingController = deepLinkInstance.viewController; + + if ([branchSharingController respondsToSelector:@selector(configureControlWithData:)]) { + [branchSharingController configureControlWithData:latestReferringParams]; + } + else { + BNCLogWarning(@"View controller does not implement configureControlWithData:"); + } + branchSharingController.deepLinkingCompletionDelegate = self; + switch (deepLinkInstance.option) { + case BNCViewControllerOptionPresent: + [self presentSharingViewController:branchSharingController]; + break; + + case BNCViewControllerOptionPush: + + if ([self.deepLinkPresentingController isKindOfClass:[UINavigationController class]]) { + + if ([[(UINavigationController*)self.deepLinkPresentingController viewControllers] + containsObject:branchSharingController]) { + [self removeViewControllerFromRootNavigationController:branchSharingController]; + [(UINavigationController*)self.deepLinkPresentingController + pushViewController:branchSharingController animated:false]; + } + else { + [(UINavigationController*)self.deepLinkPresentingController + pushViewController:branchSharingController animated:true]; + } + } + else { + deepLinkInstance.option = BNCViewControllerOptionPresent; + [self presentSharingViewController:branchSharingController]; + } + + break; + + default: + if ([self.deepLinkPresentingController isKindOfClass:[UINavigationController class]]) { + if ([self.deepLinkPresentingController respondsToSelector:@selector(showViewController:sender:)]) { + + if ([[(UINavigationController*)self.deepLinkPresentingController viewControllers] + containsObject:branchSharingController]) { + [self removeViewControllerFromRootNavigationController:branchSharingController]; + } + + [self.deepLinkPresentingController showViewController:branchSharingController sender:self]; + } + else { + deepLinkInstance.option = BNCViewControllerOptionPush; + [(UINavigationController*)self.deepLinkPresentingController + pushViewController:branchSharingController animated:true]; + } + } + else { + deepLinkInstance.option = BNCViewControllerOptionPresent; + [self presentSharingViewController:branchSharingController]; + } + break; + } + } + else { + + //Support for old API + UIViewController *branchSharingController = self.deepLinkControllers[key]; + if ([branchSharingController respondsToSelector:@selector(configureControlWithData:)]) { + [branchSharingController configureControlWithData:latestReferringParams]; + } + else { + BNCLogWarning(@"View controller does not implement configureControlWithData:"); + } + branchSharingController.deepLinkingCompletionDelegate = self; + if ([self.deepLinkPresentingController presentedViewController]) { + [self.deepLinkPresentingController dismissViewControllerAnimated:NO completion:^{ + [self.deepLinkPresentingController presentViewController:branchSharingController animated:YES completion:NULL]; + }]; + } + else { + [self.deepLinkPresentingController presentViewController:branchSharingController animated:YES completion:NULL]; + } + } + } + } +} + +- (void) sendOpenNotificationWithLinkParameters:(NSDictionary*)linkParameters + error:(NSError*)error { + + NSURL *originalURL = + (self.preferenceHelper.referringURL.length) + ? [NSURL URLWithString:self.preferenceHelper.referringURL] + : nil; + BranchLinkProperties *linkProperties = nil; + BranchUniversalObject *universalObject = nil; + + NSNumber *isBranchLink = linkParameters[BRANCH_INIT_KEY_CLICKED_BRANCH_LINK]; + if ([isBranchLink boolValue]) { + universalObject = [BranchUniversalObject objectWithDictionary:linkParameters]; + linkProperties = [BranchLinkProperties getBranchLinkPropertiesFromDictionary:linkParameters]; + } + + if (error) { + + if ([self.delegate respondsToSelector:@selector(branch:failedToStartSessionWithURL:error:)]) + [self.delegate branch:self failedToStartSessionWithURL:originalURL error:error]; + + } else { + + BranchLink *branchLink = nil; + if (universalObject) { + branchLink = [BranchLink linkWithUniversalObject:universalObject properties:linkProperties]; + } + if ([self.delegate respondsToSelector:@selector(branch:didStartSessionWithURL:branchLink:)]) + [self.delegate branch:self didStartSessionWithURL:originalURL branchLink:branchLink]; + + } + + NSMutableDictionary *userInfo = [NSMutableDictionary new]; + userInfo[BranchErrorKey] = error; + userInfo[BranchURLKey] = originalURL; + userInfo[BranchUniversalObjectKey] = universalObject; + userInfo[BranchLinkPropertiesKey] = linkProperties; + [[NSNotificationCenter defaultCenter] + postNotificationName:BranchDidStartSessionNotification + object:self + userInfo:userInfo]; + + self.preferenceHelper.referringURL = nil; +} + +-(void)removeViewControllerFromRootNavigationController:(UIViewController*)branchSharingController{ + + NSMutableArray* viewControllers = + [NSMutableArray arrayWithArray: [(UINavigationController*)self.deepLinkPresentingController viewControllers]]; + + if ([viewControllers lastObject] == branchSharingController) { + + [(UINavigationController*)self.deepLinkPresentingController popViewControllerAnimated:YES]; + }else { + [viewControllers removeObject:branchSharingController]; + ((UINavigationController*)self.deepLinkPresentingController).viewControllers = viewControllers; + } +} + +-(void)presentSharingViewController: (UIViewController *)branchSharingController{ + if ([self.deepLinkPresentingController presentedViewController]) { + [self.deepLinkPresentingController dismissViewControllerAnimated:NO completion:^{ + [self.deepLinkPresentingController presentViewController:branchSharingController animated:YES completion:NULL]; + }]; + } + else { + [self.deepLinkPresentingController presentViewController:branchSharingController animated:YES completion:NULL]; + } +} + +- (void)handleInitFailure:(NSError *)error { + self.initializationStatus = BNCInitStatusUninitialized; + + if (self.shouldCallSessionInitCallback) { + if (self.sessionInitWithParamsCallback) { + self.sessionInitWithParamsCallback([[NSDictionary alloc] init], error); + } + else if (self.sessionInitWithBranchUniversalObjectCallback) { + self.sessionInitWithBranchUniversalObjectCallback( + [[BranchUniversalObject alloc] init], + [[BranchLinkProperties alloc] init], + error + ); + } + } + + [self sendOpenNotificationWithLinkParameters:@{} error:error]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + + +#pragma mark - BranchDeepLinkingControllerCompletionDelegate methods + + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)deepLinkingControllerCompleted { + [self.deepLinkPresentingController dismissViewControllerAnimated:YES completion:NULL]; +} +#pragma clang diagnostic pop + +- (void)deepLinkingControllerCompletedFrom:(UIViewController *)viewController { + [self.deepLinkControllers enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + + if([obj isKindOfClass:[BNCDeepLinkViewControllerInstance class]]) { + BNCDeepLinkViewControllerInstance* deepLinkInstance = (BNCDeepLinkViewControllerInstance*) obj; + + if (deepLinkInstance.viewController == viewController) { + + switch (deepLinkInstance.option) { + case BNCViewControllerOptionPresent: + [viewController dismissViewControllerAnimated:YES completion:nil]; + break; + + default: + [self removeViewControllerFromRootNavigationController:viewController]; + break; + } + } + + } else { + //Support for old API + if ((UIViewController*)obj == viewController) + [self.deepLinkPresentingController dismissViewControllerAnimated:YES completion:nil]; + } + + }]; +} + +#pragma mark - FABKit methods + ++ (NSString *)bundleIdentifier { + return @"io.branch.sdk.ios"; +} + ++ (NSString *)kitDisplayVersion { + return BNC_SDK_VERSION; +} + +#pragma mark - Crashlytics reporting enhancements + ++ (void)logLowMemoryToCrashlytics { + [NSNotificationCenter.defaultCenter + addObserverForName:UIApplicationDidReceiveMemoryWarningNotification + object:nil + queue:NSOperationQueue.mainQueue + usingBlock:^(NSNotification *notification) { + BNCCrashlyticsWrapper *crashlytics = [BNCCrashlyticsWrapper wrapper]; + [crashlytics setBoolValue:YES forKey:BRANCH_CRASHLYTICS_LOW_MEMORY_KEY]; + } + ]; +} + ++ (void)addBranchSDKVersionToCrashlyticsReport { + BNCCrashlyticsWrapper *crashlytics = [BNCCrashlyticsWrapper wrapper]; + [crashlytics setObjectValue:BNC_SDK_VERSION forKey:BRANCH_CRASHLYTICS_SDK_VERSION_KEY]; +} + ++ (void) clearAll { + [[BNCServerRequestQueue getInstance] clearQueue]; + [BranchOpenRequest releaseOpenResponseLock]; + [BNCPreferenceHelper clearAll]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchActivityItemProvider.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchActivityItemProvider.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchActivityItemProvider.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchActivityItemProvider.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchActivityItemProvider.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchActivityItemProvider.m new file mode 100644 index 00000000000..cf22849a146 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchActivityItemProvider.m @@ -0,0 +1,251 @@ +// +// BranchActivityItemProvider.m +// Branch-TestBed +// +// Created by Scott Hasbrouck on 1/28/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchActivityItemProvider.h" +#import "Branch.h" +#import "BranchConstants.h" +#import "BNCSystemObserver.h" +#import "BNCDeviceInfo.h" + +@interface BranchActivityItemProvider () + +@property (strong, nonatomic) NSDictionary *params; +@property (strong, nonatomic) NSArray *tags; +@property (strong, nonatomic) NSString *feature; +@property (strong, nonatomic) NSString *stage; +@property (strong, nonatomic) NSString *campaign; +@property (strong, nonatomic) NSString *alias; +@property (strong, nonatomic) NSString *userAgentString; +@property (weak, nonatomic) id delegate; + +@end + +@implementation BranchActivityItemProvider + +- (id)initWithParams:(NSDictionary *)params + andTags:(NSArray *)tags + andFeature:(NSString *)feature + andStage:(NSString *)stage + andAlias:(NSString *)alias { + return [self initWithParams:params tags:tags feature:feature stage:stage campaign:nil alias:alias delegate:nil]; +} + +- (id)initWithParams:(NSDictionary *)params + tags:(NSArray *)tags + feature:(NSString *)feature + stage:(NSString *)stage + campaign:(NSString *)campaign + alias:(NSString *)alias + delegate:(id )delegate { + + NSString *url = + [[Branch getInstance] + getLongURLWithParams:params + andChannel:nil + andTags:tags + andFeature:feature + andStage:stage + andAlias:alias]; + + if (self.returnURL) { + if ((self = [super initWithPlaceholderItem:[NSURL URLWithString:url]])) { + _params = params; + _tags = tags; + _feature = feature; + _stage = stage; + _campaign = campaign; + _alias = alias; + _userAgentString = [BNCDeviceInfo userAgentString]; + _delegate = delegate; + } + } else { + if ((self = [super initWithPlaceholderItem:url])) { + _params = params; + _tags = tags; + _feature = feature; + _stage = stage; + _campaign = campaign; + _alias = alias; + _userAgentString = [BNCDeviceInfo userAgentString]; + _delegate = delegate; + } + } + return self; +} + +- (BOOL) returnURL { + BOOL returnURL = YES; + if ([UIDevice currentDevice].systemVersion.doubleValue >= 11.0 && + [UIDevice currentDevice].systemVersion.doubleValue < 11.2 && + [self.activityType isEqualToString:UIActivityTypeCopyToPasteboard]) { + returnURL = NO; + } + return returnURL; +} + +- (id)item { + NSString *channel = [BranchActivityItemProvider humanReadableChannelWithActivityType:self.activityType]; + + + // Allow for overrides specific to channel + NSDictionary *params = [self paramsForChannel:channel]; + NSArray *tags = [self tagsForChannel:channel]; + NSString *feature = [self featureForChannel:channel]; + NSString *stage = [self stageForChannel:channel]; + NSString *campaign = [self campaignForChannel:channel]; + NSString *alias = [self aliasForChannel:channel]; + + // Allow the channel param to be overridden, perhaps they want "fb" instead of "facebook" + if ([self.delegate respondsToSelector:@selector(activityItemOverrideChannelForChannel:)]) { + channel = [self.delegate activityItemOverrideChannelForChannel:channel]; + } + + // Because Facebook et al immediately scrape URLs, we add an additional parameter to the + // existing list, telling the backend to ignore the first click + NSArray *scrapers = @[ + @"Facebook", + @"Twitter", + @"Slack", + @"Apple Notes", + @"Skype", + @"SMS" + ]; + for (NSString *scraper in scrapers) { + if ([channel isEqualToString:scraper]) { + NSURL *URL = [NSURL URLWithString:[[Branch getInstance] + getShortURLWithParams:params + andTags:tags + andChannel:channel + andFeature:feature + andStage:stage + andCampaign:campaign + andAlias:alias + ignoreUAString:self.userAgentString + forceLinkCreation:YES]]; + return (self.returnURL) ? URL : URL.absoluteString; + } + } + + // Wrap the link in HTML content + if (self.activityType == UIActivityTypeMail && + [params objectForKey:BRANCH_LINK_DATA_KEY_EMAIL_HTML_HEADER] && + [params objectForKey:BRANCH_LINK_DATA_KEY_EMAIL_HTML_FOOTER]) { + NSURL *link = [NSURL URLWithString:[[Branch getInstance] + getShortURLWithParams:params + andTags:tags + andChannel:channel + andFeature:feature + andStage:stage + andCampaign:campaign + andAlias:alias + ignoreUAString:nil + forceLinkCreation:YES]]; + NSString *emailLink; + if ([params objectForKey:BRANCH_LINK_DATA_KEY_EMAIL_HTML_LINK_TEXT]) { + emailLink = [NSString stringWithFormat:@"%@", + link, [params objectForKey:BRANCH_LINK_DATA_KEY_EMAIL_HTML_LINK_TEXT]]; + } else { + emailLink = link.absoluteString; + } + + return [NSString stringWithFormat:@"%@%@%@", + [params objectForKey:BRANCH_LINK_DATA_KEY_EMAIL_HTML_HEADER], + emailLink, + [params objectForKey:BRANCH_LINK_DATA_KEY_EMAIL_HTML_FOOTER]]; + } + + NSURL *URL = + [NSURL URLWithString:[[Branch getInstance] + getShortURLWithParams:params + andTags:tags + andChannel:channel + andFeature:feature + andStage:stage + andCampaign:campaign + andAlias:alias + ignoreUAString:nil + forceLinkCreation:YES]]; + return (self.returnURL) ? URL : URL.absoluteString; +} + +#pragma mark - Internals + ++ (NSString *)humanReadableChannelWithActivityType:(NSString *)activityString { + NSDictionary *channelMappings = [[NSDictionary alloc] initWithObjectsAndKeys: + @"Pasteboard", UIActivityTypeCopyToPasteboard, + @"Email", UIActivityTypeMail, + @"SMS", UIActivityTypeMessage, + @"Facebook", UIActivityTypePostToFacebook, + @"Twitter", UIActivityTypePostToTwitter, + @"Weibo", UIActivityTypePostToWeibo, + @"Reading List",UIActivityTypeAddToReadingList, + @"Airdrop", UIActivityTypeAirDrop, + @"flickr", UIActivityTypePostToFlickr, + @"Tencent Weibo", UIActivityTypePostToTencentWeibo, + @"Vimeo", UIActivityTypePostToVimeo, + @"Apple Notes", @"com.apple.mobilenotes.SharingExtension", + @"Slack", @"com.tinyspeck.chatlyio.share", + @"WhatsApp", @"net.whatsapp.WhatsApp.ShareExtension", + @"WeChat", @"com.tencent.xin.sharetimeline", + @"LINE", @"jp.naver.line.Share", + @"Pinterest", @"pinterest.ShareExtension", + @"Skype", @"com.skype.skype.sharingextension", + @"Apple Reminders", @"com.apple.reminders.RemindersEditorExtension", + + // Keys for older app versions -- + + @"Facebook", @"com.facebook.Facebook.ShareExtension", + @"Twitter", @"com.atebits.Tweetie2.ShareExtension", + + nil + ]; + // Set to a more human readable string if we can identify it. + if (activityString) { + NSString*humanString = channelMappings[activityString]; + if (humanString) activityString = humanString; + } + return activityString; +} + +- (NSDictionary *)paramsForChannel:(NSString *)channel { + return ([self.delegate respondsToSelector:@selector(activityItemParamsForChannel:)]) + ? [self.delegate activityItemParamsForChannel:channel] + : self.params; +} + +- (NSArray *)tagsForChannel:(NSString *)channel { + return ([self.delegate respondsToSelector:@selector(activityItemTagsForChannel:)]) + ? [self.delegate activityItemTagsForChannel:channel] + : self.tags; +} + +- (NSString *)featureForChannel:(NSString *)channel { + return ([self.delegate respondsToSelector:@selector(activityItemFeatureForChannel:)]) + ? [self.delegate activityItemFeatureForChannel:channel] + : self.feature; +} + +- (NSString *)stageForChannel:(NSString *)channel { + return ([self.delegate respondsToSelector:@selector(activityItemStageForChannel:)]) + ? [self.delegate activityItemStageForChannel:channel] + : self.stage; +} + +- (NSString *)campaignForChannel:(NSString *)channel { + return ([self.delegate respondsToSelector:@selector(activityItemCampaignForChannel:)]) + ? [self.delegate activityItemCampaignForChannel:channel] + : self.campaign; +} + +- (NSString *)aliasForChannel:(NSString *)channel { + return ([self.delegate respondsToSelector:@selector(activityItemAliasForChannel:)]) + ? [self.delegate activityItemAliasForChannel:channel] + : self.alias; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchCSSearchableItemAttributeSet.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchCSSearchableItemAttributeSet.h new file mode 100644 index 00000000000..8b8597c8100 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchCSSearchableItemAttributeSet.h @@ -0,0 +1,42 @@ +// +// BranchCSSearchableItemAttributeSet.h +// Branch-TestBed +// +// Created by Derrick Staten on 9/8/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + +#if __has_feature(modules) +@import CoreSpotlight; +@import MobileCoreServices; +#else +#import +#import +#endif + +@interface BranchCSSearchableItemAttributeSet : CSSearchableItemAttributeSet + +- (id)init; +- (id)initWithContentType:(NSString *)type; +- (void)indexWithCallback:(void (^) (NSString * url, + NSString * spotlightIdentifier, + NSError * error))callback; + +@property (nonatomic, strong) NSDictionary *params; +@property (nonatomic, strong) NSSet *keywords; +@property (nonatomic) BOOL publiclyIndexable; //!< Defaults to YES + +@end + +#pragma clang diagnostic pop +#endif diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchCSSearchableItemAttributeSet.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchCSSearchableItemAttributeSet.m new file mode 100644 index 00000000000..7d2987ee2a1 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchCSSearchableItemAttributeSet.m @@ -0,0 +1,161 @@ +// +// BranchCSSearchableItemAttributeSet.m +// Branch-TestBed +// +// Created by Derrick Staten on 9/8/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#import "BranchCSSearchableItemAttributeSet.h" + +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000 +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + +#import "Branch.h" +#import "BNCSystemObserver.h" + +#ifndef kUTTypeGeneric +#define kUTTypeGeneric @"public.content" +#endif + +@interface BranchCSSearchableItemAttributeSet() +@property (nonatomic, strong) NSUserActivity *userActivity; +@end + +@implementation BranchCSSearchableItemAttributeSet + +- (id)init { + return [self initWithContentType:nil]; +} + +- (id)initWithContentType:(NSString *)type { + NSString *typeOrDefault = type ?: (NSString *)kUTTypeGeneric; + if (self = [super initWithItemContentType:typeOrDefault]) { + self.publiclyIndexable = YES; + } + return self; +} + +- (void)setIdentifier:(NSString *)identifier { + if (![identifier hasPrefix:BRANCH_SPOTLIGHT_PREFIX]) { + BNCLogWarning(@"Do not set BranchCSSearchableItemAttributeSet's identifier. It will be overwritten."); + } +} + +- (void)indexWithCallback:(callbackWithUrlAndSpotlightIdentifier)callback { + if ([BNCSystemObserver getOSVersion].integerValue < 9) { + if (callback) { + callback(nil, nil, [NSError branchErrorWithCode:BNCSpotlightNotAvailableError]); + } + return; + } + if (![CSSearchableIndex isIndexingAvailable]) { + if (callback) { + callback(nil, nil, [NSError branchErrorWithCode:BNCSpotlightNotAvailableError]); + } + return; + } + if (!self.title) { + if (callback) { + callback(nil, nil, [NSError branchErrorWithCode:BNCSpotlightTitleError]); + } + return; + } + + // Include spotlight info in params + NSMutableDictionary *spotlightLinkData = [[NSMutableDictionary alloc] init]; + spotlightLinkData[BRANCH_LINK_DATA_KEY_TITLE] = self.title; + spotlightLinkData[BRANCH_LINK_DATA_KEY_PUBLICLY_INDEXABLE] = @(self.publiclyIndexable); + if (self.contentType) { + spotlightLinkData[BRANCH_LINK_DATA_KEY_TYPE] = self.contentType; + } + + if (self.params) { + [spotlightLinkData addEntriesFromDictionary:self.params]; + } + + // Default the OG Title, Description, and Image Url if necessary + if (!spotlightLinkData[BRANCH_LINK_DATA_KEY_OG_TITLE]) { + spotlightLinkData[BRANCH_LINK_DATA_KEY_OG_TITLE] = self.title; + } + + if (self.contentDescription) { + spotlightLinkData[BRANCH_LINK_DATA_KEY_DESCRIPTION] = self.contentDescription; + if (!spotlightLinkData[BRANCH_LINK_DATA_KEY_OG_DESCRIPTION]) { + spotlightLinkData[BRANCH_LINK_DATA_KEY_OG_DESCRIPTION] = self.contentDescription; + } + } + + BOOL thumbnailIsRemote = self.thumbnailURL && ![self.thumbnailURL isFileURL]; + if (self.thumbnailURL.absoluteString) { + spotlightLinkData[BRANCH_LINK_DATA_KEY_THUMBNAIL_URL] = self.thumbnailURL.absoluteString; + + // Only use the thumbnail url if it is a remote url, not a file system url + if (thumbnailIsRemote && !spotlightLinkData[BRANCH_LINK_DATA_KEY_OG_IMAGE_URL]) { + spotlightLinkData[BRANCH_LINK_DATA_KEY_OG_IMAGE_URL] = self.thumbnailURL.absoluteString; + } + } + + if (self.keywords) { + spotlightLinkData[BRANCH_LINK_DATA_KEY_KEYWORDS] = [self.keywords allObjects]; + } + + [[Branch getInstance] getSpotlightUrlWithParams:spotlightLinkData callback:^(NSDictionary *data, NSError *urlError) { + if (urlError) { + if (callback) { + callback(nil, nil, urlError); + } + return; + } + + if (thumbnailIsRemote && !self.thumbnailData) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + NSData *thumbnailData = [NSData dataWithContentsOfURL:self.thumbnailURL]; + dispatch_async(dispatch_get_main_queue(), ^{ + self.thumbnailData = thumbnailData; + [self indexContentWithUrl:data[BRANCH_RESPONSE_KEY_URL] spotlightIdentifier:data[BRANCH_RESPONSE_KEY_SPOTLIGHT_IDENTIFIER] callback:callback]; + }); + }); + } + else { + [self indexContentWithUrl:data[BRANCH_RESPONSE_KEY_URL] spotlightIdentifier:data[BRANCH_RESPONSE_KEY_SPOTLIGHT_IDENTIFIER] callback:callback]; + } + }]; +} + +- (void)indexContentWithUrl:(NSString *)url spotlightIdentifier:(NSString *)spotlightIdentifier callback:(callbackWithUrlAndSpotlightIdentifier)callback { + self.identifier = spotlightIdentifier; + self.relatedUniqueIdentifier = spotlightIdentifier; + self.contentURL = [NSURL URLWithString:url]; // The content url links back to our web content + + // Index via the NSUserActivity strategy + // Currently (iOS 9 Beta 4) we need a strong reference to this, or it isn't indexed + self.userActivity = [[NSUserActivity alloc] initWithActivityType:spotlightIdentifier]; + self.userActivity.title = self.title; + self.userActivity.webpageURL = [NSURL URLWithString:url]; // This should allow indexed content to fall back to the web if user doesn't have the app installed. Unable to test as of iOS 9 Beta 4 + self.userActivity.eligibleForSearch = YES; + self.userActivity.eligibleForPublicIndexing = self.publiclyIndexable; + self.userActivity.contentAttributeSet = self; // TODO: ensure this does not create a retain cycle + self.userActivity.userInfo = self.params; // As of iOS 9 Beta 4, this gets lost and never makes it through to application:continueActivity:restorationHandler: + self.userActivity.requiredUserInfoKeys = [NSSet setWithArray:self.params.allKeys]; // This, however, seems to force the userInfo to come through. + self.userActivity.keywords = self.keywords; + [self.userActivity becomeCurrent]; + + // Index via the CoreSpotlight strategy + CSSearchableItem *item = [[CSSearchableItem alloc] initWithUniqueIdentifier:spotlightIdentifier domainIdentifier:BRANCH_SPOTLIGHT_PREFIX attributeSet:self]; + [[CSSearchableIndex defaultSearchableIndex] indexSearchableItems:@[ item ] completionHandler:^(NSError *indexError) { + if (callback) { + if (indexError) { + callback(nil, nil, indexError); + } + else { + callback(url, spotlightIdentifier, nil); + } + } + }]; +} + +@end + +#endif diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchConstants.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchConstants.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchConstants.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchConstants.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchConstants.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchConstants.m new file mode 100644 index 00000000000..bf52c186ef3 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchConstants.m @@ -0,0 +1,142 @@ +// +// BranchConstants.m +// Branch-TestBed +// +// Created by Graham Mueller on 6/10/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchConstants.h" + +NSString * const BRANCH_REQUEST_KEY_BRANCH_IDENTITY = @"identity_id"; +NSString * const BRANCH_REQUEST_KEY_DEVELOPER_IDENTITY = @"identity"; +NSString * const BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID = @"device_fingerprint_id"; +NSString * const BRANCH_REQUEST_KEY_SESSION_ID = @"session_id"; +NSString * const BRANCH_REQUEST_KEY_ACTION = @"event"; +NSString * const BRANCH_REQUEST_KEY_STATE = @"metadata"; +NSString * const BRANCH_REQUEST_KEY_BUCKET = @"bucket"; +NSString * const BRANCH_REQUEST_KEY_AMOUNT = @"amount"; +NSString * const BRANCH_REQUEST_KEY_LENGTH = @"length"; +NSString * const BRANCH_REQUEST_KEY_DIRECTION = @"direction"; +NSString * const BRANCH_REQUEST_KEY_STARTING_TRANSACTION_ID = @"begin_after_id"; +NSString * const BRANCH_REQUEST_KEY_REFERRAL_USAGE_TYPE = @"calculation_type"; +NSString * const BRANCH_REQUEST_KEY_REFERRAL_REWARD_LOCATION = @"location"; +NSString * const BRANCH_REQUEST_KEY_REFERRAL_TYPE = @"type"; +NSString * const BRANCH_REQUEST_KEY_REFERRAL_CREATION_SOURCE = @"creation_source"; +NSString * const BRANCH_REQUEST_KEY_REFERRAL_PREFIX = @"prefix"; +NSString * const BRANCH_REQUEST_KEY_REFERRAL_EXPIRATION = @"expiration"; +NSString * const BRANCH_REQUEST_KEY_URL_SOURCE = @"source"; +NSString * const BRANCH_REQUEST_KEY_URL_TAGS = @"tags"; +NSString * const BRANCH_REQUEST_KEY_URL_LINK_TYPE = @"type"; +NSString * const BRANCH_REQUEST_KEY_URL_ALIAS = @"alias"; +NSString * const BRANCH_REQUEST_KEY_URL_CHANNEL = @"channel"; +NSString * const BRANCH_REQUEST_KEY_URL_FEATURE = @"feature"; +NSString * const BRANCH_REQUEST_KEY_URL_STAGE = @"stage"; +NSString * const BRANCH_REQUEST_KEY_URL_CAMPAIGN = @"campaign"; +NSString * const BRANCH_REQUEST_KEY_URL_DURATION = @"duration"; +NSString * const BRANCH_REQUEST_KEY_URL_DATA = @"data"; +NSString * const BRANCH_REQUEST_KEY_URL_IGNORE_UA_STRING = @"ignore_ua_string"; +NSString * const BRANCH_REQUEST_KEY_HARDWARE_ID = @"hardware_id"; +NSString * const BRANCH_REQUEST_KEY_HARDWARE_ID_TYPE = @"hardware_id_type"; +NSString * const BRANCH_REQUEST_KEY_IS_HARDWARE_ID_REAL = @"is_hardware_id_real"; +NSString * const BRANCH_REQUEST_KEY_IOS_VENDOR_ID = @"ios_vendor_id"; +NSString * const BRANCH_REQUEST_KEY_AD_TRACKING_ENABLED = @"ad_tracking_enabled"; +NSString * const BRANCH_REQUEST_KEY_DEBUG = @"debug"; +NSString * const BRANCH_REQUEST_KEY_BUNDLE_ID = @"ios_bundle_id"; +NSString * const BRANCH_REQUEST_KEY_TEAM_ID = @"ios_team_id"; +NSString * const BRANCH_REQUEST_KEY_APP_VERSION = @"app_version"; +NSString * const BRANCH_REQUEST_KEY_OS = @"os"; +NSString * const BRANCH_REQUEST_KEY_OS_VERSION = @"os_version"; +NSString * const BRANCH_REQUEST_KEY_URI_SCHEME = @"uri_scheme"; +NSString * const BRANCH_REQUEST_KEY_LINK_IDENTIFIER = @"link_identifier"; +NSString * const BRANCH_REQUEST_KEY_CHECKED_FACEBOOK_APPLINKS = @"facebook_app_link_checked"; +NSString * const BRANCH_REQUEST_KEY_CHECKED_APPLE_AD_ATTRIBUTION = @"apple_ad_attribution_checked"; +NSString * const BRANCH_REQUEST_KEY_SPOTLIGHT_IDENTIFIER = @"spotlight_identifier"; +NSString * const BRANCH_REQUEST_KEY_UNIVERSAL_LINK_URL = @"universal_link_url"; +NSString * const BRANCH_REQUEST_KEY_BRAND = @"brand"; +NSString * const BRANCH_REQUEST_KEY_MODEL = @"model"; +NSString * const BRANCH_REQUEST_KEY_SCREEN_WIDTH = @"screen_width"; +NSString * const BRANCH_REQUEST_KEY_SCREEN_HEIGHT = @"screen_height"; +NSString * const BRANCH_REQUEST_KEY_IS_SIMULATOR = @"is_simulator"; +NSString * const BRANCH_REQUEST_KEY_LOG = @"log"; +NSString * const BRANCH_REQUEST_KEY_INSTRUMENTATION = @"instrumentation"; + +NSString * const BRANCH_REQUEST_ENDPOINT_SET_IDENTITY = @"profile"; +NSString * const BRANCH_REQUEST_ENDPOINT_APP_LINK_SETTINGS = @"app-link-settings"; +NSString * const BRANCH_REQUEST_ENDPOINT_LOGOUT = @"logout"; +NSString * const BRANCH_REQUEST_ENDPOINT_USER_COMPLETED_ACTION = @"event"; +NSString * const BRANCH_REQUEST_ENDPOINT_LOAD_REWARDS = @"credits"; +NSString * const BRANCH_REQUEST_ENDPOINT_REDEEM_REWARDS = @"redeem"; +NSString * const BRANCH_REQUEST_ENDPOINT_CREDIT_HISTORY = @"credithistory"; +NSString * const BRANCH_REQUEST_ENDPOINT_GET_SHORT_URL = @"url"; +NSString * const BRANCH_REQUEST_ENDPOINT_CLOSE = @"close"; +NSString * const BRANCH_REQUEST_ENDPOINT_OPEN = @"open"; +NSString * const BRANCH_REQUEST_ENDPOINT_INSTALL = @"install"; +NSString * const BRANCH_REQUEST_ENDPOINT_REGISTER_VIEW = @"register-view"; + +NSString * const BRANCH_RESPONSE_KEY_BRANCH_IDENTITY = @"identity_id"; +NSString * const BRANCH_RESPONSE_KEY_SESSION_ID = @"session_id"; +NSString * const BRANCH_RESPONSE_KEY_USER_URL = @"link"; +NSString * const BRANCH_RESPONSE_KEY_INSTALL_PARAMS = @"referring_data"; +NSString * const BRANCH_RESPONSE_KEY_REFERRER = @"referrer"; +NSString * const BRANCH_RESPONSE_KEY_REFERREE = @"referree"; +NSString * const BRANCH_RESPONSE_KEY_URL = @"url"; +NSString * const BRANCH_RESPONSE_KEY_SPOTLIGHT_IDENTIFIER = @"spotlight_identifier"; +NSString * const BRANCH_RESPONSE_KEY_DEVELOPER_IDENTITY = @"identity"; +NSString * const BRANCH_RESPONSE_KEY_DEVICE_FINGERPRINT_ID = @"device_fingerprint_id"; +NSString * const BRANCH_RESPONSE_KEY_SESSION_DATA = @"data"; +NSString * const BRANCH_RESPONSE_KEY_CLICKED_BRANCH_LINK = @"+clicked_branch_link"; +NSString * const BRANCH_RESPONSE_KEY_BRANCH_VIEW_DATA = @"branch_view_data"; +NSString * const BRANCH_RESPONSE_KEY_BRANCH_REFERRING_LINK = @"~referring_link"; + +NSString * const BRANCH_LINK_DATA_KEY_OG_TITLE = @"$og_title"; +NSString * const BRANCH_LINK_DATA_KEY_OG_DESCRIPTION = @"$og_description"; +NSString * const BRANCH_LINK_DATA_KEY_OG_IMAGE_URL = @"$og_image_url"; +NSString * const BRANCH_LINK_DATA_KEY_TITLE = @"+spotlight_title"; +NSString * const BRANCH_LINK_DATA_KEY_DESCRIPTION = @"+spotlight_description"; +NSString * const BRANCH_LINK_DATA_KEY_PUBLICLY_INDEXABLE = @"$publicly_indexable"; +NSString * const BRANCH_LINK_DATA_KEY_LOCALLY_INDEXABLE = @"$locally_indexable"; + +NSString * const BRANCH_LINK_DATA_KEY_TYPE = @"+spotlight_type"; +NSString * const BRANCH_LINK_DATA_KEY_THUMBNAIL_URL = @"+spotlight_thumbnail_url"; +NSString * const BRANCH_LINK_DATA_KEY_KEYWORDS = @"$keywords"; +NSString * const BRANCH_LINK_DATA_KEY_CANONICAL_IDENTIFIER = @"$canonical_identifier"; +NSString * const BRANCH_LINK_DATA_KEY_CANONICAL_URL = @"$canonical_url"; +NSString * const BRANCH_LINK_DATA_KEY_CONTENT_EXPIRATION_DATE = @"$exp_date"; +NSString * const BRANCH_LINK_DATA_KEY_CONTENT_TYPE = @"$content_type"; +NSString * const BRANCH_LINK_DATA_KEY_EMAIL_SUBJECT = @"$email_subject"; +NSString * const BRANCH_LINK_DATA_KEY_EMAIL_HTML_HEADER = @"$email_html_header"; +NSString * const BRANCH_LINK_DATA_KEY_EMAIL_HTML_FOOTER = @"$email_html_footer"; +NSString * const BRANCH_LINK_DATA_KEY_EMAIL_HTML_LINK_TEXT = @"$email_html_link_text"; + +NSString * const BRANCH_SPOTLIGHT_PREFIX = @"io.branch.link.v1"; + +NSString * const BRANCH_MANIFEST_VERSION_KEY = @"mv"; +NSString * const BRANCH_HASH_MODE_KEY = @"h"; +NSString * const BRANCH_MANIFEST_KEY = @"m"; +NSString * const BRANCH_PATH_KEY = @"p"; +NSString * const BRANCH_FILTERED_KEYS = @"ck"; +NSString * const BRANCH_MAX_TEXT_LEN_KEY = @"mtl"; +NSString * const BRANCH_MAX_VIEW_HISTORY_LENGTH = @"mhl"; +NSString * const BRANCH_MAX_PACKET_SIZE_KEY = @"mps"; +NSString * const BRANCH_CONTENT_DISCOVER_KEY = @"cd"; +NSString * const BRANCH_BUNDLE_IDENTIFIER = @"pn"; +NSString * const BRANCH_TIME_STAMP_KEY = @"ts"; +NSString * const BRANCH_TIME_STAMP_CLOSE_KEY = @"tc"; +NSString * const BRANCH_NAV_PATH_KEY = @"n"; +NSString * const BRANCH_REFERRAL_LINK_KEY = @"rl"; +NSString * const BRANCH_CONTENT_LINK_KEY = @"cl"; +NSString * const BRANCH_CONTENT_META_DATA_KEY = @"cm"; +NSString * const BRANCH_VIEW_KEY = @"v"; +NSString * const BRANCH_CONTENT_DATA_KEY = @"cd"; +NSString * const BRANCH_CONTENT_KEYS_KEY = @"ck"; +NSString * const BRANCH_PACKAGE_NAME_KEY = @"p"; +NSString * const BRANCH_ENTITIES_KEY = @"e"; + +NSString * const BRANCH_REQUEST_KEY_SEARCH_AD = @"search_ad_encoded"; + +NSString * const BRANCH_CRASHLYTICS_SDK_VERSION_KEY = @"io.branch.sdk.version"; +NSString * const BRANCH_CRASHLYTICS_FINGERPRINT_ID_KEY = @"io.branch.device.fingerprintid"; +NSString * const BRANCH_CRASHLYTICS_LOW_MEMORY_KEY = @"io.branch.device.lowmemory"; + +NSString * const BRANCH_REQUEST_KEY_EXTERNAL_INTENT_URI = @"external_intent_uri"; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoverer.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoverer.h new file mode 100644 index 00000000000..f526ffd4624 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoverer.h @@ -0,0 +1,19 @@ +// +// ContentDiscoverer.h +// Branch-TestBed +// +// Created by Sojan P.R. on 8/17/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import "BranchContentDiscoveryManifest.h" + +@interface BranchContentDiscoverer : NSObject + ++ (BranchContentDiscoverer *)getInstance; +- (void) startDiscoveryTaskWithManifest:(BranchContentDiscoveryManifest*)manifest; +- (void) startDiscoveryTask; +- (void) stopDiscoveryTask; + +@property (nonatomic, strong) BranchContentDiscoveryManifest* contentManifest; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoverer.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoverer.m new file mode 100644 index 00000000000..7fd90824a8a --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoverer.m @@ -0,0 +1,292 @@ +// +// ContentDiscoverer.m +// Branch-TestBed +// +// Created by Sojan P.R. on 8/17/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import +#import "BranchContentDiscoverer.h" +#import "BranchContentDiscoveryManifest.h" +#import "BranchContentPathProperties.h" +#import "BNCPreferenceHelper.h" +#import "BranchConstants.h" +#import "BNCEncodingUtils.h" +#import "BNCLog.h" +#import "UIViewController+Branch.h" + +@interface BranchContentDiscoverer () +@property (nonatomic, strong) NSString *lastViewControllerName; +@property (nonatomic, strong) NSTimer *contentDiscoveryTimer; +@property (nonatomic) NSInteger numOfViewsDiscovered; +@end + + +@implementation BranchContentDiscoverer + ++ (BranchContentDiscoverer *)getInstance { + static BranchContentDiscoverer *sharedInstance = nil; + @synchronized (self) { + if (!sharedInstance) { + sharedInstance = [[BranchContentDiscoverer alloc] init]; + } + return sharedInstance; + } +} + +- (void) dealloc { + [_contentDiscoveryTimer invalidate]; +} + +- (void) setContentManifest:(BranchContentDiscoveryManifest*)manifest { + _numOfViewsDiscovered = 0; + _contentManifest = manifest; +} + +- (void) startDiscoveryTaskWithManifest:(BranchContentDiscoveryManifest*)manifest { + self.contentManifest = manifest; + [self startDiscoveryTask]; +} + +- (void)startDiscoveryTask { + if (![NSThread isMainThread]) { + BNCLogError(@"Discovery should be called on main thread."); + } + [_contentDiscoveryTimer invalidate]; + _contentDiscoveryTimer = + [NSTimer scheduledTimerWithTimeInterval:1.0 + target:self + selector:@selector(readContentDataIfNeeded) + userInfo:nil + repeats:YES]; +} + +- (void)stopDiscoveryTask { + _lastViewControllerName = nil; + if (_contentDiscoveryTimer) { + [_contentDiscoveryTimer invalidate]; + _contentDiscoveryTimer = nil; + } +} + +- (void)readContentDataIfNeeded { + if (_numOfViewsDiscovered < self.contentManifest.maxViewHistoryLength) { + UIViewController *presentingViewController = [UIViewController bnc_currentViewController]; + if (presentingViewController) { + NSString *presentingViewControllerName = NSStringFromClass([presentingViewController class]); + if (_lastViewControllerName == nil || ![_lastViewControllerName isEqualToString:presentingViewControllerName]) { + _lastViewControllerName = presentingViewControllerName; + [self readContentData:presentingViewController]; + } + } + } else { + [self stopDiscoveryTask]; + } +} + +- (void)readContentData:(UIViewController *)viewController { + if (viewController) { + UIView *rootView = [self getRootView:viewController]; + NSMutableArray *contentDataArray = [[NSMutableArray alloc] init]; + NSMutableArray *contentKeysArray = [[NSMutableArray alloc] init]; + BOOL isClearText = YES; + + if (rootView) { + BranchContentPathProperties *pathProperties = [self.contentManifest getContentPathProperties:viewController]; + // Check for any existing path properties for this ViewController + if (pathProperties) { + isClearText = pathProperties.isClearText; + if (!pathProperties.isSkipContentDiscovery) { + NSArray *filteredKeys = [pathProperties getFilteredElements]; + if (filteredKeys == nil || filteredKeys.count == 0) { + [self discoverViewContents:rootView contentData:nil contentKeys:contentKeysArray clearText:isClearText ID:@""]; + } else { + contentKeysArray = filteredKeys.mutableCopy; + [self discoverFilteredViewContents:viewController contentData:contentDataArray contentKeys:contentKeysArray clearText:isClearText]; + } + } + } else if (self.contentManifest.referredLink) { // else discover content if this session is started by a link click + [self discoverViewContents:rootView contentData:nil contentKeys:contentKeysArray clearText:YES ID:@""]; + } + if (contentKeysArray && contentKeysArray.count > 0) { + NSMutableDictionary *contentEventObj = [[NSMutableDictionary alloc] init]; + [contentEventObj setObject:[NSString stringWithFormat:@"%f", [[NSDate date] timeIntervalSince1970]] forKey:BRANCH_TIME_STAMP_KEY]; + if (self.contentManifest.referredLink.length) { + [contentEventObj setObject:self.contentManifest.referredLink forKey:BRANCH_REFERRAL_LINK_KEY]; + } + + [contentEventObj setObject:[NSString stringWithFormat:@"/%@", _lastViewControllerName] forKey:BRANCH_VIEW_KEY]; + [contentEventObj setObject:!isClearText? @"true" : @"false" forKey:BRANCH_HASH_MODE_KEY]; + [contentEventObj setObject:contentKeysArray forKey:BRANCH_CONTENT_KEYS_KEY]; + if (contentDataArray && contentDataArray.count > 0) { + [contentEventObj setObject:contentDataArray forKey:BRANCH_CONTENT_DATA_KEY]; + } + + [[BNCPreferenceHelper preferenceHelper] saveBranchAnalyticsData:contentEventObj]; + } + } + } +} + + +- (void)discoverViewContents:(UIView *)rootView + contentData:(NSMutableArray *)contentDataArray + contentKeys:(NSMutableArray *)contentKeysArray + clearText:(BOOL)isClearText + ID:(NSString *)viewId { + if ([rootView isKindOfClass:UITableView.class] || [rootView isKindOfClass:UICollectionView.class]) { + NSArray *cells = [rootView performSelector:@selector(visibleCells) withObject:nil]; + NSInteger cellCnt = -1; + for (UIView *cell in cells) { + cellCnt++; + NSString *cellViewId = nil; + if (viewId.length > 0) { + cellViewId = [viewId stringByAppendingFormat:@"-%ld", (long) cellCnt]; + } else { + cellViewId = [NSString stringWithFormat:@"%ld", (long) cellCnt]; + } + [self discoverViewContents:cell + contentData:contentDataArray + contentKeys:contentKeysArray + clearText:isClearText + ID:cellViewId]; + } + } else { + NSString *contentData = [self getContentText:rootView]; + if (contentData) { + NSString *viewFriendlyName = [NSString stringWithFormat:@"%@:%@", [rootView class], viewId]; + [contentKeysArray addObject:viewFriendlyName]; + if (contentDataArray) { + [self addFormattedContentData:contentDataArray withText:contentData clearText:isClearText]; + } + } + NSArray *subViews = [rootView subviews]; + NSInteger childCount = 0; + for (UIView *view in subViews) { + NSString *subViewId = nil; + if (viewId.length > 0) { + subViewId = [viewId stringByAppendingFormat:@"-%ld", (long) childCount]; + } else { + subViewId = [NSString stringWithFormat:@"%ld", (long) childCount]; + } + childCount++; + [self discoverViewContents:view + contentData:contentDataArray + contentKeys:contentKeysArray + clearText:isClearText + ID:subViewId]; + } + } +} + + +- (void)discoverFilteredViewContents:(UIViewController *)viewController + contentData:(NSMutableArray *)contentDataArray + contentKeys:(NSMutableArray *)contentKeysArray + clearText:(BOOL)isClearText { + for (NSString *contentKey in contentKeysArray) { + NSString *contentData = [self getViewText:contentKey forController:viewController]; + if (contentData == nil) { + contentData = @""; + } + if (contentDataArray) { + [self addFormattedContentData:contentDataArray withText:contentData clearText:isClearText]; + } + } +} + +- (UIView *)getRootView:(UIViewController *)viewController { + UIView *rootView = [viewController view]; + if ([viewController isKindOfClass:UITableViewController.class]) { + rootView = ((UITableViewController *)viewController).tableView; + } else if ([viewController isKindOfClass:UICollectionViewController.class]) { + rootView = ((UICollectionViewController *)viewController).collectionView; + } + return rootView; +} + +- (NSString *)getViewText:(NSString *)viewId + forController:(UIViewController *)viewController { + NSString *viewTxt = @""; + if (viewController) { + UIView *rootView = [viewController view]; + NSArray *viewIDSplitArray = [viewId componentsSeparatedByString:@":"]; + if (viewIDSplitArray.count > 0) { + viewId = [[viewId componentsSeparatedByString:@":"] objectAtIndex:1]; + } + NSArray *viewIds = [viewId componentsSeparatedByString:@"-"]; + BOOL foundView = YES; + for (NSString *subViewIdStr in viewIds) { + NSInteger subviewId = [subViewIdStr intValue]; + if ([rootView isKindOfClass:UITableView.class] || [rootView isKindOfClass:UICollectionView.class]) { + NSArray *cells = [rootView performSelector:@selector(visibleCells) withObject:nil]; + if (cells.count > subviewId) { + rootView = [cells objectAtIndex:subviewId]; + } else { + foundView = NO; + break; + } + } else { + if ([rootView subviews].count > subviewId) { + rootView = [[rootView subviews] objectAtIndex:subviewId]; + } else { + foundView = NO; + break; + } + } + } + if (foundView) { + NSString *contentVal = [self getContentText:rootView]; + if (contentVal) { + viewTxt = contentVal; + } + } + } + return viewTxt; +} + +- (void)addFormattedContentData:(NSMutableArray *)contentDataArray + withText:(NSString *)contentData + clearText:(BOOL)isClearText { + if (contentData && contentData.length > self.contentManifest.maxTextLen) { + contentData = [contentData substringToIndex:self.contentManifest.maxTextLen]; + } + if (!isClearText) { + contentData = [BNCEncodingUtils md5Encode:contentData]; + } + if (contentData) + [contentDataArray addObject:contentData]; +} + +- (NSString *)getContentText:(UIView *)view { + NSString *contentData = nil; + if ([view respondsToSelector:@selector(text)]) { + contentData = [view performSelector:@selector(text) withObject:nil]; + } + if (contentData == nil || contentData.length == 0) { + if ([view respondsToSelector:@selector(attributedText)]) { + contentData = [view performSelector:@selector(attributedText) withObject:nil]; + } + } + + if (contentData == nil || contentData.length == 0) { + if ([view isKindOfClass:UIButton.class]) { + contentData = [view performSelector:@selector(titleLabel) withObject:nil]; + if (contentData) { + contentData = [(UILabel *) contentData text]; + } + } else if ([view isKindOfClass:UITextField.class]) { + contentData = [view performSelector:@selector(attributedPlaceholder) withObject:nil]; + if (contentData) { + contentData = [(NSAttributedString *) contentData string]; + } + } + } + return contentData; +} + +@end + + + diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoveryManifest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoveryManifest.h new file mode 100644 index 00000000000..1019e1d88e2 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoveryManifest.h @@ -0,0 +1,25 @@ +// +// ContentDiscoverManifest.h +// Branch-TestBed +// +// Created by Sojan P.R. on 8/18/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import "BranchContentPathProperties.h" + +@interface BranchContentDiscoveryManifest : NSObject + +@property (strong, nonatomic) NSMutableDictionary *cdManifest; +@property (strong, nonatomic) NSString *referredLink; +@property (nonatomic) NSInteger maxTextLen; +@property (nonatomic) NSInteger maxViewHistoryLength; +@property (nonatomic) NSInteger maxPktSize; +@property (nonatomic) BOOL isCDEnabled; +@property (strong, nonatomic) NSMutableArray *contentPaths; + ++ (BranchContentDiscoveryManifest *)getInstance; +- (NSString *)getManifestVersion; +- (BranchContentPathProperties *)getContentPathProperties:(UIViewController *)viewController; +- (void)onBranchInitialised:(NSDictionary *)branchInitDict withUrl:(NSString *)referringURL; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoveryManifest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoveryManifest.m new file mode 100644 index 00000000000..79caa851eac --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentDiscoveryManifest.m @@ -0,0 +1,107 @@ +// +// ContentDiscoverManifest.m +// Branch-TestBed +// +// Created by Sojan P.R. on 8/18/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + + +#import "BranchContentDiscoveryManifest.h" +#import "BNCPreferenceHelper.h" +#import "BranchContentPathProperties.h" +#import "BranchConstants.h" + + +@interface BranchContentDiscoveryManifest () +@property (nonatomic, strong) NSString *manifestVersion; +@end + + +@implementation BranchContentDiscoveryManifest + +- (instancetype)init { + self = [super init]; + if (self) { + NSDictionary *savedManifest = [[BNCPreferenceHelper preferenceHelper] getContentAnalyticsManifest]; + if (savedManifest) { + _cdManifest = [savedManifest mutableCopy]; + } else { + _cdManifest = [[NSMutableDictionary alloc] init]; + } + } + return self; +} + ++ (BranchContentDiscoveryManifest *)getInstance { + @synchronized (self) { + static BranchContentDiscoveryManifest *contentDiscoveryManifest = nil; + if (!contentDiscoveryManifest) { + contentDiscoveryManifest = [[BranchContentDiscoveryManifest alloc] init]; + } + return contentDiscoveryManifest; + } +} + +- (void)onBranchInitialised:(NSDictionary *)branchInitDict withUrl:(NSString *)referringURL { + _referredLink = referringURL; + if ([branchInitDict objectForKey:BRANCH_CONTENT_DISCOVER_KEY]) { + _isCDEnabled = YES; + NSDictionary *cdManifestDict = [branchInitDict objectForKey:BRANCH_CONTENT_DISCOVER_KEY]; + + if ([cdManifestDict objectForKey:BRANCH_MANIFEST_VERSION_KEY]) { + _manifestVersion = [cdManifestDict objectForKey:BRANCH_MANIFEST_VERSION_KEY]; + [_cdManifest setObject:_manifestVersion forKey:BRANCH_MANIFEST_VERSION_KEY]; + } + + if ([cdManifestDict objectForKey:BRANCH_MAX_VIEW_HISTORY_LENGTH]) { + _maxViewHistoryLength = [[cdManifestDict objectForKey:BRANCH_MAX_VIEW_HISTORY_LENGTH] integerValue]; + } + + if ([cdManifestDict objectForKey:BRANCH_MANIFEST_KEY]) { + _contentPaths = [cdManifestDict objectForKey:BRANCH_MANIFEST_KEY]; + [_cdManifest setObject:_contentPaths forKey:BRANCH_MANIFEST_KEY]; + } + + if ([cdManifestDict objectForKey:BRANCH_MAX_TEXT_LEN_KEY]) { + _maxTextLen = [[cdManifestDict objectForKey:BRANCH_MAX_TEXT_LEN_KEY] integerValue]; + } + + if ([cdManifestDict objectForKey:BRANCH_MAX_PACKET_SIZE_KEY]) { + _maxPktSize = [[cdManifestDict objectForKey:BRANCH_MAX_PACKET_SIZE_KEY] integerValue]; + } + + [[BNCPreferenceHelper preferenceHelper] saveContentAnalyticsManifest:_cdManifest]; + } else { + _isCDEnabled = NO; + } + +} + +- (NSString *)getManifestVersion { + NSString *mVersion = @"-1"; + if (_cdManifest && [_cdManifest objectForKey:BRANCH_MANIFEST_VERSION_KEY]) { + mVersion = [_cdManifest objectForKey:BRANCH_MANIFEST_VERSION_KEY] ; + } + return mVersion; +} + +- (BranchContentPathProperties *)getContentPathProperties:(UIViewController *)viewController { + BranchContentPathProperties *contentPathProperties; + + if (_contentPaths) { + NSString *viewPath = [NSString stringWithFormat:@"/%@", ([viewController class])]; + for (NSDictionary *pathObj in _contentPaths) { + NSString *pathStr = [pathObj objectForKey:BRANCH_PATH_KEY]; + if (pathStr && [pathStr isEqualToString:viewPath]) { + contentPathProperties = [[BranchContentPathProperties alloc] init:pathObj]; + break; + } + } + } + return contentPathProperties; +} + + + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentPathProperties.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentPathProperties.h new file mode 100644 index 00000000000..045cbcb3234 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentPathProperties.h @@ -0,0 +1,27 @@ +// +// ContentPathProperties.h +// Branch-TestBed +// +// Created by Sojan P.R. on 8/19/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +@import UIKit; +#else +#import +#import +#endif + +@interface BranchContentPathProperties : NSObject + +@property (strong, nonatomic) NSDictionary *pathInfo; +@property (nonatomic) BOOL isClearText; + +- (instancetype)init:(NSDictionary *)pathInfo; +- (NSArray *)getFilteredElements; +- (BOOL)isSkipContentDiscovery; +- (BOOL)isClearText; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentPathProperties.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentPathProperties.m new file mode 100644 index 00000000000..483f8ed3752 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchContentPathProperties.m @@ -0,0 +1,38 @@ +// +// ContentPathProperties.m +// Branch-TestBed +// +// Created by Sojan P.R. on 8/19/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import "BranchContentPathProperties.h" +#import "BranchConstants.h" + +@implementation BranchContentPathProperties + +- (instancetype)init:(NSDictionary *)pathInfo { + self = [super init]; + if (self) { + _pathInfo = pathInfo; + if ([pathInfo objectForKey:BRANCH_HASH_MODE_KEY]) { + _isClearText = ![[pathInfo objectForKey:BRANCH_HASH_MODE_KEY] boolValue]; + } + } + return self; +} + +- (NSArray *)getFilteredElements { + return [_pathInfo objectForKey:BRANCH_FILTERED_KEYS]; +} + +- (BOOL)isSkipContentDiscovery { + NSArray *filteredElements = [self getFilteredElements]; + return (filteredElements && filteredElements.count == 0); +} + +- (BOOL)isClearText { + return _isClearText; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchDeepLinkingController.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchDeepLinkingController.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchDeepLinkingController.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchDeepLinkingController.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchDelegate.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchDelegate.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchDelegate.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchDelegate.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchDelegate.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchDelegate.m new file mode 100644 index 00000000000..72b15bb5099 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchDelegate.m @@ -0,0 +1,17 @@ +// +// BranchDelegate.m +// Branch-SDK +// +// Created by Edward Smith on 6/30/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#import "BranchDelegate.h" + +NSString* const BranchWillStartSessionNotification = @"BranchWillStartSessionNotification"; +NSString* const BranchDidStartSessionNotification = @"BranchDidStartSessionNotification"; + +NSString* const BranchErrorKey = @"BranchErrorKey"; +NSString* const BranchURLKey = @"BranchURLKey"; +NSString* const BranchUniversalObjectKey = @"BranchUniversalObjectKey"; +NSString* const BranchLinkPropertiesKey = @"BranchLinkPropertiesKey"; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchEvent.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchEvent.h similarity index 98% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchEvent.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchEvent.h index 2ff070d8aee..3a7aa4d253b 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchEvent.h +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchEvent.h @@ -75,7 +75,7 @@ FOUNDATION_EXPORT BranchStandardEvent _Nonnull BranchStandardEventUnlockAchievem #pragma mark - BranchEventRequest -@interface BranchEventRequest : BNCServerRequest +@interface BranchEventRequest : BNCServerRequest - (instancetype _Nonnull) initWithServerURL:(NSURL*_Nonnull)serverURL eventDictionary:(NSDictionary*_Nullable)eventDictionary diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchEvent.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchEvent.m new file mode 100644 index 00000000000..1f6fa8ce8a6 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchEvent.m @@ -0,0 +1,266 @@ +// +// BranchEvent.m +// Branch-SDK +// +// Created by Edward Smith on 7/24/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#import "BranchEvent.h" +#import "BNCLog.h" + +#pragma mark BranchStandardEvents + +// Commerce events + +BranchStandardEvent BranchStandardEventAddToCart = @"ADD_TO_CART"; +BranchStandardEvent BranchStandardEventAddToWishlist = @"ADD_TO_WISHLIST"; +BranchStandardEvent BranchStandardEventViewCart = @"VIEW_CART"; +BranchStandardEvent BranchStandardEventInitiatePurchase = @"INITIATE_PURCHASE"; +BranchStandardEvent BranchStandardEventAddPaymentInfo = @"ADD_PAYMENT_INFO"; +BranchStandardEvent BranchStandardEventPurchase = @"PURCHASE"; +BranchStandardEvent BranchStandardEventSpendCredits = @"SPEND_CREDITS"; + +// Content Events + +BranchStandardEvent BranchStandardEventSearch = @"SEARCH"; +BranchStandardEvent BranchStandardEventViewItem = @"VIEW_ITEM"; +BranchStandardEvent BranchStandardEventViewItems = @"VIEW_ITEMS"; +BranchStandardEvent BranchStandardEventRate = @"RATE"; +BranchStandardEvent BranchStandardEventShare = @"SHARE"; + +// User Lifecycle Events + +BranchStandardEvent BranchStandardEventCompleteRegistration = @"COMPLETE_REGISTRATION"; +BranchStandardEvent BranchStandardEventCompleteTutorial = @"COMPLETE_TUTORIAL"; +BranchStandardEvent BranchStandardEventAchieveLevel = @"ACHIEVE_LEVEL"; +BranchStandardEvent BranchStandardEventUnlockAchievement = @"UNLOCK_ACHIEVEMENT"; + +@implementation BranchEventRequest + +- (instancetype) initWithServerURL:(NSURL*)serverURL + eventDictionary:(NSDictionary*)eventDictionary + completion:(void (^)(NSDictionary* response, NSError* error))completion { + + self = [super init]; + if (!self) return self; + + self.serverURL = serverURL; + self.eventDictionary = eventDictionary; + self.completion = completion; + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface + key:(NSString *)key + callback:(BNCServerCallback)callback { + [serverInterface postRequest:self.eventDictionary + url:[self.serverURL absoluteString] + key:key + callback:callback]; +} + +- (void)processResponse:(BNCServerResponse*)response + error:(NSError*)error { + NSDictionary *dictionary = + ([response.data isKindOfClass:[NSDictionary class]]) + ? (NSDictionary*) response.data + : nil; + + if (self.completion) + self.completion(dictionary, error); +} + +#pragma mark BranchEventRequest NSSecureCoding + +- (instancetype)initWithCoder:(NSCoder *)decoder { + self = [super initWithCoder:decoder]; + if (!self) return self; + + self.serverURL = [decoder decodeObjectOfClass:NSString.class forKey:@"serverURL"]; + self.eventDictionary = [decoder decodeObjectOfClass:NSDictionary.class forKey:@"eventDictionary"]; + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.serverURL forKey:@"serverURL"]; + [coder encodeObject:self.eventDictionary forKey:@"eventDictionary"]; +} + ++ (BOOL) supportsSecureCoding { + return YES; +} + +@end + +#pragma mark - BranchEvent + +@interface BranchEvent () { + NSMutableDictionary *_customData; + NSMutableArray *_contentItems; +} +@property (nonatomic, strong) NSString* eventName; +@end + +@implementation BranchEvent : NSObject + +- (instancetype) initWithName:(NSString *)name { + self = [super init]; + if (!self) return self; + _eventName = name; + return self; +} + ++ (instancetype) standardEvent:(BranchStandardEvent)standardEvent { + return [[BranchEvent alloc] initWithName:standardEvent]; +} + ++ (instancetype) standardEvent:(BranchStandardEvent)standardEvent + withContentItem:(BranchUniversalObject*)contentItem { + BranchEvent *e = [BranchEvent standardEvent:standardEvent]; + if (contentItem) { + e.contentItems = (NSMutableArray*) @[ contentItem ]; + } + return e; +} + ++ (instancetype) customEventWithName:(NSString*)name { + return [[BranchEvent alloc] initWithName:name]; +} + ++ (instancetype) customEventWithName:(NSString*)name + contentItem:(BranchUniversalObject*)contentItem { + BranchEvent *e = [[BranchEvent alloc] initWithName:name]; + if (contentItem) { + e.contentItems = (NSMutableArray*) @[ contentItem ]; + } + return e; +} + +- (NSMutableDictionary*) customData { + if (!_customData) _customData = [NSMutableDictionary new]; + return _customData; +} + +- (void) setCustomData:(NSMutableDictionary *)userInfo { + _customData = [userInfo mutableCopy]; +} + +- (NSMutableArray*) contentItems { + if (!_contentItems) _contentItems = [NSMutableArray new]; + return _contentItems; +} + +- (void) setContentItems:(NSMutableArray *)contentItems { + if ([contentItems isKindOfClass:[BranchUniversalObject class]]) { + _contentItems = [NSMutableArray arrayWithObject:contentItems]; + } else + if ([contentItems isKindOfClass:[NSArray class]]) { + _contentItems = [contentItems mutableCopy]; + } +} + +- (NSDictionary*) dictionary { + NSMutableDictionary *dictionary = [NSMutableDictionary new]; + + #define BNCFieldDefinesDictionaryFromSelf + #include "BNCFieldDefines.h" + + addString(transactionID, transaction_id); + addString(currency, currency); + addDecimal(revenue, revenue); + addDecimal(shipping, shipping); + addDecimal(tax, tax); + addString(coupon, coupon); + addString(affiliation, affiliation); + addString(eventDescription, description); + addString(searchQuery, search_query) + addDictionary(customData, custom_data); + + #include "BNCFieldDefines.h" + + return dictionary; +} + ++ (NSArray*) standardEvents { + return @[ + BranchStandardEventAddToCart, + BranchStandardEventAddToWishlist, + BranchStandardEventViewCart, + BranchStandardEventInitiatePurchase, + BranchStandardEventAddPaymentInfo, + BranchStandardEventPurchase, + BranchStandardEventSpendCredits, + BranchStandardEventSearch, + BranchStandardEventViewItem, + BranchStandardEventViewItems, + BranchStandardEventRate, + BranchStandardEventShare, + BranchStandardEventCompleteRegistration, + BranchStandardEventCompleteTutorial, + BranchStandardEventAchieveLevel, + BranchStandardEventUnlockAchievement, + ]; +} + +- (void) logEvent { + + if (![_eventName isKindOfClass:[NSString class]] || _eventName.length == 0) { + BNCLogError(@"Invalid event type '%@' or empty string.", NSStringFromClass(_eventName.class)); + return; + } + + NSMutableDictionary *eventDictionary = [NSMutableDictionary new]; + eventDictionary[@"name"] = _eventName; + + NSDictionary *propertyDictionary = [self dictionary]; + if (propertyDictionary.count) { + eventDictionary[@"event_data"] = propertyDictionary; + } + eventDictionary[@"custom_data"] = eventDictionary[@"event_data"][@"custom_data"]; + eventDictionary[@"event_data"][@"custom_data"] = nil; + + NSMutableArray *contentItemDictionaries = [NSMutableArray new]; + for (BranchUniversalObject *contentItem in self.contentItems) { + NSDictionary *dictionary = [contentItem dictionary]; + if (dictionary.count) { + [contentItemDictionaries addObject:dictionary]; + } + } + + if (contentItemDictionaries.count) { + eventDictionary[@"content_items"] = contentItemDictionaries; + } + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSString *serverURL = + ([self.class.standardEvents containsObject:self.eventName]) + ? [NSString stringWithFormat:@"%@/%@", preferenceHelper.branchAPIURL, @"v2/event/standard"] + : [NSString stringWithFormat:@"%@/%@", preferenceHelper.branchAPIURL, @"v2/event/custom"]; + + BranchEventRequest *request = + [[BranchEventRequest alloc] + initWithServerURL:[NSURL URLWithString:serverURL] + eventDictionary:eventDictionary + completion:nil]; + + [[Branch getInstance] sendServerRequestWithoutSession:request]; +} + +- (NSString*_Nonnull) description { + return [NSString stringWithFormat: + @"<%@ 0x%016llx %@ txID: %@ Amt: %@ %@ desc: %@ items: %ld customData: %@>", + NSStringFromClass(self.class), + (uint64_t) self, + self.eventName, + self.transactionID, + self.currency, + self.revenue, + self.eventDescription, + (long) self.contentItems.count, + self.customData + ]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchLinkProperties.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchLinkProperties.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchLinkProperties.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchLinkProperties.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchLinkProperties.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchLinkProperties.m new file mode 100644 index 00000000000..91c10965da8 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchLinkProperties.m @@ -0,0 +1,71 @@ +// +// BranchLinkProperties.m +// Branch-TestBed +// +// Created by Derrick Staten on 10/16/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#import "BranchLinkProperties.h" +#import "BranchConstants.h" + +@implementation BranchLinkProperties + +- (NSDictionary *)controlParams { + if (!_controlParams) { + _controlParams = [[NSDictionary alloc] init]; + } + return _controlParams; +} + +- (void)addControlParam:(NSString *)controlParam withValue:(NSString *)value { + if (!controlParam) return; + NSMutableDictionary *temp = [self.controlParams mutableCopy]; + temp[controlParam] = value; + _controlParams = [temp copy]; +} + ++ (BranchLinkProperties *)getBranchLinkPropertiesFromDictionary:(NSDictionary *)dictionary { + BranchLinkProperties *linkProperties = [[BranchLinkProperties alloc] init]; + + if (dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_TAGS]]) { + linkProperties.tags = dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_TAGS]]; + } + if (dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_FEATURE]]) { + linkProperties.feature = dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_FEATURE]]; + } + if (dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_ALIAS]]) { + linkProperties.alias = dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_ALIAS]]; + } + if (dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_CHANNEL]]) { + linkProperties.channel = dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_CHANNEL]]; + } + if (dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_STAGE]]) { + linkProperties.stage = dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_STAGE]]; + } + if (dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_CAMPAIGN]]) { + linkProperties.campaign = dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_CAMPAIGN]]; + } + if (dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_DURATION]]) { + linkProperties.matchDuration = [dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_DURATION]] intValue]; + } + if (dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_STAGE]]) { + linkProperties.stage = dictionary[[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_STAGE]]; + } + + NSMutableDictionary *controlParams = [[NSMutableDictionary alloc] init]; + for (NSString *oneKey in dictionary.allKeys) { + if ([oneKey hasPrefix:@"$"]) { + controlParams[oneKey] = dictionary[oneKey]; + } + } + linkProperties.controlParams = controlParams; + + return linkProperties; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"BranchLinkProperties | tags: %@ \n feature: %@ \n alias: %@ \n channel: %@ \n stage: %@ \n campaign: %@ \n matchDuration: %lu \n controlParams: %@", self.tags, self.feature, self.alias, self.channel, self.stage, self.campaign, (long)self.matchDuration, self.controlParams]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchShareLink.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchShareLink.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchShareLink.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchShareLink.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchShareLink.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchShareLink.m new file mode 100644 index 00000000000..7e94165afcc --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchShareLink.m @@ -0,0 +1,301 @@ +// +// BranchShareLink.m +// Branch-SDK +// +// Created by Edward Smith on 3/13/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#import "BranchShareLink.h" +#import "BranchConstants.h" +#import "BNCFabricAnswers.h" +#import "BranchActivityItemProvider.h" +#import "BNCDeviceInfo.h" +#import "BNCAvailability.h" +#import "BNCLog.h" +#import "Branch.h" +@class BranchShareActivityItem; + +typedef NS_ENUM(NSInteger, BranchShareActivityItemType) { + BranchShareActivityItemTypeBranchURL = 0, + BranchShareActivityItemTypeShareText, + BranchShareActivityItemTypeOther, +}; + +#pragma mark BranchShareLink + +@interface BranchShareLink () { + NSMutableArray* _activityItems; +} + +- (id) shareObjectForItem:(BranchShareActivityItem*)activityItem + activityType:(UIActivityType)activityType; + +@property (nonatomic, strong) NSURL *shareURL; +@end + +#pragma mark - BranchShareActivityItem + +@interface BranchShareActivityItem : UIActivityItemProvider +@property (nonatomic, assign) BranchShareActivityItemType itemType; +@property (nonatomic, weak) BranchShareLink *parent; // Weak pointer to avoid retain cycle. +@end + +@implementation BranchShareActivityItem + +- (id) initWithPlaceholderItem:(id)placeholderItem { + self = [super initWithPlaceholderItem:placeholderItem]; + if (!self) return self; + + if ([placeholderItem isKindOfClass:NSString.class]) { + self.itemType = BranchShareActivityItemTypeShareText; + } else { + self.itemType = BranchShareActivityItemTypeOther; + } + + return self; +} + +- (id) item { + return [self.parent shareObjectForItem:self activityType:self.activityType]; +} + +- (NSString*) subject { + NSString *subject = self.parent.linkProperties.controlParams[BRANCH_LINK_DATA_KEY_EMAIL_SUBJECT]; + if (subject.length == 0) subject = self.parent.emailSubject; + return subject; +} + +- (NSString*) subjectForActivityType:(UIActivityType)activityType { + return self.subject; +} + +- (NSString*) activityViewController:(UIActivityViewController*)activityViewController + subjectForActivityType:(UIActivityType)activityType { + return self.subject; +} + +@end + +#pragma mark - BranchShareLink + +@implementation BranchShareLink + +- (instancetype _Nonnull) initWithUniversalObject:(BranchUniversalObject*_Nonnull)universalObject + linkProperties:(BranchLinkProperties*_Nonnull)linkProperties { + self = [super init]; + if (!self) return self; + + _universalObject = universalObject; + _linkProperties = linkProperties; + return self; +} + +- (void) shareDidComplete:(BOOL)completed activityError:(NSError*)error { + if ([self.delegate respondsToSelector:@selector(branchShareLink:didComplete:withError:)]) { + [self.delegate branchShareLink:self didComplete:completed withError:error]; + } + if (completed && !error) { + [[BranchEvent customEventWithName:BNCShareCompletedEvent contentItem:self.universalObject] logEvent]; + NSDictionary *attributes = [self.universalObject getDictionaryWithCompleteLinkProperties:self.linkProperties]; + [BNCFabricAnswers sendEventWithName:@"Branch Share" andAttributes:attributes]; + } +} + +- (NSArray*_Nonnull) activityItems { + if (_activityItems) { + return _activityItems; + } + + // Make sure we can share + + if (!(self.universalObject.canonicalIdentifier || + self.universalObject.canonicalUrl || + self.universalObject.title)) { + BNCLogWarning(@"A canonicalIdentifier, canonicalURL, or title are required to uniquely" + " identify content. In order to not break the end user experience with sharing," + " Branch SDK will proceed to create a URL, but content analytics may not properly" + " include this URL."); + } + + self.serverParameters = + [[self.universalObject getParamsForServerRequestWithAddedLinkProperties:self.linkProperties] + mutableCopy]; + if (self.linkProperties.matchDuration) { + self.serverParameters[BRANCH_REQUEST_KEY_URL_DURATION] = @(self.linkProperties.matchDuration); + } + + // Log share initiated event + [[BranchEvent customEventWithName:BNCShareInitiatedEvent contentItem:self.universalObject] logEvent]; + + _activityItems = [NSMutableArray new]; + BranchShareActivityItem *item = nil; + if (self.shareText.length) { + item = [[BranchShareActivityItem alloc] initWithPlaceholderItem:self.shareText]; + item.itemType = BranchShareActivityItemTypeShareText; + item.parent = self; + [_activityItems addObject:item]; + } + + NSString *URLString = + [[Branch getInstance] + getLongURLWithParams:self.serverParameters + andChannel:self.linkProperties.channel + andTags:self.linkProperties.tags + andFeature:self.linkProperties.feature + andStage:self.linkProperties.stage + andAlias:self.linkProperties.alias]; + self.shareURL = [[NSURL alloc] initWithString:URLString]; + if (self.returnURL) + item = [[BranchShareActivityItem alloc] initWithPlaceholderItem:self.shareURL]; + else + item = [[BranchShareActivityItem alloc] initWithPlaceholderItem:self.shareURL.absoluteString]; + item.itemType = BranchShareActivityItemTypeBranchURL; + item.parent = self; + [_activityItems addObject:item]; + + if (self.shareObject) { + item = [[BranchShareActivityItem alloc] initWithPlaceholderItem:self.shareObject]; + item.itemType = BranchShareActivityItemTypeOther; + item.parent = self; + [_activityItems addObject:item]; + } + + return _activityItems; +} + +- (void) presentActivityViewControllerFromViewController:(UIViewController*_Nullable)viewController + anchor:(id _Nullable)anchorViewOrButtonItem { + + UIActivityViewController *shareViewController = + [[UIActivityViewController alloc] + initWithActivityItems:self.activityItems + applicationActivities:nil]; + shareViewController.title = self.title; + + if ([shareViewController respondsToSelector:@selector(completionWithItemsHandler)]) { + + shareViewController.completionWithItemsHandler = + ^ (NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { + [self shareDidComplete:completed activityError:activityError]; + }; + + } else { + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + shareViewController.completionHandler = + ^ (UIActivityType activityType, BOOL completed) { + [self shareDidComplete:completed activityError:nil]; + }; + #pragma clang diagnostic pop + + } + + NSString *emailSubject = self.linkProperties.controlParams[BRANCH_LINK_DATA_KEY_EMAIL_SUBJECT]; + if (emailSubject.length <= 0) emailSubject = self.emailSubject; + if (emailSubject.length) { + @try { + [shareViewController setValue:emailSubject forKey:@"subject"]; + } + @catch (NSException*) { + BNCLogWarning( + @"Unable to setValue 'emailSubject' forKey 'subject' on UIActivityViewController." + ); + } + } + + UIViewController *presentingViewController = nil; + if ([viewController respondsToSelector:@selector(presentViewController:animated:completion:)]) { + presentingViewController = viewController; + } else { + UIViewController *rootController = [UIViewController bnc_currentViewController]; + if ([rootController respondsToSelector:@selector(presentViewController:animated:completion:)]) { + presentingViewController = rootController; + } + } + + if (!presentingViewController) { + BNCLogError(@"No view controller is present to show the share sheet. Not showing sheet."); + return; + } + + // Required for iPad/Universal apps on iOS 8+ + if ([presentingViewController respondsToSelector:@selector(popoverPresentationController)]) { + if ([anchorViewOrButtonItem isKindOfClass:UIBarButtonItem.class]) { + UIBarButtonItem *anchor = (UIBarButtonItem*) anchorViewOrButtonItem; + shareViewController.popoverPresentationController.barButtonItem = anchor; + } else + if ([anchorViewOrButtonItem isKindOfClass:UIView.class]) { + UIView *anchor = (UIView*) anchorViewOrButtonItem; + shareViewController.popoverPresentationController.sourceView = anchor; + shareViewController.popoverPresentationController.sourceRect = anchor.bounds; + } else { + shareViewController.popoverPresentationController.sourceView = presentingViewController.view; + shareViewController.popoverPresentationController.sourceRect = CGRectMake(0.0, 0.0, 40.0, 40.0); + } + } + [presentingViewController presentViewController:shareViewController animated:YES completion:nil]; +} + +- (id) shareObjectForItem:(BranchShareActivityItem*)activityItem + activityType:(UIActivityType)activityType { + + _activityType = [activityType copy]; + self.linkProperties.channel = + [BranchActivityItemProvider humanReadableChannelWithActivityType:self.activityType]; + + if ([self.delegate respondsToSelector:@selector(branchShareLinkWillShare:)]) { + [self.delegate branchShareLinkWillShare:self]; + } + if (activityItem.itemType == BranchShareActivityItemTypeShareText) { + return self.shareText; + } + if (activityItem.itemType == BranchShareActivityItemTypeOther) { + return self.shareObject; + } + + // Else activityItem.itemType == BranchShareActivityItemTypeURL + + // Because Facebook et al immediately scrape URLs, we add an additional parameter to the + // existing list, telling the backend to ignore the first click. + + NSSet*scrapers = [NSSet setWithArray:@[ + @"Facebook", + @"Twitter", + @"Slack", + @"Apple Notes", + @"Skype", + @"SMS", + @"Apple Reminders" + ]]; + NSString *userAgentString = nil; + if (self.linkProperties.channel && [scrapers containsObject:self.linkProperties.channel]) { + userAgentString = [BNCDeviceInfo userAgentString]; + } + NSString *URLString = + [[Branch getInstance] + getShortURLWithParams:self.serverParameters + andTags:self.linkProperties.tags + andChannel:self.linkProperties.channel + andFeature:self.linkProperties.feature + andStage:self.linkProperties.stage + andCampaign:self.linkProperties.campaign + andAlias:self.linkProperties.alias + ignoreUAString:userAgentString + forceLinkCreation:YES]; + self.shareURL = [NSURL URLWithString:URLString]; + return (self.returnURL) ? self.shareURL :self.shareURL.absoluteString; +} + +- (BOOL) returnURL { + BOOL returnURL = YES; + if ([UIDevice currentDevice].systemVersion.doubleValue >= 11.0 && + [UIDevice currentDevice].systemVersion.doubleValue < 11.2 && + [self.activityType isEqualToString:UIActivityTypeCopyToPasteboard]) { + returnURL = NO; + } + return returnURL; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchUniversalObject.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchUniversalObject.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchUniversalObject.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchUniversalObject.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchUniversalObject.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchUniversalObject.m new file mode 100644 index 00000000000..7dd120e06bb --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchUniversalObject.m @@ -0,0 +1,739 @@ +// +// BranchUniversalObject.m +// Branch-SDK +// +// Created by Derrick Staten on 10/16/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#import "BranchUniversalObject.h" +#import "BNCError.h" +#import "BranchConstants.h" +#import "BNCFabricAnswers.h" +#import "BNCDeviceInfo.h" +#import "BNCLog.h" +#import "BNCLocalization.h" +#import "BNCEncodingUtils.h" +#import "Branch.h" + +#pragma mark BranchContentSchema + +BranchContentSchema _Nonnull BranchContentSchemaCommerceAuction = @"COMMERCE_AUCTION"; +BranchContentSchema _Nonnull BranchContentSchemaCommerceBusiness = @"COMMERCE_BUSINESS"; +BranchContentSchema _Nonnull BranchContentSchemaCommerceOther = @"COMMERCE_OTHER"; +BranchContentSchema _Nonnull BranchContentSchemaCommerceProduct = @"COMMERCE_PRODUCT"; +BranchContentSchema _Nonnull BranchContentSchemaCommerceRestaurant = @"COMMERCE_RESTAURANT"; +BranchContentSchema _Nonnull BranchContentSchemaCommerceService = @"COMMERCE_SERVICE"; +BranchContentSchema _Nonnull BranchContentSchemaCommerceTravelFlight= @"COMMERCE_TRAVEL_FLIGHT"; +BranchContentSchema _Nonnull BranchContentSchemaCommerceTravelHotel = @"COMMERCE_TRAVEL_HOTEL"; +BranchContentSchema _Nonnull BranchContentSchemaCommerceTravelOther = @"COMMERCE_TRAVEL_OTHER"; +BranchContentSchema _Nonnull BranchContentSchemaGameState = @"GAME_STATE"; +BranchContentSchema _Nonnull BranchContentSchemaMediaImage = @"MEDIA_IMAGE"; +BranchContentSchema _Nonnull BranchContentSchemaMediaMixed = @"MEDIA_MIXED"; +BranchContentSchema _Nonnull BranchContentSchemaMediaMusic = @"MEDIA_MUSIC"; +BranchContentSchema _Nonnull BranchContentSchemaMediaOther = @"MEDIA_OTHER"; +BranchContentSchema _Nonnull BranchContentSchemaMediaVideo = @"MEDIA_VIDEO"; +BranchContentSchema _Nonnull BranchContentSchemaOther = @"OTHER"; +BranchContentSchema _Nonnull BranchContentSchemaTextArticle = @"TEXT_ARTICLE"; +BranchContentSchema _Nonnull BranchContentSchemaTextBlog = @"TEXT_BLOG"; +BranchContentSchema _Nonnull BranchContentSchemaTextOther = @"TEXT_OTHER"; +BranchContentSchema _Nonnull BranchContentSchemaTextRecipe = @"TEXT_RECIPE"; +BranchContentSchema _Nonnull BranchContentSchemaTextReview = @"TEXT_REVIEW"; +BranchContentSchema _Nonnull BranchContentSchemaTextSearchResults = @"TEXT_SEARCH_RESULTS"; +BranchContentSchema _Nonnull BranchContentSchemaTextStory = @"TEXT_STORY"; +BranchContentSchema _Nonnull BranchContentSchemaTextTechnicalDoc = @"TEXT_TECHNICAL_DOC"; + +#pragma mark - BranchCondition + +BranchCondition _Nonnull BranchConditionOther = @"OTHER"; +BranchCondition _Nonnull BranchConditionExcellent = @"EXCELLENT"; +BranchCondition _Nonnull BranchConditionNew = @"NEW"; +BranchCondition _Nonnull BranchConditionGood = @"GOOD"; +BranchCondition _Nonnull BranchConditionFair = @"FAIR"; +BranchCondition _Nonnull BranchConditionPoor = @"POOR"; +BranchCondition _Nonnull BranchConditionUsed = @"USED"; +BranchCondition _Nonnull BranchConditionRefurbished = @"REFURBISHED"; + +#pragma mark - BranchContentMetadata + +@interface BranchContentMetadata () { + NSMutableArray *_imageCaptions; + NSMutableDictionary *_customMetadata; +} +@end + +@implementation BranchContentMetadata + +- (NSDictionary*_Nonnull) dictionary { + NSMutableDictionary*dictionary = [NSMutableDictionary new]; + + for (NSString *key in self.customMetadata.keyEnumerator) { + NSString *value = self.customMetadata[key]; + dictionary[key] = value; + } + + #define BNCFieldDefinesDictionaryFromSelf + #include "BNCFieldDefines.h" + + addString(contentSchema, $content_schema); + addDouble(quantity, $quantity); + addDecimal(price, $price); + addString(currency, $currency); + addString(sku, $sku); + addString(productName, $product_name); + addString(productBrand, $product_brand); + addString(productCategory, $product_category); + addString(productVariant, $product_variant); + addString(condition, $condition); + addDouble(ratingAverage, $rating_average); + addInteger(ratingCount, $rating_count); + addDouble(ratingMax, $rating_max); + addDouble(rating, $rating); + addString(addressStreet, $address_street); + addString(addressCity, $address_city); + addString(addressRegion, $address_region); + addString(addressCountry, $address_country); + addString(addressPostalCode,$address_postal_code); + addDouble(latitude, $latitude); + addDouble(longitude, $longitude); + addStringArray(imageCaptions,$image_captions); + + #include "BNCFieldDefines.h" + + return dictionary; +} + ++ (BranchContentMetadata*_Nonnull) contentMetadataWithDictionary:(NSDictionary*_Nullable)dictionary { + BranchContentMetadata*object = [BranchContentMetadata new]; + if (!dictionary) return object; + + #define BNCFieldDefinesObjectFromDictionary + #include "BNCFieldDefines.h" + + addString(contentSchema, $content_schema); + addDouble(quantity, $quantity); + addDecimal(price, $price); + addString(currency, $currency); + addString(sku, $sku); + addString(productName, $product_name); + addString(productBrand, $product_brand); + addString(productCategory, $product_category); + addString(productVariant, $product_variant); + addString(condition, $condition); + addDouble(ratingAverage, $rating_average); + addInteger(ratingCount, $rating_count); + addDouble(ratingMax, $rating_max); + addDouble(rating, $rating); + addString(addressStreet, $address_street); + addString(addressCity, $address_city); + addString(addressRegion, $address_region); + addString(addressCountry, $address_country); + addString(addressPostalCode,$address_postal_code); + addDouble(latitude, $latitude); + addDouble(longitude, $longitude); + addStringArray(imageCaptions,$image_captions); + + #include "BNCFieldDefines.h" + + return object; +} + +- (NSMutableDictionary*) customMetadata { + if (!_customMetadata) _customMetadata = [NSMutableDictionary new]; + return _customMetadata; +} + +- (void) setCustomMetadata:(NSMutableDictionary*)dictionary { + _customMetadata = [dictionary mutableCopy]; +} + +- (void) setImageCaptions:(NSMutableArray *)imageCaptions { + _imageCaptions = [imageCaptions mutableCopy]; +} + +- (NSMutableArray *) imageCaptions { + if (!_imageCaptions) _imageCaptions = [NSMutableArray new]; + return _imageCaptions; +} + +- (NSString*) description { + return [NSString stringWithFormat:@"<%@ 0x%016llx schema: %@ userData: %ld items>", + NSStringFromClass(self.class), + (uint64_t) self, + _contentSchema, + (long) _customMetadata.count + ]; +} + +@end + +#pragma mark - BranchUniversalObject + +@implementation BranchUniversalObject + +- (instancetype)initWithCanonicalIdentifier:(NSString *)canonicalIdentifier { + if ((self = [super init])) { + self.canonicalIdentifier = canonicalIdentifier; + } + return self; +} + +- (instancetype)initWithTitle:(NSString *)title { + if ((self = [super init])) { + self.title = title; + } + return self; +} + +#pragma mark - Deprecated Fields + +- (NSDictionary *)metadata { + return self.contentMetadata.customMetadata; +} + +- (void) setMetadata:(NSDictionary *)metadata { + self.contentMetadata.customMetadata = (NSMutableDictionary*) metadata; +} + +- (void)addMetadataKey:(NSString *)key value:(NSString *)value { + if (key) [self.contentMetadata.customMetadata setValue:value forKey:key]; +} + +- (CGFloat) price { + return [self.contentMetadata.price floatValue]; +} + +- (void) setPrice:(CGFloat)price { + NSString *string = [NSString stringWithFormat:@"%f", price]; + self.contentMetadata.price = [NSDecimalNumber decimalNumberWithString:string]; +} + +- (NSString*) currency { + return self.contentMetadata.currency; +} + +- (void) setCurrency:(NSString *)currency { + self.contentMetadata.currency = currency; +} + +- (NSString*) type { + return self.contentMetadata.contentSchema; +} + +- (void) setType:(NSString*)type { + self.contentMetadata.contentSchema = type; +} + +- (BranchContentIndexMode) contentIndexMode { + if (self.publiclyIndex) + return BranchContentIndexModePublic; + else + return BranchContentIndexModePrivate; +} + +- (void) setContentIndexMode:(BranchContentIndexMode)contentIndexMode { + if (contentIndexMode == BranchContentIndexModePublic) + self.publiclyIndex = YES; + else + self.locallyIndex = YES; +} + +- (BOOL) automaticallyListOnSpotlight { + return self.locallyIndex; +} + +- (void) setAutomaticallyListOnSpotlight:(BOOL)automaticallyListOnSpotlight { + self.locallyIndex = automaticallyListOnSpotlight; +} + +#pragma mark - Setters / Getters / Standard Methods + +- (BranchContentMetadata*) contentMetadata { + if (!_contentMetadata) _contentMetadata = [BranchContentMetadata new]; + return _contentMetadata; +} + +- (NSString *)description { + return [NSString stringWithFormat: + @"<%@ 0x%016llx" + "\n canonicalIdentifier: %@" + "\n title: %@" + "\n contentDescription: %@" + "\n imageUrl: %@" + "\n metadata: %@" + "\n type: %@" + "\n locallyIndex: %d" + "\n publiclyIndex: %d" + "\n keywords: %@" + "\n expirationDate: %@" + "\n>", + NSStringFromClass(self.class), (uint64_t) self, + self.canonicalIdentifier, + self.title, + self.contentDescription, + self.imageUrl, + self.contentMetadata.customMetadata, + self.contentMetadata.contentSchema, + self.locallyIndex, + self.publiclyIndex, + self.keywords, + self.expirationDate]; +} + +#pragma mark - User Event Logging + +- (void)registerView { + [self registerViewWithCallback:nil]; +} + +- (void)registerViewWithCallback:(callbackWithParams)callback { + if (!self.canonicalIdentifier && !self.title) { + NSString *message = BNCLocalizedString(@"Could not register view."); + NSError *error = [NSError branchErrorWithCode:BNCContentIdentifierError localizedMessage:message]; + BNCLogWarning(@"%@", error); + if (callback) callback([[NSDictionary alloc] init], error); + return; + } + if (self.locallyIndex) { + [self listOnSpotlight]; + } + [[BranchEvent standardEvent:BranchStandardEventViewItem withContentItem:self] logEvent]; + if (callback) callback(@{}, nil); +} + +- (void)userCompletedAction:(NSString *)action { + [self userCompletedAction:action withState:nil]; +} + +- (void)userCompletedAction:(NSString *)action withState:(NSDictionary *)state { + if (state) [self.contentMetadata.customMetadata addEntriesFromDictionary:state]; + [[BranchEvent customEventWithName:action contentItem:self] logEvent]; + + // Maybe list on spotlight -- + NSDictionary *linkParams = self.dictionary; + if (self.locallyIndex && self.canonicalIdentifier && linkParams) { + + NSMutableDictionary *actionPayload = [[NSMutableDictionary alloc] init]; + actionPayload[BNCCanonicalIdList] = @[self.canonicalIdentifier]; + actionPayload[self.canonicalIdentifier] = linkParams; + if (state) [actionPayload addEntriesFromDictionary:state]; + + if ([action isEqualToString:BNCRegisterViewEvent]) + [self listOnSpotlight]; + } +} + +#pragma mark - Link Creation Methods + +- (NSString *)getShortUrlWithLinkProperties:(BranchLinkProperties *)linkProperties { + if (!self.canonicalIdentifier && !self.title) { + BNCLogWarning(@"A canonicalIdentifier or title are required to uniquely identify content, so could not generate a URL."); + return nil; + } + + return [[Branch getInstance] getShortUrlWithParams:[self getParamsForServerRequestWithAddedLinkProperties:linkProperties] + andTags:linkProperties.tags + andAlias:linkProperties.alias + andChannel:linkProperties.channel + andFeature:linkProperties.feature + andStage:linkProperties.stage + andCampaign:linkProperties.campaign + andMatchDuration:linkProperties.matchDuration]; +} + +- (void)getShortUrlWithLinkProperties:(BranchLinkProperties *)linkProperties andCallback:(callbackWithUrl)callback { + if (!self.canonicalIdentifier && !self.title) { + NSString *message = BNCLocalizedString(@"Could not generate a URL."); + NSError *error = [NSError branchErrorWithCode:BNCContentIdentifierError localizedMessage:message]; + BNCLogWarning(@"%@", error); + if (callback) callback([BNCPreferenceHelper preferenceHelper].userUrl, error); + return; + } + + [[Branch getInstance] getShortUrlWithParams:[self getParamsForServerRequestWithAddedLinkProperties:linkProperties] + andTags:linkProperties.tags + andAlias:linkProperties.alias + andMatchDuration:linkProperties.matchDuration + andChannel:linkProperties.channel + andFeature:linkProperties.feature + andStage:linkProperties.stage + andCampaign:linkProperties.campaign + andCallback:callback]; +} + +- (NSString *)getShortUrlWithLinkPropertiesAndIgnoreFirstClick:(BranchLinkProperties *)linkProperties { + if (!self.canonicalIdentifier && !self.title) { + NSString *message = BNCLocalizedString(@"Could not generate a URL."); + NSError *error = [NSError branchErrorWithCode:BNCContentIdentifierError localizedMessage:message]; + BNCLogWarning(@"%@", error); + return nil; + } + // keep this operation outside of sync operation below. + NSString *UAString = [BNCDeviceInfo userAgentString]; + + return [[Branch getInstance] getShortURLWithParams:[self getParamsForServerRequestWithAddedLinkProperties:linkProperties] + andTags:linkProperties.tags + andChannel:linkProperties.channel + andFeature:linkProperties.feature + andStage:linkProperties.stage + andCampaign:linkProperties.campaign + andAlias:linkProperties.alias + ignoreUAString:UAString + forceLinkCreation:YES]; +} + +- (NSString *)getLongUrlWithChannel:(NSString *)channel + andTags:(NSArray *)tags + andFeature:(NSString *)feature + andStage:(NSString *)stage + andAlias:(NSString *)alias { + NSString *urlString = + [[Branch getInstance] + getLongURLWithParams:self.dictionary + andChannel:channel + andTags:tags + andFeature:feature + andStage:stage + andAlias:alias]; + return urlString; +} + +#pragma mark - Share Sheets + +- (UIActivityItemProvider *)getBranchActivityItemWithLinkProperties:(BranchLinkProperties *)linkProperties { + if (!self.canonicalIdentifier && !self.canonicalUrl && !self.title) { + BNCLogWarning(@"A canonicalIdentifier, canonicalURL, or title are required to uniquely identify content. " + "In order to not break the end user experience with sharing, Branch SDK will proceed to create a URL, " + "but content analytics may not properly include this URL."); + } + + NSMutableDictionary *params = [[self getParamsForServerRequestWithAddedLinkProperties:linkProperties] mutableCopy]; + if (linkProperties.matchDuration) { + [params setObject:@(linkProperties.matchDuration) forKey:BRANCH_REQUEST_KEY_URL_DURATION]; + } + + return [Branch getBranchActivityItemWithParams:params + feature:linkProperties.feature + stage:linkProperties.stage + campaign:linkProperties.campaign + tags:linkProperties.tags + alias:linkProperties.alias]; +} + +- (void)showShareSheetWithShareText:(NSString *)shareText + completion:(void (^ _Nullable)(NSString * _Nullable activityType, BOOL completed))completion { + [self showShareSheetWithLinkProperties:nil andShareText:shareText fromViewController:nil completion:completion]; +} + +- (void)showShareSheetWithLinkProperties:(BranchLinkProperties *)linkProperties + andShareText:(NSString *)shareText + fromViewController:(UIViewController *)viewController + completion:(void (^ _Nullable)(NSString * _Nullable activityType, BOOL completed))completion { + [self showShareSheetWithLinkProperties:linkProperties andShareText:shareText + fromViewController:viewController anchor:nil completion:completion orCompletionWithError:nil]; +} + +- (void)showShareSheetWithLinkProperties:(BranchLinkProperties *)linkProperties + andShareText:(NSString *)shareText + fromViewController:(UIViewController *)viewController + completionWithError:(void (^ _Nullable)(NSString * _Nullable activityType, BOOL completed, NSError*_Nullable error))completion { + [self showShareSheetWithLinkProperties:linkProperties andShareText:shareText + fromViewController:viewController anchor:nil completion:nil orCompletionWithError:completion]; +} + +- (void)showShareSheetWithLinkProperties:(nullable BranchLinkProperties *)linkProperties + andShareText:(nullable NSString *)shareText + fromViewController:(nullable UIViewController *)viewController + anchor:(nullable id)anchor + completion:(void (^ _Nullable)(NSString * _Nullable activityType, BOOL completed))completion { + [self showShareSheetWithLinkProperties:linkProperties andShareText:shareText + fromViewController:viewController anchor:anchor completion:completion orCompletionWithError:nil]; +} + +- (void)showShareSheetWithLinkProperties:(nullable BranchLinkProperties *)linkProperties + andShareText:(nullable NSString *)shareText + fromViewController:(nullable UIViewController *)viewController + anchor:(nullable id)anchor + completionWithError:(void (^ _Nullable)(NSString * _Nullable activityType, BOOL completed, NSError*_Nullable error))completion { + [self showShareSheetWithLinkProperties:linkProperties andShareText:shareText + fromViewController:viewController anchor:anchor completion:nil orCompletionWithError:completion]; +} + +- (void)showShareSheetWithLinkProperties:(BranchLinkProperties *)linkProperties + andShareText:(NSString *)shareText + fromViewController:(UIViewController *)viewController + anchor:(nullable id)anchorViewOrButtonItem + completion:(void (^ _Nullable)(NSString * _Nullable activityType, BOOL completed))completion + orCompletionWithError:(void (^ _Nullable)(NSString * _Nullable activityType, BOOL completed, NSError*_Nullable error))completionError { + + // Log share initiated event + [[BranchEvent customEventWithName:BNCShareInitiatedEvent contentItem:self] logEvent]; + UIActivityItemProvider *itemProvider = [self getBranchActivityItemWithLinkProperties:linkProperties]; + NSMutableArray *items = [NSMutableArray arrayWithObject:itemProvider]; + if (shareText) { + [items insertObject:shareText atIndex:0]; + } + UIActivityViewController *shareViewController = + [[UIActivityViewController alloc] initWithActivityItems:items applicationActivities:nil]; + + if ([shareViewController respondsToSelector:@selector(completionWithItemsHandler)]) { + shareViewController.completionWithItemsHandler = + ^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { + // Log share completed event + if (completed && !activityError) { + [[BranchEvent customEventWithName:BNCShareCompletedEvent contentItem:self] logEvent]; + [BNCFabricAnswers sendEventWithName:@"Branch Share" andAttributes: + [self getDictionaryWithCompleteLinkProperties:linkProperties]]; + } + if (completion) + completion(activityType, completed); + else + if (completionError) + completionError(activityType, completed, activityError); + }; + } else { + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + // Deprecated in iOS 8. Safe to hide deprecation warnings as the new completion handler is checked for above + shareViewController.completionHandler = completion; + #pragma clang diagnostic pop + } + + UIViewController *presentingViewController = nil; + if (viewController && [viewController respondsToSelector:@selector(presentViewController:animated:completion:)]) { + presentingViewController = viewController; + } + else { + UIViewController *rootViewController = [UIViewController bnc_currentViewController]; + if ([rootViewController respondsToSelector:@selector(presentViewController:animated:completion:)]) { + presentingViewController = rootViewController; + } + } + + if (linkProperties.controlParams[BRANCH_LINK_DATA_KEY_EMAIL_SUBJECT]) { + @try { + [shareViewController setValue:linkProperties.controlParams[BRANCH_LINK_DATA_KEY_EMAIL_SUBJECT] forKey:@"subject"]; + } + @catch (NSException*) { + BNCLogWarning(@"Unable to setValue 'emailSubject' forKey 'subject' on UIActivityViewController."); + } + } + + if (presentingViewController) { + // Required for iPad/Universal apps on iOS 8+ + if ([presentingViewController respondsToSelector:@selector(popoverPresentationController)]) { + if ([anchorViewOrButtonItem isKindOfClass:UIBarButtonItem.class]) { + UIBarButtonItem *anchor = (UIBarButtonItem*) anchorViewOrButtonItem; + shareViewController.popoverPresentationController.barButtonItem = anchor; + } else + if ([anchorViewOrButtonItem isKindOfClass:UIView.class]) { + UIView *anchor = (UIView*) anchorViewOrButtonItem; + shareViewController.popoverPresentationController.sourceView = anchor; + shareViewController.popoverPresentationController.sourceRect = anchor.bounds; + } else { + shareViewController.popoverPresentationController.sourceView = presentingViewController.view; + shareViewController.popoverPresentationController.sourceRect = CGRectMake(0.0, 0.0, 40.0, 40.0); + } + } + [presentingViewController presentViewController:shareViewController animated:YES completion:nil]; + } + else { + BNCLogWarning(@"Unable to show the share sheet since no view controller is present."); + } +} + +#pragma mark - Spotlight + +- (void)listOnSpotlight { + [self listOnSpotlightWithCallback:nil]; +} + +- (void)listOnSpotlightWithCallback:(callbackWithUrl)callback { + [[Branch getInstance] + indexOnSpotlightWithBranchUniversalObject:self + linkProperties:nil + completion:^(BranchUniversalObject *universalObject, NSString *url, NSError *error) { + if (callback) callback(url,error); + }]; +} + +//This one uses a callback that returns the SpotlightIdentifier +- (void)listOnSpotlightWithIdentifierCallback:(callbackWithUrlAndSpotlightIdentifier)spotlightCallback { + BOOL publiclyIndexable; + if (self.contentIndexMode == BranchContentIndexModePrivate) { + publiclyIndexable = NO; + } + else { + publiclyIndexable = YES; + } + + NSMutableDictionary *metadataAndProperties = [self.metadata mutableCopy]; + if (self.canonicalIdentifier) { + metadataAndProperties[BRANCH_LINK_DATA_KEY_CANONICAL_IDENTIFIER] = self.canonicalIdentifier; + } + if (self.canonicalUrl) { + metadataAndProperties[BRANCH_LINK_DATA_KEY_CANONICAL_URL] = self.canonicalUrl; + } + + [[Branch getInstance] createDiscoverableContentWithTitle:self.title + description:self.contentDescription + thumbnailUrl:[NSURL URLWithString:self.imageUrl] + canonicalId:self.canonicalIdentifier + linkParams:metadataAndProperties.copy + type:self.type + publiclyIndexable:publiclyIndexable + keywords:[NSSet setWithArray:self.keywords] + expirationDate:self.expirationDate + spotlightCallback:spotlightCallback]; +} + +- (void)listOnSpotlightWithLinkProperties:(BranchLinkProperties*_Nullable)linkproperties + callback:(void (^_Nullable)(NSString * _Nullable url, + NSError * _Nullable error))completion { + [[Branch getInstance] + indexOnSpotlightWithBranchUniversalObject:self + linkProperties:linkproperties + completion:^(BranchUniversalObject *universalObject, NSString *url, NSError *error) { + if (completion) completion(url,error); + }]; +} + +- (void) removeFromSpotlightWithCallback:(void (^_Nullable)(NSError * _Nullable error))completion{ + if (self.locallyIndex) { + [[Branch getInstance] removeSearchableItemWithBranchUniversalObject:self + callback:^(NSError *error) { + if (completion) { + completion(error); + } + }]; + } else { + NSError *error = [NSError branchErrorWithCode:BNCSpotlightPublicIndexError + localizedMessage:@"Publically indexed cannot be removed from Spotlight"]; + if (completion) completion(error); + } +} + +#pragma mark - Dictionary Methods + +- (NSDictionary *)getParamsForServerRequestWithAddedLinkProperties:(BranchLinkProperties *)linkProperties { + // TODO: Add warnings if controlParams contains non-control params + NSMutableDictionary *temp = self.dictionary; + [temp addEntriesFromDictionary:[linkProperties.controlParams copy]]; + return temp; +} + +- (NSDictionary *)getDictionaryWithCompleteLinkProperties:(BranchLinkProperties *)linkProperties { + NSMutableDictionary *temp = [[self getParamsForServerRequestWithAddedLinkProperties:linkProperties] mutableCopy]; + + [self safeSetValue:linkProperties.tags forKey:[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_TAGS] onDict:temp]; + [self safeSetValue:linkProperties.feature forKey:[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_FEATURE] onDict:temp]; + [self safeSetValue:linkProperties.alias forKey:[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_ALIAS] onDict:temp]; + [self safeSetValue:linkProperties.channel forKey:[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_CHANNEL] onDict:temp]; + [self safeSetValue:linkProperties.stage forKey:[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_STAGE] onDict:temp]; + [self safeSetValue:linkProperties.campaign forKey:[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_CAMPAIGN] onDict:temp]; + [self safeSetValue:@(linkProperties.matchDuration) forKey:[NSString stringWithFormat:@"~%@", BRANCH_REQUEST_KEY_URL_DURATION] onDict:temp]; + + return [temp copy]; +} +- (void)safeSetValue:(NSObject *)value forKey:(NSString *)key onDict:(NSMutableDictionary *)dict { + if (value) { + dict[key] = value; + } +} + ++ (BranchUniversalObject*_Nonnull) objectWithDictionary:(NSDictionary*_Null_unspecified)dictionary { + BranchUniversalObject *object = [BranchUniversalObject new]; + + #define BNCFieldDefinesObjectFromDictionary + #include "BNCFieldDefines.h" + + addString(canonicalIdentifier, $canonical_identifier); + addString(canonicalUrl, $canonical_url); + addDate(creationDate, $creation_timestamp); + addDate(expirationDate, $exp_date); + addStringArray(keywords, $keywords); + addBoolean(locallyIndex, $locally_indexable); + addString(contentDescription, $og_description); + addString(imageUrl, $og_image_url); + addString(title, $og_title); + addBoolean(publiclyIndex, $publicly_indexable); + + #include "BNCFieldDefines.h" + + BranchContentMetadata *data = [BranchContentMetadata contentMetadataWithDictionary:dictionary]; + object.contentMetadata = data; + + NSSet *fieldsAdded = [NSSet setWithArray:@[ + @"$canonical_identifier", + @"$canonical_url", + @"$creation_timestamp", + @"$exp_date", + @"$keywords", + @"$locally_indexable", + @"$og_description", + @"$og_image_url", + @"$og_title", + @"$publicly_indexable", + @"$content_schema", + @"$quantity", + @"$price", + @"$currency", + @"$sku", + @"$product_name", + @"$product_brand", + @"$product_category", + @"$product_variant", + @"$condition", + @"$rating_average", + @"$rating_count", + @"$rating_max", + @"$rating", + @"$address_street", + @"$address_city", + @"$address_region", + @"$address_country", + @"$address_postal_code", + @"$latitude", + @"$longitude", + @"$image_captions", + @"$custom_fields", + ]]; + + // Add any extra fields to the content object.contentMetadata.customMetadata + + for (NSString* key in dictionary.keyEnumerator) { + if (![fieldsAdded containsObject:key]) { + object.contentMetadata.customMetadata[key] = dictionary[key]; + } + } + + return object; +} + +- (NSDictionary*_Nonnull) dictionary { + + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + NSDictionary *contentDictionary = [self.contentMetadata dictionary]; + if (contentDictionary.count) [dictionary addEntriesFromDictionary:contentDictionary]; + + #define BNCFieldDefinesDictionaryFromSelf + #include "BNCFieldDefines.h" + + addString(canonicalIdentifier, $canonical_identifier); + addString(canonicalUrl, $canonical_url); + addDate(creationDate, $creation_timestamp); + addDate(expirationDate, $exp_date); + addStringArray(keywords, $keywords); + addBoolean(locallyIndex, $locally_indexable); + addString(contentDescription, $og_description); + addString(imageUrl, $og_image_url); + addString(title, $og_title); + addBoolean(publiclyIndex, $publicly_indexable); + + #include "BNCFieldDefines.h" + + return dictionary; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchView.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchView.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchView.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchView.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchView.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchView.m new file mode 100644 index 00000000000..575ac595602 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchView.m @@ -0,0 +1,45 @@ +// +// BranchView.m +// Branch-TestBed +// +// Created by Sojan P.R. on 3/4/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import "BranchView.h" +#import "BNCPreferenceHelper.h" + +static NSInteger const BRANCH_VIEW_USAGE_UNLIMITED = -1; +static NSString * const BRANCH_VIEW_ID = @"id"; +static NSString * const BRANCH_VIEW_ACTION = @"action"; +static NSString * const BRANCH_VIEW_NUM_USE = @"number_of_use"; +static NSString * const BRANCH_VIEW_WEBURL = @"url"; +static NSString * const BRANCH_VIEW_WEBHTML = @"html"; + +@interface BranchView() +@end + +@implementation BranchView + +- (id)initWithBranchView:(NSDictionary *)branchViewDict andActionName:(NSString *)actionName { + if ((self = [super init])) { + self.branchViewAction = actionName; + self.branchViewID = [branchViewDict objectForKey:BRANCH_VIEW_ID]; + self.numOfUse = [[branchViewDict objectForKey:BRANCH_VIEW_NUM_USE] integerValue]; + self.webUrl = [branchViewDict objectForKey:BRANCH_VIEW_WEBURL]; + self.webHtml = [branchViewDict objectForKey:BRANCH_VIEW_WEBHTML]; + } + return self; +} + +- (BOOL)isAvailable { + NSInteger currentUsage = [[BNCPreferenceHelper preferenceHelper] getBranchViewCount:self.branchViewID]; + return ((self.numOfUse > currentUsage )|| (self.numOfUse == BRANCH_VIEW_USAGE_UNLIMITED)); +} + +- (void)updateUsageCount { + [[BNCPreferenceHelper preferenceHelper] updateBranchViewCount:self.branchViewID]; +} + +@end + diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchViewHandler.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchViewHandler.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BranchViewHandler.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchViewHandler.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchViewHandler.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchViewHandler.m new file mode 100644 index 00000000000..62e8e3b3c57 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/BranchViewHandler.m @@ -0,0 +1,151 @@ +// +// BranchViewHandler.m +// Branch-TestBed +// +// Created by Sojan P.R. on 3/3/16. +// Copyright © 2016 Branch Metrics. All rights reserved. +// + +#import "BranchViewHandler.h" +#import "Branch.h" +#import "BranchView.h" + +@interface BranchViewHandler() + +@property (nonatomic, strong) BranchView *pendingBranchView; +@property (nonatomic, strong) UIWebView *pendingWebview; + +@end + +static NSString * const BRANCH_VIEW_REDIRECT_SCHEME = @"branch-cta"; +static NSString * const BRANCH_VIEW_REDIRECT_ACTION_ACCEPT = @"accept"; +static NSString * const BRANCH_VIEW_REDIRECT_ACTION_CANCEL = @"cancel"; +static NSInteger const BRANCH_VIEW_ERR_TEMP_UNAVAILABLE = -202; + +@implementation BranchViewHandler + +static BranchViewHandler *branchViewHandler; +static BOOL isBranchViewAccepted = NO; +static NSString *currentActionName; +static NSString *currentBranchViewID; + ++ (BranchViewHandler *)getInstance { + if (!branchViewHandler) { + branchViewHandler = [[BranchViewHandler alloc] init]; + } + return branchViewHandler; +} + +- (BOOL)showBranchView:(NSString *)actionName withBranchViewDictionary:(NSDictionary*)branchViewDict andWithDelegate:(id)callback { + BranchView *branchView = [[BranchView alloc] initWithBranchView:branchViewDict andActionName:actionName]; + return [self showBranchView:branchView withDelegate:callback]; +} + +- (BOOL)showBranchView:(BranchView *)branchView withDelegate:(id)callback { + if ([branchView isAvailable]) { + self.branchViewCallback = callback; + [self showView:branchView]; + return YES; + } else { + return NO; + } +} + +- (void)showView:(BranchView *)branchView { + CGRect screenRect = [[UIScreen mainScreen] bounds]; + UIWebView *webview = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, screenRect.size.width, screenRect.size.height)]; + + webview.scrollView.scrollEnabled = NO; + webview.scrollView.bounces = NO; + webview.delegate = self; + + if (branchView.webHtml && ![branchView.webHtml isKindOfClass:[NSNull class]]) { + [webview loadHTMLString:branchView.webHtml baseURL:nil]; + } + else if (branchView.webUrl && ![branchView.webUrl isKindOfClass:[NSNull class]]) { + NSURL *url = [NSURL URLWithString:branchView.webUrl]; + NSURLRequest *requestObj = [NSURLRequest requestWithURL:url]; + [webview loadRequest:requestObj]; + } + else { + return; + } + + isBranchViewAccepted = NO; + currentActionName = branchView.branchViewAction; + currentBranchViewID = branchView.branchViewID; + + if (self.pendingBranchView == nil) { + self.pendingBranchView = branchView; + } + if (self.pendingWebview == nil) { + self.pendingWebview = webview; + } + // Now delay showing the webview until a successful load completes. +} + +- (void)closeBranchView { + UIViewController *presentingViewController = [UIViewController bnc_currentViewController]; + [presentingViewController dismissViewControllerAnimated:YES completion:nil]; + + if (self.branchViewCallback) { + if (isBranchViewAccepted) { + [self.branchViewCallback branchViewAccepted:currentActionName withID:currentBranchViewID]; + } + else { + [self.branchViewCallback branchViewCancelled:currentActionName withID:currentBranchViewID]; + } + } +} + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + BOOL isRedirectHandled = [self handleUserActionRedirects:request]; + if (isRedirectHandled) { + [self closeBranchView]; + } + return !isRedirectHandled; +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + NSCachedURLResponse *resp = [[NSURLCache sharedURLCache] cachedResponseForRequest:webView.request]; + NSInteger statusCode = [(NSHTTPURLResponse*)resp.response statusCode]; + if (statusCode == 200) { + if (self.pendingBranchView != nil && self.pendingWebview != nil) { + UIViewController *holderView = [[UIViewController alloc] init]; + [holderView.view insertSubview:self.pendingWebview atIndex:0]; + + UIViewController *presentingViewController = [UIViewController bnc_currentViewController]; + [presentingViewController presentViewController:holderView animated:YES completion:nil]; + + [self.pendingBranchView updateUsageCount]; + + if (self.branchViewCallback) { + [self.branchViewCallback branchViewVisible:self.pendingBranchView.branchViewAction withID:self.pendingBranchView.branchViewID]; + } + } + } else { + if (self.branchViewCallback) { + NSString *message = [NSString stringWithFormat:@"%ld: %@", (long)statusCode, [NSHTTPURLResponse localizedStringForStatusCode:statusCode]]; + [self.branchViewCallback branchViewErrorCode:BRANCH_VIEW_ERR_TEMP_UNAVAILABLE message:message actionName:self.pendingBranchView.branchViewAction withID:self.pendingBranchView.branchViewID]; + } + } + self.pendingBranchView = nil; + self.pendingWebview = nil; +} + +- (BOOL)handleUserActionRedirects:(NSURLRequest *)request { + BOOL isRedirectionHandled = NO; + if ([[request.URL scheme] isEqualToString:BRANCH_VIEW_REDIRECT_SCHEME]) { + if ([[request.URL host] isEqualToString:BRANCH_VIEW_REDIRECT_ACTION_ACCEPT]) { + isBranchViewAccepted = YES; + } + else if ([[request.URL host] isEqualToString:BRANCH_VIEW_REDIRECT_ACTION_CANCEL]) { + isBranchViewAccepted = NO; + } + isRedirectionHandled = YES; + } + return isRedirectionHandled; +} + + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSMutableDictionary+Branch.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSMutableDictionary+Branch.h new file mode 100644 index 00000000000..6a0feef1e60 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSMutableDictionary+Branch.h @@ -0,0 +1,22 @@ +// +// NSMutableDictionary+Branch.h +// Branch +// +// Created by Edward Smith on 1/11/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +void BNCForceNSMutableDictionaryCategoryToLoad(void) __attribute__((constructor)); + +@interface NSMutableDictionary (Branch) + +- (void) bnc_safeSetObject:(id)anObject forKey:(id)aKey; +- (void) bnc_safeAddEntriesFromDictionary:(NSDictionary,id> *)otherDictionary; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSMutableDictionary+Branch.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSMutableDictionary+Branch.m new file mode 100644 index 00000000000..9815a267b1a --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSMutableDictionary+Branch.m @@ -0,0 +1,36 @@ +// +// NSMutableDictionary+Branch.m +// Branch +// +// Created by Edward Smith on 1/11/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + + +#import "NSMutableDictionary+Branch.h" + + +@implementation NSMutableDictionary (Branch) + +- (void) bnc_safeSetObject:(id)anObject forKey:(id)aKey { + if (anObject && aKey) { + [self setObject:anObject forKey:aKey]; + } +} + +- (void) bnc_safeAddEntriesFromDictionary:(NSDictionary,id> *)otherDictionary { + if ([otherDictionary isKindOfClass:[NSDictionary class]]) { + NSDictionary *deepCopy = + [[NSDictionary alloc] + initWithDictionary:otherDictionary + copyItems:YES]; + [self addEntriesFromDictionary:deepCopy]; + } +} + +@end + + +__attribute__((constructor)) void BNCForceNSMutableDictionaryCategoryToLoad(void) { + // Does nothing. But will force the linker to include this category. +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSString+Branch.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSString+Branch.h new file mode 100644 index 00000000000..bac2681ee61 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSString+Branch.h @@ -0,0 +1,35 @@ +/** + @file NSString+Branch.h + @package Branch-SDK + @brief NSString Additions + + @author Edward Smith + @date February 2017 + @copyright Copyright © 2017 Branch. All rights reserved. +*/ + +#if __has_feature(modules) +@import Foundation; +#else +#import +#endif + +@interface NSString (Branch) + +///@discussion Compares the receiver to a masked string. Masked characters (the '*' character) are +/// ignored for purposes of the compare. +/// +///@return YES if string (ignoring any masked characters) is equal to the receiver. +- (BOOL) bnc_isEqualToMaskedString:(NSString*_Nullable)string; + +///@return Returns a string that is truncated at the first null character. +- (NSString*_Nonnull) bnc_stringTruncatedAtNull; + +///@discusion The `containsString:` method isn't supported pre-iOS 8. Here we roll our own. +// +///@param string The string to for comparison. +///@return Reurns true if the instance contains the string. +- (BOOL) bnc_containsString:(NSString*_Nullable)string; +@end + +void BNCForceNSStringCategoryToLoad(void) __attribute__((constructor)); diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSString+Branch.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSString+Branch.m new file mode 100644 index 00000000000..11e0d4be8dc --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/NSString+Branch.m @@ -0,0 +1,45 @@ +/** + @file NSString+Branch.m + @package Branch-SDK + @brief NSString Additions + + @author Edward Smith + @date February 2017 + @copyright Copyright © 2017 Branch. All rights reserved. +*/ + +#import "NSString+Branch.h" + +__attribute__((constructor)) void BNCForceNSStringCategoryToLoad() { + // Nothing here, but forces linker to load the category. +} + +@implementation NSString (Branch) + +- (BOOL) bnc_isEqualToMaskedString:(NSString*_Nullable)string { + // Un-comment for debugging: + // NSLog(@"bnc_isEqualToMaskedString self/string:\n%@\n%@.", self, string); + if (!string) return NO; + if (self.length != string.length) return NO; + for (NSUInteger idx = 0; idx < self.length; idx++) { + unichar p = [self characterAtIndex:idx]; + unichar q = [string characterAtIndex:idx]; + if (q != '*' && p != q) return NO; + } + return YES; +} + +- (NSString*_Nonnull) bnc_stringTruncatedAtNull { + NSRange range = [self rangeOfString:@"\0"]; + if (range.location == NSNotFound) + return self; + range.length = range.location; + range.location = 0; + return [self substringWithRange:range]; +} + +- (BOOL) bnc_containsString:(NSString*_Nullable)string { + return (string && [self rangeOfString:(NSString*_Nonnull)string].location != NSNotFound); +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCNetworkService.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCNetworkService.h new file mode 100644 index 00000000000..17e6556791c --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCNetworkService.h @@ -0,0 +1,46 @@ +// +// BNCNetworkService.h +// Branch-SDK +// +// Created by Edward Smith on 5/30/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#import "BNCNetworkServiceProtocol.h" + +/** + BNCNetworkService and BNCNetworkOperation + + The BNCNetworkService and BNCNetworkOperation classes are concrete implementations of the + BNCNetworkServiceProtocol and BNCNetworkOperationProtocol protocols. +*/ + +#pragma mark BNCNetworkOperation + +@interface BNCNetworkOperation : NSObject +@property (readonly, copy) NSURLRequest *request; +@property (readonly, copy) NSHTTPURLResponse *response; +@property (readonly, strong) NSData *responseData; +@property (readonly, copy) NSError *error; +@property (readonly, copy) NSDate *startDate; +@property (readonly, copy) NSDate *timeoutDate; +@property (strong) NSDictionary *userInfo; + +- (void) start; +- (void) cancel; +@end + +#pragma mark - BNCNetworkService + +@interface BNCNetworkService : NSObject ++ (instancetype) new; + +- (void) cancelAllOperations; + +- (BNCNetworkOperation*) networkOperationWithURLRequest:(NSMutableURLRequest*)request + completion:(void (^)(idoperation))completion; + +- (NSError*) pinSessionToPublicSecKeyRefs:(NSArray/***/*)publicKeys; + +@property (strong) NSDictionary *userInfo; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCNetworkService.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCNetworkService.m new file mode 100644 index 00000000000..17f91896d26 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCNetworkService.m @@ -0,0 +1,309 @@ +// +// BNCNetworkService.m +// Branch-SDK +// +// Created by Edward Smith on 5/30/17. +// Copyright © 2017 Branch Metrics. All rights reserved. +// + +#import "BNCNetworkService.h" +#import "BNCEncodingUtils.h" +#import "BNCLog.h" +#import "BNCDebug.h" +#import "BNCError.h" +#import "BNCConfig.h" + +#pragma mark BNCNetworkOperation + +@interface BNCNetworkOperation () +@property (copy) NSURLRequest *request; +@property (copy) NSHTTPURLResponse *response; +@property (strong) NSData *responseData; +@property (copy) NSError *error; +@property (copy) NSDate *startDate; +@property (copy) NSDate *timeoutDate; +@property (strong) BNCNetworkService *networkService; +@property (strong) NSURLSessionTask *sessionTask; +@property (copy) void (^completionBlock)(BNCNetworkOperation*operation); +@end + +#pragma mark - BNCNetworkService + +@interface BNCNetworkService () { + NSURLSession *_session; + NSTimeInterval _defaultTimeoutInterval; + NSInteger _maximumConcurrentOperations; + NSMutableArray *_pinnedPublicKeys; +} + +- (void) startOperation:(BNCNetworkOperation*)operation; + +@property (strong, atomic, readonly) NSURLSession *session; +@property (strong, atomic) NSOperationQueue *sessionQueue; +@end + +#pragma mark - BNCNetworkOperation + +@implementation BNCNetworkOperation + +- (void) start { + [self.networkService startOperation:self]; +} + +- (void) cancel { + [self.sessionTask cancel]; +} + +- (NSString*) stringFromResponseData { + NSString *string = nil; + if ([self.responseData isKindOfClass:[NSData class]]) { + string = [[NSString alloc] initWithData:(NSData*)self.responseData encoding:NSUTF8StringEncoding]; + } + if (!string && [self.responseData isKindOfClass:[NSData class]]) { + string = [NSString stringWithFormat:@"", + (long)[(NSData*)self.responseData length]]; + } + if (!string) { + string = self.responseData.description; + } + return string; +} + +@end + +#pragma mark - BNCNetworkService + +@implementation BNCNetworkService + ++ (instancetype) new { + return [[self alloc] init]; +} + +- (instancetype) init { + self = [super init]; + if (!self) return self; + _defaultTimeoutInterval = 15.0; + _maximumConcurrentOperations = 3; + return self; +} + +#pragma mark - Getters & Setters + +- (NSError*) pinSessionToPublicSecKeyRefs:(NSArray/***/*)publicKeys { + @synchronized (self) { + _pinnedPublicKeys = [NSMutableArray array]; + for (id secKey in publicKeys) { + if (CFGetTypeID((SecKeyRef)secKey) == SecKeyGetTypeID()) + [_pinnedPublicKeys addObject:secKey]; + else { + return [NSError branchErrorWithCode:BNCInvalidNetworkPublicKeyError]; + } + } + return nil; + } +} + +- (NSArray*) pinnedPublicKeys { + @synchronized (self) { + return _pinnedPublicKeys; + } +} + +- (void) setDefaultTimeoutInterval:(NSTimeInterval)defaultTimeoutInterval { + @synchronized (self) { + _defaultTimeoutInterval = MAX(defaultTimeoutInterval, 0.0); + } +} + +- (NSTimeInterval) defaultTimeoutInterval { + @synchronized (self) { + return _defaultTimeoutInterval; + } +} + +- (void) setMaximumConcurrentOperations:(NSInteger)maximumConcurrentOperations { + @synchronized (self) { + _maximumConcurrentOperations = MAX(maximumConcurrentOperations, 0); + self.sessionQueue.maxConcurrentOperationCount = _maximumConcurrentOperations; + } +} + +- (NSInteger) maximumConcurrentOperations { + @synchronized (self) { + return _maximumConcurrentOperations; + } +} + +- (NSURLSession*) session { + @synchronized (self) { + if (_session) return _session; + + NSURLSessionConfiguration *configuration = + [NSURLSessionConfiguration defaultSessionConfiguration]; + configuration.timeoutIntervalForRequest = self.defaultTimeoutInterval; + configuration.timeoutIntervalForResource = self.defaultTimeoutInterval; + configuration.URLCache = nil; + + self.sessionQueue = [NSOperationQueue new]; + self.sessionQueue.name = @"io.branch.sdk.network.queue"; + self.sessionQueue.maxConcurrentOperationCount = self.maximumConcurrentOperations; + if ([self.sessionQueue respondsToSelector:@selector(setQualityOfService:)]) { + // qualityOfService is iOS 8 and above. + self.sessionQueue.qualityOfService = NSQualityOfServiceUserInteractive; + } + + _session = + [NSURLSession sessionWithConfiguration:configuration + delegate:self + delegateQueue:self.sessionQueue]; + _session.sessionDescription = @"io.branch.sdk.network.session"; + + return _session; + } +} + +- (void) setSuspendOperations:(BOOL)suspendOperations { + self.sessionQueue.suspended = suspendOperations; +} + +- (BOOL) operationsAreSuspended { + return self.sessionQueue.isSuspended; +} + +#pragma mark - Operations + +- (BNCNetworkOperation*) networkOperationWithURLRequest:(NSMutableURLRequest*)request + completion:(void (^)(idoperation))completion { + + BNCNetworkOperation *operation = [BNCNetworkOperation new]; + if (![request isKindOfClass:[NSMutableURLRequest class]]) { + BNCLogError(@"A `NSMutableURLRequest` request parameter was expected."); + return nil; + } + operation.request = request; + operation.networkService = self; + operation.completionBlock = completion; + return operation; +} + +- (void) startOperation:(BNCNetworkOperation*)operation { + operation.networkService = self; + if (!operation.startDate) { + operation.startDate = [NSDate date]; + } + if (!operation.timeoutDate) { + NSTimeInterval timeoutInterval = operation.request.timeoutInterval; + if (timeoutInterval < 0.0) + timeoutInterval = self.defaultTimeoutInterval; + operation.timeoutDate = + [[operation startDate] dateByAddingTimeInterval:timeoutInterval]; + } + if ([operation.request isKindOfClass:[NSMutableURLRequest class]]) { + ((NSMutableURLRequest*)operation.request).timeoutInterval = + [operation.timeoutDate timeIntervalSinceDate:[NSDate date]]; + } else { + BNCLogError(@"SDK logic error. Expected mutable request in `start` method."); + } + operation.sessionTask = + [self.session dataTaskWithRequest:operation.request + completionHandler: + ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + operation.responseData = data; + operation.response = (NSHTTPURLResponse*) response; + operation.error = error; + if (operation.response.statusCode == 404) { + /* Don't print 404 messages because they look like an error. + BNCLogDebugSDK(@"Network finish operation %@ %1.3fs. Status %ld.", + operation.request.URL.absoluteString, + [[NSDate date] timeIntervalSinceDate:operation.startDate], + (long)operation.response.statusCode); + */ + } else { + BNCLogDebug(@"Network finish operation %@ %1.3fs. Status %ld error %@.\n%@.", + operation.request.URL.absoluteString, + [[NSDate date] timeIntervalSinceDate:operation.startDate], + (long)operation.response.statusCode, + operation.error, + operation.stringFromResponseData); + } + if (operation.completionBlock) + operation.completionBlock(operation); + }]; + BNCLogDebug(@"Network start operation %@.", operation.request.URL); + [operation.sessionTask resume]; +} + +- (void) cancelAllOperations { + @synchronized (self) { + [self.session invalidateAndCancel]; + _session = nil; + } +} + +- (void) URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task +didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge + completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { + + BOOL trusted = NO; + SecTrustResultType trustResult = 0; + OSStatus err = 0; + NSArray *localPinnedKeys = self.pinnedPublicKeys; // Keep a local copy in case it mutates. + + // Release these: + SecKeyRef key = nil; + SecPolicyRef hostPolicy = nil; + + // Get remote certificate + SecTrustRef serverTrust = challenge.protectionSpace.serverTrust; + @synchronized ((__bridge id)serverTrust) { + + // Set SSL policies for domain name check + hostPolicy = SecPolicyCreateSSL(true, (__bridge CFStringRef)challenge.protectionSpace.host); + if (!hostPolicy) goto exit; + SecTrustSetPolicies(serverTrust, (__bridge CFTypeRef _Nonnull)(@[ (__bridge id)hostPolicy ])); + + // Evaluate server certificate + SecTrustEvaluate(serverTrust, &trustResult); + if (! (trustResult == kSecTrustResultUnspecified || trustResult == kSecTrustResultProceed)) { + goto exit; + } + + if (localPinnedKeys.count == 0) { + trusted = YES; + goto exit; + } + + key = SecTrustCopyPublicKey(serverTrust); + if (!key) goto exit; + } + + for (id pinnedKey in localPinnedKeys) { + if ([pinnedKey isEqual:(__bridge id)key]) { + trusted = YES; + goto exit; + } + } + + if (!BNC_API_PINNED) { + trusted = YES; + goto exit; + } + +exit: + if (err) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil]; + BNCLogError(@"Error while validating cert: %@.", error); + } + if (key) CFRelease(key); + if (hostPolicy) CFRelease(hostPolicy); + + if (trusted) { + NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust]; + completionHandler(NSURLSessionAuthChallengeUseCredential, credential); + } else { + completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL); + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCNetworkServiceProtocol.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCNetworkServiceProtocol.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCNetworkServiceProtocol.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCNetworkServiceProtocol.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerInterface.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerInterface.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerInterface.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerInterface.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerInterface.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerInterface.m new file mode 100644 index 00000000000..fd7829181eb --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerInterface.m @@ -0,0 +1,876 @@ +// +// BNCServerInterface.m +// Branch-SDK +// +// Created by Alex Austin on 6/6/14. +// Copyright (c) 2014 Branch Metrics. All rights reserved. +// + +#import "BNCServerInterface.h" +#import "BNCConfig.h" +#import "BNCEncodingUtils.h" +#import "BNCError.h" +#import "BranchConstants.h" +#import "BNCDeviceInfo.h" +#import "NSMutableDictionary+Branch.h" +#import "BNCLog.h" +#import "Branch.h" +#import "BNCLocalization.h" +#import "NSString+Branch.h" + +@interface BNCServerInterface () +@property (strong) NSString *requestEndpoint; +@property (strong) id networkService; +@end + +@implementation BNCServerInterface + +- (instancetype) init { + self = [super init]; + if (!self) return self; + + self.networkService = [[Branch networkServiceClass] new]; + if ([self.networkService respondsToSelector:@selector(pinSessionToPublicSecKeyRefs:)]) { + NSError *error = [self.networkService pinSessionToPublicSecKeyRefs:self.class.publicSecKeyRefs]; + if (error) { + BNCLogError(@"Certificate pinning failed. Continuing without pinning: %@.", error); + } + } + + return self; +} + +- (void) dealloc { + [self.networkService cancelAllOperations]; + self.networkService = nil; +} + ++ (NSArray/***/*) publicSecKeyRefs { + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wobjc-string-concatenation" + + NSArray *hexKeys = @[ + // star_branch_io.pub.p12 + @"30821d9102010330821d5706092a864886f70d010701a0821d4804821d44" + "30821d40308217f706092a864886f70d010706a08217e8308217e4020100" + "308217dd06092a864886f70d010701301c060a2a864886f70d010c010630" + "0e04081719fadd81059fec02020800808217b0b1d82a5e9c29eb4cb6fa3b" + "21e38c81d4343136db6b5961e8f58ac7d6a6a8687f1fa999743eaf23d916" + "e59c53457bcb7dd573d741fdac9c73142f76adbd1032ec5fea5145b823fe" + "74a13a6318cb404f00230bab846d1bd6f179f6667d29ad93001cfe4933ba" + "5ed90d0456ab1ef9ebb7bfb456cc5c58bec9c9cbf319911cc4733731e3a6" + "59afee427166f77ddb354c00d23fb073b22e8649dae598edfad04d58b248" + "803e6e4c8036bfee5884bd0f6a4052918ea748a7a281d019723b071ea799" + "c5748a0cd9b71b773bd38dd07c98ee8ab7e8ac3f6de898747db3e4691b5a" + "2315e17595eeee61ed95198d33fd738e8beeca09e4dbadb92b1431e5496e" + "a5a38ae64a147b2efba4b418e77f52fa38ba64f263b4e4942e0e713b9f4a" + "330b339a021f262fecb400115f3162da2849c53dd226498f781f57768d3f" + "d08cbe7e791b093515ec8154a83fad46a346f28a2e1283810491d2e8b98e" + "ac0a6622c65ddb96c3dea602d4b052898baa93d24e63d52fcc66bc8b7ab8" + "252d4bb2ee7923dda59b944c3dacd91e787786bf120cae120e7c20fedac2" + "521344f436c61f7570ac79b5862ed28e9e90186c3f350b26a15e2358efa0" + "7d73366f509646c3b5aaddeac19c1777376fb48c976d3ad0884e13619301" + "e196dbc249841c9e563e879287ae781e430711a3fd706ce3fe68897ab26c" + "3e26e71d4abf22cae06cc07e2dc3d8e3ea6f90aa926a0cb5becd93d93aa0" + "df9373d74de67b4d4242b27e5fc95506396d6e33ffb89ded52b52fd72532" + "0cda26ab555730c21fc12902204ea52670f704f90df71c46eeaea4fed60a" + "0191069274725fa072d1666965372679b5023a429a11ccf653a429f63ae4" + "512d6f83db14848f192fd8aa2c20853f5527542e51d480d951cddbc95c4c" + "863f277180170e6ef9752ad516297b42ecf70bc4e98bd1e68ee8b04b440c" + "f22cd6b7b8a3486abbca1a900b208029ff2b14c9a8ba4c4754b8d70a42b1" + "4ff1c6782084bf74f231a762270f05f62a6680f914b62007dfe0dd73f3e2" + "5f22cdf32c03fb8ec55de51b9bb7d1989813c746d9152e0391d6643985f9" + "cb5349c1f6acf219095e959ce8a91c5de8fc7d521410328e2d7b280e761b" + "1a58b1d3774b1e987f297a31058829813838d904d45d8497cf788ab0c916" + "6f8f30a97e7b8594e0779da590d58f5b7cc8fb75a055e774efb55b876608" + "f5b125ac01dfa410cde94bdd9bda54195326fd1f9d007e4e074d51bb5fbf" + "f25783e970e885f0e4a5270f4efbed66b3366a2f41636a21347c56539731" + "d5259bcddae780da9cfa873592347f18475ec417ec32ec1fe3ff36ff1aad" + "8fcd308470b152daee41bffa0123f106e17738eec27b47107a63b4b0757f" + "ab2a9be6861e2b603f5a4d2eaeedfa263ada57b559bf03f36f22e2912930" + "d8880de0a65b0f20d54c34338f5da2dbe88b092365eeff971179f3ff4a2b" + "6da7daff59cfcd0e3c9ddf75edb0a7b2c6b13b39dc7a06ea81ac9a728d06" + "67904bdb976905d4f331f6498e89ce2dc5e4f63d2285eff62d0e38f5226d" + "aeb66649791b5a780edc909f6b85ae5daf528c4adbfb7cbaaefb0ac1ef36" + "af27a7e54d5b6cbc31c9bd2d7340eb468139c1d307a5a0f9c5ee04970191" + "cd825e692802afe607b56511201b11242d50d1ca0822aa1a64ea4db13f83" + "d86340962638f0dbece4620e71d842f96fc0ad43007afac512406d68b333" + "87c6b1fb4b251d1f2b40493d1ffeb3fe81f5528d556507c5953a0eccb063" + "455cb3fcc1dee1a2d70ea3200de8cff1113f8a645ab9c3a9a0e32dee830b" + "c6b0436678a3ccdf9d359d28f4df20e6d30715211ab9f0ece94250f19c0e" + "4f77c1320697ba2beb3d2719e75c3e1c21f4c20fe35bdc004d270ee92e8b" + "63820fed3e27529081e39f9ec032dba216f5fd5506d086af0793aa574de2" + "ba7c5b1ef6f8fcfc977b0fa7380c33b631d79e9c2f65e6b8cfd4bc298c6f" + "6df9edd84220602b60e77c0ef3503c44021487e9e06806021000020c7c27" + "5a440244a4b46bec3f290bd6fb7406872ff969f0f5a8f60080c69937d084" + "4ba8721b14071de4dced03562441eec7b5f251298b318a19c31bce01947b" + "672e59b6fcdd366f11f039d0ca29b34ad2196ee108872644c7c2b36c0b1c" + "953183fb948873df04c29c06cd8d0d88412bdf09702473ddf4acf6cf8b6b" + "d360033aed2cba9e6fd9a8e092ee89655e185865173c84eeaf44d6f91459" + "17fb09acf1c7523f02e008222cdb6c92b4ef1aa083125738a86f1c6a6bed" + "c7c62cb1f862450e051e520aca4ea6dc43001d1f9f7dfb4063c4a3cbecef" + "a7cc289b6ff9236a724775ab9070ace955bef51f53a1c3bedd4991dec85e" + "d4ae058115069c4f8dffd9e0c5a7c2324da41b4cef05bd042f76ed7a552e" + "c8cc6a437e84a422607f2192b916638b0359c096fb92d2cb636e5be89538" + "5789e9ec05eaf279e5735cae74d3169294eff1283d0789046c6f04b169e0" + "be04705ae8254c0d1cc568232ebcc2243b65c86fc77e145612569565a045" + "ced0ec5a0d8b5c20f62c53925ee7beb499466a74d52641676fb12f8c005f" + "2458b9b5652dc4b8d56ac427c6d06e8059b6a09c1b39547ab033f2888fe8" + "a62a5fc57cda0fa660480d93772d35cef1e5541d562ec6aa72717bdc8eb0" + "86d77aa3700068159b67f4c2f8a5f4318aa46ce7180f1dedf54fd0ee2e08" + "954465c07d2f717637dbe55dce9f7e9b98b957c25ad9ebd7aef7f0af32b9" + "c03e952d4a926bf70800cd3b14ead5a242f8a753fdd126ecd842011f812d" + "ba1c1c2c59d96fe94494b24e5ba18ad86358a12cf805c2e9646d16b47197" + "5910ea78c0527caf49f4b3093fd5b86374b3e6e5e90597d33d74b8f29d7e" + "8e230df4e19a2dc8da85ebb8810f6ae3dc1b1e9cbe78743f5a02b9b21991" + "a71e624b5c882f8720e89b0ae8e3da2d7f7111bf5a0ae38ca8c365bfe014" + "a6a8a8b6547bab923d44bea26e03f42e741e4c2098a0b5229c13baac3c52" + "f6d20cff7174d153216f48874a0b37ad9667cb467a8e5954840133ea9b49" + "ca102a739c81d7b3cf07da4bdfd83590253d72b0f23b253876b90476978f" + "b7965b2eba9e2af849aabe496aef5e1253ef28a848b7b57625b5b0820698" + "143cf886f762fc2510ae2746a73df0b6bd7710b0b2f9a2972c22898885d6" + "dc3d10dfd726e63a2100615322a100549d9cbcbd1e9527af58624a42eb44" + "6522b1173ddead0045af72ca22740352856a07bf7d92a1e6c3720ce6cf28" + "dd44dfb391457d526e141e23932876759d130bcc593db05e1fae1e7a4480" + "6f03c31bbfd4940c23f3ed8171656c2dde108630e62ff6387b7787bf3b84" + "bf625f7016cf2a460313367843db50ce08c99cc882d37e6f16720d3959d8" + "4d9eb14a3509fccbef187647e52c1652a2963ba2af0e2dda4a821fc07876" + "dba979e31293622f696354eba78a2a13fbd9d0145e475e3344a016a1cbda" + "a333d6012695a23e9fffd9fb08467aee23d367daf78350137daa8101bdc9" + "46bf23f9129d32fe59d79dee8c35b2c710cd5e8144ec5df0582b0d1d73ee" + "52be0ed58fd2d9ca5d3641493d42f267214c9b97c7abcf59536761df604e" + "43a12c3bc272656f5ee3501f27ce40f1021c1bf02b500bd5b122c98a04b0" + "b3fd5bd7987b3e5112a9180df6d9612d8225d3e2bc921d70be3f2f6e873a" + "0dc3fc08ba2f5cfed2d2b8d497ee508cb6e78667c99ccb1ff2f601015184" + "b5520439b560cddccd590d7a95509cf5bdee6536838d4d512e9be335ae06" + "cb822db11ed4b3e9a644a19b229a41efce4818f1c9b2b2d419d66be2d6dc" + "d04d33debbdd48cf7498c60aacb00586bafca798e135688346c418db8fbd" + "9411bd4348dc790d0d31e3bc073dbe7302375b0485ae3d0e341a82dc2847" + "1bf1285605d6d7ed03f4373acd18ffca8f561c828899bfd3d333f21c488f" + "de42eb7b9b635edbfe8a5910dabed8b75783bb994fd1c44b383f6bc0ecc7" + "d055870092f27f6c15e5b0e3768a4b3204038bb2343bf2ba2888e7b9d25a" + "530886799e22f3cab9102566c097c57dfa078c944da6ea365305b3c603ac" + "f4acbb3d43fb2886ce3ba872be262978d7a3c2a3de7b3be26b35e521ad07" + "d3c2acab616c7672bea5a26a5992a23507f6afaa1151c57209956e8d3c11" + "a0d13fcc78f97b080ea1a858170550c00fc5a2c9b3b6b99b971a6e53fd59" + "32ff36141934cec6b92b6cc36a3de68ce23214718d345198bd46c6f7bc89" + "397b485149a2ced3c5939baa614e694090bbd4941909406daf213eaeb5c2" + "a152abc1e836219372c3655ab180f945ddd57683ce20b052718b9bb09328" + "c50d3b9e9839aaba310a7131a112585dc25875cc1ef43f54e102f3b679f8" + "51136de5636dca53a8b320a7fd83c463632d649fc0cdb08e51474b316949" + "b6bfecdec4f98a9ef607ec70ec7a0d320759da6625487bf64e3f031c9a14" + "5f9e7553fd44cda1082ff319c495d3da39d24a3aa8d4799740c6a71c00a4" + "5aacd20fddeeb4e25ed79f56295e90d7a6ffe85f817f1396f2ad3084b568" + "bec8e47305c0ceb4f650efe84ddd1746fd8515c67fc8f9441231943578f1" + "0d9309f0470fd88ed5766513b61864c543dbcbd5f503513fad6891bdde3b" + "d8cde30c390ebc1753f9fb6146132b096da9eea9d910d715b0abc9b9b8a2" + "225d4d1fbfdf212689f3408043ada48c7a8f3c957e5893a095f1eb616ef8" + "8a4f4d2286b9d6ee16e6292bf63c1af4689262dfeb7b1ad5f800a25fe962" + "08109fdbe1b9d045cb4eb531161f259f7d0ee05cfb44d4ed28215368c1d2" + "f8cdc31516424f766671185eb375171aa4723cd7f2a5156d3737e92a3703" + "b7560b9d53bd97de47a2b83f5b2f877a1febe759b50d37a2616700f0e043" + "b07229982c5fd4a2b89a023751bb6c926fefa56a14dad8878c3c6123106c" + "482a93de2ad34e162d092e24243a46286f17b32729303b0e3daed29cc4a5" + "b0a458f575e6063d67d8bb9fafe1fa1fb91fcb7b14c3f3015ddce212b586" + "0cef6bf27d833a273b09f25148e264bfa2491de08bf52f8eb9f3c8f140f7" + "447dfa3a2c38c06615cc83358559ea3f370982c7eeb5221720aa3ed1bb1e" + "8ea5ea2a9db403b1bb16c0d245f9c0e0d4bd41722bcc3a94b1f51faa16e0" + "d09841a5e0c6627ed567fe3657cb3b32141ee399ad7e9f11d090fca04172" + "7c3e9a3ee8948f8f10e84127781c2c9bed4f616f1810f3dfaf4bc89e564b" + "6a9f017bad8bf2a3852de3defd5c9ff92c1442081ca08e61662ede9e9389" + "b2a981662f3a4efe6f40fbb323de7df7df8713f0c27b2a8eb51afff09336" + "792bb6cf0bb19237b1b92ed03e52b9dc8528c197b703a2c792db7023403b" + "b6e700f0e0bf02634297efeb06bf9397e9283b0b37919ffef68ae54486a4" + "11e7cbc9b76d2cb30f9f873da480088a4e3aeda59996aedd98fe18f62274" + "6f01038ed9faaf0fbbc0889efbad86afc401c22be2289a2979d161cb1618" + "380dd61dc122837eec977de875a49099dfe8208b6608ed67973192f3c53c" + "fa58096c79568aae7d114454a6ef9f5d14697d6d59fde24f14ea694bb7ea" + "2ed4fbf1b16aa3ed11bd5d3d22c75ef51574d2fa4d1f0e34a919ef83b82d" + "99cfc010fb60a8d861adabe66c171ac2d2adb5f0f349ae7cb40a7affa18a" + "f72ea0cea6e1c2cfdb4f955618a0ffc57e1a2e896a39d83fc9fbe69eb349" + "a57d1fe312697542cf9192ea2021b0f9872894b87198af516fc235397112" + "5d827cf8444f432b917719d18c79b6a941ab5e243b4b02e77a67d3b23f4b" + "85d0dacff367d363a3c2c19d6a82125a54bdbaffff61e23e399c967115ba" + "5b4ec265f0575bb847635d9e0a17385c612916f401b1321da3086d33cac8" + "a09c4602acbd06e8d049bdac08dd3404ce011815d160770401f293dbd1b0" + "a713110829ec0b5318ae2e19a5a613ba8db7c06e6231f9349f9ffd0e6a5b" + "8025f26e6057d319d913a680245a541ddce8a97037d646f80ee1b1ae4597" + "10738db07ff58e7f06aaca03426d9d6f8dec2f22b60a2f71804ce77b3bd9" + "923fd6d8ee4bdf7fde3bcb9661c409d925ddcb92e6f0ff1f739900dd1316" + "f2dc2ac75fab028a4db87039f208c7b5d77b8a76f6aec96b38edeb3ea8ef" + "089b6050eeaf964892ec29a2c592c23dec14c619bb573bf5edebb41d257b" + "b6f527f852e83cfa5c0c0aef99f587306cb511689ea1b0f0e2ce1fb598b7" + "3e6866d495ad30b7ae771e4724f06e4f50d3aadab7bcbcb87cb45e9cc1b8" + "ce95ada91a48bba07902c24638135e18000c41aa5c2bcd0df7af19d5ee92" + "ba923f55d2bcb31edffe33c2ec8b5149eb5317d759e9f3084c47bf64eee7" + "921b226c7e223b668bf10f19bffc49c962aaa04ec561532469fb8f1929e9" + "f9f1bc2b9dfd0989d13a1f329016c865b489b754a60c88e558ba7f8bd71f" + "e796d6f378ee874ba37351630efcf5a4715e9107b4a93afe4d543a570bf8" + "659a31e5aa47b1caa6bce24d4797e575335f5bcd7fa7b94ebaa9f80852b4" + "c1a8e41d41903b087df44c349463e261c00944fdd07af6df9dc9aa077b42" + "7084b8069626a8b455ff54693f220c054de6c1c47c11580bacedc0a1aef0" + "e49badd0e2bb421567364f535264f0e14ea22676aff0e48b3143388c22e6" + "ccb077578f6e4686d93c9d29b91f2aa476b94cac609d9878bd8cbc51e752" + "4555acc8cbce0da2dcb26ed222ce218b2f01e25faa41b0af9fd93ce5e07e" + "5655a1e4af07f4c549800276d3276abbe48693430240059698f7b2f24a33" + "bc4df087ce12c6487ef27a3378a70327c81ba091b981003341847312f6b6" + "4ae58ea9d83f013de5a592e211fe226f229772ac35741b8bf3297aebcf8c" + "d512e2de1a79e89d3125273c875c124e4b6bae0cba2419a3e1198a505fca" + "68a60f813875b0f2322719f6ca75f8cec0e25456858619d58db1e431062e" + "ae60cccf809d213be77f1e54fa0adfb84682353a77b6e99fcdf172e13397" + "b07a1a235a506a926d3dac91bbd4f900716036c99d54108f368221745759" + "6eccefd3b06614585330e520d044ce3368805ba180ea145a4a10c35a2962" + "b927cd7d422b3a4d86a2bced9263fbd9c20f65a83f1435086524de172035" + "c4c115ee04bf9b5e5d5e07179058ce99c0c6ea2dca1182872f97488cc5bf" + "182173fe478ec7bde8bd57ce02dadd8441ecf3c0220a2e36337ec44a3bf3" + "b5e95eb35f2a1cc56ccd063a10bf1da59c55be2059220b3b5e1de9503353" + "d8a0bbaca99c0ad8271d359bab02b228411b99f588717e9ee0fd0ca645ad" + "20f4e009c01d136598daec59435728806481f9bd60f64ef320f00450a884" + "e5f9980446245ca692168ba73efd3dff64cb5d4a23dec2a4c4b4d11665bf" + "cd0c3792c1d642403c7ae678bacde4e7051b3825f5f7ea6858231d70fa5a" + "e7da4ad7ce2741a674c90f470493f22031f6ed64cfdb5989b346561bd557" + "2d12e1fcdaf7cb74064e6f3946167ac0ed35d879bc599a314245507d3026" + "57ec98d45b2c3267404b1e734af4a7c0cc531910222a75a54d34ad56e4d9" + "106b307b682166e933152f1dd60f0ffdd6c339d4f8dc7ea0df87bec294c0" + "08ceb8827064a4c42fc3b03a86fb787df25773ec0fcf30156149fe43c5e9" + "015d58933e885bf6c99089ba4ddd253a52c3853e1895df6fa8f737a04854" + "45f8f1e2fdf9de2f2093c258519cc70a5c9eea97e315e99626eea5d07296" + "df760d5aac0ae1844cf35088f09e99ce1df40d067d4ba5538bd61c55cc9f" + "a4e7cfc917b1524aa71b8ab581c687de7968e1b0de773d279c90da964a7b" + "091c8ac86c96da1da13b4ec54df0c4eb5a124b9d0b602b82e1aab2278102" + "c70bbc2b6e57641dd15199e014cb3583ee80901ea0b9a9c49404a2e169e3" + "5fddee9777c5b236bff13963517f534b5fd79ccd46086d75b91374d76de7" + "46145359e8c7561577e33dc90f3eb5e132d48202f67706e6e606bc18df24" + "6fb8adc8d5b67829049e1d614a82d9d688186189ec39374b93ccc7b291a4" + "cf6c1e5d0aa638428e2f765dda4397d5e9b0e29706b50803093d49f84eab" + "1e364e009e7205c8747461988719dd3b28945aafa1034f747c9fcd2e7fe8" + "c48f299587034ecfbeadb13a82e5a7505f4eb1beb72feaae8b342982fd63" + "eb93cc9f66b772a1e01bc6aa20a9e2eedadb4202e939b0cc34d4d6f6d530" + "0e7e29ef0a841a16bc76665b0a0807ac3e1adb3bf6ad674f349d17f82826" + "af1f4febebb5f786af625c57bf37f5c74231ce5874cd2b53bfe82c8b051d" + "8cf613e4cb0322bc600fe78d572a0d2d03e0f5e08b6c29576c871327cf87" + "f0c968f9a54fcdb58e41cc52659ae5fc48bd98212c6c371e91dabab2e581" + "79be16722b52777fd0c5f9db3d73179277bdd36971bdd9f224f12d1077a6" + "779b3e52240aca0ef4ac8eea4b3baeea36ecbb936ae9f9ff470dd52d786d" + "a3b74c3e05ee3be7e7511d8e83ffb920f7f99afc93dc102ee19d830fabd6" + "b3c0a80a364140376d6a3917974ffbbbe3d01a96ba1da6e2266a943298fc" + "fb68daebe117b4fed13619e9d6fbe8b90424493d46bb469ee7f2c3554ca0" + "a0c0729055164fbde9051f96531722bfe5bf5d323a4c28ae4ce129acf719" + "e1605be616f92ae88b8372f39bd580cf6cbd901d1a642d3082054106092a" + "864886f70d010701a08205320482052e3082052a30820526060b2a864886" + "f70d010c0a0102a08204ee308204ea301c060a2a864886f70d010c010330" + "0e04085b3bf84b13079d0002020800048204c8194223d7a0c479fc2ee690" + "d036228e05ac153a061b4cf3763afda35d3f7776ec67bbdf66edf3b9c813" + "ce1e4cf5f748a1de513ddcb80830f060eedc2864927d8b53708d6cb7b5fc" + "1da50066b60f6ab28e7566393ff667033dd11e2d6a580abec48ea8521f3e" + "956f296cb24cc3484ddf341e7ad8b19d35f309ffc0b327aef552307107a8" + "784882e1a4f4e1521a5037dc524907c67a93c4e58becf8dd4726893d7a1f" + "1c6cca488af81d4fdb7c17156fd8b9dd5f7699b4186cca9d00b6dd25e3ba" + "c09f12883fcd25fba6cfba0c6c4591500c88514cfa5c7073699975407da8" + "4f7c760301c22b7d574dbe0aa121c41fc50d5f74d28664b9b1af3ab6d0ef" + "854142d0e425cd42147e3eddedef0a8b43893466d58ac4568504b25beba5" + "9592a5af146b0769d30f523f328e54ed69e9e0499fb88d3cdfebeeb0f304" + "cc332f63c453f078f4ef960625e6f5ccaa2b9d8c71eaf5aedd0592cbf88f" + "8cdfce6ed4dcf77b8cdf90c08f196222d93116ddb18a3ebc0dba8a06af14" + "39657f1a0f832cac6a11351fce714c2a4b637e6930709b10f7f1a470258b" + "5153a5c7719a34565352c48e72ee5477bebb0a2630a7d36f1b8fca6079e3" + "cdabcf7ca0ce5ea10dfb13ce9b7815073b86dec4d758f2fa17cff3df5252" + "50a76ea9023cee3a920f8f95b965795c3d1ce2bd59e9d66280443eed94ff" + "01a042fbe21ebdbd77396edd94216cea47fcc24a5c46a6b31af99b2e4134" + "eb27030cfe98ec97b32a358ab594a2eda3f98843da4e009c21b02c27caec" + "a657cf39d04034fc21fd31f4c2ddcb69561ce6d1eb69448cb213f6d4896c" + "b47de1dd1d0421ce28b5fe501af3abab42ff9f2baf8e43b385b01db37df6" + "818a6e216b78579ef14dc91074f7f882292f934ec7ffa8d71b908a11ac77" + "5372d215453beb3f10944e3da8b5ecd23be2cc9d93e08bf32357f92082e5" + "40c3a170f10ec399ab75a34ff239832c338fe944f48d9645560d947823d8" + "285d18210870fcdbab6b20b253c9ad5a77804f786c06923dbc74bd195035" + "0b130dc85aa0b1d50a3a9947c18631d0c7e62fca02948197801dae422059" + "d078182c1d2e53d44be5e438ef0ebc6ff4943eb28c3701b2202f15841e96" + "edc9499437f47e2bf82c290049bdeec637092bcca2580c3d7b18025c9659" + "f8bb5f141c50ee2c79dd7696a7c3f55b47d62cf9bdd1345be022957e51cd" + "e17133797c4826c424f0d163a1af905e12788f016e4cfce8d57ed5512abf" + "41fe303a1b93f6dd18455a983156d59bc2bcdda69aea0fa2d2c3f54d5ac9" + "2a1ca9130106e345611c4f29796798a1f4996f3d5a3fbee6c14efe8e2f34" + "395ac7bbc3d4c5b484a0146218b1165def63c14b52cf88492bc3228b8da1" + "ae63bc904feab3a766b2867115c9d8a5ab26ea4344909e64c94eb7aef913" + "d694cd65fed02312341aa89774668bd7e302c8b377432807ccb184407196" + "b954fc86607ee5ab6bba9741955d61b8a4317971701512a1160186e5e89d" + "9b1217314a4a8632e7f3372f43b61cddff07e72f17b7d356a14cf18c6e59" + "5d9e4b3b449fbd26fb2795807ca4a5576621d803d2b0ff032d5a3c14cc3b" + "c261062844d015c9283d20414dc71e0a8bbf2a94c67b915a03edee764bdd" + "5c03e72d8cc0c66a33124aaaf777b6e58089b9e0b838eb396b828352d460" + "3023715ab2b87878d9f206210bddad1b7b701f36de4173eb1ec553e19908" + "e188b38dbd0ff3692d13894c943125302306092a864886f70d0109153116" + "041433d43dfd9efcdf7b8d8708b8cee57e9d59896bb83031302130090605" + "2b0e03021a05000414c5febe88e845ea6e974e04fc48434b2c063adc2e04" + "08e31d2b0bdc35a19302020800", + + // backup.pub.p12 + @"30820a31020103308209f706092a864886f70d010701a08209e8048209e4" + "308209e03082049706092a864886f70d010706a082048830820484020100" + "3082047d06092a864886f70d010701301c060a2a864886f70d010c010630" + "0e04085d4c303f4024c4ac02020800808204503c4cacf46cae0d5dfc7ead" + "39046850063e536759df5e2cb48c9ecad20c50aa600f3e5a29bd8df7ae53" + "5a7379df01fedba23141ccc776a05e621193dfea53cf01b8297cbc895d64" + "ad6b87208b59e58a70a0919e746402eb47acce65ce2b3368b44b0186a57f" + "adc81a55bd886e20ecad6f30d10b5f69193e603eb508636209c682f740b7" + "2f4f97f50ce157c42769f07d9d0d5c7eea2864c5fcf9b47165810c42afbd" + "82c4537742eaa15069af7acca7585f041bb54f97d37eb11b5ded9077e98c" + "b5fad12f6a5b91b4a1d201d1da7b41d2c457ced09bec8d33576bdb6ee1a3" + "aaf2b5de9c19ec97616adea7c29cf8aa7be5fda07a9adfde4343195f5fcd" + "f65154453753fd298c52ccf0f469068fa404d1faed762664ac02c35fc4da" + "8a015cd00369fcb3e8b58ff4e292dbfdcea3469469d46abc06ff99360b5b" + "993b1330a21f051c384932b3221b63b71278bb1c642ad47027df4fddc8fe" + "19231d7d3759c08d1b4decfbc85f08a6273a9820e12d5e0f23e7c1517aea" + "d6afcbe3f14339c948bed6ecc9a6c7816710186ac1daabc66e6d5caf909a" + "6e2c240b8ddc85275084fafd8be781b2d99adc411bfb110d0118d32a5482" + "d4b34c8ec833edfc0f27b8275b8785b5584277c87ca00ce9c339c8a64dfe" + "e6e0bc43d088d5974f81f4c72ab13a2b8e6bcfe8276e6b8aac0f626deb8c" + "d835f6ba6c27184cf311f75521f873dd4ebce72c99aa096db377a5120791" + "811e8ec816ebc313ea07112d7041bd6f0b86d39ba0acd15fac86ea49e22f" + "da1a743376250e89b33b9608e10d76b91ab05934748a0551bc18c0416252" + "79adea084833de8e9666757e901b9595a7ebb2e321f67efb6b728c62b3f3" + "6709ce491d33a6cb4ff53af08b0a33390959ae4f5b251a455ff2f13d8c96" + "ee1b7bc9c0711a28fcf052429c6db413e14bb4d850b42972880d6951d237" + "0de0123a29a0d13594e99ed602ab5b369542956d5e07392785fd7af7e91d" + "f9646f58591bb98d3b5efc3aebd3b4cac45285e4f99ac316d011d15072cc" + "4aa75a8fcf79ef1df10a4b8cad87d4aafc70a73ec77bd4a42eeef898a600" + "16b0cfcd5ee79d0c02992d0953fb779cb0818e78f14001931ab2b74c06cf" + "3299fcd68cbf29da5324e9741ac39fd3c66555d1311948ea8ed93406ff04" + "b59de5c56673221f04863058d429b0411762b8dfe5b0c46dd678fc32b9a0" + "1b54bdcf02508a78044fd1884cff43224b94c2bef3db0ae7a82f58f6ecf4" + "a62bbdebe7c3deddc4801a67604c7dc390d9b9f7e398942209a4cfca3e68" + "a81a797f82e1bb7b4a6105be6e7772c6f875a1c4bc6a50aca6f9f911c8d8" + "9dd8c8134203e44fc82901d4078262037d447ac0fb3166512cdb1877fea0" + "6193349d10acc5b6af7462a811adfe78ce2899eccad87f10f4ecb0c9d97b" + "12daee1c350b9729c9abc145860e192f406e0cf80db45b03386b91fe9d8e" + "028af7891b3681b5ff79cc3696e40211bc3692d85624e697205dd8611f80" + "0920956b9b814e9a89184e3d80b60a94e424fc1aba581b399e817925cdf7" + "97bcc845243bde101f87675cd03082054106092a864886f70d010701a082" + "05320482052e3082052a30820526060b2a864886f70d010c0a0102a08204" + "ee308204ea301c060a2a864886f70d010c0103300e04080a30e62339817c" + "0302020800048204c8f0f4b8734e85c4b2763da2baa5e74dae7f5f7a26a4" + "67dbd1cd36ccbfa49ab525866e8a0a6d099964250a187bebf4767870bb4b" + "7b5d3147af7c86be232377f00888d992e21572fa9238284931b39fd349a7" + "69ef7485e334408ac45b73017a65c08813d72b1a1d6bf6e2271793a58021" + "5639e3280520396ba298e4716a1dcf6853ec02f4236f2c3eac0743cc908e" + "01738ef62d4ed2dcb4264227c446cc7dc093a64345ad9cdc11d3aca666a8" + "91d8c345be73308ab4012194325df439facd71abbc7a0aeaae4c60c90b0f" + "8457b209dd1deb76709b860314dae14720d7e152091bc8dd04ea7af96785" + "024194ce868299720065b8520a94d2bb5db1b3353687cb62bedf4ee4d7b9" + "39c81b4cc6be37695f8228a9c0ce40b2603d05978748e772d5b40268993a" + "9d2e5521c3a79e203b4065d89b049ced139be6549601100138ee3968542e" + "2fd52e8307591ec5a825658a320f7e39a69566a30be654d4ec592d774002" + "6410c3f8215bf2ee373c5af3e55841785366f8f9b55ffbe5f511d71c8f26" + "4a2ee960fc017a7afbb8564b7510aeb5c3ec05b1250731af167a605897c0" + "368776a1ce36664719e0afd10fc22580a1d170a5f852b8d3a0e9fbe80f76" + "1cc40e449ffd936d963e5f95ff9b13cba96249f98711510490a5701a2f79" + "ba13e1e26dcd85704ac88c46e2c17e69c79cad76380f7d1e35dc752dae06" + "0ed8c42d2cfbbaf5fd23c19ea82ffe333d067eec52a12a83cbec72914dfe" + "a31ca9e35c715939e2362805a6cd12798bd035a20c4a504c8cc652a3010f" + "35c929f24c0fc2b3fca50fbfdd7cd6456f635e80c701651d0085eef132e5" + "fd9c8089ed7ce4193a8a8d5ea80d332f258aa7b110a6ef94b69a96fef5c9" + "4ac4f4dd6c1eff6564e7bb5cbfe818acd80b5b2d7ade528accfe69905ffc" + "1a467e65dec4fc8c04063e3634197daff410e2dc92a6411bad3e1235a6cd" + "5579ea8a1101222d7da3bd6995b609a2b25a1a93782f597e5e938e7623a9" + "3e448d06e2db34444f476929e9a50d425285b619733d9797249d1b035634" + "8afde198052fef79691571289da371bafb56e3750bd500dd1e3a424433af" + "ab44656ccd5dedb7ec8c2c970e7774495a06ceb072519360addae38aceac" + "4f27fcf117c9f527efac193625a4c5916f51e557fed555c0678cc93cc018" + "134e0edb283a71ee50237a9bb150759169fcbd7b24a75c3493a6b4194c5b" + "8cd79da37ebef2a63bc2ac9b9b75502fc0b43cdd6f942e4943804dc0ed0f" + "b4c7b101c5d5aee4f6e0a18398b35bf25c69b598e1244cc722defb5878a3" + "b3b6ea797341615cae10239736d94a63497debccc54fe2cc7a5f8d15efa2" + "f65df4fe91ffab81072f545e41dbf4c8d7f273d608e492c1c5a32268bf82" + "ed51acd729c2121027a6de1903ce4b23c13ad548647c6836ee7788918124" + "7660c55a9825babd3f086ebede5c4725618178bb0db5d17d594cabed63b6" + "83e6a500d8a6ee8b2ac287213d6710579db444730c0d9fe24649c368bce5" + "ac1e61b3ca854c1da6b9ef47d4886c6b4a1c840d644eac8d2bc1ea8f443c" + "83239d351e1fdd3ba6338a94920b382161b4519da6b6dd3ce717b24f96f4" + "acddf70a241be2ef1e964bd83c0eac927d5ccb503b5f9a06850b1f532d23" + "d6c82a4fba847d7e867bae4b950d7760e941664526e9b8f4dd98907d90ef" + "a2e8d35f8cb4294b642a29af61b5534a540befa5e71562e8efdda232fde7" + "fb6c963125302306092a864886f70d010915311604142942010915a82174" + "5e3f65fd1a7b0e1daaf6d56930313021300906052b0e03021a050004143c" + "0c50131fbfdf668827900dc7c8ca72a38609a70408d50691caa0a421c902" + "020800" + ]; + #pragma clang diagnostic pop + + NSMutableArray *array = [NSMutableArray array]; + for (NSString* hexKey in hexKeys) { + NSData *data = [BNCEncodingUtils dataFromHexString:hexKey]; + if (data) { + SecKeyRef secKey = [self publicSecKeyFromPKCS12CertChainData:data]; + if (secKey) [array addObject:(__bridge_transfer id)secKey]; + } + } + return array; +} + ++ (SecKeyRef) publicSecKeyFromPKCS12CertChainData:(NSData*)keyData { + OSStatus status = errSecSuccess; + NSArray *items = nil; + SecKeyRef secKey = NULL; + SecTrustResultType trustType = kSecTrustResultInvalid; + + // Release these + CFArrayRef itemsRef = NULL; + + NSDictionary *options = @{ + (id)kSecImportExportPassphrase: @"", + }; + if (!keyData) { + goto exit; + } + status = SecPKCS12Import((CFDataRef) keyData, (CFDictionaryRef)options, &itemsRef); + if (status != errSecSuccess || !itemsRef || CFArrayGetCount(itemsRef) == 0) goto exit; + + items = (__bridge NSArray*) itemsRef; + SecTrustRef trust = (__bridge SecTrustRef)(items[0][(id)kSecImportItemTrust]); + if (!trust) goto exit; + + status = SecTrustEvaluate(trust, &trustType); + if (trustType != kSecTrustResultInvalid) { + secKey = SecTrustCopyPublicKey(trust); + } else { + status = errSecDecode; + } + +exit: + if (secKey == NULL && status == errSecSuccess) { + status = errSecItemNotFound; + } + if (status != errSecSuccess) { + NSError *error = [NSError errorWithDomain:NSOSStatusErrorDomain code:status userInfo:nil]; + BNCLogError(@"Can't import public key from pkcs12 data: %@.", error); + } + if (itemsRef) CFRelease(itemsRef); + return secKey; +} + +#pragma mark - GET methods + +- (void)getRequest:(NSDictionary *)params + url:(NSString *)url + key:(NSString *)key + callback:(BNCServerCallback)callback { + [self getRequest:params url:url key:key retryNumber:0 callback:callback]; +} + +- (void)getRequest:(NSDictionary *)params + url:(NSString *)url + key:(NSString *)key + retryNumber:(NSInteger)retryNumber + callback:(BNCServerCallback)callback { + NSURLRequest *request = [self prepareGetRequest:params url:url key:key retryNumber:retryNumber]; + + [self genericHTTPRequest:request retryNumber:retryNumber callback:callback + retryHandler:^NSURLRequest *(NSInteger lastRetryNumber) { + return [self prepareGetRequest:params url:url key:key retryNumber:lastRetryNumber+1]; + }]; +} + +#pragma mark - POST methods + +- (void)postRequest:(NSDictionary *)post + url:(NSString *)url + key:(NSString *)key + callback:(BNCServerCallback)callback { + [self postRequest:post url:url retryNumber:0 key:key callback:callback]; +} + +- (BOOL) isV2APIURL:(NSString*)urlstring { + NSRange range = [urlstring rangeOfString:@"branch.io/v2/"]; + return (range.location != NSNotFound); +} + +- (void)postRequest:(NSDictionary *)post + url:(NSString *)url + retryNumber:(NSInteger)retryNumber + key:(NSString *)key + callback:(BNCServerCallback)callback { + + NSMutableDictionary *extendedParams = nil; + if ([self isV2APIURL:url]) { + extendedParams = [NSMutableDictionary new]; + if (post) [extendedParams addEntriesFromDictionary:post]; + NSDictionary *d = [[BNCDeviceInfo getInstance] v2dictionary]; + if (d.count) extendedParams[@"user_data"] = d; + } else { + extendedParams = [self updateDeviceInfoToParams:post]; + } + NSURLRequest *request = [self preparePostRequest:extendedParams url:url key:key retryNumber:retryNumber]; + + // Instrumentation metrics + self.requestEndpoint = [self.preferenceHelper getEndpointFromURL:url]; + + [self genericHTTPRequest:request + retryNumber:retryNumber + callback:callback + retryHandler:^ NSURLRequest *(NSInteger lastRetryNumber) { + return [self preparePostRequest:extendedParams url:url key:key retryNumber:lastRetryNumber+1]; + }]; +} + +- (BNCServerResponse *)postRequestSynchronous:(NSDictionary *)post + url:(NSString *)url + key:(NSString *)key { + NSDictionary *extendedParams = [self updateDeviceInfoToParams:post]; + NSURLRequest *request = [self preparePostRequest:extendedParams url:url key:key retryNumber:0]; + return [self genericHTTPRequestSynchronous:request]; +} + +#pragma mark - Generic requests + +- (void)genericHTTPRequest:(NSURLRequest *)request callback:(BNCServerCallback)callback { + [self genericHTTPRequest:request retryNumber:0 callback:callback + retryHandler:^NSURLRequest *(NSInteger lastRetryNumber) { + return request; + }]; +} + +- (void)genericHTTPRequest:(NSURLRequest *)request + retryNumber:(NSInteger)retryNumber + callback:(BNCServerCallback)callback + retryHandler:(NSURLRequest *(^)(NSInteger))retryHandler { + + void (^completionHandler)(idoperation) = + ^void (idoperation) { + + BNCServerResponse *serverResponse = + [self processServerResponse:operation.response data:operation.responseData error:operation.error]; + [self collectInstrumentationMetricsWithOperation:operation]; + + NSError *underlyingError = operation.error; + NSInteger status = [serverResponse.statusCode integerValue]; + + // If the phone is in a poor network condition, + // iOS will return statuses such as -1001, -1003, -1200, -9806 + // indicating various parts of the HTTP post failed. + // We should retry in those conditions in addition to the case where the server returns a 500 + + // Status 53 means the request was killed by the OS because we're still in the background. + // This started happening in iOS 12 / Xcode 10 production when we're called from continueUserActivity: + // but we're not fully out of the background yet. + + BOOL isRetryableStatusCode = status >= 500 || status < 0 || status == 53; + + // Retry the request if appropriate + if (retryNumber < self.preferenceHelper.retryCount && isRetryableStatusCode) { + dispatch_time_t dispatchTime = + dispatch_time(DISPATCH_TIME_NOW, self.preferenceHelper.retryInterval * NSEC_PER_SEC); + dispatch_after(dispatchTime, dispatch_get_main_queue(), ^{ + BNCLogDebug(@"Retrying request with url %@", request.URL.relativePath); + // Create the next request + NSURLRequest *retryRequest = retryHandler(retryNumber); + [self genericHTTPRequest:retryRequest + retryNumber:(retryNumber + 1) + callback:callback retryHandler:retryHandler]; + }); + + // Do not continue on if retrying, else the callback will be called incorrectly + return; + } + + NSError *branchError = nil; + + // Wrap up bad statuses w/ specific error messages + if (status >= 500) { + branchError = [NSError branchErrorWithCode:BNCServerProblemError error:underlyingError]; + } + else if (status == 409) { + branchError = [NSError branchErrorWithCode:BNCDuplicateResourceError error:underlyingError]; + } + else if (status >= 400) { + NSString *errorString = [serverResponse.data objectForKey:@"error"]; + if (![errorString isKindOfClass:[NSString class]]) + errorString = nil; + if (!errorString) + errorString = underlyingError.localizedDescription; + if (!errorString) + errorString = BNCLocalizedString(@"The request was invalid."); + branchError = [NSError branchErrorWithCode:BNCBadRequestError localizedMessage:errorString]; + } + else if (underlyingError) { + branchError = [NSError branchErrorWithCode:BNCServerProblemError error:underlyingError]; + } + + if (branchError) { + BNCLogError(@"An error prevented request to %@ from completing: %@", + request.URL.absoluteString, branchError); + } + + // Don't call on the main queue since it might be blocked. + if (callback) + callback(serverResponse, branchError); + }; + + if (Branch.trackingDisabled) { + NSString *endpoint = request.URL.absoluteString; + BNCPreferenceHelper *prefs = [BNCPreferenceHelper preferenceHelper]; + if (([endpoint bnc_containsString:@"/v1/install"] || + [endpoint bnc_containsString:@"/v1/open"]) && + ((prefs.linkClickIdentifier.length > 0 ) || + (prefs.spotlightIdentifier.length > 0 ) || + (prefs.universalLinkUrl.length > 0))) { + // Allow this network operation since it's an open/install to resolve a link. + } else { + [[BNCPreferenceHelper preferenceHelper] clearTrackingInformation]; + NSError *error = [NSError branchErrorWithCode:BNCTrackingDisabledError]; + BNCLogError(@"Network service error: %@.", error); + if (callback) { + callback(nil, error); + } + return; + } + } + id operation = + [self.networkService networkOperationWithURLRequest:request.copy completion:completionHandler]; + [operation start]; + NSError *error = [self verifyNetworkOperation:operation]; + if (error) { + BNCLogError(@"Network service error: %@.", error); + if (callback) { + callback(nil, error); + } + return; + } +} + +- (NSError*) verifyNetworkOperation:(id)operation { + + if (!operation) { + NSString *message = BNCLocalizedString( + @"A network operation instance is expected to be returned by the" + " networkOperationWithURLRequest:completion: method." + ); + NSError *error = [NSError branchErrorWithCode:BNCNetworkServiceInterfaceError localizedMessage:message]; + return error; + } + if (![operation conformsToProtocol:@protocol(BNCNetworkOperationProtocol)]) { + NSString *message = + BNCLocalizedFormattedString( + @"Network operation of class '%@' does not conform to the BNCNetworkOperationProtocol.", + NSStringFromClass([operation class])); + NSError *error = [NSError branchErrorWithCode:BNCNetworkServiceInterfaceError localizedMessage:message]; + return error; + } + if (!operation.startDate) { + NSString *message = BNCLocalizedString( + @"The network operation start date is not set. The Branch SDK expects the network operation" + " start date to be set by the network provider." + ); + NSError *error = [NSError branchErrorWithCode:BNCNetworkServiceInterfaceError localizedMessage:message]; + return error; + } + if (!operation.timeoutDate) { + NSString*message = BNCLocalizedString( + @"The network operation timeout date is not set. The Branch SDK expects the network operation" + " timeout date to be set by the network provider." + ); + NSError *error = [NSError branchErrorWithCode:BNCNetworkServiceInterfaceError localizedMessage:message]; + return error; + } + if (!operation.request) { + NSString *message = BNCLocalizedString( + @"The network operation request is not set. The Branch SDK expects the network operation" + " request to be set by the network provider." + ); + NSError *error = [NSError branchErrorWithCode:BNCNetworkServiceInterfaceError localizedMessage:message]; + return error; + } + return nil; +} + +- (BNCServerResponse *)genericHTTPRequestSynchronous:(NSURLRequest *)request { + + __block BNCServerResponse *serverResponse = nil; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + + id operation = + [self.networkService + networkOperationWithURLRequest:request.copy + completion:^void (idoperation) { + serverResponse = + [self processServerResponse:operation.response + data:operation.responseData error:operation.error]; + [self collectInstrumentationMetricsWithOperation:operation]; + dispatch_semaphore_signal(semaphore); + }]; + [operation start]; + NSError *error = [self verifyNetworkOperation:operation]; + if (!error) { + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + } + return serverResponse; +} + +#pragma mark - Internals + +- (NSURLRequest *)prepareGetRequest:(NSDictionary *)params + url:(NSString *)url + key:(NSString *)key + retryNumber:(NSInteger)retryNumber { + + NSDictionary *preparedParams = + [self prepareParamDict:params key:key retryNumber:retryNumber requestType:@"GET"]; + NSString *requestUrlString = + [NSString stringWithFormat:@"%@%@", url, [BNCEncodingUtils encodeDictionaryToQueryString:preparedParams]]; + BNCLogDebug(@"URL: %@", requestUrlString); + + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:[NSURL URLWithString:requestUrlString] + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:self.preferenceHelper.timeout]; + [request setHTTPMethod:@"GET"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + + return request; +} + +- (NSURLRequest *)preparePostRequest:(NSDictionary *)params + url:(NSString *)url + key:(NSString *)key + retryNumber:(NSInteger)retryNumber { + + NSMutableDictionary *preparedParams = + [self prepareParamDict:params key:key retryNumber:retryNumber requestType:@"POST"]; + if ([self isV2APIURL:url]) { + preparedParams[@"sdk"] = nil; + } + if (Branch.trackingDisabled) { + preparedParams[@"tracking_disabled"] = (__bridge NSNumber*) kCFBooleanTrue; + preparedParams[@"local_ip"] = nil; + preparedParams[@"lastest_update_time"] = nil; + preparedParams[@"previous_update_time"] = nil; + preparedParams[@"latest_install_time"] = nil; + preparedParams[@"first_install_time"] = nil; + preparedParams[@"ios_vendor_id"] = nil; + preparedParams[@"hardware_id"] = nil; + preparedParams[@"hardware_id_type"] = nil; + preparedParams[@"is_hardware_id_real"] = nil; + preparedParams[@"device_fingerprint_id"] = nil; + preparedParams[@"identity_id"] = nil; + preparedParams[@"identity"] = nil; + preparedParams[@"update"] = nil; + } + NSData *postData = [BNCEncodingUtils encodeDictionaryToJsonData:preparedParams]; + NSString *postLength = [NSString stringWithFormat:@"%lu", (unsigned long)[postData length]]; + + BNCLogDebug(@"URL: %@.", url); + BNCLogDebug(@"Body: %@\nJSON: %@.", + preparedParams, + [[NSString alloc] initWithData:postData encoding:NSUTF8StringEncoding] + ); + + NSMutableURLRequest *request = + [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url] + cachePolicy:NSURLRequestReloadIgnoringLocalCacheData + timeoutInterval:self.preferenceHelper.timeout]; + [request setHTTPMethod:@"POST"]; + [request setValue:postLength forHTTPHeaderField:@"Content-Length"]; + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + [request setHTTPBody:postData]; + + return request; +} + +- (NSMutableDictionary *)prepareParamDict:(NSDictionary *)params + key:(NSString *)key + retryNumber:(NSInteger)retryNumber + requestType:(NSString *)reqType { + + NSMutableDictionary *fullParamDict = [[NSMutableDictionary alloc] init]; + [fullParamDict bnc_safeAddEntriesFromDictionary:params]; + fullParamDict[@"sdk"] = [NSString stringWithFormat:@"ios%@", BNC_SDK_VERSION]; + + // using rangeOfString instead of containsString to support devices running pre iOS 8 + if ([[[NSBundle mainBundle] executablePath] rangeOfString:@".appex/"].location != NSNotFound) { + fullParamDict[@"ios_extension"] = @(1); + } + fullParamDict[@"retryNumber"] = @(retryNumber); + fullParamDict[@"branch_key"] = key; + + NSMutableDictionary *metadata = [[NSMutableDictionary alloc] init]; + [metadata bnc_safeAddEntriesFromDictionary:self.preferenceHelper.requestMetadataDictionary]; + [metadata bnc_safeAddEntriesFromDictionary:fullParamDict[BRANCH_REQUEST_KEY_STATE]]; + if (metadata.count) { + fullParamDict[BRANCH_REQUEST_KEY_STATE] = metadata; + } + // we only send instrumentation info in the POST body request + if (self.preferenceHelper.instrumentationDictionary.count && [reqType isEqualToString:@"POST"]) { + fullParamDict[BRANCH_REQUEST_KEY_INSTRUMENTATION] = self.preferenceHelper.instrumentationDictionary; + } + + return fullParamDict; +} + +- (BNCServerResponse *)processServerResponse:(NSURLResponse *)response + data:(NSData *)data + error:(NSError *)error { + BNCServerResponse *serverResponse = [[BNCServerResponse alloc] init]; + if (!error) { + serverResponse.statusCode = @([(NSHTTPURLResponse *)response statusCode]); + serverResponse.data = [BNCEncodingUtils decodeJsonDataToDictionary:data]; + } + else { + serverResponse.statusCode = @(error.code); + serverResponse.data = error.userInfo; + } + BNCLogDebug(@"Server returned: %@.", serverResponse); + return serverResponse; +} + +- (void) collectInstrumentationMetricsWithOperation:(id)operation { + // multiplying by negative because startTime happened in the past + NSTimeInterval elapsedTime = [operation.startDate timeIntervalSinceNow] * -1000.0; + NSString *lastRoundTripTime = [[NSNumber numberWithDouble:floor(elapsedTime)] stringValue]; + NSString * brttKey = [NSString stringWithFormat:@"%@-brtt", self.requestEndpoint]; + [self.preferenceHelper clearInstrumentationDictionary]; + [self.preferenceHelper addInstrumentationDictionaryKey:brttKey value:lastRoundTripTime]; +} + +- (void)updateDeviceInfoToMutableDictionary:(NSMutableDictionary *)dict { + BNCDeviceInfo *deviceInfo = [BNCDeviceInfo getInstance]; + + NSString *hardwareId = [deviceInfo.hardwareId copy]; + NSString *hardwareIdType = [deviceInfo.hardwareIdType copy]; + NSNumber *isRealHardwareId = @(deviceInfo.isRealHardwareId); + if (hardwareId != nil && hardwareIdType != nil && isRealHardwareId != nil) { + dict[BRANCH_REQUEST_KEY_HARDWARE_ID] = hardwareId; + dict[BRANCH_REQUEST_KEY_HARDWARE_ID_TYPE] = hardwareIdType; + dict[BRANCH_REQUEST_KEY_IS_HARDWARE_ID_REAL] = isRealHardwareId; + } + + [self safeSetValue:deviceInfo.vendorId forKey:BRANCH_REQUEST_KEY_IOS_VENDOR_ID onDict:dict]; + [self safeSetValue:deviceInfo.brandName forKey:BRANCH_REQUEST_KEY_BRAND onDict:dict]; + [self safeSetValue:deviceInfo.modelName forKey:BRANCH_REQUEST_KEY_MODEL onDict:dict]; + [self safeSetValue:deviceInfo.osName forKey:BRANCH_REQUEST_KEY_OS onDict:dict]; + [self safeSetValue:deviceInfo.osVersion forKey:BRANCH_REQUEST_KEY_OS_VERSION onDict:dict]; + [self safeSetValue:deviceInfo.screenWidth forKey:BRANCH_REQUEST_KEY_SCREEN_WIDTH onDict:dict]; + [self safeSetValue:deviceInfo.screenHeight forKey:BRANCH_REQUEST_KEY_SCREEN_HEIGHT onDict:dict]; + + [self safeSetValue:deviceInfo.browserUserAgent forKey:@"user_agent" onDict:dict]; + [self safeSetValue:deviceInfo.country forKey:@"country" onDict:dict]; + [self safeSetValue:deviceInfo.language forKey:@"language" onDict:dict]; + dict[@"local_ip"] = deviceInfo.localIPAddress; + + dict[BRANCH_REQUEST_KEY_AD_TRACKING_ENABLED] = @(deviceInfo.isAdTrackingEnabled); +} + +- (NSMutableDictionary*)updateDeviceInfoToParams:(NSDictionary *)params { + NSMutableDictionary *extendedParams=[[NSMutableDictionary alloc] init]; + [extendedParams addEntriesFromDictionary:params]; + [self updateDeviceInfoToMutableDictionary:extendedParams]; + return extendedParams; +} + +- (void)safeSetValue:(NSObject *)value forKey:(NSString *)key onDict:(NSMutableDictionary *)dict { + if (value) { + dict[key] = value; + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequest.h similarity index 89% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerRequest.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequest.h index e532290d761..5da9f76be6b 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerRequest.h +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequest.h @@ -8,7 +8,7 @@ #import "BNCServerInterface.h" -@interface BNCServerRequest : NSObject +@interface BNCServerRequest : NSObject - (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback; - (void)processResponse:(BNCServerResponse *)response error:(NSError *)error; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequest.m new file mode 100644 index 00000000000..2eff1e6067e --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequest.m @@ -0,0 +1,40 @@ +// +// BNCServerRequest.m +// Branch-SDK +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" +#import "BNCLog.h" + +@implementation BNCServerRequest + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + BNCLogError(@"BNCServerRequest subclasses must implement makeRequest:key:callback:."); +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + BNCLogError(@"BNCServerRequest subclasses must implement processResponse:error:."); +} + +- (id)initWithCoder:(NSCoder *)aDecoder { + return self = [super init]; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + // Nothing going on here +} + +- (void)safeSetValue:(NSObject *)value forKey:(NSString *)key onDict:(NSMutableDictionary *)dict { + if (value) { + dict[key] = value; + } +} + ++ (BOOL) supportsSecureCoding { + return YES; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerRequestQueue.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequestQueue.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerRequestQueue.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequestQueue.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequestQueue.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequestQueue.m new file mode 100755 index 00000000000..a38ef68240a --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerRequestQueue.m @@ -0,0 +1,424 @@ +// +// BNCServerRequestQueue.m +// Branch-SDK +// +// Created by Qinwei Gong on 9/6/14. +// Copyright (c) 2014 Branch Metrics. All rights reserved. +// + + +#import "BNCServerRequestQueue.h" +#import "BNCPreferenceHelper.h" +#import "BranchCloseRequest.h" +#import "BranchOpenRequest.h" +#import "BNCLog.h" + + +static NSString * const BRANCH_QUEUE_FILE = @"BNCServerRequestQueue"; +static NSTimeInterval const BATCH_WRITE_TIMEOUT = 3.0; + + +static inline uint64_t BNCNanoSecondsFromTimeInterval(NSTimeInterval interval) { + return interval * ((NSTimeInterval) NSEC_PER_SEC); +} + + +@interface BNCServerRequestQueue() +@property (strong) NSMutableArray *queue; +@property (strong) dispatch_queue_t asyncQueue; +@property (strong) dispatch_source_t persistTimer; +@end + + +@implementation BNCServerRequestQueue + +- (id)init { + self = [super init]; + if (!self) return self; + + self.queue = [NSMutableArray array]; + self.asyncQueue = dispatch_queue_create("io.branch.persist_queue", DISPATCH_QUEUE_SERIAL); + return self; +} + +- (void) dealloc { + @synchronized (self) { + if (self.persistTimer) { + dispatch_source_cancel(self.persistTimer); + self.persistTimer = nil; + } + [self persistImmediately]; + self.queue = nil; + } +} + +- (void)enqueue:(BNCServerRequest *)request { + @synchronized (self) { + if (request) { + [self.queue addObject:request]; + [self persistEventually]; + } + } +} + +- (void)insert:(BNCServerRequest *)request at:(NSUInteger)index { + @synchronized (self) { + if (index > self.queue.count) { + BNCLogError(@"Invalid queue operation: index out of bound!"); + return; + } + if (request) { + [self.queue insertObject:request atIndex:index]; + [self persistEventually]; + } + } +} + +- (BNCServerRequest *)dequeue { + @synchronized (self) { + BNCServerRequest *request = nil; + if (self.queue.count > 0) { + request = [self.queue objectAtIndex:0]; + [self.queue removeObjectAtIndex:0]; + [self persistEventually]; + } + return request; + } +} + +- (BNCServerRequest *)removeAt:(NSUInteger)index { + @synchronized (self) { + BNCServerRequest *request = nil; + if (index >= self.queue.count) { + BNCLogError(@"Invalid queue operation: index out of bound!"); + return nil; + } + request = [self.queue objectAtIndex:index]; + [self.queue removeObjectAtIndex:index]; + [self persistEventually]; + return request; + } +} + +- (void)remove:(BNCServerRequest *)request { + @synchronized (self) { + [self.queue removeObject:request]; + [self persistEventually]; + } +} + +- (BNCServerRequest *)peek { + @synchronized (self) { + return [self peekAt:0]; + } +} + +- (BNCServerRequest *)peekAt:(NSUInteger)index { + @synchronized (self) { + if (index >= self.queue.count) { + BNCLogError(@"Invalid queue operation: index out of bound!"); + return nil; + } + BNCServerRequest *request = nil; + request = [self.queue objectAtIndex:index]; + return request; + } +} + +- (NSInteger)queueDepth { + @synchronized (self) { + return (NSInteger) self.queue.count; + } +} + +- (NSString *)description { + @synchronized(self) { + return [self.queue description]; + } +} + +- (void)clearQueue { + @synchronized (self) { + [self.queue removeAllObjects]; + [self persistImmediately]; + } +} + +- (BOOL)containsInstallOrOpen { + @synchronized (self) { + for (NSUInteger i = 0; i < self.queue.count; i++) { + BNCServerRequest *req = [self.queue objectAtIndex:i]; + // Install extends open, so only need to check open. + if ([req isKindOfClass:[BranchOpenRequest class]]) { + return YES; + } + } + return NO; + } +} + +- (BOOL)removeInstallOrOpen { + @synchronized (self) { + for (NSUInteger i = 0; i < self.queue.count; i++) { + BranchOpenRequest *req = [self.queue objectAtIndex:i]; + // Install extends open, so only need to check open. + if ([req isKindOfClass:[BranchOpenRequest class]]) { + BNCLogDebugSDK(@"Removing open request."); + req.callback = nil; + [self remove:req]; + return YES; + } + } + return NO; + } +} + +- (BranchOpenRequest *)moveInstallOrOpenToFront:(NSInteger)networkCount { + @synchronized (self) { + + BOOL requestAlreadyInProgress = networkCount > 0; + + BNCServerRequest *openOrInstallRequest; + for (NSUInteger i = 0; i < self.queue.count; i++) { + BNCServerRequest *req = [self.queue objectAtIndex:i]; + if ([req isKindOfClass:[BranchOpenRequest class]]) { + + // Already in front, nothing to do + if (i == 0 || (i == 1 && requestAlreadyInProgress)) { + return (BranchOpenRequest *)req; + } + + // Otherwise, pull this request out and stop early + openOrInstallRequest = [self removeAt:i]; + break; + } + } + + if (!openOrInstallRequest) { + BNCLogError(@"No install or open request in queue while trying to move it to the front."); + return nil; + } + + if (!requestAlreadyInProgress || !self.queue.count) { + [self insert:openOrInstallRequest at:0]; + } + else { + [self insert:openOrInstallRequest at:1]; + } + + return (BranchOpenRequest *)openOrInstallRequest; + } +} + +- (BOOL)containsClose { + @synchronized (self) { + for (NSUInteger i = 0; i < self.queue.count; i++) { + BNCServerRequest *req = [self.queue objectAtIndex:i]; + if ([req isKindOfClass:[BranchCloseRequest class]]) { + return YES; + } + } + return NO; + } +} + +#pragma mark - Private Methods + +- (void)persistEventually { + @synchronized (self) { + if (self.persistTimer) return; + + self.persistTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.asyncQueue); + if (!self.persistTimer) return; + + dispatch_time_t startTime = + dispatch_time(DISPATCH_TIME_NOW, BNCNanoSecondsFromTimeInterval(BATCH_WRITE_TIMEOUT)); + dispatch_source_set_timer( + self.persistTimer, + startTime, + BNCNanoSecondsFromTimeInterval(BATCH_WRITE_TIMEOUT), + BNCNanoSecondsFromTimeInterval(BATCH_WRITE_TIMEOUT / 10.0) + ); + __weak __typeof(self) weakSelf = self; + dispatch_source_set_event_handler(self.persistTimer, ^ { + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf) { + [strongSelf cancelTimer]; + [strongSelf persistImmediately]; + } + }); + dispatch_resume(self.persistTimer); + } +} + +- (void) cancelTimer { + @synchronized (self) { + if (self.persistTimer) { + dispatch_source_cancel(self.persistTimer); + self.persistTimer = nil; + } + } +} + +- (void)persistImmediately { + @synchronized (self) { + @try { + if (!self.queue) return; + NSArray *requestsToPersist = [self.queue copy]; + NSMutableArray *encodedRequests = [[NSMutableArray alloc] init]; + for (BNCServerRequest *req in requestsToPersist) { + // Don't persist these requests + if ([req isKindOfClass:[BranchCloseRequest class]]) { + continue; + } + NSData *encodedReq = [NSKeyedArchiver archivedDataWithRootObject:req]; + if (encodedReq) [encodedRequests addObject:encodedReq]; + } + NSData *data = [NSKeyedArchiver archivedDataWithRootObject:encodedRequests]; + if (!data) { + BNCLogError(@"Cannot create archive data."); + return; + } + NSError *error = nil; + [data writeToURL:self.class.URLForQueueFile + options:NSDataWritingAtomic error:&error]; + if (error) { + BNCLogError(@"Failed to persist queue to disk: %@.", error); + } + } + @catch (NSException *exception) { + BNCLogError( + @"An exception occurred while attempting to save the queue. Exception information:\n\n%@.", + [self.class exceptionString:exception] + ); + } + } +} + +- (BOOL) isDirty { + @synchronized (self) { + return (self.persistTimer != nil); + } +} + +- (void)retrieve { + @synchronized (self) { + NSMutableArray *queue = [[NSMutableArray alloc] init]; + NSArray *encodedRequests = nil; + + // Capture exception while loading the queue file + @try { + NSError *error = nil; + NSData *data = [NSData dataWithContentsOfURL:self.class.URLForQueueFile options:0 error:&error]; + if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == NSFileReadNoSuchFileError) { + encodedRequests = [NSArray new]; + } else if (!error && data) + encodedRequests = [NSKeyedUnarchiver unarchiveObjectWithData:data]; + if (![encodedRequests isKindOfClass:[NSArray class]]) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:@"Saved server queue is invalid." userInfo:nil]; + } + } + @catch (NSException *exception) { + BNCLogError( + @"An exception occurred while attempting to load the queue file, " + "proceeding without requests. Exception information:\n\n%@.", + [self.class exceptionString:exception] + ); + self.queue = queue; + return; + } + + for (NSData *encodedRequest in encodedRequests) { + BNCServerRequest *request; + + // Capture exceptions while parsing individual request objects + @try { + request = [NSKeyedUnarchiver unarchiveObjectWithData:encodedRequest]; + } + @catch (NSException*) { + BNCLogWarning(@"An exception occurred while attempting to parse a queued request, discarding."); + continue; + } + + // Throw out invalid request types + if (![request isKindOfClass:[BNCServerRequest class]]) { + BNCLogWarning(@"Found an invalid request object, discarding. Object is: %@.", request); + continue; + } + + // Throw out persisted close requests + if ([request isKindOfClass:[BranchCloseRequest class]]) { + continue; + } + + [queue addObject:request]; + } + + self.queue = queue; + } +} + ++ (NSString *)exceptionString:(NSException *)exception { + return [NSString stringWithFormat:@"Name: %@\nReason: %@\nStack:\n\t%@\n\n", + exception.name, exception.reason, + [exception.callStackSymbols componentsJoinedByString:@"\n\t"]]; +} + ++ (NSString *)queueFile_deprecated { + NSString *path = + [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) + firstObject] + stringByAppendingPathComponent:BRANCH_QUEUE_FILE]; + return path; +} + ++ (NSURL* _Nonnull) URLForQueueFile { + NSURL *URL = BNCURLForBranchDirectory(); + URL = [URL URLByAppendingPathComponent:BRANCH_QUEUE_FILE isDirectory:NO]; + return URL; +} + ++ (void) moveOldQueueFile { + NSURL *oldURL = [NSURL fileURLWithPath:self.queueFile_deprecated]; + NSURL *newURL = [self URLForQueueFile]; + + if (!oldURL || !newURL) { return; } + + NSError *error = nil; + [[NSFileManager defaultManager] + moveItemAtURL:oldURL + toURL:newURL + error:&error]; + + if (error && error.code != NSFileNoSuchFileError) { + if (error.code == NSFileWriteFileExistsError) { + [[NSFileManager defaultManager] + removeItemAtURL:oldURL + error:&error]; + } else { + BNCLogError(@"Failed to move the queue file: %@.", error); + } + } +} + ++ (void) initialize { + if (self == [BNCServerRequestQueue self]) { + [self moveOldQueueFile]; + } +} + +#pragma mark - Shared Method + ++ (id)getInstance { + static BNCServerRequestQueue *sharedQueue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^ { + sharedQueue = [[BNCServerRequestQueue alloc] init]; + [sharedQueue retrieve]; + BNCLogDebugSDK(@"Retrieved from storage: %@.", sharedQueue); + }); + return sharedQueue; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerResponse.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerResponse.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/BNCServerResponse.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerResponse.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerResponse.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerResponse.m new file mode 100644 index 00000000000..91d1ce8662e --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/BNCServerResponse.m @@ -0,0 +1,17 @@ +// +// BNCServerResponse.m +// Branch-SDK +// +// Created by Qinwei Gong on 10/10/14. +// Copyright (c) 2014 Branch Metrics. All rights reserved. +// + +#import "BNCServerResponse.h" + +@implementation BNCServerResponse + +- (NSString *)description { + return [NSString stringWithFormat:@"Status: %@; Data: %@", self.statusCode, self.data]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCloseRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCloseRequest.h new file mode 100644 index 00000000000..e4ca5252c04 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCloseRequest.h @@ -0,0 +1,12 @@ +// +// BranchCloseRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/26/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" + +@interface BranchCloseRequest : BNCServerRequest +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCloseRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCloseRequest.m new file mode 100644 index 00000000000..ae4cf7dcaef --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCloseRequest.m @@ -0,0 +1,44 @@ +// +// BranchCloseRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/26/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchCloseRequest.h" +#import "BNCPreferenceHelper.h" +#import "BranchConstants.h" +#import "BranchContentDiscoveryManifest.h" + +@implementation BranchCloseRequest + +- (void)makeRequest:(BNCServerInterface *)serverInterface + key:(NSString *)key + callback:(BNCServerCallback)callback { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + NSDictionary *branchAnalyticsObj = [preferenceHelper getBranchAnalyticsData]; + if (branchAnalyticsObj && branchAnalyticsObj.count > 0) { + NSData *data = + [NSPropertyListSerialization + dataWithPropertyList:branchAnalyticsObj + format:NSPropertyListBinaryFormat_v1_0 + options:0 error:NULL]; + if ([data length] < (NSUInteger) [BranchContentDiscoveryManifest getInstance].maxPktSize) { + params[BRANCH_CONTENT_DISCOVER_KEY] = branchAnalyticsObj; + } + [preferenceHelper clearBranchAnalyticsData]; + } + [serverInterface postRequest:params url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_CLOSE] key:key callback:callback]; + +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + // Nothing to see here +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCreditHistoryRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCreditHistoryRequest.h new file mode 100644 index 00000000000..67dfdd263e5 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCreditHistoryRequest.h @@ -0,0 +1,16 @@ +// +// BranchCreditHistoryRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" +#import "Branch.h" + +@interface BranchCreditHistoryRequest : BNCServerRequest + +- (id)initWithBucket:(NSString *)bucket creditTransactionId:(NSString *)creditTransactionId length:(NSInteger)length order:(BranchCreditHistoryOrder)order callback:(callbackWithList)callback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCreditHistoryRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCreditHistoryRequest.m new file mode 100644 index 00000000000..f70c6402dcb --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchCreditHistoryRequest.m @@ -0,0 +1,104 @@ +// +// BranchCreditHistoryRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchCreditHistoryRequest.h" +#import "BNCPreferenceHelper.h" +#import "BranchConstants.h" + +@interface BranchCreditHistoryRequest () + +@property (strong, nonatomic) callbackWithList callback; +@property (strong, nonatomic) NSString *bucket; +@property (strong, nonatomic) NSString *creditTransactionId; +@property (assign, nonatomic) NSInteger length; +@property (assign, nonatomic) BranchCreditHistoryOrder order; + +@end + +@implementation BranchCreditHistoryRequest + +- (id)initWithBucket:(NSString *)bucket creditTransactionId:(NSString *)creditTransactionId length:(NSInteger)length order:(BranchCreditHistoryOrder)order callback:(callbackWithList)callback { + if ((self = [super init])) { + _bucket = bucket; + _creditTransactionId = creditTransactionId; + _length = length; + _order = order; + _callback = callback; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + params[BRANCH_REQUEST_KEY_LENGTH] = @(self.length); + params[BRANCH_REQUEST_KEY_DIRECTION] = self.order == BranchMostRecentFirst ? @"desc" : @"asc"; + + if (self.bucket) { + params[BRANCH_REQUEST_KEY_BUCKET] = self.bucket; + } + + if (self.creditTransactionId) { + params[BRANCH_REQUEST_KEY_STARTING_TRANSACTION_ID] = self.creditTransactionId; + } + + [serverInterface postRequest:params url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_CREDIT_HISTORY] key:key callback:callback]; +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + if (error) { + if (self.callback) { + self.callback(nil, error); + } + return; + } + + for (NSMutableDictionary *transaction in response.data) { + if ([transaction[BRANCH_RESPONSE_KEY_REFERRER] isEqual:[NSNull null]]) { + [transaction removeObjectForKey:BRANCH_RESPONSE_KEY_REFERRER]; + } + if ([transaction[BRANCH_RESPONSE_KEY_REFERREE] isEqual:[NSNull null]]) { + [transaction removeObjectForKey:BRANCH_RESPONSE_KEY_REFERREE]; + } + } + + if (self.callback) { + self.callback(response.data, nil); + } +} + +#pragma mark - NSSecureCoding methods + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super initWithCoder:decoder])) { + _bucket = [decoder decodeObjectOfClass:NSString.class forKey:@"bucket"]; + _creditTransactionId = [decoder decodeObjectOfClass:NSString.class forKey:@"creditTransactionId"]; + _length = [decoder decodeIntegerForKey:@"length"]; + _order = [decoder decodeIntegerForKey:@"order"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.bucket forKey:@"bucket"]; + [coder encodeObject:self.creditTransactionId forKey:@"creditTransactionId"]; + [coder encodeInteger:self.length forKey:@"length"]; + [coder encodeInteger:self.order forKey:@"order"]; +} + ++ (BOOL) supportsSecureCoding { + return YES; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchInstallRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchInstallRequest.h new file mode 100644 index 00000000000..d1fbaaab43c --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchInstallRequest.h @@ -0,0 +1,13 @@ +// +// BranchInstallRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/26/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchOpenRequest.h" + +@interface BranchInstallRequest : BranchOpenRequest +- (NSString *)getActionName; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchInstallRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchInstallRequest.m new file mode 100644 index 00000000000..e9b2a217bc1 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchInstallRequest.m @@ -0,0 +1,77 @@ +// +// BranchInstallRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/26/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchInstallRequest.h" +#import "BNCPreferenceHelper.h" +#import "BNCSystemObserver.h" +#import "BranchConstants.h" +#import "BNCStrongMatchHelper.h" +#import "BNCEncodingUtils.h" +#import "BNCApplication.h" + +@implementation BranchInstallRequest + +- (id)initWithCallback:(callbackWithStatus)callback { + return [super initWithCallback:callback isInstall:YES]; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + + + [self safeSetValue:[BNCSystemObserver getBundleID] forKey:BRANCH_REQUEST_KEY_BUNDLE_ID onDict:params]; + [self safeSetValue:[BNCSystemObserver getTeamIdentifier] forKey:BRANCH_REQUEST_KEY_TEAM_ID onDict:params]; + [self safeSetValue:[BNCSystemObserver getAppVersion] forKey:BRANCH_REQUEST_KEY_APP_VERSION onDict:params]; + [self safeSetValue:[BNCSystemObserver getDefaultUriScheme] forKey:BRANCH_REQUEST_KEY_URI_SCHEME onDict:params]; + [self safeSetValue:[NSNumber numberWithBool:preferenceHelper.checkedFacebookAppLinks] + forKey:BRANCH_REQUEST_KEY_CHECKED_FACEBOOK_APPLINKS onDict:params]; + [self safeSetValue:[NSNumber numberWithBool:preferenceHelper.checkedAppleSearchAdAttribution] + forKey:BRANCH_REQUEST_KEY_CHECKED_APPLE_AD_ATTRIBUTION onDict:params]; + [self safeSetValue:preferenceHelper.linkClickIdentifier forKey:BRANCH_REQUEST_KEY_LINK_IDENTIFIER onDict:params]; + [self safeSetValue:preferenceHelper.spotlightIdentifier forKey:BRANCH_REQUEST_KEY_SPOTLIGHT_IDENTIFIER onDict:params]; + [self safeSetValue:preferenceHelper.universalLinkUrl forKey:BRANCH_REQUEST_KEY_UNIVERSAL_LINK_URL onDict:params]; + + params[BRANCH_REQUEST_KEY_DEBUG] = @(preferenceHelper.isDebug); + + if (preferenceHelper.appleSearchAdNeedsSend) { + NSString *encodedSearchData = nil; + @try { + NSData *jsonData = [BNCEncodingUtils encodeDictionaryToJsonData:preferenceHelper.appleSearchAdDetails]; + encodedSearchData = [BNCEncodingUtils base64EncodeData:jsonData]; + } @catch (id) { } + [self safeSetValue:encodedSearchData + forKey:BRANCH_REQUEST_KEY_SEARCH_AD + onDict:params]; + } + + BNCApplication *application = [BNCApplication currentApplication]; + params[@"lastest_update_time"] = BNCWireFormatFromDate(application.currentBuildDate); + params[@"previous_update_time"] = BNCWireFormatFromDate(preferenceHelper.previousAppBuildDate); + params[@"latest_install_time"] = BNCWireFormatFromDate(application.currentInstallDate); + params[@"first_install_time"] = BNCWireFormatFromDate(application.firstInstallDate); + params[@"update"] = [self.class appUpdateState]; + + if ([[BNCStrongMatchHelper strongMatchHelper] shouldDelayInstallRequest]) { + NSInteger delay = 750; + if (preferenceHelper.installRequestDelay > 0) { + delay = preferenceHelper.installRequestDelay; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_MSEC)), dispatch_get_main_queue(), ^{ + [serverInterface postRequest:params url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_INSTALL] key:key callback:callback]; + }); + } else { + [serverInterface postRequest:params url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_INSTALL] key:key callback:callback]; + } +} + +- (NSString *)getActionName { + return @"install"; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLoadRewardsRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLoadRewardsRequest.h new file mode 100644 index 00000000000..caa63096dd6 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLoadRewardsRequest.h @@ -0,0 +1,16 @@ +// +// BranchLoadRewardsRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" +#import "Branch.h" + +@interface BranchLoadRewardsRequest : BNCServerRequest + +- (id)initWithCallback:(callbackWithStatus)callback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLoadRewardsRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLoadRewardsRequest.m new file mode 100644 index 00000000000..2a2d5506b38 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLoadRewardsRequest.m @@ -0,0 +1,88 @@ +// +// BranchLoadRewardsRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchLoadRewardsRequest.h" +#import "BNCPreferenceHelper.h" +#import "BranchConstants.h" + +@interface BranchLoadRewardsRequest () + +@property (copy) callbackWithStatus callback; + +@end + +@implementation BranchLoadRewardsRequest + +- (id)initWithCallback:(callbackWithStatus)callback { + if ((self = [super init])) { + _callback = callback; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSString *endpoint = [BRANCH_REQUEST_ENDPOINT_LOAD_REWARDS stringByAppendingPathComponent:preferenceHelper.identityID]; + [serverInterface getRequest:nil url:[preferenceHelper getAPIURL:endpoint] key:key callback:callback]; +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + if (error) { + if (self.callback) { + self.callback(NO, error); + } + return; + } + + BOOL hasUpdated = NO; + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + [preferenceHelper synchronize]; + NSDictionary *currentCreditDictionary = [preferenceHelper getCreditDictionary]; + NSArray *responseKeys = [response.data allKeys]; + NSArray *storedKeys = [currentCreditDictionary allKeys]; + + if ([responseKeys count] && ([response.data isKindOfClass:[NSDictionary class]] || [response.data isKindOfClass:[NSMutableDictionary class]])) { + + for (NSString *key in response.data) { + if (![key isKindOfClass:[NSString class]]) { continue; } + + NSInteger credits = [preferenceHelper getCreditCountForBucket:key]; + if (response.data[key] && [response.data[key] respondsToSelector:@selector(integerValue)]) { + credits = [response.data[key] integerValue]; + } + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + if (credits != [preferenceHelper getCreditCountForBucket:key]) { + hasUpdated = YES; + } + + [preferenceHelper setCreditCount:credits forBucket:key]; + } + for(NSString *key in storedKeys) { + if (![key isKindOfClass:[NSString class]]) { continue; } + + if(![response.data objectForKey:key]) { + [preferenceHelper removeCreditCountForBucket:key]; + hasUpdated = YES; + } + } + } else { + if ([storedKeys count]) { + [preferenceHelper clearUserCredits]; + hasUpdated = YES; + } + } + + [preferenceHelper synchronize]; + if (self.callback) { + self.callback(hasUpdated, nil); + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLogoutRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLogoutRequest.h new file mode 100644 index 00000000000..8b9db30c4ea --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLogoutRequest.h @@ -0,0 +1,16 @@ +// +// BranchLogoutRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" +#import "Branch.h" + +@interface BranchLogoutRequest : BNCServerRequest + +- (id)initWithCallback:(callbackWithStatus)callback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLogoutRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLogoutRequest.m new file mode 100644 index 00000000000..9da8904184f --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchLogoutRequest.m @@ -0,0 +1,61 @@ +// +// BranchLogoutRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + + +#import "BranchLogoutRequest.h" +#import "BNCPreferenceHelper.h" +#import "BranchConstants.h" +#import "BNCEncodingUtils.h" + +@interface BranchLogoutRequest () +@property (copy) callbackWithStatus callback; +@end + + +@implementation BranchLogoutRequest + +- (id)initWithCallback:(callbackWithStatus)callback { + if ((self = [super init])) { + _callback = callback; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSMutableDictionary *params = [NSMutableDictionary new]; + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + [serverInterface postRequest:params url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_LOGOUT] key:key callback:callback]; +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + if (error) { + if (self.callback) { + self.callback(NO, error); + } + return; + } + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + preferenceHelper.sessionID = response.data[BRANCH_RESPONSE_KEY_SESSION_ID]; + preferenceHelper.identityID = BNCStringFromWireFormat(response.data[BRANCH_RESPONSE_KEY_BRANCH_IDENTITY]); + preferenceHelper.userUrl = response.data[BRANCH_RESPONSE_KEY_USER_URL]; + preferenceHelper.userIdentity = nil; + preferenceHelper.installParams = nil; + preferenceHelper.sessionParams = nil; + [preferenceHelper clearUserCreditsAndCounts]; + + if (self.callback) { + self.callback(YES, nil); + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchOpenRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchOpenRequest.h new file mode 100644 index 00000000000..fb988ff10fb --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchOpenRequest.h @@ -0,0 +1,24 @@ +// +// BranchOpenRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/26/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" +#import "BNCCallbacks.h" + +@interface BranchOpenRequest : BNCServerRequest + +@property (copy) callbackWithStatus callback; + ++ (void) waitForOpenResponseLock; ++ (void) releaseOpenResponseLock; ++ (void) setWaitNeededForOpenResponseLock; + +- (id)initWithCallback:(callbackWithStatus)callback; +- (id)initWithCallback:(callbackWithStatus)callback isInstall:(BOOL)isInstall; ++ (NSNumber*) appUpdateState; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchOpenRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchOpenRequest.m new file mode 100644 index 00000000000..3fdc9999a22 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchOpenRequest.m @@ -0,0 +1,335 @@ +// +// BranchOpenRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/26/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchOpenRequest.h" +#import "BNCSystemObserver.h" +#import "BranchConstants.h" +#import "BranchContentDiscoveryManifest.h" +#import "BranchContentDiscoverer.h" +#import "NSMutableDictionary+Branch.h" +#import "BNCEncodingUtils.h" +#import "BNCCrashlyticsWrapper.h" +#import "BNCFabricAnswers.h" +#import "BNCDeviceInfo.h" +#import "Branch.h" +#import "BNCApplication.h" + +@interface BranchOpenRequest () +@property (assign, nonatomic) BOOL isInstall; +@end + + +@implementation BranchOpenRequest + +- (id)initWithCallback:(callbackWithStatus)callback { + return [self initWithCallback:callback isInstall:NO]; +} + +- (id)initWithCallback:(callbackWithStatus)callback isInstall:(BOOL)isInstall { + if ((self = [super init])) { + _callback = callback; + _isInstall = isInstall; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + if (preferenceHelper.deviceFingerprintID) { + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + } + + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + params[BRANCH_REQUEST_KEY_DEBUG] = @(preferenceHelper.isDebug); + + [self safeSetValue:[BNCSystemObserver getBundleID] forKey:BRANCH_REQUEST_KEY_BUNDLE_ID onDict:params]; + [self safeSetValue:[BNCSystemObserver getTeamIdentifier] forKey:BRANCH_REQUEST_KEY_TEAM_ID onDict:params]; + [self safeSetValue:[BNCSystemObserver getAppVersion] forKey:BRANCH_REQUEST_KEY_APP_VERSION onDict:params]; + [self safeSetValue:[BNCSystemObserver getDefaultUriScheme] forKey:BRANCH_REQUEST_KEY_URI_SCHEME onDict:params]; + [self safeSetValue:[NSNumber numberWithBool:preferenceHelper.checkedFacebookAppLinks] + forKey:BRANCH_REQUEST_KEY_CHECKED_FACEBOOK_APPLINKS onDict:params]; + [self safeSetValue:[NSNumber numberWithBool:preferenceHelper.checkedAppleSearchAdAttribution] + forKey:BRANCH_REQUEST_KEY_CHECKED_APPLE_AD_ATTRIBUTION onDict:params]; + [self safeSetValue:preferenceHelper.linkClickIdentifier forKey:BRANCH_REQUEST_KEY_LINK_IDENTIFIER onDict:params]; + [self safeSetValue:preferenceHelper.spotlightIdentifier forKey:BRANCH_REQUEST_KEY_SPOTLIGHT_IDENTIFIER onDict:params]; + [self safeSetValue:preferenceHelper.universalLinkUrl forKey:BRANCH_REQUEST_KEY_UNIVERSAL_LINK_URL onDict:params]; + [self safeSetValue:preferenceHelper.externalIntentURI forKey:BRANCH_REQUEST_KEY_EXTERNAL_INTENT_URI onDict:params]; + if (preferenceHelper.limitFacebookTracking) + params[@"limit_facebook_tracking"] = (__bridge NSNumber*) kCFBooleanTrue; + + NSMutableDictionary *cdDict = [[NSMutableDictionary alloc] init]; + BranchContentDiscoveryManifest *contentDiscoveryManifest = [BranchContentDiscoveryManifest getInstance]; + [cdDict bnc_safeSetObject:[contentDiscoveryManifest getManifestVersion] forKey:BRANCH_MANIFEST_VERSION_KEY]; + [cdDict bnc_safeSetObject:[BNCSystemObserver getBundleID] forKey:BRANCH_BUNDLE_IDENTIFIER]; + [self safeSetValue:cdDict forKey:BRANCH_CONTENT_DISCOVER_KEY onDict:params]; + + if (preferenceHelper.appleSearchAdNeedsSend) { + NSString *encodedSearchData = nil; + @try { + NSData *jsonData = [BNCEncodingUtils encodeDictionaryToJsonData:preferenceHelper.appleSearchAdDetails]; + encodedSearchData = [BNCEncodingUtils base64EncodeData:jsonData]; + } @catch (id) { } + [self safeSetValue:encodedSearchData + forKey:BRANCH_REQUEST_KEY_SEARCH_AD + onDict:params]; + } + + BNCApplication *application = [BNCApplication currentApplication]; + params[@"lastest_update_time"] = BNCWireFormatFromDate(application.currentBuildDate); + params[@"previous_update_time"] = BNCWireFormatFromDate(preferenceHelper.previousAppBuildDate); + params[@"latest_install_time"] = BNCWireFormatFromDate(application.currentInstallDate); + params[@"first_install_time"] = BNCWireFormatFromDate(application.firstInstallDate); + params[@"update"] = [self.class appUpdateState]; + + [serverInterface postRequest:params + url:[preferenceHelper + getAPIURL:BRANCH_REQUEST_ENDPOINT_OPEN] + key:key + callback:callback]; +} + +typedef NS_ENUM(NSInteger, BNCUpdateState) { + BNCUpdateStateInstall = 0, // App was recently installed. + BNCUpdateStateNonUpdate = 1, // App was neither newly installed nor updated. + BNCUpdateStateUpdate = 2, // App was recently updated. + +// BNCUpdateStateError = 3, // Error determining update state. +// BNCUpdateStateReinstall = 4 // App was re-installed. +}; + ++ (NSNumber*) appUpdateState { + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + BNCApplication *application = [BNCApplication currentApplication]; + NSTimeInterval first_install_time = application.firstInstallDate.timeIntervalSince1970; + NSTimeInterval latest_install_time = application.currentInstallDate.timeIntervalSince1970; + NSTimeInterval latest_update_time = application.currentBuildDate.timeIntervalSince1970; + NSTimeInterval previous_update_time = preferenceHelper.previousAppBuildDate.timeIntervalSince1970; + NSTimeInterval const kOneDay = 1.0 * 24.0 * 60.0 * 60.0; + + BNCUpdateState update_state = 0; + if (first_install_time <= 0.0 || + latest_install_time <= 0.0 || + latest_update_time <= 0.0 || + previous_update_time > latest_update_time) + update_state = BNCUpdateStateNonUpdate; // Error: Send Non-update. + else + if ((latest_update_time - kOneDay) <= first_install_time && previous_update_time <= 0) + update_state = BNCUpdateStateInstall; + else + if (first_install_time < latest_install_time && previous_update_time <= 0) + update_state = BNCUpdateStateUpdate; // Re-install: Send Update. + else + if (latest_update_time > first_install_time && previous_update_time < latest_update_time) + update_state = BNCUpdateStateUpdate; + else + update_state = BNCUpdateStateNonUpdate; + + return @(update_state); +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + if (error && preferenceHelper.blacklistURLOpen) { + // Ignore this response from the server. Dummy up a response: + error = nil; + response.data = @{ + BRANCH_RESPONSE_KEY_SESSION_DATA: @{ + BRANCH_RESPONSE_KEY_CLICKED_BRANCH_LINK: @0 + } + }; + } else + if (error) { + [BranchOpenRequest releaseOpenResponseLock]; + if (self.callback) { + self.callback(NO, error); + } + return; + } + + NSDictionary *data = response.data; + + // Handle possibly mis-parsed identity. + id userIdentity = data[BRANCH_RESPONSE_KEY_DEVELOPER_IDENTITY]; + if ([userIdentity isKindOfClass:[NSNumber class]]) { + userIdentity = [userIdentity stringValue]; + } + + preferenceHelper.deviceFingerprintID = data[BRANCH_RESPONSE_KEY_DEVICE_FINGERPRINT_ID]; + preferenceHelper.userUrl = data[BRANCH_RESPONSE_KEY_USER_URL]; + preferenceHelper.userIdentity = userIdentity; + preferenceHelper.sessionID = data[BRANCH_RESPONSE_KEY_SESSION_ID]; + preferenceHelper.previousAppBuildDate = [BNCApplication currentApplication].currentBuildDate; + + if (Branch.enableFingerprintIDInCrashlyticsReports) { + BNCCrashlyticsWrapper *crashlytics = [BNCCrashlyticsWrapper wrapper]; + [crashlytics setObjectValue:preferenceHelper.deviceFingerprintID + forKey:BRANCH_CRASHLYTICS_FINGERPRINT_ID_KEY]; + } + + NSString *sessionData = data[BRANCH_RESPONSE_KEY_SESSION_DATA]; + if (sessionData == nil || [sessionData isKindOfClass:[NSString class]]) { + } else + if ([sessionData isKindOfClass:[NSDictionary class]]) { + BNCLogWarning(@"Received session data of type '%@' data is '%@'.", + NSStringFromClass(sessionData.class), sessionData); + sessionData = [BNCEncodingUtils encodeDictionaryToJsonString:(NSDictionary*)sessionData]; + } else + if ([sessionData isKindOfClass:[NSArray class]]) { + BNCLogWarning(@"Received session data of type '%@' data is '%@'.", + NSStringFromClass(sessionData.class), sessionData); + sessionData = [BNCEncodingUtils encodeArrayToJsonString:(NSArray*)sessionData]; + } else { + BNCLogError(@"Received session data of type '%@' data is '%@'.", + NSStringFromClass(sessionData.class), sessionData); + sessionData = nil; + } + + // Update session params + + if (preferenceHelper.spotlightIdentifier) { + NSMutableDictionary *sessionDataDict = + [NSMutableDictionary dictionaryWithDictionary: [BNCEncodingUtils decodeJsonStringToDictionary:sessionData]]; + NSDictionary *spotlightDic = @{BRANCH_RESPONSE_KEY_SPOTLIGHT_IDENTIFIER:preferenceHelper.spotlightIdentifier}; + [sessionDataDict addEntriesFromDictionary:spotlightDic]; + sessionData = [BNCEncodingUtils encodeDictionaryToJsonString:sessionDataDict]; + } + + preferenceHelper.sessionParams = sessionData; + + // Scenarios: + // If no data, data isn't from a link click, or isReferrable is false, don't set, period. + // Otherwise, + // * On Install: set. + // * On Open and installParams set: don't set. + // * On Open and stored installParams are empty: set. + if (sessionData.length) { + NSDictionary *sessionDataDict = [BNCEncodingUtils decodeJsonStringToDictionary:sessionData]; + BOOL dataIsFromALinkClick = [sessionDataDict[BRANCH_RESPONSE_KEY_CLICKED_BRANCH_LINK] isEqual:@1]; + BOOL storedParamsAreEmpty = YES; + + if ([preferenceHelper.installParams isKindOfClass:[NSString class]]) { + storedParamsAreEmpty = !preferenceHelper.installParams.length; + } + + if (dataIsFromALinkClick && (self.isInstall || storedParamsAreEmpty)) { + preferenceHelper.installParams = sessionData; + } + + if (dataIsFromALinkClick) { + NSString * eventName = + [@"Branch " stringByAppendingString:[[self getActionName] capitalizedString]]; + [BNCFabricAnswers sendEventWithName:eventName andAttributes:sessionDataDict]; + } + } + + NSString *referringURL = nil; + if (preferenceHelper.universalLinkUrl.length) { + referringURL = preferenceHelper.universalLinkUrl; + } + else if (preferenceHelper.externalIntentURI.length) { + referringURL = preferenceHelper.externalIntentURI; + } + else { + NSDictionary *sessionDataDict = [BNCEncodingUtils decodeJsonStringToDictionary:sessionData]; + NSString *link = sessionDataDict[BRANCH_RESPONSE_KEY_BRANCH_REFERRING_LINK]; + if (link.length) referringURL = link; + } + + // Clear link identifiers so they don't get reused on the next open + preferenceHelper.checkedFacebookAppLinks = NO; + preferenceHelper.linkClickIdentifier = nil; + preferenceHelper.spotlightIdentifier = nil; + preferenceHelper.universalLinkUrl = nil; + preferenceHelper.externalIntentURI = nil; + preferenceHelper.appleSearchAdNeedsSend = NO; + preferenceHelper.referringURL = referringURL; + preferenceHelper.blacklistURLOpen = NO; + + NSString *string = BNCStringFromWireFormat(data[BRANCH_RESPONSE_KEY_BRANCH_IDENTITY]); + if (string) preferenceHelper.identityID = string; + + [BranchOpenRequest releaseOpenResponseLock]; + + BranchContentDiscoveryManifest *cdManifest = [BranchContentDiscoveryManifest getInstance]; + [cdManifest onBranchInitialised:data withUrl:referringURL]; + if ([cdManifest isCDEnabled]) { + [[BranchContentDiscoverer getInstance] startDiscoveryTaskWithManifest:cdManifest]; + } + + // Check if there is any Branch View to show + NSObject *branchViewDict = data[BRANCH_RESPONSE_KEY_BRANCH_VIEW_DATA]; + if ([branchViewDict isKindOfClass:[NSDictionary class]]) { + [[BranchViewHandler getInstance] + showBranchView:[self getActionName] + withBranchViewDictionary:(NSDictionary *)branchViewDict + andWithDelegate:nil]; + } + + if (self.callback) { + self.callback(YES, nil); + } +} + +- (NSString *)getActionName { + return @"open"; +} + + +#pragma - Open Response Lock Handling + + +// Instead of semaphores, the lock is handled by scheduled dispatch_queues. +// This is the 'new' way to lock and is handled better optimized for iOS. +// Also, since implied lock is handled by a scheduler and not a hard semaphore it's less error +// prone. + + +static dispatch_queue_t openRequestWaitQueue = NULL; +static BOOL openRequestWaitQueueIsSuspended = NO; + + ++ (void) initialize { + if (self != [BranchOpenRequest self]) + return; + openRequestWaitQueue = + dispatch_queue_create("io.branch.sdk.openqueue", DISPATCH_QUEUE_CONCURRENT); +} + ++ (void) setWaitNeededForOpenResponseLock { + @synchronized (self) { + if (!openRequestWaitQueueIsSuspended) { + BNCLogDebugSDK(@"Suspended for openRequestWaitQueue."); + openRequestWaitQueueIsSuspended = YES; + dispatch_suspend(openRequestWaitQueue); + } + } +} + ++ (void) waitForOpenResponseLock { + BNCLogDebugSDK(@"Waiting for openRequestWaitQueue."); + [BNCDeviceInfo userAgentString]; // Make sure we do this lock first to prevent a deadlock. + dispatch_sync(openRequestWaitQueue, ^ { + BNCLogDebugSDK(@"Finished waitForOpenResponseLock."); + }); +} + ++ (void) releaseOpenResponseLock { + @synchronized (self) { + if (openRequestWaitQueueIsSuspended) { + BNCLogDebugSDK(@"Resuming openRequestWaitQueue."); + openRequestWaitQueueIsSuspended = NO; + dispatch_resume(openRequestWaitQueue); + } + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRedeemRewardsRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRedeemRewardsRequest.h new file mode 100644 index 00000000000..912be7eefde --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRedeemRewardsRequest.h @@ -0,0 +1,16 @@ +// +// BranchRedeemRewardsRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" +#import "Branch.h" + +@interface BranchRedeemRewardsRequest : BNCServerRequest + +- (id)initWithAmount:(NSInteger)amount bucket:(NSString *)bucket callback:(callbackWithStatus)callback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRedeemRewardsRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRedeemRewardsRequest.m new file mode 100644 index 00000000000..a21ab248c0c --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRedeemRewardsRequest.m @@ -0,0 +1,78 @@ +// +// BranchRedeemRewardsRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchRedeemRewardsRequest.h" +#import "BNCPreferenceHelper.h" +#import "BranchConstants.h" + +@interface BranchRedeemRewardsRequest () +@property (assign, nonatomic) NSInteger amount; +@property (strong, nonatomic) NSString *bucket; +@property (copy) callbackWithStatus callback; +@end + +@implementation BranchRedeemRewardsRequest + +- (id)initWithAmount:(NSInteger)amount bucket:(NSString *)bucket callback:(callbackWithStatus)callback { + if ((self = [super init])) { + _amount = amount; + _bucket = bucket; + _callback = callback; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSMutableDictionary *params = [NSMutableDictionary new]; + params[BRANCH_REQUEST_KEY_BUCKET] = self.bucket; + params[BRANCH_REQUEST_KEY_AMOUNT] = @(self.amount); + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + [serverInterface postRequest:params url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_REDEEM_REWARDS] key:key callback:callback]; +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + if (error) { + if (self.callback) { + self.callback(NO, error); + } + return; + } + + // Update local balance + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSInteger currentAvailableCredits = [preferenceHelper getCreditCountForBucket:self.bucket]; + NSInteger updatedBalance = currentAvailableCredits - self.amount; + [preferenceHelper setCreditCount:updatedBalance forBucket:self.bucket]; + + if (self.callback) { + self.callback(YES, nil); + } +} + +#pragma mark - NSSecureCoding methods + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super initWithCoder:decoder])) { + _amount = [decoder decodeIntegerForKey:@"amount"]; + _bucket = [decoder decodeObjectOfClass:NSString.class forKey:@"bucket"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeInteger:self.amount forKey:@"amount"]; + [coder encodeObject:self.bucket forKey:@"bucket"]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRegisterViewRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRegisterViewRequest.h new file mode 100644 index 00000000000..5b17d6014f2 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRegisterViewRequest.h @@ -0,0 +1,16 @@ +// +// BranchRegisterViewRequest.h +// Branch-TestBed +// +// Created by Derrick Staten on 10/16/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" +#import "Branch.h" + +@interface BranchRegisterViewRequest : BNCServerRequest + +- (id)initWithParams:(NSDictionary *)params andCallback:(callbackWithParams)callback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRegisterViewRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRegisterViewRequest.m new file mode 100644 index 00000000000..d030b4195d2 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchRegisterViewRequest.m @@ -0,0 +1,80 @@ +// +// BranchRegisterViewRequest.m +// Branch-TestBed +// +// Created by Derrick Staten on 10/16/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#import "BranchRegisterViewRequest.h" +#import "BNCPreferenceHelper.h" +#import "BranchConstants.h" +#import "BNCSystemObserver.h" + +@interface BranchRegisterViewRequest () + +@property (strong, nonatomic) NSDictionary *params; +@property (strong, nonatomic) callbackWithParams callback; + +@end + +@implementation BranchRegisterViewRequest + +- (id)initWithParams:(NSDictionary *)params andCallback:(callbackWithParams)callback { + if ((self = [super init])) { + _params = params; + if (!_params) { + _params = [[NSDictionary alloc] init]; + } + _callback = callback; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + NSMutableDictionary *data = [NSMutableDictionary dictionary]; + if (self.params) { + data[BRANCH_REQUEST_KEY_URL_DATA] = [self.params copy]; + } + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + [self safeSetValue:preferenceHelper.deviceFingerprintID forKey:BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID onDict:data]; + [self safeSetValue:preferenceHelper.identityID forKey:BRANCH_REQUEST_KEY_BRANCH_IDENTITY onDict:data]; + [self safeSetValue:preferenceHelper.sessionID forKey:BRANCH_REQUEST_KEY_SESSION_ID onDict:data]; + [self safeSetValue:@(preferenceHelper.isDebug) forKey:BRANCH_REQUEST_KEY_DEBUG onDict:data]; + [self safeSetValue:@([BNCSystemObserver isSimulator]) forKey:BRANCH_REQUEST_KEY_IS_SIMULATOR onDict:data]; + + [self safeSetValue:[BNCSystemObserver getAppVersion] forKey:BRANCH_REQUEST_KEY_APP_VERSION onDict:data]; + + [serverInterface postRequest:data url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_REGISTER_VIEW] key:key callback:callback]; +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + if (error) { + if (self.callback) { + self.callback([[NSDictionary alloc] init], error); + } + return; + } + + if (self.callback) { + self.callback(response.data, error); + } +} + +#pragma mark - NSCoding methods + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super initWithCoder:decoder])) { + _params = [decoder decodeObjectOfClass:NSDictionary.class forKey:@"params"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.params forKey:@"params"]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSetIdentityRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSetIdentityRequest.h new file mode 100644 index 00000000000..97928d7b278 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSetIdentityRequest.h @@ -0,0 +1,16 @@ +// +// BranchSetIdentityRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "Branch.h" +#import "BNCServerRequest.h" + +@interface BranchSetIdentityRequest : BNCServerRequest + +- (id)initWithUserId:(NSString *)userId callback:(callbackWithParams)callback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSetIdentityRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSetIdentityRequest.m new file mode 100644 index 00000000000..dd7bcb78681 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSetIdentityRequest.m @@ -0,0 +1,89 @@ +// +// BranchSetIdentityRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchSetIdentityRequest.h" +#import "BNCPreferenceHelper.h" +#import "BNCEncodingUtils.h" +#import "BranchConstants.h" + +@interface BranchSetIdentityRequest () + +@property (strong, nonatomic) NSString *userId; +@property (strong, nonatomic) callbackWithParams callback; +@property (assign, nonatomic) BOOL shouldCallCallback; + +@end + +@implementation BranchSetIdentityRequest + +- (id)initWithUserId:(NSString *)userId callback:(callbackWithParams)callback { + if ((self = [super init])) { + _userId = userId; + _callback = callback; + _shouldCallCallback = YES; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key callback:(BNCServerCallback)callback { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSMutableDictionary *params = [NSMutableDictionary new]; + params[BRANCH_REQUEST_KEY_DEVELOPER_IDENTITY] = self.userId; + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + [serverInterface postRequest:params url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_SET_IDENTITY] key:key callback:callback]; +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + if (error) { + if (self.callback && self.shouldCallCallback) { + self.callback([[NSDictionary alloc] init], error); + } + + self.shouldCallCallback = NO; // don't call the callback next time around + return; + } + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + preferenceHelper.identityID = BNCStringFromWireFormat(response.data[BRANCH_RESPONSE_KEY_BRANCH_IDENTITY]); + preferenceHelper.userUrl = response.data[BRANCH_RESPONSE_KEY_USER_URL]; + preferenceHelper.userIdentity = self.userId; + if (response.data[BRANCH_RESPONSE_KEY_SESSION_ID]) { + preferenceHelper.sessionID = response.data[BRANCH_RESPONSE_KEY_SESSION_ID]; + } + + if (response.data[BRANCH_RESPONSE_KEY_INSTALL_PARAMS]) { + preferenceHelper.installParams = response.data[BRANCH_RESPONSE_KEY_INSTALL_PARAMS]; + } + + if (self.callback && self.shouldCallCallback) { + NSString *storedParams = preferenceHelper.installParams; + NSDictionary *installParams = [BNCEncodingUtils decodeJsonStringToDictionary:storedParams]; + self.callback(installParams, nil); + } +} + +#pragma mark - NSCoding methods + +// No need to do anything with callback, as the callback itself is gone after the end of a run + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super initWithCoder:decoder])) { + _userId = [decoder decodeObjectOfClass:NSString.class forKey:@"userId"]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.userId forKey:@"userId"]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlRequest.h new file mode 100644 index 00000000000..10ae8f8184f --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlRequest.h @@ -0,0 +1,18 @@ +// +// BranchShortUrlRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/26/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BNCServerRequest.h" +#import "Branch.h" + +@interface BranchShortUrlRequest : BNCServerRequest + +@property (nonatomic, assign) BOOL isSpotlightRequest; + +- (id)initWithTags:(NSArray *)tags alias:(NSString *)alias type:(BranchLinkType)type matchDuration:(NSInteger)duration channel:(NSString *)channel feature:(NSString *)feature stage:(NSString *)stage campaign:(NSString *)campaign params:(NSDictionary *)params linkData:(BNCLinkData *)linkData linkCache:(BNCLinkCache *)linkCache callback:(callbackWithUrl)callback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlRequest.m new file mode 100644 index 00000000000..3330594f9a7 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlRequest.m @@ -0,0 +1,181 @@ +// +// BranchShortUrlRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/26/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchShortUrlRequest.h" +#import "BNCPreferenceHelper.h" +#import "BNCEncodingUtils.h" +#import "BranchConstants.h" + +@interface BranchShortUrlRequest () + +@property (strong, nonatomic) NSArray *tags; +@property (strong, nonatomic) NSString *alias; +@property (assign, nonatomic) BranchLinkType type; +@property (assign, nonatomic) NSInteger matchDuration; +@property (strong, nonatomic) NSString *channel; +@property (strong, nonatomic) NSString *feature; +@property (strong, nonatomic) NSString *stage; +@property (strong, nonatomic) NSString *campaign; +@property (strong, nonatomic) NSDictionary *params; +@property (strong, nonatomic) BNCLinkCache *linkCache; +@property (strong, nonatomic) BNCLinkData *linkData; +@property (strong, nonatomic) callbackWithUrl callback; + +@end + +@implementation BranchShortUrlRequest + +- (id)initWithTags:(NSArray *)tags alias:(NSString *)alias type:(BranchLinkType)type matchDuration:(NSInteger)duration channel:(NSString *)channel feature:(NSString *)feature stage:(NSString *)stage campaign:campaign params:(NSDictionary *)params linkData:(BNCLinkData *)linkData linkCache:(BNCLinkCache *)linkCache callback:(callbackWithUrl)callback { + if ((self = [super init])) { + _tags = tags; + _alias = alias; + _type = type; + _matchDuration = duration; + _channel = channel; + _feature = feature; + _stage = stage; + _campaign = campaign; + _params = params; + _callback = callback; + _linkCache = linkCache; + _linkData = linkData; + _isSpotlightRequest = NO; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface + key:(NSString *)key + callback:(BNCServerCallback)callback { + NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithDictionary:self.linkData.data]; + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + if (!preferenceHelper.trackingDisabled) { + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + if (!_isSpotlightRequest) + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + } + + [serverInterface postRequest:params + url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_GET_SHORT_URL] + key:key + callback:callback]; +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + if (error) { + if (self.callback) { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSString *baseUrl = preferenceHelper.userUrl; + if (baseUrl.length) + baseUrl = [preferenceHelper sanitizedMutableBaseURL:baseUrl]; + else + if (Branch.branchKeyIsSet) { + baseUrl = [[NSMutableString alloc] initWithFormat:@"%@/a/%@?", + BNC_LINK_URL, + Branch.branchKey]; + } + if (baseUrl) + baseUrl = [self createLongUrlForUserUrl:baseUrl]; + self.callback(baseUrl, error); + } + return; + } + + NSString *url = response.data[BRANCH_RESPONSE_KEY_URL]; + + // cache the link + if (url) { + [self.linkCache setObject:url forKey:self.linkData]; + } + if (self.callback) { + self.callback(url, nil); + } +} + +- (NSString *)createLongUrlForUserUrl:(NSString *)userUrl { + NSMutableString *longUrl = [[BNCPreferenceHelper preferenceHelper] sanitizedMutableBaseURL:userUrl]; + for (NSString *tag in self.tags) { + [longUrl appendFormat:@"tags=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:tag]]; + } + + if ([self.alias length]) { + [longUrl appendFormat:@"alias=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:self.alias]]; + } + + if ([self.channel length]) { + [longUrl appendFormat:@"channel=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:self.channel]]; + } + + if ([self.feature length]) { + [longUrl appendFormat:@"feature=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:self.feature]]; + } + + if ([self.stage length]) { + [longUrl appendFormat:@"stage=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:self.stage]]; + } + if (self.type) { + [longUrl appendFormat:@"type=%ld&", (long)self.type]; + } + if (self.matchDuration) { + [longUrl appendFormat:@"duration=%ld&", (long)self.matchDuration]; + } + + NSData *jsonData = [BNCEncodingUtils encodeDictionaryToJsonData:self.params]; + NSString *base64EncodedParams = [BNCEncodingUtils base64EncodeData:jsonData]; + [longUrl appendFormat:@"source=ios&data=%@", base64EncodedParams]; + + return longUrl; +} + +#pragma mark - NSCoding methods + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super initWithCoder:decoder])) { + _tags = [decoder decodeObjectOfClass:NSArray.class forKey:@"tags"]; + _alias = [decoder decodeObjectOfClass:NSString.class forKey:@"alias"]; + _type = [decoder decodeIntegerForKey:@"type"]; + _matchDuration = [decoder decodeIntegerForKey:@"duration"]; + _channel = [decoder decodeObjectOfClass:NSString.class forKey:@"channel"]; + _feature = [decoder decodeObjectOfClass:NSString.class forKey:@"feature"]; + _stage = [decoder decodeObjectOfClass:NSString.class forKey:@"stage"]; + _campaign = [decoder decodeObjectOfClass:NSString.class forKey:@"campaign"]; + _params = [BNCEncodingUtils decodeJsonStringToDictionary: + [decoder decodeObjectOfClass:NSString.class forKey:@"params"]]; + + // Set up link data + self.linkData = [[BNCLinkData alloc] init]; + [self.linkData setupType:_type]; + [self.linkData setupTags:_tags]; + [self.linkData setupChannel:_channel]; + [self.linkData setupFeature:_feature]; + [self.linkData setupStage:_stage]; + [self.linkData setupCampaign:_campaign]; + [self.linkData setupAlias:_alias]; + [self.linkData setupMatchDuration:_matchDuration]; + [self.linkData setupParams:_params]; + } + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.tags forKey:@"tags"]; + [coder encodeObject:self.alias forKey:@"alias"]; + [coder encodeInteger:self.type forKey:@"type"]; + [coder encodeInteger:self.matchDuration forKey:@"duration"]; + [coder encodeObject:self.channel forKey:@"channel"]; + [coder encodeObject:self.feature forKey:@"feature"]; + [coder encodeObject:self.stage forKey:@"stage"]; + [coder encodeObject:self.campaign forKey:@"campaign"]; + [coder encodeObject:[BNCEncodingUtils encodeDictionaryToJsonString:self.params] forKey:@"params"]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlSyncRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlSyncRequest.h new file mode 100644 index 00000000000..4d45bad17c2 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlSyncRequest.h @@ -0,0 +1,39 @@ +// +// BranchShortUrlSyncRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/27/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "Branch.h" + +@interface BranchShortUrlSyncRequest : NSObject + +- (id)initWithTags:(NSArray *)tags + alias:(NSString *)alias + type:(BranchLinkType)type + matchDuration:(NSInteger)duration + channel:(NSString *)channel + feature:(NSString *)feature + stage:(NSString *)stage + campaign:(NSString *)campaign + params:(NSDictionary *)params + linkData:(BNCLinkData *)linkData + linkCache:(BNCLinkCache *)linkCache; + +- (BNCServerResponse *)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key; + +- (NSString *)processResponse:(BNCServerResponse *)response; + ++ (NSString *)createLinkFromBranchKey:(NSString *)branchKey + tags:(NSArray *)tags + alias:(NSString *)alias + type:(BranchLinkType)type + matchDuration:(NSInteger)duration + channel:(NSString *)channel + feature:(NSString *)feature + stage:(NSString *)stage + params:(NSDictionary *)params; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlSyncRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlSyncRequest.m new file mode 100644 index 00000000000..8ea7942c4f0 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchShortUrlSyncRequest.m @@ -0,0 +1,149 @@ +// +// BranchShortUrlSyncRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/27/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchShortUrlSyncRequest.h" +#import "BNCPreferenceHelper.h" +#import "BNCEncodingUtils.h" +#import "BranchConstants.h" +#import "BNCConfig.h" +#import "BNCLog.h" + +@interface BranchShortUrlSyncRequest () + +@property (strong, nonatomic) NSArray *tags; +@property (strong, nonatomic) NSString *alias; +@property (assign, nonatomic) BranchLinkType type; +@property (assign, nonatomic) NSInteger matchDuration; +@property (strong, nonatomic) NSString *channel; +@property (strong, nonatomic) NSString *feature; +@property (strong, nonatomic) NSString *stage; +@property (strong, nonatomic) NSString *campaign; +@property (strong, nonatomic) NSDictionary *params; +@property (strong, nonatomic) BNCLinkCache *linkCache; +@property (strong, nonatomic) BNCLinkData *linkData; + +@end + +@implementation BranchShortUrlSyncRequest + +- (id)initWithTags:(NSArray *)tags alias:(NSString *)alias type:(BranchLinkType)type matchDuration:(NSInteger)duration channel:(NSString *)channel feature:(NSString *)feature stage:(NSString *)stage campaign:(NSString *)campaign params:(NSDictionary *)params linkData:(BNCLinkData *)linkData linkCache:(BNCLinkCache *)linkCache { + if ((self = [super init])) { + _tags = tags; + _alias = alias; + _type = type; + _matchDuration = duration; + _channel = channel; + _feature = feature; + _stage = stage; + _campaign = campaign; + _params = params; + _linkCache = linkCache; + _linkData = linkData; + } + + return self; +} + +- (BNCServerResponse *)makeRequest:(BNCServerInterface *)serverInterface key:(NSString *)key { + NSMutableDictionary *params = [[NSMutableDictionary alloc] initWithDictionary:self.linkData.data]; + + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + if (!preferenceHelper.trackingDisabled) { + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + } + + return [serverInterface postRequestSynchronous:params + url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_GET_SHORT_URL] + key:key]; +} + +- (NSString *)processResponse:(BNCServerResponse *)response { + if (![response.statusCode isEqualToNumber:@200]) { + BNCLogWarning(@"Short link creation received HTTP status code %@. Using long link instead.", + response.statusCode); + NSString *failedUrl = nil; + NSString *userUrl = [BNCPreferenceHelper preferenceHelper].userUrl; + if (userUrl) { + failedUrl = [self createLongUrlForUserUrl:userUrl]; + } + + return failedUrl; + } + + NSString *url = response.data[BRANCH_RESPONSE_KEY_URL]; + + // cache the link + if (url) { + [self.linkCache setObject:url forKey:self.linkData]; + } + + return url; +} + +- (NSString *)createLongUrlForUserUrl:(NSString *)userUrl { + NSMutableString *baseUrl = [[NSMutableString alloc] initWithFormat:@"%@?", userUrl]; + return [BranchShortUrlSyncRequest createLongUrlWithBaseUrl:baseUrl tags:self.tags alias:self.alias type:self.type matchDuration:self.matchDuration channel:self.channel feature:self.feature stage:self.stage params:self.params]; +} + ++ (NSString *)createLinkFromBranchKey:(NSString *)branchKey tags:(NSArray *)tags alias:(NSString *)alias type:(BranchLinkType)type matchDuration:(NSInteger)duration channel:(NSString *)channel feature:(NSString *)feature stage:(NSString *)stage params:(NSDictionary *)params { + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + NSMutableString *baseUrl; + + if (preferenceHelper.userUrl) + baseUrl = [preferenceHelper sanitizedMutableBaseURL:preferenceHelper.userUrl]; + else + baseUrl = [[NSMutableString alloc] initWithFormat:@"%@/a/%@?", BNC_LINK_URL, branchKey]; + + return [BranchShortUrlSyncRequest createLongUrlWithBaseUrl:baseUrl tags:tags alias:alias type:type matchDuration:duration channel:channel feature:feature stage:stage params:params]; +} + ++ (NSString *)createLongUrlWithBaseUrl:(NSMutableString *)baseUrl + tags:(NSArray *)tags + alias:(NSString *)alias + type:(BranchLinkType)type + matchDuration:(NSInteger)duration + channel:(NSString *)channel + feature:(NSString *)feature + stage:(NSString *)stage + params:(NSDictionary *)params { + + baseUrl = [[BNCPreferenceHelper preferenceHelper] sanitizedMutableBaseURL:baseUrl]; + for (NSString *tag in tags) { + [baseUrl appendFormat:@"tags=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:tag]]; + } + + if ([alias length]) { + [baseUrl appendFormat:@"alias=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:alias]]; + } + + if ([channel length]) { + [baseUrl appendFormat:@"channel=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:channel]]; + } + + if ([feature length]) { + [baseUrl appendFormat:@"feature=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:feature]]; + } + + if ([stage length]) { + [baseUrl appendFormat:@"stage=%@&", [BNCEncodingUtils stringByPercentEncodingStringForQuery:stage]]; + } + + [baseUrl appendFormat:@"type=%ld&", (long)type]; + [baseUrl appendFormat:@"duration=%ld&", (long)duration]; + + NSData *jsonData = [BNCEncodingUtils encodeDictionaryToJsonData:params]; + NSString *base64EncodedParams = [BNCEncodingUtils base64EncodeData:jsonData]; + NSString *urlEncodedBase64EncodedParams = [BNCEncodingUtils urlEncodedString:base64EncodedParams]; + [baseUrl appendFormat:@"source=ios&data=%@", urlEncodedBase64EncodedParams]; + + return baseUrl; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSpotlightUrlRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSpotlightUrlRequest.h new file mode 100644 index 00000000000..f8893de11fe --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSpotlightUrlRequest.h @@ -0,0 +1,15 @@ +// +// BranchSpotlightUrlRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 7/23/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#import "BranchShortUrlRequest.h" + +@interface BranchSpotlightUrlRequest : BranchShortUrlRequest + +- (id)initWithParams:(NSDictionary *)params callback:(callbackWithParams)callback; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSpotlightUrlRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSpotlightUrlRequest.m new file mode 100644 index 00000000000..e9437b7cd52 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchSpotlightUrlRequest.m @@ -0,0 +1,40 @@ +// +// BranchSpotlightUrlRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 7/23/15. +// Copyright © 2015 Branch Metrics. All rights reserved. +// + +#import "BranchSpotlightUrlRequest.h" + +@interface BranchSpotlightUrlRequest () +@property (strong, nonatomic) callbackWithParams spotlightCallback; +@end + +@implementation BranchSpotlightUrlRequest + +- (id)initWithParams:(NSDictionary *)params callback:(callbackWithParams)callback { + BNCLinkData *linkData = [[BNCLinkData alloc] init]; + [linkData setupParams:params]; + [linkData setupChannel:@"spotlight"]; + + if ((self = [super initWithTags:nil alias:nil type:BranchLinkTypeUnlimitedUse matchDuration:0 channel:@"spotlight" feature:BRANCH_FEATURE_TAG_SHARE stage:nil campaign:nil params:params linkData:linkData linkCache:nil callback:nil])) { + self.isSpotlightRequest = YES; + _spotlightCallback = callback; + } + return self; +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + if (error) { + if (self.spotlightCallback) { + self.spotlightCallback([[NSDictionary alloc] init], error); + } + } + else if (self.spotlightCallback) { + self.spotlightCallback(response.data, nil); + } +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchUserCompletedActionRequest.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchUserCompletedActionRequest.h new file mode 100644 index 00000000000..09f329c4a61 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchUserCompletedActionRequest.h @@ -0,0 +1,15 @@ +// +// BranchUserCompletedActionRequest.h +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "Branch.h" +#import "BNCServerRequest.h" + +@interface BranchUserCompletedActionRequest : BNCServerRequest +- (id)initWithAction:(NSString *)action state:(NSDictionary *)state; +- (id)initWithAction:(NSString *)action state:(NSDictionary *)state withBranchViewCallback:(id)callback ; +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchUserCompletedActionRequest.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchUserCompletedActionRequest.m new file mode 100644 index 00000000000..71f6586a9b0 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/Networking/Requests/BranchUserCompletedActionRequest.m @@ -0,0 +1,95 @@ +// +// BranchLoadActionsRequest.m +// Branch-TestBed +// +// Created by Graham Mueller on 5/22/15. +// Copyright (c) 2015 Branch Metrics. All rights reserved. +// + +#import "BranchUserCompletedActionRequest.h" +#import "BNCPreferenceHelper.h" +#import "BranchConstants.h" +#import "BranchViewHandler.h" +#import "BNCEncodingUtils.h" +#import "BNCLog.h" + +@interface BranchUserCompletedActionRequest () + +@property (strong, nonatomic) NSString *action; +@property (strong, nonatomic) NSDictionary *state; +@property (strong, nonatomic) id branchViewcallback; + +@end + +@implementation BranchUserCompletedActionRequest + +- (id)initWithAction:(NSString *)action state:(NSDictionary *)state { + return [self initWithAction:action state:state withBranchViewCallback:nil]; +} + +- (id)initWithAction:(NSString *)action state:(NSDictionary *)state withBranchViewCallback:(id)callback { + if ((self = [super init])) { + _action = action; + _state = state; + _branchViewcallback = callback; + } + + return self; +} + +- (void)makeRequest:(BNCServerInterface *)serverInterface + key:(NSString *)key + callback:(BNCServerCallback)callback { + + // Emit a warning if the action is collides with the Branch commerce 'purchase' event. + if (self.action && [self.action isEqualToString:@"purchase"]) { + BNCLogWarning(@"You are sending a purchase event with our non-dedicated purchase " + "method. Please use the sendCommerceEvent:metadata:withCompletion: method."); + } + + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + BNCPreferenceHelper *preferenceHelper = [BNCPreferenceHelper preferenceHelper]; + params[BRANCH_REQUEST_KEY_ACTION] = self.action; + params[BRANCH_REQUEST_KEY_DEVICE_FINGERPRINT_ID] = preferenceHelper.deviceFingerprintID; + params[BRANCH_REQUEST_KEY_BRANCH_IDENTITY] = preferenceHelper.identityID; + params[BRANCH_REQUEST_KEY_SESSION_ID] = preferenceHelper.sessionID; + if (preferenceHelper.limitFacebookTracking) + params[@"limit_facebook_tracking"] = (__bridge NSNumber*) kCFBooleanTrue; + + if (self.state) { + params[BRANCH_REQUEST_KEY_STATE] = self.state; + } + + [serverInterface postRequest:params url:[preferenceHelper getAPIURL:BRANCH_REQUEST_ENDPOINT_USER_COMPLETED_ACTION] key:key callback:callback]; + +} + +- (void)processResponse:(BNCServerResponse *)response error:(NSError *)error { + // Check if there is any Branch View to show + if (!error) { + NSDictionary *data = response.data; + NSObject *branchViewDict = data[BRANCH_RESPONSE_KEY_BRANCH_VIEW_DATA]; + if ([branchViewDict isKindOfClass:[NSDictionary class]]) { + [[BranchViewHandler getInstance] showBranchView:_action withBranchViewDictionary:(NSDictionary *)branchViewDict andWithDelegate:_branchViewcallback]; + } + } +} + +#pragma mark - NSCoding methods + +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super initWithCoder:decoder])) { + _action = [decoder decodeObjectOfClass:NSString.class forKey:@"action"]; + _state = [decoder decodeObjectOfClass:NSDictionary.class forKey:@"state"]; + } + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)coder { + [super encodeWithCoder:coder]; + [coder encodeObject:self.action forKey:@"action"]; + [coder encodeObject:self.state forKey:@"state"]; +} + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/UIViewController+Branch.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/UIViewController+Branch.h similarity index 100% rename from ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Headers/UIViewController+Branch.h rename to ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/UIViewController+Branch.h diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/UIViewController+Branch.m b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/UIViewController+Branch.m new file mode 100644 index 00000000000..f1d8e482bea --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Branch-SDK/UIViewController+Branch.m @@ -0,0 +1,69 @@ +// +// UIViewController+Branch.m +// Branch-SDK +// +// Created by Edward Smith on 11/16/17. +// Copyright © 2017 Branch. All rights reserved. +// + +#import "UIViewController+Branch.h" + +@implementation UIViewController (Branch) + ++ (UIWindow*_Nullable) bnc_currentWindow { + Class UIApplicationClass = NSClassFromString(@"UIApplication"); + if (UIApplicationClass) { + UIWindow *keyWindow = nil; + + if ([[UIApplicationClass sharedApplication].delegate respondsToSelector:@selector(window)]) { + keyWindow = [UIApplicationClass sharedApplication].delegate.window; + } + if (keyWindow && !keyWindow.isHidden && keyWindow.rootViewController) return keyWindow; + + keyWindow = [UIApplicationClass sharedApplication].keyWindow; + if (keyWindow && !keyWindow.isHidden && keyWindow.rootViewController) return keyWindow; + + for (keyWindow in [UIApplicationClass sharedApplication].windows.reverseObjectEnumerator) { + if (!keyWindow.isHidden && keyWindow.rootViewController) return keyWindow; + } + } + + // ToDo: Put different code for extensions here. + + return nil; +} + ++ (UIViewController*_Nullable) bnc_currentViewController { + UIWindow *window = [UIViewController bnc_currentWindow]; + return [window.rootViewController bnc_currentViewController]; +} + +- (UIViewController*_Nonnull) bnc_currentViewController { + if ([self isKindOfClass:[UINavigationController class]]) { + return [((UINavigationController *)self).visibleViewController bnc_currentViewController]; + } + + if ([self isKindOfClass:[UITabBarController class]]) { + return [((UITabBarController *)self).selectedViewController bnc_currentViewController]; + } + + if ([self isKindOfClass:[UISplitViewController class]]) { + return [((UISplitViewController *)self).viewControllers.lastObject bnc_currentViewController]; + } + + if ([self isKindOfClass:[UIPageViewController class]]) { + return [((UIPageViewController*)self).viewControllers.lastObject bnc_currentViewController]; + } + + if (self.presentedViewController != nil && !self.presentedViewController.isBeingDismissed) { + return [self.presentedViewController bnc_currentViewController]; + } + + return self; +} + +@end + +__attribute__((constructor)) void BNCForceUIViewControllerCategoryToLoad() { + // Nothing here, but forces linker to load the category. +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/ANSCompatibility.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/ANSCompatibility.h new file mode 100644 index 00000000000..6ec011d93e4 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/ANSCompatibility.h @@ -0,0 +1,31 @@ +// +// ANSCompatibility.h +// AnswersKit +// +// Copyright (c) 2015 Crashlytics, Inc. All rights reserved. +// + +#pragma once + +#if !__has_feature(nullability) +#define nonnull +#define nullable +#define _Nullable +#define _Nonnull +#endif + +#ifndef NS_ASSUME_NONNULL_BEGIN +#define NS_ASSUME_NONNULL_BEGIN +#endif + +#ifndef NS_ASSUME_NONNULL_END +#define NS_ASSUME_NONNULL_END +#endif + +#if __has_feature(objc_generics) +#define ANS_GENERIC_NSARRAY(type) NSArray +#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary +#else +#define ANS_GENERIC_NSARRAY(type) NSArray +#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary +#endif diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Answers.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Answers.h new file mode 100644 index 00000000000..55892856d7c --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Answers.h @@ -0,0 +1,237 @@ +// +// Answers.h +// Crashlytics +// +// Copyright (c) 2015 Crashlytics, Inc. All rights reserved. +// + +#import +#import "ANSCompatibility.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * This class exposes the Answers Events API, allowing you to track key + * user user actions and metrics in your app. + */ +@interface Answers : NSObject + +/** + * Log a Sign Up event to see users signing up for your app in real-time, understand how + * many users are signing up with different methods and their success rate signing up. + * + * @param signUpMethodOrNil The method by which a user logged in, e.g. Twitter or Digits. + * @param signUpSucceededOrNil The ultimate success or failure of the login + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logSignUpWithMethod:(nullable NSString *)signUpMethodOrNil + success:(nullable NSNumber *)signUpSucceededOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log an Log In event to see users logging into your app in real-time, understand how many + * users are logging in with different methods and their success rate logging into your app. + * + * @param loginMethodOrNil The method by which a user logged in, e.g. email, Twitter or Digits. + * @param loginSucceededOrNil The ultimate success or failure of the login + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logLoginWithMethod:(nullable NSString *)loginMethodOrNil + success:(nullable NSNumber *)loginSucceededOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Share event to see users sharing from your app in real-time, letting you + * understand what content they're sharing from the type or genre down to the specific id. + * + * @param shareMethodOrNil The method by which a user shared, e.g. email, Twitter, SMS. + * @param contentNameOrNil The human readable name for this piece of content. + * @param contentTypeOrNil The type of content shared. + * @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item. + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logShareWithMethod:(nullable NSString *)shareMethodOrNil + contentName:(nullable NSString *)contentNameOrNil + contentType:(nullable NSString *)contentTypeOrNil + contentId:(nullable NSString *)contentIdOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log an Invite Event to track how users are inviting other users into + * your application. + * + * @param inviteMethodOrNil The method of invitation, e.g. GameCenter, Twitter, email. + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logInviteWithMethod:(nullable NSString *)inviteMethodOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Purchase event to see your revenue in real-time, understand how many users are making purchases, see which + * items are most popular, and track plenty of other important purchase-related metrics. + * + * @param itemPriceOrNil The purchased item's price. + * @param currencyOrNil The ISO4217 currency code. Example: USD + * @param purchaseSucceededOrNil Was the purchase succesful or unsuccesful + * @param itemNameOrNil The human-readable form of the item's name. Example: + * @param itemTypeOrNil The type, or genre of the item. Example: Song + * @param itemIdOrNil The machine-readable, unique item identifier Example: SKU + * @param customAttributesOrNil A dictionary of custom attributes to associate with this purchase. + */ ++ (void)logPurchaseWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil + currency:(nullable NSString *)currencyOrNil + success:(nullable NSNumber *)purchaseSucceededOrNil + itemName:(nullable NSString *)itemNameOrNil + itemType:(nullable NSString *)itemTypeOrNil + itemId:(nullable NSString *)itemIdOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Level Start Event to track where users are in your game. + * + * @param levelNameOrNil The level name + * @param customAttributesOrNil A dictionary of custom attributes to associate with this level start event. + */ ++ (void)logLevelStart:(nullable NSString *)levelNameOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Level End event to track how users are completing levels in your game. + * + * @param levelNameOrNil The name of the level completed, E.G. "1" or "Training" + * @param scoreOrNil The score the user completed the level with. + * @param levelCompletedSuccesfullyOrNil A boolean representing whether or not the level was completed succesfully. + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logLevelEnd:(nullable NSString *)levelNameOrNil + score:(nullable NSNumber *)scoreOrNil + success:(nullable NSNumber *)levelCompletedSuccesfullyOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log an Add to Cart event to see users adding items to a shopping cart in real-time, understand how + * many users start the purchase flow, see which items are most popular, and track plenty of other important + * purchase-related metrics. + * + * @param itemPriceOrNil The purchased item's price. + * @param currencyOrNil The ISO4217 currency code. Example: USD + * @param itemNameOrNil The human-readable form of the item's name. Example: + * @param itemTypeOrNil The type, or genre of the item. Example: Song + * @param itemIdOrNil The machine-readable, unique item identifier Example: SKU + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logAddToCartWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil + currency:(nullable NSString *)currencyOrNil + itemName:(nullable NSString *)itemNameOrNil + itemType:(nullable NSString *)itemTypeOrNil + itemId:(nullable NSString *)itemIdOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Start Checkout event to see users moving through the purchase funnel in real-time, understand how many + * users are doing this and how much they're spending per checkout, and see how it related to other important + * purchase-related metrics. + * + * @param totalPriceOrNil The total price of the cart. + * @param currencyOrNil The ISO4217 currency code. Example: USD + * @param itemCountOrNil The number of items in the cart. + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logStartCheckoutWithPrice:(nullable NSDecimalNumber *)totalPriceOrNil + currency:(nullable NSString *)currencyOrNil + itemCount:(nullable NSNumber *)itemCountOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Rating event to see users rating content within your app in real-time and understand what + * content is most engaging, from the type or genre down to the specific id. + * + * @param ratingOrNil The integer rating given by the user. + * @param contentNameOrNil The human readable name for this piece of content. + * @param contentTypeOrNil The type of content shared. + * @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item. + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logRating:(nullable NSNumber *)ratingOrNil + contentName:(nullable NSString *)contentNameOrNil + contentType:(nullable NSString *)contentTypeOrNil + contentId:(nullable NSString *)contentIdOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Content View event to see users viewing content within your app in real-time and + * understand what content is most engaging, from the type or genre down to the specific id. + * + * @param contentNameOrNil The human readable name for this piece of content. + * @param contentTypeOrNil The type of content shared. + * @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item. + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logContentViewWithName:(nullable NSString *)contentNameOrNil + contentType:(nullable NSString *)contentTypeOrNil + contentId:(nullable NSString *)contentIdOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Search event allows you to see users searching within your app in real-time and understand + * exactly what they're searching for. + * + * @param queryOrNil The user's query. + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. + */ ++ (void)logSearchWithQuery:(nullable NSString *)queryOrNil + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +/** + * Log a Custom Event to see user actions that are uniquely important for your app in real-time, to see how often + * they're performing these actions with breakdowns by different categories you add. Use a human-readable name for + * the name of the event, since this is how the event will appear in Answers. + * + * @param eventName The human-readable name for the event. + * @param customAttributesOrNil A dictionary of custom attributes to associate with this event. Attribute keys + * must be NSString and and values must be NSNumber or NSString. + * @discussion How we treat NSNumbers: + * We will provide information about the distribution of values over time. + * + * How we treat NSStrings: + * NSStrings are used as categorical data, allowing comparison across different category values. + * Strings are limited to a maximum length of 100 characters, attributes over this length will be + * truncated. + * + * When tracking the Tweet views to better understand user engagement, sending the tweet's length + * and the type of media present in the tweet allows you to track how tweet length and the type of media influence + * engagement. + */ ++ (void)logCustomEventWithName:(NSString *)eventName + customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil; + +@end + +NS_ASSUME_NONNULL_END + + +/* + * Below are a number of statically defined methods which allow SDK developers to easily + * and safely log events to Answers if it is installed in a host application. + * + */ + +// Safely gets the Answers class if it is available at runtime. +// This is useful for SDKs dynamically checking if Answers is available, and +// only performing some actions if so. +static _Nullable Class ANSGetAnswersClass() { + Class answersClass = NSClassFromString(@"Answers"); + if ([answersClass respondsToSelector:@selector(logCustomEventWithName:customAttributes:)]) { + return answersClass; + } + return nil; +} +// Safely logs an AnswersCustomEvent if Answers is available at runtime. +// This is useful for SDKs dynamically logging events to Answers if it is available. +static void ANSLogCustomEvent(NSString *_Nonnull eventName, NSDictionary *_Nullable customAttributesOrNil) { + Class answersClass = ANSGetAnswersClass(); + if (!answersClass) { + return; + } + [answersClass logCustomEventWithName:eventName customAttributes:customAttributesOrNil]; +} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/FABAttributes.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/FABAttributes.h new file mode 100644 index 00000000000..e35f9ce37c1 --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/FABAttributes.h @@ -0,0 +1,60 @@ +// +// FABAttributes.h +// Fabric +// +// Copyright (C) 2015 Twitter, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#pragma once + +#define FAB_UNAVAILABLE(x) __attribute__((unavailable(x))) + +#if __has_feature(nullability) + #define fab_nullable nullable + #define fab_nonnull nonnull + #define fab_null_unspecified null_unspecified + #define fab_null_resettable null_resettable + #define __fab_nullable __nullable + #define __fab_nonnull __nonnull + #define __fab_null_unspecified __null_unspecified +#else + #define fab_nullable + #define fab_nonnull + #define fab_null_unspecified + #define fab_null_resettable + #define __fab_nullable + #define __fab_nonnull + #define __fab_null_unspecified +#endif + +#ifndef NS_ASSUME_NONNULL_BEGIN + #define NS_ASSUME_NONNULL_BEGIN +#endif + +#ifndef NS_ASSUME_NONNULL_END + #define NS_ASSUME_NONNULL_END +#endif + + +/** + * The following macros are defined here to provide + * backwards compatability. If you are still using + * them you should migrate to the new versions that + * are defined above. + */ +#define FAB_NONNULL __fab_nonnull +#define FAB_NULLABLE __fab_nullable +#define FAB_START_NONNULL NS_ASSUME_NONNULL_BEGIN +#define FAB_END_NONNULL NS_ASSUME_NONNULL_END diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/FABKitProtocol.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/FABKitProtocol.h new file mode 100644 index 00000000000..f24ab4bd04c --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/FABKitProtocol.h @@ -0,0 +1,59 @@ +// +// FABKitProtocol.h +// Fabric +// +// Copyright (C) 2015 Twitter, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import + +/** + * Protocol that a class in a Fabric Kit must conform to to provide information to Fabric at runtime. + */ +@protocol FABKit + +@required + +/** + * Required. The globally unique identifier of the Kit. + * We encourage the use of reverse-DNS notation. + * Example: @"io.fabric.sdk.ios" + */ ++ (NSString *)bundleIdentifier; + +/** + * Required. Must return the current version of the Kit that is being used at runtime. + * We encourage the use of semantic versioning (http://semver.org/), without prefixing the version with a "v". + * This is commonly referred to as the "marketing version". + * Example: @"1.2.3" + */ ++ (NSString *)kitDisplayVersion; + +@optional + +/** + * The build version of the kit. Should be monotonically increasing and unique. + * Example: 137 + */ ++ (NSString *)kitBuildVersion; + +/** + * Perform any necessary initialization. + * This method will be invoked on the Kit when the user calls +[Fabric initializeKits]. + * @note This method being called does not necessarily imply that the developer has started using the Kit yet. + */ ++ (void)initializeIfNeeded; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Fabric+FABKits.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Fabric+FABKits.h new file mode 100644 index 00000000000..5493ceb73ab --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Fabric+FABKits.h @@ -0,0 +1,38 @@ +// +// Fabric+FABKits.h +// Fabric +// +// Copyright (C) 2015 Twitter, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "Fabric.h" + +@protocol FABKit; +// Use this category for methods that kits can call on Fabric. +@interface Fabric (FABKits) + +/** + * Returns a dictionary containing the kit configuration info for the provided kit. + * The configuration information is parsed from the application's Info.plist. This + * method is primarily intended to be used by kits to retrieve their configuration. + * + * @param kitClass The class of the kit whose configuration should be returned. + * It should conform to the FABKit protocol. + * + * @return A dictionary containing kit specific configuration information or nil if none exists. + */ ++ (fab_nonnull NSDictionary *)configurationDictionaryForKitClass:(fab_nonnull Class)kitClass; + +@end diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Fabric.h b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Fabric.h new file mode 100644 index 00000000000..f3c2425ff8a --- /dev/null +++ b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch-SDK/Fabric/Fabric.h @@ -0,0 +1,67 @@ +// +// Fabric.h +// Fabric +// +// Copyright (C) 2015 Twitter, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import "FABAttributes.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Fabric Base. Coordinates configuration and starts all provided kits. + */ +@interface Fabric : NSObject + +/** + * Initialize Fabric and all provided kits. Call this method within your App Delegate's `application:didFinishLaunchingWithOptions:` and provide the kits you wish to use. + * + * For example, in Objective-C: + * + * `[Fabric with:@[[Crashlytics class], [Twitter class], [Digits class], [MoPub class]]];` + * + * Swift: + * + * `Fabric.with([Crashlytics.self(), Twitter.self(), Digits.self(), MoPub.self()])` + * + * Only the first call to this method is honored. Subsequent calls are no-ops. + * + * @param kitClasses An array of kit Class objects + * + * @return Returns the shared Fabric instance. In most cases this can be ignored. + */ ++ (instancetype)with:(NSArray *)kitClasses; + +/** + * Returns the Fabric singleton object. + */ ++ (instancetype)sharedSDK; + +/** + * This BOOL enables or disables debug logging, such as kit version information. The default value is NO. + */ +@property (nonatomic, assign) BOOL debug; + +/** + * Unavailable. Use `+sharedSDK` to retrieve the shared Fabric instance. + */ +- (id)init FAB_UNAVAILABLE("Use +sharedSDK to retrieve the shared Fabric instance."); + +@end + +NS_ASSUME_NONNULL_END + diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Branch b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Branch deleted file mode 100755 index d8953636e65..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Branch and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Info.plist b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Info.plist deleted file mode 100644 index 9c7c43909c5..00000000000 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Info.plist and /dev/null differ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Modules/module.modulemap b/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Modules/module.modulemap deleted file mode 100644 index d676d938494..00000000000 --- a/ios/SAPTracker/src/xcode/SAPTracker/Libraries/Branch.framework/Modules/module.modulemap +++ /dev/null @@ -1,6 +0,0 @@ -framework module Branch { - umbrella header "Branch.h" - - export * - module * { export * } -} diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SAPTracker.entitlements b/ios/SAPTracker/src/xcode/SAPTracker/SAPTracker.entitlements index 06eb649cdd7..e1306df4032 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/SAPTracker.entitlements +++ b/ios/SAPTracker/src/xcode/SAPTracker/SAPTracker.entitlements @@ -4,10 +4,8 @@ com.apple.developer.associated-domains - applinks:d-labs.app.link - applinks:d-labs-alternate.app.link - applinks:d-labs.test-app.link - applinks:d-labs-alternate.test-app.link + applinks:sailinsight-app.sapsailing.com + applinks:sapsailinsight-alternate.app.link diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SessionViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/SessionViewController.swift index e61b455bcc8..30caba8776b 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/SessionViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/SessionViewController.swift @@ -192,27 +192,6 @@ class SessionViewController: UIViewController { navigationController?.popViewController(animated: true) } - fileprivate func showStartTrackingWiFiAlert() { - let alertController = UIAlertController( - title: "INFO", - message: "WIFI IS ON BUT NOT CONNECTED", - preferredStyle: .alert - ) - let settingsAction = UIAlertAction(title: Translation.Common.Settings.String, style: .default) { action in - if let settingsURL = URL(string: "prefs:root=WIFI") { - UIApplication.shared.openURL(settingsURL) - } - } - let okAction = UIAlertAction(title: Translation.Common.OK.String, style: .default) { [weak self] action in - self?.startTracking() - } - let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .cancel, handler: nil) - alertController.addAction(settingsAction) - alertController.addAction(okAction) - alertController.addAction(cancelAction) - present(alertController, animated: true, completion: nil) - } - fileprivate func showStartTrackingFailureAlert(message: String) { let alertController = UIAlertController( title: Translation.Common.Warning.String, @@ -220,8 +199,8 @@ class SessionViewController: UIViewController { preferredStyle: .alert ) let settingsAction = UIAlertAction(title: Translation.Common.Settings.String, style: .default) { action in - if let locationServiceURL = URL(string: "prefs:root=LOCATION_SERVICES") { - UIApplication.shared.openURL(locationServiceURL) + if let url = URL(string: UIApplicationOpenSettingsURLString) { + UIApplication.shared.openURL(url) } } let cancelAction = UIAlertAction(title: Translation.Common.Cancel.String, style: .default, handler: nil) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings b/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings deleted file mode 100644 index fb997d137b2..00000000000 --- a/ios/SAPTracker/src/xcode/SAPTracker/SignUp.strings +++ /dev/null @@ -1,47 +0,0 @@ -/* - SignUp.strings - SAPTracker - - Created by Raimund Wege on 10.08.17. - Copyright © 2017 com.sap.sailing. All rights reserved. -*/ - -// MARK: - Common - -"Common.Email" = "E-Mail"; -"Common.Password" = "Passwort"; -"Common.Server" = "Server"; -"Common.UserName" = "Benutzername"; - -// MARK: - ForgotPasswordView - -"ForgotPasswordView.Title" = "Passwort vergessen"; -"ForgotPasswordView.InfoLabel.Text" = "Benutzernamen oder E-Mail Adresse angeben"; -"ForgotPasswordView.ResetPasswordButton.Title" = "Passwort zurücksetzen"; - -// MARK: - LoginView - -"LoginView.Title" = "Anmelden"; -"LoginView.ForgotPasswordButton.Title" = "Passwort vergessen"; -"LoginView.LoginButton.Title" = "Anmelden"; - -// MARK: - SignUpControllerError - -"SignUpControllerError.LoginFailed" = "Anmeldung fehlgeschlagen. Zugangsdaten überprüfen."; - -// MARK: - SignUpView - -"SignUpView.Title" = "Registrieren"; -"SignUpView.InfoLabel.Text" = "Herzlich Willkommen."; -"SignUpView.FullNameLabel.Text" = "Vollständiger Name"; -"SignUpView.CompanyLabel.Text" = "Unternehmen"; -"SignUpView.PasswordTextField.Placeholder" = "Geben Sie ein neues Passwort ein"; -"SignUpView.RepeatPasswordLabel.Text" = "Passwort Wiederholung"; -"SignUpView.RepeatPasswordTextField.Placeholder" = "Wiederholen Sie das Passwort"; -"SignUpView.SignUpButton.Title" = "Registrieren"; - -// MARK: - SignUpViewError - -"SignUpViewError.UserNameIsToShort" = "Bitte einen Benutzername mit mindestens 3 Zeichen eingeben."; -"SignUpViewError.PasswordIsToShort" = "Bitte ein Passwort mit mindestens 5 Zeichen eingeben."; -"SignUpViewError.PasswordsNotEqual" = "Passwort und Passwortwiederholung müssen übereinstimmen."; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/bridge.h b/ios/SAPTracker/src/xcode/SAPTracker/bridge.h index 979389a8d38..75acc00fba5 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/bridge.h +++ b/ios/SAPTracker/src/xcode/SAPTracker/bridge.h @@ -15,5 +15,6 @@ #import "Libraries/UIKit+AFNetworking/UIImageView+AFNetworking.h" #import "Libraries/UIKit+AFNetworking/AFNetworkActivityIndicatorManager.h" #import "Libraries/SMTWiFiStatus/SMTWiFiStatus.h" +#import "Libraries/Branch-SDK/Branch-SDK/Branch.h" #endif diff --git a/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/InfoPlist.strings b/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/InfoPlist.strings index e3fca47d2f6..a5625c27c50 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/InfoPlist.strings +++ b/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/InfoPlist.strings @@ -1,11 +1,12 @@ -// -// InfoPlist.strings -// SAPTracker -// -// Created by Raimund Wege on 04.11.16. -// Copyright © 2016 com.sap.sailing. All rights reserved. -// +/* + InfoPlist.strings + SAPTracker + + Created by Raimund Wege on 04.11.16. + Copyright © 2016 com.sap.sailing. All rights reserved. +*/ +"NSLocationAlwaysUsageDescription" = "Um das ganze Rennen mit Deinem Gerät zu tracken, muss das Tracking auch dann fortgesetzt werden, wenn der Bildschirm abgeschaltet oder gesperrt wird. Das hilft, die Batterielaufzeit zu verlängern und versehentlich das Tracking zu stoppen."; "NSLocationWhenInUseUsageDescription" = "Um das ganze Rennen mit Deinem Gerät zu tracken, muss das Tracking auch dann fortgesetzt werden, wenn der Bildschirm abgeschaltet oder gesperrt wird. Das hilft, die Batterielaufzeit zu verlängern und versehentlich das Tracking zu stoppen."; "NSLocationAlwaysAndWhenInUseUsageDescription" = "Um das ganze Rennen mit Deinem Gerät zu tracken, muss das Tracking auch dann fortgesetzt werden, wenn der Bildschirm abgeschaltet oder gesperrt wird. Das hilft, die Batterielaufzeit zu verlängern und versehentlich das Tracking zu stoppen."; "NSCameraUsageDescription" = "Die Kamera wird benötigt, um per QR-Code einen Check-In auszuführen."; diff --git a/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/Localizable.strings b/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/Localizable.strings index e3645843144..fb9255a4529 100644 Binary files a/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/Localizable.strings and b/ios/SAPTracker/src/xcode/SAPTracker/de.lproj/Localizable.strings differ diff --git a/java/com.sap.sailing.declination.test/resources/colorado.xml b/java/com.sap.sailing.declination.test/resources/colorado.xml index f38588d2801..326db4f7ad2 100644 --- a/java/com.sap.sailing.declination.test/resources/colorado.xml +++ b/java/com.sap.sailing.declination.test/resources/colorado.xml @@ -1,87 +1,33 @@ - -0.5.0.7 - + 0.5.0.7 - -2018.06027 - - -10.00000 - - -20.00000 - - -0.00000 - - -1.89255 - - --1.78053 - - -34687.2 - - -34704.0 - - -34668.3 - - -1145.5 - - --1078.3 - - -0.10649 - - --0.01038 - - -11.3 - - -11.5 - - -9.2 - - -64.8 - - --6.6 - - -0.29 - - -0.22 - - -133 - - -152 - - -89 - - -138 - - -165 - + 2018.06027 + 10.00000 + 20.00000 + 0.00000 + 1.91357 + -1.63269 + 34676.9 + 34691.0 + 34657.5 + 1157.9 + -988.4 + 0.11020 + 0.05118 + 7.2 + 6.4 + 5.0 + 66.9 + 30.8 + 0.29 + 0.22 + 133 + 152 + 89 + 138 + 165 - -1.1 - + 1.1 \ No newline at end of file diff --git a/java/com.sap.sailing.declination.test/src/com/sap/sailing/declination/test/ColoradoDeclinationServiceTest.java b/java/com.sap.sailing.declination.test/src/com/sap/sailing/declination/test/ColoradoDeclinationServiceTest.java index 9f4e7cbb2d6..ce397cc0c51 100644 --- a/java/com.sap.sailing.declination.test/src/com/sap/sailing/declination/test/ColoradoDeclinationServiceTest.java +++ b/java/com.sap.sailing.declination.test/src/com/sap/sailing/declination/test/ColoradoDeclinationServiceTest.java @@ -27,7 +27,7 @@ public class ColoradoDeclinationServiceTest extends DeclinationServiceTest deviceIdentifierServiceFinder; private final TypeBasedServiceFinder raceTrackingConnectivityParamsServiceFinder; @@ -1052,7 +1052,7 @@ public class MongoObjectFactoryImpl implements MongoObjectFactory { storeRaceLogEventProperties(event, result); result.put(FieldNames.RACE_LOG_EVENT_CLASS.name(), RaceLogDenoteForTrackingEvent.class.getSimpleName()); result.put(FieldNames.RACE_NAME.name(), event.getRaceName()); - result.put(FieldNames.BOAT_CLASS_NAME.name(), event.getBoatClass().getName()); + result.put(FieldNames.BOAT_CLASS_NAME.name(), event.getBoatClass()==null?null:event.getBoatClass().getName()); result.put(FieldNames.RACE_ID.name(), event.getRaceId()); return result; } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/NoAddingRaceLogWrapper.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/NoAddingRaceLogWrapper.java index c6ade560840..0fb879831f5 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/NoAddingRaceLogWrapper.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/race/impl/NoAddingRaceLogWrapper.java @@ -159,6 +159,12 @@ public class NoAddingRaceLogWrapper implements RaceLog { return innerRaceLog.getRawFixesIterator(startingAt, inclusive); } + @Override + public Iterator getRawFixesIterator(TimePoint startingAt, boolean startingAtInclusive, + TimePoint endingAt, boolean endingAtInclusive) { + return innerRaceLog.getRawFixesIterator(startingAt, startingAtInclusive, endingAt, endingAtInclusive); + } + @Override public Serializable getId() { return innerRaceLog; diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/regatta/tracking/analyzing/impl/RegattaLogDeviceMappingFinder.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/regatta/tracking/analyzing/impl/RegattaLogDeviceMappingFinder.java index 983684147c1..4a7b9da2d40 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/regatta/tracking/analyzing/impl/RegattaLogDeviceMappingFinder.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/abstractlog/regatta/tracking/analyzing/impl/RegattaLogDeviceMappingFinder.java @@ -49,7 +49,8 @@ public class RegattaLogDeviceMappingFinder protected DeviceMappingWithRegattaLogEvent createMapping(DeviceIdentifier device, ItemT item, TimePoint from, TimePoint toInclusive, Serializable originalEventId, RegattaLogDeviceMappingEvent event) { - return new DeviceMappingWithRegattaLogEventImpl(item, device, new TimeRangeImpl(from, toInclusive, /* inclusive */ true), + return new DeviceMappingWithRegattaLogEventImpl(item, device, new TimeRangeImpl(from, + toInclusive != null && toInclusive.before(from) ? from : toInclusive, /* inclusive */ true), Collections.singletonList(originalEventId), event); } diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/Track.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/Track.java index 21f32616e4c..75010f03c8d 100755 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/Track.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/Track.java @@ -201,6 +201,17 @@ public interface Track extends Serializable { */ Iterator getRawFixesIterator(TimePoint startingAt, boolean inclusive); + /** + * Returns an iterator starting at the first raw fix after startingAt (or "at or after" in case + * startingAtInclusive is true) and ending at the endingAt time point or just before + * in case endingAtIncluive is false. The fixes returned by the iterator are the raw fixes (see also + * {@link #getRawFixes()}, without any smoothening or dampening applied. + * + * Callers must have called {@link #lockForRead()} before calling this method. This will be checked, and an exception + * will be thrown in case the caller has failed to do so. + */ + Iterator getRawFixesIterator(TimePoint startingAt, boolean startingAtInclusive, TimePoint endingAt, boolean endingAtInclusive); + /** * Returns a descending iterator starting at the first fix before startingAt (or "at or before" in case * inclusive is true). The fixes returned by the iterator are the smoothened fixes (see diff --git a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java index 4cc8b21766c..d0d9cf9b890 100644 --- a/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java +++ b/java/com.sap.sailing.domain.shared.android/src/com/sap/sailing/domain/tracking/impl/TrackImpl.java @@ -335,25 +335,14 @@ public class TrackImpl implements Track { @Override public Iterator getFixesIterator(TimePoint startingAt, boolean inclusive) { assertReadLock(); - Iterator result = (Iterator) getInternalFixes().tailSet( - getDummyFix(startingAt), inclusive).iterator(); - return result; + return getTimeConstrainedFixesIterator(getInternalFixes(), startingAt, inclusive, /* endingAt */ null, /* endingAtInclusive */ false); } @Override public Iterator getFixesIterator(TimePoint startingAt, boolean startingAtInclusive, TimePoint endingAt, boolean endingAtInclusive) { assertReadLock(); - NavigableSet set = getInternalFixes(); - if (startingAt != null && endingAt != null) { - set = set.subSet(getDummyFix(startingAt), startingAtInclusive, getDummyFix(endingAt), endingAtInclusive); - } else if (endingAt != null) { - set = set.headSet(getDummyFix(endingAt), endingAtInclusive); - } else if (startingAt != null) { - set = set.tailSet(getDummyFix(startingAt), startingAtInclusive); - } - Iterator result = set.iterator(); - return result; + return getTimeConstrainedFixesIterator(getInternalFixes(), startingAt, startingAtInclusive, endingAt, endingAtInclusive); } @Override @@ -380,10 +369,29 @@ public class TrackImpl implements Track { @Override public Iterator getRawFixesIterator(TimePoint startingAt, boolean inclusive) { assertReadLock(); - Iterator result = (Iterator) getInternalRawFixes().tailSet( - getDummyFix(startingAt), inclusive).iterator(); + return getTimeConstrainedFixesIterator(getInternalRawFixes(), startingAt, inclusive, /* endingAt */ null, /* endingAtInclusive */ false); + } + + private Iterator getTimeConstrainedFixesIterator(NavigableSet set, TimePoint startingAt, boolean startingAtInclusive, + TimePoint endingAt, boolean endingAtInclusive) { + assertReadLock(); + if (startingAt != null && endingAt != null) { + set = set.subSet(getDummyFix(startingAt), startingAtInclusive, getDummyFix(endingAt), endingAtInclusive); + } else if (endingAt != null) { + set = set.headSet(getDummyFix(endingAt), endingAtInclusive); + } else if (startingAt != null) { + set = set.tailSet(getDummyFix(startingAt), startingAtInclusive); + } + Iterator result = set.iterator(); return result; } + + @Override + public Iterator getRawFixesIterator(TimePoint startingAt, boolean startingAtInclusive, + TimePoint endingAt, boolean endingAtInclusive) { + assertReadLock(); + return getTimeConstrainedFixesIterator(getInternalRawFixes(), startingAt, startingAtInclusive, endingAt, endingAtInclusive); + } @Override public Iterator getRawFixesDescendingIterator(TimePoint startingAt, boolean inclusive) { diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java index c1d5d482d6d..47293d5eefb 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/DomainFactory.java @@ -46,11 +46,14 @@ public interface DomainFactory { Nationality getOrCreateNationality(String threeLetterIOCCode); - Pair createCompetitorWithoutID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, String raceId, BoatClass boatClass); + Pair createCompetitorWithoutID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, + String raceId, BoatClass boatClass, RaceTrackingHandler raceTrackingHandler); - Pair createCompetitorWithID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, BoatClass boatClass); + Pair createCompetitorWithID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, + BoatClass boatClass, RaceTrackingHandler raceTrackHandler); - Pair createCompetitorWithoutID(String competitorId, String threeLetterIOCCode, String name, String raceId, BoatClass boatClass); + Pair createCompetitorWithoutID(String competitorId, String threeLetterIOCCode, String name, + String raceId, BoatClass boatClass, RaceTrackingHandler raceTrackingHandler); String getCompetitorID(String boatID, RaceType raceType); diff --git a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java index 42c30bed7be..dae87549241 100755 --- a/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.swisstimingadapter/src/com/sap/sailing/domain/swisstimingadapter/impl/DomainFactoryImpl.java @@ -131,7 +131,7 @@ public class DomainFactoryImpl implements DomainFactory { } @Override - public Pair createCompetitorWithID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, BoatClass boatClass) { + public Pair createCompetitorWithID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, BoatClass boatClass, RaceTrackingHandler raceTrackHandler) { CompetitorAndBoatStore competitorAndBoatStore = baseDomainFactory.getCompetitorAndBoatStore(); final Serializable competitorId = UUIDHelper.tryUuidConversion(competitor.getIdAsString()); CompetitorWithBoat domainCompetitor = competitorAndBoatStore.getExistingCompetitorWithBoatById(competitorId); @@ -149,15 +149,18 @@ public class DomainFactoryImpl implements DomainFactory { } } DynamicTeam team = new TeamImpl(competitor.getName(), teamMembers, /* coach */ null); - final DynamicBoat domainBoat = competitorAndBoatStore.getOrCreateBoat(competitorId, /* name */ null, boatClass, competitor.getBoatID(), /* color */ null); - domainCompetitor = competitorAndBoatStore.getOrCreateCompetitorWithBoat(competitorId, competitor.getName(), null /* shortName */, null /*displayColor*/, null /*email*/, null, team, + final DynamicBoat domainBoat = raceTrackHandler.getOrCreateBoat(competitorAndBoatStore, competitorId, /* name */ null, boatClass, competitor.getBoatID(), /* color */ null); + domainCompetitor = raceTrackHandler.getOrCreateCompetitorWithBoat(competitorAndBoatStore, competitorId, + competitor.getName(), null /* shortName */, null /* displayColor */, null /* email */, null, team, /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null, domainBoat); } return new Pair(domainCompetitor, domainCompetitor.getBoat()); } @Override - public Pair createCompetitorWithoutID(com.sap.sailing.domain.swisstimingadapter.Competitor competitor, String raceId, BoatClass boatClass) { + public Pair createCompetitorWithoutID( + com.sap.sailing.domain.swisstimingadapter.Competitor competitor, String raceId, BoatClass boatClass, + RaceTrackingHandler raceTrackHandler) { CompetitorAndBoatStore competitorAndBoatStore = baseDomainFactory.getCompetitorAndBoatStore(); List teamMembers = new ArrayList(); for (String teamMemberName : competitor.getName().split("[-+&]")) { @@ -168,15 +171,17 @@ public class DomainFactoryImpl implements DomainFactory { String competitorID = getCompetitorID(competitor.getBoatID(), competitor.getName(), raceId, boatClass); // TODO wouldn't the boat also need to be constructed using competitorAndBoatStore.getOrCreateBoat...? DynamicBoat domainBoat = new BoatImpl(UUID.randomUUID(), null, boatClass, competitor.getBoatID(), null); - CompetitorWithBoat domainCompetitor = competitorAndBoatStore.getOrCreateCompetitorWithBoat(competitorID, + CompetitorWithBoat domainCompetitor = raceTrackHandler.getOrCreateCompetitorWithBoat(competitorAndBoatStore, + competitorID, competitor.getName(), null /* short name */, null /*displayColor*/, null /*email*/, null, team, /* timeOnTimeFactor */ null, /* timeOnDistanceAllowancePerNauticalMile */ null, null, domainBoat); return new Pair(domainCompetitor, domainCompetitor.getBoat()); } @Override - public Pair createCompetitorWithoutID(String boatID, String threeLetterIOCCode, String name, String raceId, BoatClass boatClass) { - return createCompetitorWithoutID(new CompetitorWithoutID(boatID, threeLetterIOCCode, name), raceId, boatClass); + public Pair createCompetitorWithoutID(String boatID, String threeLetterIOCCode, String name, + String raceId, BoatClass boatClass, RaceTrackingHandler raceTrackingHandler) { + return createCompetitorWithoutID(new CompetitorWithoutID(boatID, threeLetterIOCCode, name), raceId, boatClass,raceTrackingHandler); } private String getCompetitorID(String boatID, String name, String raceId, BoatClass boatClass) { @@ -232,7 +237,8 @@ public class DomainFactoryImpl implements DomainFactory { public RaceDefinition createRaceDefinition(Regatta regatta, Race race, StartList startList, Course course, RaceTrackingHandler raceTrackingHandler) { com.sap.sailing.domain.base.Course domainCourse = createCourse(race.getDescription(), course); - Map competitorsAndBoats = createCompetitorsAndBoats(startList, race.getRaceID(), race.getBoatClass()); + Map competitorsAndBoats = createCompetitorsAndBoats(startList, race.getRaceID(), + race.getBoatClass(), raceTrackingHandler); logger.info("Creating RaceDefinitionImpl for race "+race.getRaceID()); BoatClass boatClass = race.getBoatClass() != null ? race.getBoatClass() : getRaceTypeFromRaceID(race.getRaceID()).getBoatClass(); RaceDefinition result = raceTrackingHandler.createRaceDefinition(regatta, race.getRaceName(), domainCourse, boatClass, competitorsAndBoats, race.getRaceID()); @@ -285,14 +291,16 @@ public class DomainFactoryImpl implements DomainFactory { return result; } - private Map createCompetitorsAndBoats(StartList startList, String raceId, BoatClass boatClass) { + private Map createCompetitorsAndBoats(StartList startList, String raceId, BoatClass boatClass, + RaceTrackingHandler raceTrackHandler) { Map result = new LinkedHashMap<>(); for (com.sap.sailing.domain.swisstimingadapter.Competitor swissTimingCompetitor : startList.getCompetitors()) { Pair domainCompetitorAndBoat; if (swissTimingCompetitor.getIdAsString() != null) { - domainCompetitorAndBoat = createCompetitorWithID(swissTimingCompetitor, boatClass); + domainCompetitorAndBoat = createCompetitorWithID(swissTimingCompetitor, boatClass, raceTrackHandler); } else { - domainCompetitorAndBoat = createCompetitorWithoutID(swissTimingCompetitor, raceId, boatClass); + domainCompetitorAndBoat = createCompetitorWithoutID(swissTimingCompetitor, raceId, boatClass, + raceTrackHandler); } result.put(domainCompetitorAndBoat.getA(), domainCompetitorAndBoat.getB()); } diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter.test/resources/SAM009903.20120731_sapsailing.replay b/java/com.sap.sailing.domain.swisstimingreplayadapter.test/resources/SAM009903.20120731_sapsailing.replay deleted file mode 100644 index a2297dcb485..00000000000 Binary files a/java/com.sap.sailing.domain.swisstimingreplayadapter.test/resources/SAM009903.20120731_sapsailing.replay and /dev/null differ diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingReplayAdapterServiceTest.java b/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingReplayAdapterServiceTest.java index d26794303b6..364f550508b 100644 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingReplayAdapterServiceTest.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter.test/src/com/sap/sailing/domain/swisstimingreplayadapter/test/SwissTimingReplayAdapterServiceTest.java @@ -15,7 +15,6 @@ import java.net.URL; import java.util.List; import java.util.Map.Entry; -import org.junit.Ignore; import org.junit.Test; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; @@ -215,57 +214,30 @@ public class SwissTimingReplayAdapterServiceTest { } @Test - @Ignore public void printRaceData_SAW005906_20120805() throws Exception { new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAW005906.20120805.replay"), new SwissTimingReplayPrintListener()); } @Test - @Ignore - public void printRaceData_SAM009903_20120731_sapsailing_replay() throws Exception { - new SwissTimingReplayParserImpl().readData( - getClass().getResourceAsStream("/SAM009903.20120731_sapsailing.replay"), - new SwissTimingReplayPrintListener()); - } - - @Test - @Ignore public void printRaceData_SAM009903_20120731_replay() throws Exception { new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAM009903.20120731.replay"), new SwissTimingReplayPrintListener()); } @Test - @Ignore public void printReadRaceData_SAW010955_20120802() throws Exception { new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAW010955.20120802.replay"), new SwissTimingReplayPrintListener()); } @Test - @Ignore - public void printReadRaceData_SAM002901() throws Exception { - new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAM002901.replay"), - new SwissTimingReplayPrintListener()); - } - - @Test - @Ignore - public void printReadRaceData_SAM009904_20120731() throws Exception { - new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAM009904.20120731.replay"), - new SwissTimingReplayPrintListener()); - } - - @Test - @Ignore public void printRaceData_SAW102101_20120807() throws Exception { new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAW102101.20120807.replay"), new SwissTimingReplayPrintListener()); } @Test - @Ignore public void printRaceData_SAM102101_20120807() throws Exception { new SwissTimingReplayParserImpl().readData(getClass().getResourceAsStream("/SAM102101.20120807.replay"), new SwissTimingReplayPrintListener()); diff --git a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java index f96f2692e7c..693952d20f2 100755 --- a/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java +++ b/java/com.sap.sailing.domain.swisstimingreplayadapter/src/com/sap/sailing/domain/swisstimingreplayadapter/impl/SwissTimingReplayToDomainAdapter.java @@ -305,7 +305,8 @@ public class SwissTimingReplayToDomainAdapter extends SwissTimingReplayAdapter i short ctPoints_x10_Winner) { if (boatType == BoatType.Competitor) { Pair competitorAndBoat = domainFactory.createCompetitorWithoutID(sailNumberOrTrackerID, threeLetterIOCCode.trim(), name.trim(), - currentRaceID, domainFactory.getRaceTypeFromRaceID(currentRaceID).getBoatClass()); + currentRaceID, domainFactory.getRaceTypeFromRaceID(currentRaceID).getBoatClass(), + raceTrackingHandler); Map competitorAndBoatsOfCurrentRace = competitorsAndBoatsPerRaceID.get(currentRaceID); if (competitorAndBoatsOfCurrentRace == null) { competitorAndBoatsOfCurrentRace = new HashMap<>(); diff --git a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy).launch b/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy).launch deleted file mode 100644 index 0af5786014b..00000000000 --- a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy).launch +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, No Tunnel).launch b/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, No Tunnel).launch deleted file mode 100644 index 9860c5f4d43..00000000000 --- a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, No Tunnel).launch +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, Remote Profiling).launch b/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, Remote Profiling).launch deleted file mode 100644 index 11fec4bc9a7..00000000000 --- a/java/com.sap.sailing.domain.test/SailingDomainTest (Proxy, Remote Profiling).launch +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/AbstractCandidateFilterTestSupport.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/AbstractCandidateFilterTestSupport.java new file mode 100755 index 00000000000..0ff8f07fde5 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/AbstractCandidateFilterTestSupport.java @@ -0,0 +1,78 @@ +package com.sap.sailing.domain.markpassingcalculation.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.NavigableSet; +import java.util.TreeSet; + +import org.junit.Before; + +import com.sap.sailing.domain.markpassingcalculation.Candidate; +import com.sap.sailing.domain.tracking.impl.TimedComparator; +import com.sap.sse.common.Duration; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.MillisecondsTimePoint; + +public class AbstractCandidateFilterTestSupport { + protected Candidate c1, c2, c3, c4, c5, c6, c7, c8, c9, c10; + protected NavigableSet competitorCandidates; + protected Comparator candidateComparator; + protected Candidate start; + protected Candidate end; + + @Before + public void setUp() { + final TimePoint now = MillisecondsTimePoint.now(); + start = new CandidateImpl(0, now.minus(Duration.ONE_HOUR), 1.0, null); + end = new CandidateImpl(6, now.plus(Duration.ONE_HOUR), 1.0, null); + candidateComparator = (c1, c2)->TimedComparator.INSTANCE.compare(c1, c2); + competitorCandidates = new TreeSet<>(candidateComparator); + final Duration gapless = MostProbableCandidatesInSmallTimeRangeFilter.CANDIDATE_FILTER_TIME_WINDOW.divide(2); + c1 = candidate(now, "c1"); + c2 = candidate(c1.getTimePoint().plus(gapless), "c2"); + c3 = candidate(c2.getTimePoint().plus(gapless), "c3"); + c4 = candidate(c3.getTimePoint().plus(gapless), "c4"); + c5 = candidate(c4.getTimePoint().plus(gapless), "c5"); + // add a cap here that is greater than the threshold but not twice as long; + // this way, adding one in the middle of this gap is the interesting case + c6 = candidate(c5.getTimePoint().plus(MostProbableCandidatesInSmallTimeRangeFilter.CANDIDATE_FILTER_TIME_WINDOW.times(1.5)), "c6"); + c7 = candidate(c6.getTimePoint().plus(gapless), "c7"); + c8 = candidate(c7.getTimePoint().plus(gapless), "c8"); + c9 = candidate(c8.getTimePoint().plus(gapless), "c9"); + c10= candidate(c9.getTimePoint().plus(gapless), "c10"); + competitorCandidates.add(c1); + competitorCandidates.add(c2); + competitorCandidates.add(c3); + competitorCandidates.add(c4); + competitorCandidates.add(c5); + competitorCandidates.add(c6); + competitorCandidates.add(c7); + competitorCandidates.add(c8); + competitorCandidates.add(c9); + competitorCandidates.add(c10); + } + + protected void assertContainsExactly(Iterable candidates, Candidate... candidatesExpected) { + assertEquals(""+candidates+"'s size does not match that of "+Arrays.toString(candidatesExpected)+"; ", + candidatesExpected.length, Util.size(candidates)); + assertContains(candidates, candidatesExpected); + } + + protected void assertContains(Iterable candidates, Candidate...candidatesExpected) { + for (final Candidate candidateExpected : candidatesExpected) { + assertTrue("Expected "+candidateExpected+" to be in "+candidates+" but it wasn't; ", + Util.contains(candidates, candidateExpected)); + } + } + + protected Candidate candidate(TimePoint timePoint, String name) { + return new CandidateImpl(1, timePoint, 1.0, null) { + private static final long serialVersionUID = 8547646939054562751L; + @Override public String toString() { return name; } + }; + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilterTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilterTest.java new file mode 100755 index 00000000000..ea52e1ccb0a --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilterTest.java @@ -0,0 +1,72 @@ +package com.sap.sailing.domain.markpassingcalculation.impl; + +import java.util.NavigableSet; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.markpassingcalculation.Candidate; + +public class MostProbableCandidatesInSmallTimeRangeFilterTest extends AbstractCandidateFilterTestSupport { + private MostProbableCandidatesInSmallTimeRangeFilter filter; + + @Before + public void setUp() { + super.setUp(); + filter = new MostProbableCandidatesInSmallTimeRangeFilter( + candidateComparator, start, end); + } + + @Test + public void testGetTimeWiseContiguousDistanceCandidates() { + final NavigableSet result = filter.getTimeWiseContiguousCandidates(competitorCandidates, c3, /* includeStartFrom */ true); + assertContainsExactly(result, c1, c2, c3, c4, c5); + } + + @Test + public void testGetTimeWiseContiguousDistanceCandidatesOnRightBorder() { + final NavigableSet result = filter.getTimeWiseContiguousCandidates(competitorCandidates, c5, /* includeStartFrom */ true); + assertContainsExactly(result, c1, c2, c3, c4, c5); + } + + @Test + public void testGetTimeWiseContiguousDistanceCandidatesOnLeftBorder() { + final NavigableSet result = filter.getTimeWiseContiguousCandidates(competitorCandidates, c6, /* includeStartFrom */ true); + assertContainsExactly(result, c6, c7, c8, c9, c10); + } + + @Test + public void testGetTimeWiseContiguousDistanceCandidatesInMiddleOfGapNotInserted() { + final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap"); + final NavigableSet result = filter.getTimeWiseContiguousCandidates(competitorCandidates, middleOfGap, + /* includeStartFrom */ true); + assertContainsExactly(result, c1, c2, c3, c4, c5, middleOfGap, c6, c7, c8, c9, c10); + } + + @Test + public void testGetTimeWiseContiguousDistanceCandidatesInMiddleOfGapNotInsertedDontIncludeStartFrom() { + final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap"); + final NavigableSet result = filter.getTimeWiseContiguousCandidates(competitorCandidates, middleOfGap, + /* includeStartFrom */ false); + assertContainsExactly(result, c1, c2, c3, c4, c5, c6, c7, c8, c9, c10); + } + + @Test + public void testGetTimeWiseContiguousDistanceCandidatesInMiddleOfGapInserted() { + final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap"); + competitorCandidates.add(middleOfGap); + final NavigableSet result = filter.getTimeWiseContiguousCandidates(competitorCandidates, middleOfGap, + /* includeStartFrom */ true); + assertContainsExactly(result, c1, c2, c3, c4, c5, middleOfGap, c6, c7, c8, c9, c10); + } + + @Test + public void testGetTimeWiseContiguousDistanceCandidatesInMiddleOfWidenedGap() { + competitorCandidates.remove(c5); + competitorCandidates.remove(c6); + final Candidate middleOfGap = candidate(c5.getTimePoint().plus(c5.getTimePoint().until(c6.getTimePoint()).divide(2)), "gap"); + final NavigableSet result = filter.getTimeWiseContiguousCandidates(competitorCandidates, middleOfGap, + /* includeStartFrom */ true); + assertContainsExactly(result, middleOfGap); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilterTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilterTest.java new file mode 100755 index 00000000000..082c3959d80 --- /dev/null +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilterTest.java @@ -0,0 +1,122 @@ +package com.sap.sailing.domain.markpassingcalculation.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.util.NavigableSet; +import java.util.TreeSet; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.common.Position; +import com.sap.sailing.domain.common.SpeedWithBearing; +import com.sap.sailing.domain.common.impl.DegreePosition; +import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; +import com.sap.sailing.domain.common.tracking.GPSFixMoving; +import com.sap.sailing.domain.common.tracking.impl.GPSFixMovingImpl; +import com.sap.sailing.domain.markpassingcalculation.Candidate; +import com.sap.sailing.domain.test.TrackBasedTest; +import com.sap.sailing.domain.tracking.DynamicGPSFixTrack; +import com.sap.sailing.domain.tracking.impl.DynamicGPSFixMovingTrackImpl; +import com.sap.sse.common.Bearing; +import com.sap.sse.common.Duration; +import com.sap.sse.common.Speed; +import com.sap.sse.common.TimePoint; +import com.sap.sse.common.Util; +import com.sap.sse.common.impl.DegreeBearingImpl; + +public class StationarySequenceBasedFilterTest extends AbstractCandidateFilterTestSupport { + private StationarySequence stationarySequence; + private DynamicGPSFixTrack track; + private final static int FIXES_BETWEEN_CANDIDATES = 3; + + @Before + public void setUp() { + super.setUp(); + track = new DynamicGPSFixMovingTrackImpl(TrackBasedTest.createCompetitorWithBoat("Someone"), /* millisecondsOverWhichToAverage */ 15000); + // construct a track such that all ten candidates at first end up within a single stationary sequence: + Position position = new DegreePosition(0, 0); + final SpeedWithBearing verySmallSpeed = new KnotSpeedWithBearingImpl(StationarySequence.CANDIDATE_FILTER_DISTANCE.scale(0.5/ /* number of candidates */ 10. / (double) FIXES_BETWEEN_CANDIDATES). + inTime(c1.getTimePoint().until(c10.getTimePoint())).getKnots(), new DegreeBearingImpl(0)); + TimePoint previousTimePoint = c1.getTimePoint(); + for (final Candidate candidate : new Candidate[] { c2, c3, c4, c5, c6, c7, c8, c9, c10 }) { + final Duration durationBetweenFixes = previousTimePoint.until(candidate.getTimePoint()).divide(FIXES_BETWEEN_CANDIDATES); + TimePoint timePoint = previousTimePoint; + for (int i=0; i candidatesEffectivelyAdded = new TreeSet<>(candidateComparator); + TreeSet candidatesEffectivelyRemoved = new TreeSet<>(candidateComparator); + for (final Candidate candidate : new Candidate[] { c2, c3, c4, c5, c6, c7, c8, c9, c10 }) { + assertTrue(stationarySequence.tryToExtendAfterLast(candidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved)); + } + } + + @Test + public void basicStationarySequenceTest() { + assertEquals(10, stationarySequence.size()); + assertContainsExactly(stationarySequence.getAllCandidates(), c1, c2, c3, c4, c5, c6, c7, c8, c9, c10); + final Iterable validCandidates = stationarySequence.getValidCandidates(); + assertContains(validCandidates, c1, c10); // at least the first and the last are expected to be contained + // but those in the middle, particularly with the gap between c5 and c6, are not expected to be contained + assertFalse(Util.contains(validCandidates, c5)); + assertFalse(Util.contains(validCandidates, c6)); + } + + @Test + public void testAddingOutlierFix() { + // additional set-up: + NavigableSet originalValidCandidates = new TreeSet<>(candidateComparator); + Util.addAll(stationarySequence.getValidCandidates(), originalValidCandidates); + final GPSFixMoving fixAtC2 = track.getFirstFixAtOrAfter(c2.getTimePoint()); + final GPSFixMoving fixAfterFixAtC2 = track.getFirstFixAfter(fixAtC2.getTimePoint()); + assertNotSame(fixAtC2, fixAfterFixAtC2); + final Bearing outlierCourse = fixAtC2.getSpeed().getBearing().add(new DegreeBearingImpl(90)); + // add fix in between, traveling more than StationarySequence.CANDIDATE_FILTER_DISTANCE + final Position outlierPosition = fixAtC2.getPosition().translateGreatCircle( + outlierCourse, StationarySequence.CANDIDATE_FILTER_DISTANCE.scale(2)); + final TimePoint inBetweenTimePoint = fixAtC2.getTimePoint().plus(fixAtC2.getTimePoint().until(fixAfterFixAtC2.getTimePoint()).divide(2)); + final Speed outlierSpeed = fixAtC2.getPosition().getDistance(outlierPosition).inTime(fixAtC2.getTimePoint().until(inBetweenTimePoint)); + final GPSFixMoving fixInBetween = new GPSFixMovingImpl(outlierPosition, inBetweenTimePoint, new KnotSpeedWithBearingImpl(outlierSpeed.getKnots(), outlierCourse)); + track.add(fixInBetween); + TreeSet candidatesEffectivelyAdded = new TreeSet<>(candidateComparator); + TreeSet candidatesEffectivelyRemoved = new TreeSet<>(candidateComparator); + final StationarySequence splitResult = stationarySequence.tryToAddFix(fixInBetween, candidatesEffectivelyAdded, candidatesEffectivelyRemoved, + /* stationarySequenceSetToUpdate */ null, /* isReplacement */ false); + // assertions: + assertEquals(2, stationarySequence.size()); + assertContainsExactly(stationarySequence.getAllCandidates(), c1, c2); + assertContainsExactly(stationarySequence.getValidCandidates(), c1, c2); + assertEquals(8, splitResult.size()); + assertContainsExactly(splitResult.getAllCandidates(), c3, c4, c5, c6, c7, c8, c9, c10); + assertContains(splitResult.getValidCandidates(), c3, c10); + NavigableSet validCandidates = new TreeSet<>(candidateComparator); + Util.addAll(stationarySequence.getValidCandidates(), validCandidates); + Util.addAll(splitResult.getValidCandidates(), validCandidates); + assertCorrectDiff(originalValidCandidates, validCandidates, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + } + + private void assertCorrectDiff(NavigableSet originalValidCandidates, + NavigableSet validCandidates, TreeSet candidatesEffectivelyAdded, + TreeSet candidatesEffectivelyRemoved) { + NavigableSet newValidCandidates = new TreeSet<>(candidateComparator); + newValidCandidates.addAll(validCandidates); + newValidCandidates.removeAll(originalValidCandidates); + assertContainsExactly(candidatesEffectivelyAdded, newValidCandidates.toArray(new Candidate[0])); + NavigableSet removedValidCandidates = new TreeSet<>(candidateComparator); + removedValidCandidates.addAll(originalValidCandidates); + removedValidCandidates.removeAll(validCandidates); + assertContainsExactly(candidatesEffectivelyRemoved, removedValidCandidates.toArray(new Candidate[0])); + } +} diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java index e9dd2a19c63..c57aa6b3718 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java @@ -1150,6 +1150,203 @@ public class LeaderboardScoringAndRankingTest extends LeaderboardScoringAndRanki assertTrue(rankedCompetitors.indexOf(c[0]) == rankedCompetitors.indexOf(c[1])-1); } + /** + * Tests the {@link LowPointTieBreakBasedOnLastSeriesOnly} scoring scheme's tie breaking rule by setting up a + * leaderboard with a qualification and a final series such that the competitors have equal points but different + * number of wins in the final series, leading to a different order than it would if sorting by the total number + * of wins. + */ + @Test + public void testTieBreakBasedOnLastNonMedalSeriesNoMedal() throws NoWindException { + Competitor[] c = createCompetitors(3).toArray(new Competitor[0]); + Competitor[] q1 = new Competitor[] { c[1], c[0], c[2] }; // c[0] wins no race in the qualification, c[1] wins two, + Competitor[] q2 = new Competitor[] { c[1], c[0], c[2] }; // but that doesn't help c[1] because c[0] wins one in the final + Competitor[] f1 = new Competitor[] { c[2], c[0], c[1] }; // where c[1] wins none. The tie is broken in favor of c[0]. + Competitor[] f2 = new Competitor[] { c[0], c[1], c[2] }; + Regatta regatta = createRegatta(/* qualifying */2, new String[] { "Default" }, /* final */2, new String[] { "Default" }, + /* medal */ false, /* medal */ 0, "testTieBreakWithTieBreakBasedOnLastNonMedalSeriesNoMedal", + DomainFactory.INSTANCE.getOrCreateBoatClass("49er", /* typicallyStartsUpwind */true), + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY)); + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + createAndAttachTrackedRaces(series.get(0), "Default", /* withScores */ true, q1, q2); + TimePoint later = createAndAttachTrackedRaces(series.get(1), "Default", /* withScores */ true, f1, f2); + List rankedCompetitors = leaderboard.getCompetitorsFromBestToWorst(later); + assertEquals(leaderboard.getNetPoints(c[0], later), leaderboard.getNetPoints(c[1], later), 0.000000001); + assertTrue(leaderboard.getNetPoints(c[0], later) < leaderboard.getNetPoints(c[2], later)); + assertTrue(rankedCompetitors.indexOf(c[0]) < rankedCompetitors.indexOf(c[1])); + } + + /** + * Tests the {@link LowPointTieBreakBasedOnLastSeriesOnly} scoring scheme's tie breaking rule. c[0] and c[2] have + * equal scores after excluding their worst score each. They both exclude a score from the final series. c[1]'s and + * c[3]'s net points sum is worse than that of c[0] and c[2], so c[1] is not tied. Comparing c[0]'s and c[2]'s + * non-excluded final series scores does not break the tie. They both have as their only non-excluded race a win in + * the final series. So the tie break needs to continue by comparing the sorted non-excluded scores from the + * qualification series. c[2]'s excluded score from the final series is worse than the one c[0] excludes in the + * final series. Yet, in the qualification, c[2] has the better ordered scores, so is expected to be found the + * winner. + */ + @Test + public void testTieBreakBasedOnLastNonMedalSeriesNoMedalOneDiscard() throws NoWindException { + Competitor[] c = createCompetitors(4).toArray(new Competitor[0]); + Competitor[] q1 = new Competitor[] { c[0], c[1], c[2], c[3] }; // c[2]'s net points: 6.0; c[0]'s net points: 6.0 + Competitor[] q2 = new Competitor[] { c[2], c[0], c[1], c[3] }; // c[2] has two wins in the qualification, c[0] only one + Competitor[] q3 = new Competitor[] { c[2], c[0], c[1], c[3] }; + Competitor[] f1 = new Competitor[] { c[2], c[1], c[0], c[3] }; // c[0] discards this race; remaining is one win in the final series + Competitor[] f2 = new Competitor[] { c[0], c[1], c[3], c[2] }; // c[2] discards this race; remaining is one win in the final series + Regatta regatta = createRegatta(/* qualifying */3, new String[] { "Default" }, /* final */2, new String[] { "Default" }, + /* medal */ false, /* medal */ 0, "testTieBreakWithTieBreakBasedOnLastNonMedalSeriesNoMedal", + DomainFactory.INSTANCE.getOrCreateBoatClass("49er", /* typicallyStartsUpwind */true), + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY)); + Leaderboard leaderboard = createLeaderboard(regatta, + /* discarding thresholds: one discard when four races have been completed */ new int[] { 4 }); + createAndAttachTrackedRaces(series.get(0), "Default", /* withScores */ true, q1, q2, q3); + TimePoint later = createAndAttachTrackedRaces(series.get(1), "Default", /* withScores */ true, f1, f2); + List rankedCompetitors = leaderboard.getCompetitorsFromBestToWorst(later); + assertEquals(leaderboard.getNetPoints(c[0], later), leaderboard.getNetPoints(c[2], later), 0.000000001); + assertTrue(rankedCompetitors.indexOf(c[2]) < rankedCompetitors.indexOf(c[0])); + } + + /** + * Tests the {@link LowPointTieBreakBasedOnLastSeriesOnly} scoring scheme's tie breaking rule. c[0] and c[2] have + * equal scores after excluding their worst score each. c[0] excludes a score from the final series, c[2] from the + * qualification. The way we understand the rules up to now (see also + * https://bugzilla.sapsailing.com/bugzilla/show_bug.cgi?id=4717#c4) we have to sort by series as the primary and by + * score as the secondary criterion, making c[0]'s second score from the top the best score from the qualification, + * whereas for c[2] the second score from the top will be the worst score from the final series. This will break the + * tie in c[0]'s favor. + *

+ * + * c[1]'s and c[3]'s net points sum is worse than that of c[0] and c[2], so c[1] is not tied. + */ + @Test + public void testTieBreakBasedOnLastNonMedalSeriesNoMedalDiscardsInDifferentSeries() throws NoWindException { + Competitor[] c = createCompetitors(4).toArray(new Competitor[0]); + Competitor[] q1 = new Competitor[] { c[0], c[1], c[3], c[2] }; // c[2] discards this race + Competitor[] q2 = new Competitor[] { c[2], c[0], c[1], c[3] }; // c[2] net points: 6.0; c[0] net points: 6.0 + Competitor[] q3 = new Competitor[] { c[2], c[0], c[1], c[3] }; + Competitor[] f1 = new Competitor[] { c[2], c[1], c[0], c[3] }; // c[0] discards this race + Competitor[] f2 = new Competitor[] { c[0], c[1], c[2], c[3] }; // c[0] and c[2] won one race in the final series; next, c[2]'s 3.0 points in f2 compare to c[0]'s win in q1 + Regatta regatta = createRegatta(/* qualifying */3, new String[] { "Default" }, /* final */2, new String[] { "Default" }, + /* medal */ false, /* medal */ 0, "testTieBreakWithTieBreakBasedOnLastNonMedalSeriesNoMedal", + DomainFactory.INSTANCE.getOrCreateBoatClass("49er", /* typicallyStartsUpwind */true), + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY)); + Leaderboard leaderboard = createLeaderboard(regatta, + /* discarding thresholds: one discard when four races have been completed */ new int[] { 4 }); + createAndAttachTrackedRaces(series.get(0), "Default", /* withScores */ true, q1, q2, q3); + TimePoint later = createAndAttachTrackedRaces(series.get(1), "Default", /* withScores */ true, f1, f2); + List rankedCompetitors = leaderboard.getCompetitorsFromBestToWorst(later); + assertEquals(leaderboard.getNetPoints(c[0], later), leaderboard.getNetPoints(c[2], later), 0.000000001); + assertTrue(rankedCompetitors.indexOf(c[0]) < rankedCompetitors.indexOf(c[2])); + } + + + /** + * Tests the {@link LowPointTieBreakBasedOnLastSeriesOnly} scoring scheme's tie breaking rule by setting up a + * leaderboard with a qualification and a final series such that the competitors have equal points but different + * number of wins in the final series, leading to a different order than it would if sorting by the total number + * of wins. + */ + @Test + public void testTieBreakBasedOnLastNonMedalSeriesNoMedalDecisionByLastRace() throws NoWindException { + Competitor[] c = createCompetitors(3).toArray(new Competitor[0]); + Competitor[] q1 = new Competitor[] { c[1], c[0], c[2] }; // c[0] and c[1] have equal wins and scores, + Competitor[] q2 = new Competitor[] { c[0], c[1], c[2] }; // but c[1] has won the last race + Competitor[] f1 = new Competitor[] { c[0], c[1], c[2] }; + Competitor[] f2 = new Competitor[] { c[1], c[0], c[2] }; + Regatta regatta = createRegatta(/* qualifying */2, new String[] { "Default" }, /* final */2, new String[] { "Default" }, + /* medal */ false, /* medal */ 0, "testTieBreakWithTieBreakBasedOnLastNonMedalSeriesNoMedal", + DomainFactory.INSTANCE.getOrCreateBoatClass("49er", /* typicallyStartsUpwind */true), + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY)); + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + createAndAttachTrackedRaces(series.get(0), "Default", /* withScores */ true, q1, q2); + TimePoint later = createAndAttachTrackedRaces(series.get(1), "Default", /* withScores */ true, f1, f2); + List rankedCompetitors = leaderboard.getCompetitorsFromBestToWorst(later); + assertEquals(leaderboard.getNetPoints(c[0], later), leaderboard.getNetPoints(c[1], later), 0.000000001); + assertTrue(rankedCompetitors.indexOf(c[1]) < rankedCompetitors.indexOf(c[0])); // based on last race f2 + } + + /** + * Tests the {@link LowPointTieBreakBasedOnLastSeriesOnly} scoring scheme's tie breaking rule by setting up a + * leaderboard with a qualification and a final series such that the competitors have equal points, and equal + * scores in the final series, but one of them having more wins than the other in the qualification. In this + * case, the qualification counts. + */ + @Test + public void testTieBreakBasedOnLastNonMedalSeriesNoMedalDefaultingToQualification() throws NoWindException { + Competitor[] c = createCompetitors(3).toArray(new Competitor[0]); + Competitor[] q1 = new Competitor[] { c[1], c[0], c[2] }; // c[0] and c[1] have an equal number of wins in the final series and equal total points; + Competitor[] q2 = new Competitor[] { c[2], c[0], c[1] }; // tie breaking is expected to then take the wins in the qualification series into account + Competitor[] f1 = new Competitor[] { c[0], c[1], c[2] }; // where c[1] has one more than c[0] + Competitor[] f2 = new Competitor[] { c[1], c[0], c[2] }; // tie breaking will have to resort to the qualification, finding the win of c[1] + Regatta regatta = createRegatta(/* qualifying */2, new String[] { "Default" }, /* final */2, new String[] { "Default" }, + /* medal */ false, /* medal */ 0, "testTieBreakWithTieBreakBasedOnLastNonMedalSeriesNoMedalDefaultingToQualification", + DomainFactory.INSTANCE.getOrCreateBoatClass("49er", /* typicallyStartsUpwind */true), + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY)); + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + createAndAttachTrackedRaces(series.get(0), "Default", /* withScores */ true, q1, q2); + TimePoint later = createAndAttachTrackedRaces(series.get(1), "Default", /* withScores */ true, f1, f2); + List rankedCompetitors = leaderboard.getCompetitorsFromBestToWorst(later); + assertEquals(leaderboard.getNetPoints(c[0], later), leaderboard.getNetPoints(c[1], later), 0.000000001); + assertTrue(rankedCompetitors.indexOf(c[1]) < rankedCompetitors.indexOf(c[0])); + } + + /** + * Tests the {@link LowPointTieBreakBasedOnLastSeriesOnly} scoring scheme's tie breaking rule by setting up a + * leaderboard with a qualification and a final series as well as a medal race such that the competitors have equal + * points but different numbers of wins in the final series + */ + @Test + public void testTieBreakBasedOnLastNonMedalSeriesWithMedal() throws NoWindException { + Competitor[] c = createCompetitors(3).toArray(new Competitor[0]); + Competitor[] q1 = new Competitor[] { c[1], c[0], c[2] }; // c[0] wins no race in the qualification, c[1] wins two, + Competitor[] q2 = new Competitor[] { c[1], c[0], c[2] }; // but that doesn't help c[1] because c[0] wins one in the final + Competitor[] f1 = new Competitor[] { c[2], c[0], c[1] }; // where c[1] wins none. The tie is broken in favor of c[0]. + Competitor[] f2 = new Competitor[] { c[0], c[1], c[2] }; + Competitor[] m = new Competitor[] { c[0], c[1] }; // c[0] and c[1] got promoted to the medal race, both get disqualified; but in f2 c[2] won, so has to win overall due to count-back + Regatta regatta = createRegatta(/* qualifying */2, new String[] { "Default" }, /* final */2, new String[] { "Default" }, + /* medal */ true, /* medal */ 1, "testTieBreakWithTieBreakBasedOnLastNonMedalSeriesWithMedal", + DomainFactory.INSTANCE.getOrCreateBoatClass("49er", /* typicallyStartsUpwind */true), + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY)); + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + createAndAttachTrackedRaces(series.get(0), "Default", /* withScores */ true, q1, q2); + createAndAttachTrackedRaces(series.get(1), "Default", /* withScores */ true, f1, f2); + TimePoint later = createAndAttachTrackedRaces(series.get(2), "Medal", /* withScores */ true, m); + leaderboard.getScoreCorrection().setMaxPointsReason(c[0], series.get(2).getRaceColumns().iterator().next(), MaxPointsReason.DSQ); + leaderboard.getScoreCorrection().setMaxPointsReason(c[1], series.get(2).getRaceColumns().iterator().next(), MaxPointsReason.DSQ); + List rankedCompetitors = leaderboard.getCompetitorsFromBestToWorst(later); + assertEquals(leaderboard.getNetPoints(c[0], later), leaderboard.getNetPoints(c[1], later), 0.000000001); + assertTrue(rankedCompetitors.indexOf(c[0]) < rankedCompetitors.indexOf(c[1])); // due to more wins in final series + } + + /** + * Tests the {@link LowPointTieBreakBasedOnLastSeriesOnly} scoring scheme's tie breaking rule by setting up a + * leaderboard with a qualification and a final series as well as a medal race such that the competitors have equal + * points but different numbers of wins in the final series + */ + @Test + public void testTieBreakBasedOnLastNonMedalSeriesWithMedalDecisionByLastRace() throws NoWindException { + Competitor[] c = createCompetitors(3).toArray(new Competitor[0]); + Competitor[] q1 = new Competitor[] { c[1], c[0], c[2] }; // c[0] and c[1] have equal wins and scores, + Competitor[] q2 = new Competitor[] { c[0], c[1], c[2] }; // but c[1] has won the last race before the medal + Competitor[] f1 = new Competitor[] { c[0], c[1], c[2] }; + Competitor[] f2 = new Competitor[] { c[1], c[0], c[2] }; + Competitor[] m = new Competitor[] { c[0], c[1] }; // c[0] and c[1] got promoted to the medal race, both get disqualified; but in f2 c[2] won, so has to win overall due to count-back + Regatta regatta = createRegatta(/* qualifying */2, new String[] { "Default" }, /* final */2, new String[] { "Default" }, + /* medal */ true, /* medal */ 1, "testTieBreakWithTieBreakBasedOnLastNonMedalSeriesWithMedal", + DomainFactory.INSTANCE.getOrCreateBoatClass("49er", /* typicallyStartsUpwind */true), + DomainFactory.INSTANCE.createScoringScheme(ScoringSchemeType.LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY)); + Leaderboard leaderboard = createLeaderboard(regatta, /* discarding thresholds */ new int[0]); + createAndAttachTrackedRaces(series.get(0), "Default", /* withScores */ true, q1, q2); + createAndAttachTrackedRaces(series.get(1), "Default", /* withScores */ true, f1, f2); + TimePoint later = createAndAttachTrackedRaces(series.get(2), "Medal", /* withScores */ true, m); + leaderboard.getScoreCorrection().setMaxPointsReason(c[0], series.get(2).getRaceColumns().iterator().next(), MaxPointsReason.DSQ); + leaderboard.getScoreCorrection().setMaxPointsReason(c[1], series.get(2).getRaceColumns().iterator().next(), MaxPointsReason.DSQ); + List rankedCompetitors = leaderboard.getCompetitorsFromBestToWorst(later); + assertEquals(leaderboard.getNetPoints(c[0], later), leaderboard.getNetPoints(c[1], later), 0.000000001); + assertTrue(rankedCompetitors.indexOf(c[1]) < rankedCompetitors.indexOf(c[0])); // due to last non-medal race + } + /** * Reported by Clemens Fackeldey:

* diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestBase.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestBase.java index d650dec691f..169ef1b0971 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestBase.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTestBase.java @@ -38,6 +38,7 @@ import com.sap.sailing.domain.tracking.MarkPassing; import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.impl.MarkPassingImpl; import com.sap.sailing.domain.tracking.impl.TimedComparator; +import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; import com.sap.sse.util.impl.ArrayListNavigableSet; @@ -98,6 +99,12 @@ public class LeaderboardScoringAndRankingTestBase extends AbstractLeaderboardTes result.add(new MarkPassingImpl(lastMarkPassingTimes.get(competitor), finish, competitor)); return result; } + @Override + public Duration getTimeSailedSinceRaceStart(Competitor competitor, TimePoint timePoint) { + final TimePoint timePointOfFinishMarkPassing = getMarkPassings(competitor).last().getTimePoint(); + final TimePoint to = timePointOfFinishMarkPassing.before(timePoint) ? timePointOfFinishMarkPassing : timePoint; + return to.before(getStartOfRace()) ? null : getStartOfRace().until(to); + } }; trackedRace.getRace().getCourse().addWaypoint(0, start); trackedRace.getRace().getCourse().addWaypoint(1, finish); diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java index ae98abb81b2..e59f4edb4b8 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/DomainFactory.java @@ -83,11 +83,12 @@ public interface DomainFactory { Sideline createSideline(String name, Iterable controlPoints); - com.sap.sailing.domain.base.Boat getOrCreateBoat(Serializable boatId, String boatName, BoatClass boatClass, String sailId, Color boatColor); + com.sap.sailing.domain.base.Boat getOrCreateBoat(Serializable boatId, String boatName, BoatClass boatClass, + String sailId, Color boatColor, RaceTrackingHandler raceTrackingHandler); com.sap.sailing.domain.base.Competitor resolveCompetitor(ICompetitor competitor); - void updateCompetitor(ICompetitor competitor); + void updateCompetitor(ICompetitor competitor, RaceTrackingHandler raceTrackingHandler); /** * Looks up or, if not found, creates a {@link Nationality} object and re-uses threeLetterIOCCode also as the @@ -264,7 +265,7 @@ public interface DomainFactory { RaceDefinition getAndWaitForRaceDefinition(UUID raceId, long timeoutInMilliseconds); Map getOrCreateCompetitorsAndTheirBoats(DynamicTrackedRegatta trackedRegatta, LeaderboardGroupResolver LeaderboardGroupResolver, - IRace race, BoatClass defaultBoatClass); + IRace race, BoatClass defaultBoatClass, RaceTrackingHandler raceTrackingHandler); BoatClass resolveDominantBoatClassOfRace(IRace race); diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java index 086f3cf6e84..f237b3a6a23 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/DomainFactoryImpl.java @@ -266,13 +266,13 @@ public class DomainFactoryImpl implements DomainFactory { } @Override - public void updateCompetitor(ICompetitor competitor) { + public void updateCompetitor(ICompetitor competitor, RaceTrackingHandler raceTrackingHandler) { Competitor domainCompetitor = this.resolveCompetitor(competitor); if (domainCompetitor != null) { if (domainCompetitor.hasBoat()) { - getOrCreateCompetitorWithBoat(competitor); + getOrCreateCompetitorWithBoat(competitor, raceTrackingHandler); } else { - getOrCreateCompetitor(competitor); + getOrCreateCompetitor(competitor, raceTrackingHandler); } logger.info("Competitor " + competitor + " was updated on TracTrac side. Maybe consider updating in competitor store as well. " @@ -283,23 +283,27 @@ public class DomainFactoryImpl implements DomainFactory { } } - private Competitor getOrCreateCompetitor(ICompetitor competitor) { + private Competitor getOrCreateCompetitor(ICompetitor competitor, RaceTrackingHandler raceTrackingHandler) { Competitor result = getOrCreateCompetitor(competitor.getId(), competitor.getNationality(), competitor.getName(), - competitor.getShortName(), competitor.getHandicapToT(), competitor.getHandicapToD(), null); + competitor.getShortName(), competitor.getHandicapToT(), competitor.getHandicapToD(), null, + raceTrackingHandler); return result; } - private CompetitorWithBoat getOrCreateCompetitorWithBoat(ICompetitor competitor) { + private CompetitorWithBoat getOrCreateCompetitorWithBoat(ICompetitor competitor, + final RaceTrackingHandler raceTrackingHandler) { final String sailId = competitor.getShortName(); // we take the sailId from the shortName attribute final String competitorClassName = competitor.getCompetitorClass()==null?null:competitor.getCompetitorClass().getName(); CompetitorWithBoat result = getOrCreateCompetitorWithBoat(competitor.getId(), competitor.getNationality(), competitor.getName(), - /* shortName */ null, competitor.getHandicapToT(), competitor.getHandicapToD(), null, competitorClassName, sailId); + /* shortName */ null, competitor.getHandicapToT(), competitor.getHandicapToD(), null, + competitorClassName, sailId, raceTrackingHandler); return result; } private CompetitorWithBoat getOrCreateCompetitorWithBoat(final UUID competitorId, final String nationalityAsString, final String name, final String shortName, float timeOnTimeFactor, - float timeOnDistanceAllowanceInSecondsPerNauticalMile, String searchTag, String competitorClassName, String sailId) { + float timeOnDistanceAllowanceInSecondsPerNauticalMile, String searchTag, String competitorClassName, + String sailId, final RaceTrackingHandler raceTrackingHandler) { CompetitorAndBoatStore competitorStore = baseDomainFactory.getCompetitorAndBoatStore(); CompetitorWithBoat domainCompetitor = competitorStore.getExistingCompetitorWithBoatById(competitorId); if (domainCompetitor == null || competitorStore.isCompetitorToUpdateDuringGetOrCreate(domainCompetitor)) { @@ -316,7 +320,8 @@ public class DomainFactoryImpl implements DomainFactory { DynamicTeam team = createTeam(name, nationality, competitorId); DynamicBoat boat = (DynamicBoat) competitorStore.getOrCreateBoat(domainCompetitor == null ? UUID.randomUUID() : domainCompetitor.getBoat().getId(), null /* no boat name available */, boatClass, sailId, /* color */ null); - domainCompetitor = competitorStore.getOrCreateCompetitorWithBoat(competitorId, name, shortName, null /* displayColor */, + domainCompetitor = raceTrackingHandler.getOrCreateCompetitorWithBoat(competitorStore, competitorId, name, + shortName, null /* displayColor */, null /* email */, null /* flagImag */, team, (double) timeOnTimeFactor, new MillisecondsDurationImpl((long) (timeOnDistanceAllowanceInSecondsPerNauticalMile*1000)), searchTag, (DynamicBoat) boat); } @@ -325,7 +330,8 @@ public class DomainFactoryImpl implements DomainFactory { private Competitor getOrCreateCompetitor(final UUID competitorId, final String nationalityAsString, final String name, final String shortName, float timeOnTimeFactor, - float timeOnDistanceAllowanceInSecondsPerNauticalMile, String searchTag) { + float timeOnDistanceAllowanceInSecondsPerNauticalMile, String searchTag, + RaceTrackingHandler raceTrackingHandler) { CompetitorAndBoatStore competitorStore = baseDomainFactory.getCompetitorAndBoatStore(); Competitor domainCompetitor = competitorStore.getExistingCompetitorById(competitorId); if (domainCompetitor == null || competitorStore.isCompetitorToUpdateDuringGetOrCreate(domainCompetitor)) { @@ -338,18 +344,22 @@ public class DomainFactoryImpl implements DomainFactory { logger.log(Level.SEVERE, "Unknown nationality "+nationalityAsString+" for competitor "+name+"; leaving null", iae); } DynamicTeam team = createTeam(name, nationality, competitorId); - domainCompetitor = competitorStore.getOrCreateCompetitor(competitorId, name, shortName, null /* displayColor */, + domainCompetitor = raceTrackingHandler.getOrCreateCompetitor(competitorStore, competitorId, name, shortName, + null /* displayColor */, null /* email */, null /* flagImag */, team, (double) timeOnTimeFactor, new MillisecondsDurationImpl((long) (timeOnDistanceAllowanceInSecondsPerNauticalMile*1000)), searchTag); } return domainCompetitor; } - public Boat getOrCreateBoat(Serializable boatId, String boatName, BoatClass boatClass, String sailId, Color boatColor) { + @Override + public Boat getOrCreateBoat(Serializable boatId, String boatName, BoatClass boatClass, String sailId, + Color boatColor, RaceTrackingHandler raceTrackingHandler) { CompetitorAndBoatStore competitorStore = baseDomainFactory.getCompetitorAndBoatStore(); Boat domainBoat = competitorStore.getExistingBoatById(boatId); if (domainBoat == null) { - domainBoat = baseDomainFactory.getCompetitorAndBoatStore().getOrCreateBoat(boatId, boatName, boatClass, sailId, boatColor); + domainBoat = raceTrackingHandler.getOrCreateBoat(baseDomainFactory.getCompetitorAndBoatStore(), boatId, + boatName, boatClass, sailId, boatColor); } return domainBoat; } @@ -729,7 +739,7 @@ public class DomainFactoryImpl implements DomainFactory { */ @Override public Map getOrCreateCompetitorsAndTheirBoats(DynamicTrackedRegatta trackedRegatta, LeaderboardGroupResolver leaderboardGroupResolver, - IRace race, BoatClass defaultBoatClass) { + IRace race, BoatClass defaultBoatClass, RaceTrackingHandler raceTrackingHandler) { final CompetitorAndBoatStore competitorAndBoatStore = baseDomainFactory.getCompetitorAndBoatStore(); final Map competitorsAndBoats = new HashMap<>(); Regatta regatta = trackedRegatta.getRegatta(); @@ -818,22 +828,25 @@ public class DomainFactoryImpl implements DomainFactory { competitorToUse = existingCompetitor; } } else { - competitorToUse = getOrCreateCompetitor(rc.getCompetitor()); + competitorToUse = getOrCreateCompetitor(rc.getCompetitor(), raceTrackingHandler); } Boat boatToUse; if (existingBoat != null) { boatToUse = existingBoat; } else { if (competitorBoatInfo != null) { - boatToUse = getOrCreateBoat(boatId, competitorBoatInfo.getName(), defaultBoatClass, sailId, AbstractColor.getCssColor(competitorBoatInfo.getColor())); + boatToUse = getOrCreateBoat(boatId, competitorBoatInfo.getName(), defaultBoatClass, sailId, + AbstractColor.getCssColor(competitorBoatInfo.getColor()), raceTrackingHandler); } else { - boatToUse = getOrCreateBoat(boatId, /* boat name*/ null, defaultBoatClass, sailId, null); + boatToUse = getOrCreateBoat(boatId, /* boat name */ null, defaultBoatClass, sailId, null, + raceTrackingHandler); } } competitorsAndBoats.put(competitorToUse, boatToUse); } else { // Case 2 we assume here that the boat is contained in competitor as it's always the same - CompetitorWithBoat competitorWithBoat = getOrCreateCompetitorWithBoat(rc.getCompetitor()); + CompetitorWithBoat competitorWithBoat = getOrCreateCompetitorWithBoat(rc.getCompetitor(), + raceTrackingHandler); competitorsAndBoats.put(competitorWithBoat, competitorWithBoat.getBoat()); } }); diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java index 1721fec0d03..6959368b97d 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/RaceCourseReceiver.java @@ -176,7 +176,9 @@ public class RaceCourseReceiver extends AbstractReceiverWithQueue competitorAndBoats = getDomainFactory().getOrCreateCompetitorsAndTheirBoats(getTrackedRegatta(), leaderboardGroupResolver, tractracRace, dominantBoatClass); + Map competitorAndBoats = getDomainFactory().getOrCreateCompetitorsAndTheirBoats( + getTrackedRegatta(), leaderboardGroupResolver, tractracRace, dominantBoatClass, + raceTrackingHandler); trackedRace = getDomainFactory().getOrCreateRaceDefinitionAndTrackedRace( getTrackedRegatta(), tractracRace.getId(), tractracRace.getName(), dominantBoatClass, competitorAndBoats, course, sidelines, windStore, delayToLiveInMillis, diff --git a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java index bc57f7f1f73..161a560fd3a 100755 --- a/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java +++ b/java/com.sap.sailing.domain.tractracadapter/src/com/sap/sailing/domain/tractracadapter/impl/TracTracRaceTrackerImpl.java @@ -340,7 +340,7 @@ public class TracTracRaceTrackerImpl extends AbstractRaceTrackerImpl @Override public void updateCompetitor(ICompetitor competitor) { if (!competitor.isNonCompeting()) { - TracTracRaceTrackerImpl.this.domainFactory.updateCompetitor(competitor); + TracTracRaceTrackerImpl.this.domainFactory.updateCompetitor(competitor, raceTrackingHandler); } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoringScheme.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoringScheme.java index 8964a073e76..546e4464f58 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoringScheme.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/ScoringScheme.java @@ -3,6 +3,8 @@ package com.sap.sailing.domain.leaderboard; import java.io.Serializable; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import com.sap.sailing.domain.base.Competitor; @@ -86,12 +88,19 @@ public interface ScoringScheme extends Serializable { TimePoint timePoint, Leaderboard leaderboard); /** - * @param competitor1Scores scores of the first competitor, in the order of race columns in the leaderboard - * @param competitor2Scores scores of the second competitor, in the order of race columns in the leaderboard - * @param leaderboard TODO + * @param competitor1Scores + * scores of the first competitor, in the order of race columns in the leaderboard + * @param competitor2Scores + * scores of the second competitor, in the order of race columns in the leaderboard + * @param discardedRaceColumnsPerCompetitor + * for each competitor holds the result of {@link Leaderboard#getResultDiscardingRule() + * Leaderborad.getResultDiscardingRule()}{@code .}{@link ResultDiscardingRule#getDiscardedRaceColumns(Competitor, Leaderboard, Iterable, TimePoint) + * getDiscardedRaceColumns(...)}. This accelerates things considerable because we do not have to make this expensive calculation + * for each competitor again. */ - int compareByBetterScore(Competitor o1, - List> competitor1Scores, Competitor o2, List> competitor2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard); + int compareByBetterScore(Competitor o1, List> competitor1Scores, Competitor o2, + List> competitor2Scores, boolean nullScoresAreBetter, TimePoint timePoint, + Leaderboard leaderboard, Map> discardedRaceColumnsPerCompetitor); /** * In case two competitors scored in different numbers of races, this scoring scheme decides whether this @@ -103,9 +112,13 @@ public interface ScoringScheme extends Serializable { ScoringSchemeType getType(); /** - * Usually, when all other sorting criteria end up in a tie, the last race sailed is used to decide. - * @param o1 TODO - * @param o2 TODO + * Usually, when all other sorting criteria end up in a tie, the last race sailed is used to decide, and from there + * backwards. This implements Racing Rules of Sailing (RRS) rule A8.2: + *

+ * + * "A8.2 If a tie remains between two or more boats, they shall be ranked in order of their scores in the last + * race. Any remaining ties shall be broken by using the tied boats’ scores in the next-to-last race and so on until + * all ties are broken. These scores shall be used even if some of them are excluded scores." */ int compareByLastRace(List> o1Scores, List> o2Scores, boolean nullScoresAreBetter, Competitor o1, Competitor o2); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointExtremeSailingSeriesOverall.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointExtremeSailingSeriesOverall.java index 48d0964a3b9..9cdfd4edc82 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointExtremeSailingSeriesOverall.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointExtremeSailingSeriesOverall.java @@ -3,6 +3,8 @@ package com.sap.sailing.domain.leaderboard.impl; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; +import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import com.sap.sailing.domain.base.Competitor; @@ -55,7 +57,7 @@ public abstract class AbstractHighPointExtremeSailingSeriesOverall extends HighP * Implements rule 13.5 of the Extreme Sailing Series notice of race as of August 2012. */ @Override - public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard) { + public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard, Map> discardedRaceColumnsPerCompetitor) { int o1Wins = getWins(o1Scores); int o2Wins = getWins(o2Scores); int result = o2Wins - o1Wins; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java index 800f4125459..5b62ef9bedf 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java @@ -1,6 +1,8 @@ package com.sap.sailing.domain.leaderboard.impl; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import com.sap.sailing.domain.abstractlog.race.RaceLog; @@ -37,7 +39,7 @@ public abstract class AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie extends } @Override - public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard) { + public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard, Map> discardedRaceColumnsPerCompetitor) { Double o1Highest = getHighestScore(o1Scores); Double o2Highest = getHighestScore(o2Scores); return o2Highest.compareTo(o1Highest); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java index 6eb406ee3c7..db9d59f35a4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractLeaderboardWithCache.java @@ -27,8 +27,10 @@ import java.util.logging.Logger; import com.sap.sailing.domain.abstractlog.race.InvalidatesLeaderboardCache; import com.sap.sailing.domain.abstractlog.race.RaceLogEvent; +import com.sap.sailing.domain.base.Boat; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorWithBoat; import com.sap.sailing.domain.base.Course; import com.sap.sailing.domain.base.DomainFactory; import com.sap.sailing.domain.base.Fleet; @@ -118,13 +120,14 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { private final Distance averageAbsoluteCrossTrackError; private final Distance averageSignedCrossTrackError; private final Duration gapToLeaderInOwnTime; + private final Duration timeSailedSinceRaceStart; private final Duration correctedTime; private final Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; public RaceDetails(List legDetails, Distance windwardDistanceToCompetitorFarthestAhead, Distance averageAbsoluteCrossTrackError, Distance averageSignedCrossTrackError, - Duration gapToLeaderInOwnTime, Duration correctedTime, - Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { + Duration gapToLeaderInOwnTime, Duration timeSailedSinceRaceStart, + Duration correctedTime, Duration correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead) { super(); this.legDetails = legDetails; this.windwardDistanceToCompetitorFarthestAhead = windwardDistanceToCompetitorFarthestAhead; @@ -132,6 +135,7 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { this.averageSignedCrossTrackError = averageSignedCrossTrackError; this.gapToLeaderInOwnTime = gapToLeaderInOwnTime; this.correctedTime = correctedTime; + this.timeSailedSinceRaceStart = timeSailedSinceRaceStart; this.correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = correctedTimeAtEstimatedArrivalAtCompetitorFarthestAhead; } public List getLegDetails() { @@ -149,6 +153,9 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { public Duration getGapToLeaderInOwnTime() { return gapToLeaderInOwnTime; } + public Duration getTimeSailedSinceRaceStart() { + return timeSailedSinceRaceStart; + } public Duration getCorrectedTime() { return correctedTime; } @@ -580,8 +587,24 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { LeaderboardEntryDTO entryDTO = new LeaderboardEntryDTO(); TrackedRace trackedRace = raceColumn.getTrackedRace(competitor); entryDTO.race = trackedRace == null ? null : trackedRace.getRaceIdentifier(); - // TODO bug2822: Should this rather come from raceColumn.getBoatOfCompetitor() ? - entryDTO.boat = trackedRace == null ? null : baseDomainFactory.convertToBoatDTO(trackedRace.getBoatOfCompetitor(competitor)); + Boat boat; + if (trackedRace != null) { + boat = trackedRace.getBoatOfCompetitor(competitor); + } else { + // check if it's a CompetitorWithBoat: + if (competitor.hasBoat()) { + boat = ((CompetitorWithBoat) competitor).getBoat(); + } else { + final Fleet fleetOfCompetitor = raceColumn.getFleetOfCompetitor(competitor); + if (fleetOfCompetitor != null) { + final Map competitorsAndTheirBoats = raceColumn.getCompetitorsRegisteredInRacelog(fleetOfCompetitor); + boat = competitorsAndTheirBoats.get(competitor); + } else { + boat = null; + } + } + } + entryDTO.boat = boat == null ? null : baseDomainFactory.convertToBoatDTO(boat); entryDTO.totalPoints = entry.getTotalPoints(); if (fillTotalPointsUncorrected) { entryDTO.totalPointsUncorrected = entry.getTotalPointsUncorrected(); @@ -618,6 +641,7 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { : raceDetails.getAverageAbsoluteCrossTrackError().getMeters(); entryDTO.averageSignedCrossTrackErrorInMeters = raceDetails.getAverageSignedCrossTrackError() == null ? null : raceDetails.getAverageSignedCrossTrackError().getMeters(); + entryDTO.timeSailedSinceRaceStart = raceDetails.getTimeSailedSinceRaceStart(); entryDTO.calculatedTime = raceDetails.getCorrectedTime(); entryDTO.calculatedTimeAtEstimatedArrivalAtCompetitorFarthestAhead = raceDetails.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead(); entryDTO.gapToLeaderInOwnTime = raceDetails.getGapToLeaderInOwnTime(); @@ -874,6 +898,7 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { final CompetitorRankingInfo competitorRankingInfo = rankingInfo.getCompetitorRankingInfo().apply(competitor); return new RaceDetails(legDetails, windwardDistanceToCompetitorFarthestAhead, averageAbsoluteCrossTrackError, averageSignedCrossTrackError, trackedRace.getRankingMetric().getGapToLeaderInOwnTime(rankingInfo, competitor, cache), + trackedRace.getTimeSailedSinceRaceStart(competitor, timePoint), trackedRace.getRankingMetric().getCorrectedTime(competitor, timePoint), competitorRankingInfo == null ? null : competitorRankingInfo.getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead()); } finally { @@ -1202,32 +1227,12 @@ public abstract class AbstractLeaderboardWithCache implements Leaderboard { Duration result = null; for (TrackedRace trackedRace : getTrackedRaces()) { if (Util.contains(trackedRace.getRace().getCompetitors(), competitor)) { - NavigableSet markPassings = trackedRace.getMarkPassings(competitor); - if (!markPassings.isEmpty()) { - TimePoint from = trackedRace.getStartOfRace(); // start counting at race start, not when the competitor passed the line - if (from != null && !timePoint.before(from)) { // but only if the race started after timePoint - TimePoint to; - if (timePoint.after(markPassings.last().getTimePoint()) - && markPassings.last().getWaypoint() == trackedRace.getRace().getCourse() - .getLastWaypoint()) { - // stop counting when competitor finished the race - to = markPassings.last().getTimePoint(); - } else { - if (trackedRace.getEndOfTracking() != null - && timePoint.after(trackedRace.getEndOfTracking())) { - result = null; // race not finished until end of tracking; no reasonable value can be - // computed for competitor - break; - } else { - to = timePoint; - } - } - Duration timeSpent = from.until(to); - if (result == null) { - result = timeSpent; - } else { - result=result.plus(timeSpent); - } + final Duration timeSpent = trackedRace.getTimeSailedSinceRaceStart(competitor, timePoint); + if (timeSpent != null) { + if (result == null) { + result = timeSpent; + } else { + result=result.plus(timeSpent); } } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractScoringSchemeImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractScoringSchemeImpl.java index cf443cfb28c..f30a10e2c0a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractScoringSchemeImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractScoringSchemeImpl.java @@ -8,6 +8,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.Map; import java.util.Set; import com.sap.sailing.domain.base.Competitor; @@ -121,28 +122,41 @@ public abstract class AbstractScoringSchemeImpl implements ScoringScheme { } /** - * Assuming both competitors scored in the same number of races, compares the sorted scores. + * Assuming both competitors scored in the same number of races, compares the sorted scores based on World Sailing's + * Racing Rules of Sailing (RRS) addendum A8.1:

+ * + * "A8.1 If there is a series-score tie between two or more boats, each boat’s race scores shall be listed in + * order of best to worst, and at the first point(s) where there is a difference the tie shall be broken in favour + * of the boat(s) with the best score(s). No excluded scores shall be used." */ @Override - public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard) { - final Comparator pureScoreComparator = getScoreComparator(nullScoresAreBetter); + public int compareByBetterScore(Competitor o1, List> o1Scores, + Competitor o2, List> o2Scores, boolean nullScoresAreBetter, + TimePoint timePoint, Leaderboard leaderboard, Map> discardedRaceColumnsPerCompetitor) { + final Comparator> ruleA8_1ScoreComparator = getRuleA8_1ScoreComparator(nullScoresAreBetter); // needs to compare net points; therefore, divide the total points by the column factor for comparison: - List o1NetScores = new ArrayList<>(); + List> o1NetScores = new ArrayList<>(); + final Set o1Discards = discardedRaceColumnsPerCompetitor.get(o1); for (com.sap.sse.common.Util.Pair o1ColumnAndScore : o1Scores) { - o1NetScores.add(o1ColumnAndScore.getB() / getScoreFactor(o1ColumnAndScore.getA())); + if (!o1Discards.contains(o1ColumnAndScore.getA())) { + o1NetScores.add(new Pair<>(o1ColumnAndScore.getA(), o1ColumnAndScore.getB() / getScoreFactor(o1ColumnAndScore.getA()))); + } } - List o2NetScores = new ArrayList<>(); + List> o2NetScores = new ArrayList<>(); + final Set o2Discards = discardedRaceColumnsPerCompetitor.get(o2); for (com.sap.sse.common.Util.Pair o2ColumnAndScore : o2Scores) { - o2NetScores.add(o2ColumnAndScore.getB() / getScoreFactor(o2ColumnAndScore.getA())); + if (!o2Discards.contains(o2ColumnAndScore.getA())) { + o2NetScores.add(new Pair<>(o2ColumnAndScore.getA(), o2ColumnAndScore.getB() / getScoreFactor(o2ColumnAndScore.getA()))); + } } - Collections.sort(o1NetScores, pureScoreComparator); - Collections.sort(o2NetScores, pureScoreComparator); + Collections.sort(o1NetScores, ruleA8_1ScoreComparator); + Collections.sort(o2NetScores, ruleA8_1ScoreComparator); // now both lists are sorted from best to worst score - Iterator o1Iter = o1NetScores.iterator(); - Iterator o2Iter = o2NetScores.iterator(); + Iterator> o1Iter = o1NetScores.iterator(); + Iterator> o2Iter = o2NetScores.iterator(); int result = 0; while (result == 0 && o1Iter.hasNext() && o2Iter.hasNext()) { - result = pureScoreComparator.compare(o1Iter.next(), o2Iter.next()); + result = getScoreComparator(nullScoresAreBetter).compare(o1Iter.next().getB(), o2Iter.next().getB()); } if (o1Iter.hasNext() != o2Iter.hasNext()) { // if, as may be allowed by some scoring scheme variants, competitors with different numbers of scored races are compared @@ -152,6 +166,16 @@ public abstract class AbstractScoringSchemeImpl implements ScoringScheme { return result; } + /** + * Obtains a comparator that compares two non-discarded scores according the rule A8.1, or any + * modification thereof (which subclasses can provide by overriding this method). This default implementation + * simply uses the {@link #getScoreComparator(boolean) score comparator} to compare the net scores + * directly. + */ + protected Comparator> getRuleA8_1ScoreComparator(boolean nullScoresAreBetter) { + return (p1, p2)->getScoreComparator(nullScoresAreBetter).compare(p1.getB(), p2.getB()); + } + /** * By default, this scoring scheme implementation does not compare the numbers of races scored. */ @@ -161,8 +185,18 @@ public abstract class AbstractScoringSchemeImpl implements ScoringScheme { } /** - * @param o1ScoresIncludingDiscarded scores that include the points for those races that have been discarded (total points) - * @param o2ScoresIncludingDiscarded scores that include the points for those races that have been discarded (total points) + * Looks backwards starting at the last race until the first score difference is found, including the discarded + * scored. This implements Racing Rules of Sailing (RRS) section A8.2: + *

+ * + * "A8.2 If a tie remains between two or more boats, they shall be ranked in order of their scores in the last race. + * Any remaining ties shall be broken by using the tied boats’ scores in the next-to-last race and so on until all + * ties are broken. These scores shall be used even if some of them are excluded scores." + * + * @param o1ScoresIncludingDiscarded + * scores that include the points for those races that have been discarded (total points) + * @param o2ScoresIncludingDiscarded + * scores that include the points for those races that have been discarded (total points) */ @Override public int compareByLastRace(List> o1ScoresIncludingDiscarded, diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGets10LastBreaksTie.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGets10LastBreaksTie.java index 81bff24d097..3ea20a098f3 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGets10LastBreaksTie.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGets10LastBreaksTie.java @@ -1,6 +1,8 @@ package com.sap.sailing.domain.leaderboard.impl; import java.util.List; +import java.util.Map; +import java.util.Set; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.RaceColumn; @@ -22,7 +24,7 @@ public class HighPointFirstGets10LastBreaksTie extends HighPointFirstGetsFixedSc } @Override - public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard) { + public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard, Map> discardedRaceColumnsPerCompetitor) { return 0; } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGets1LastBreaksTie.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGets1LastBreaksTie.java index ae12da23843..970b5ec1da2 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGets1LastBreaksTie.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointFirstGets1LastBreaksTie.java @@ -6,6 +6,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; import com.sap.sailing.domain.base.Competitor; @@ -55,7 +56,7 @@ public class HighPointFirstGets1LastBreaksTie extends HighPointFirstGetsFixedSco @Override public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, - List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard) { + List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard, Map> discardedRaceColumnsPerCompetitor) { // Construct a graph in which all competitors with equal points are represented as nodes; // edges represent a RaceColumn/Fleet where the "from" node in the edge has won over the "to" // node in the edge. This graph will be sorted such that competitors precede those over which diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointLastBreaksTie.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointLastBreaksTie.java index 59d5ea4d725..2647af65ab5 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointLastBreaksTie.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/HighPointLastBreaksTie.java @@ -1,6 +1,8 @@ package com.sap.sailing.domain.leaderboard.impl; import java.util.List; +import java.util.Map; +import java.util.Set; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.RaceColumn; @@ -21,7 +23,7 @@ public class HighPointLastBreaksTie extends HighPoint { private static final long serialVersionUID = -5338636946886101669L; @Override - public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard) { + public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard, Map> discardedRaceColumnsPerCompetitor) { return 0; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java index 5a94b862a3e..567c007a681 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LeaderboardTotalRankComparator.java @@ -55,6 +55,7 @@ public class LeaderboardTotalRankComparator implements Comparator { private final ScoringScheme scoringScheme; private final Map, Double> netPointsCache; private final Map, Double> totalPointsCache; + private final Map> discardedRaceColumnsPerCompetitor; private final boolean nullScoresAreBetter; private final TimePoint timePoint; @@ -84,11 +85,13 @@ public class LeaderboardTotalRankComparator implements Comparator { this.timePoint = timePoint; this.scoringScheme = scoringScheme; this.nullScoresAreBetter = nullScoresAreBetter; - netPointsCache = new HashMap, Double>(); - totalPointsCache = new HashMap, Double>(); + netPointsCache = new HashMap<>(); + totalPointsCache = new HashMap<>(); + discardedRaceColumnsPerCompetitor = new HashMap<>(); for (Competitor competitor : leaderboard.getCompetitors()) { Set discardedRaceColumns = leaderboard.getResultDiscardingRule().getDiscardedRaceColumns( competitor, leaderboard, raceColumnsToConsider, timePoint); + this.discardedRaceColumnsPerCompetitor.put(competitor, discardedRaceColumns); for (RaceColumn raceColumn : raceColumnsToConsider) { Pair key = new Util.Pair(competitor, raceColumn); netPointsCache.put(key, leaderboard.getNetPoints(competitor, raceColumn, timePoint, discardedRaceColumns)); @@ -471,11 +474,12 @@ public class LeaderboardTotalRankComparator implements Comparator { /** * Assuming both competitors scored in the same number of races, and assuming they scored the same net score, * break the tie according to the {@link #scoringScheme scoring scheme} set for this comparator. - * @see ScoringScheme#compareByBetterScore(Competitor, List, Competitor, List, boolean, TimePoint, Leaderboard) + * @see ScoringScheme#compareByBetterScore(Competitor, List, Competitor, List, boolean, TimePoint, Leaderboard, Map) */ protected int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, TimePoint timePoint) { - return scoringScheme.compareByBetterScore(o1, o1Scores, o2, o2Scores, nullScoresAreBetter, timePoint, leaderboard); + return scoringScheme.compareByBetterScore(o1, o1Scores, o2, o2Scores, nullScoresAreBetter, timePoint, leaderboard, + Collections.unmodifiableMap(discardedRaceColumnsPerCompetitor)); } /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointForLeagueOverallLeaderboard.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointForLeagueOverallLeaderboard.java index 1f99e5b47f0..c7fcbd49728 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointForLeagueOverallLeaderboard.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointForLeagueOverallLeaderboard.java @@ -1,6 +1,8 @@ package com.sap.sailing.domain.leaderboard.impl; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -41,7 +43,7 @@ public class LowPointForLeagueOverallLeaderboard extends LowPoint { */ @Override public int compareByBetterScore(Competitor o1, List> o1Scores, - Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard) { + Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint, Leaderboard leaderboard, Map> discardedRaceColumnsPerCompetitor) { final Double o1ScoreSum = o1Scores.stream().collect(Collectors.summingDouble(rcAndScore->rcAndScore.getB())); final Double o2ScoreSum = o2Scores.stream().collect(Collectors.summingDouble(rcAndScore->rcAndScore.getB())); assert Math.abs(o1ScoreSum - o2ScoreSum) < 0.00001; diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointTieBreakBasedOnLastSeriesOnly.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointTieBreakBasedOnLastSeriesOnly.java index 419526398d3..31d6b91ad3d 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointTieBreakBasedOnLastSeriesOnly.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/LowPointTieBreakBasedOnLastSeriesOnly.java @@ -1,6 +1,13 @@ package com.sap.sailing.domain.leaderboard.impl; +import java.util.Comparator; + +import com.sap.sailing.domain.base.RaceColumn; +import com.sap.sailing.domain.base.RaceColumnInSeries; +import com.sap.sailing.domain.base.Regatta; +import com.sap.sailing.domain.base.Series; import com.sap.sailing.domain.common.ScoringSchemeType; +import com.sap.sse.common.Util.Pair; public class LowPointTieBreakBasedOnLastSeriesOnly extends LowPoint { private static final long serialVersionUID = -2837189757937229157L; @@ -9,4 +16,59 @@ public class LowPointTieBreakBasedOnLastSeriesOnly extends LowPoint { public ScoringSchemeType getType() { return ScoringSchemeType.LOW_POINT_TIE_BREAK_BASED_ON_LAST_SERIES_ONLY; } + + /** + * Obtains a comparator that compares two non-discarded, non medal-race scores according the rule 21.9: + *

+ * + * "21.9 RRS A8 is changed as follows: 21.9.1 For boats assigned to compete in a medal race, ties in the regatta + * score will be broken by the medal race score. This changes RRS A8. 21.9.2 For tied boats with the same points + * score in the medal race, ties will be broken applying RRS A8 to the opening series scores. 21.9.3 For boats that + * sailed a Qualifying Series and a Final Series, ties will be broken using A8 but using the scores in the Final + * Series before using any scores in the Qualifying Series when applying A8.1. This applies also to boats that were + * tied after the medal race and had the same score in the medal race." + *

+ * + * For this, the position of the series in the regatta is determined, and if one score comes from the last non-medal + * series and the other does not, the one from the last non-medal series is considered better. Otherwise, the + * {@link #getScoreComparator(boolean) score comparator} is used to compare the scores numerically. + */ + @Override + protected Comparator> getRuleA8_1ScoreComparator(boolean nullScoresAreBetter) { + return (Pair p1, Pair p2)->{ + final int result; + if (isFromLastNonMedalSeries(p1) != isFromLastNonMedalSeries(p2)) { + result = isFromLastNonMedalSeries(p1) ? -1 // p1 is better, so less, than p2 + : 1; // p1 is worse, so greater, than p2 + } else { + result = getScoreComparator(nullScoresAreBetter).compare(p1.getB(), p2.getB()); + } + return result; + }; + } + + private boolean isFromLastNonMedalSeries(Pair raceColumnAndNetPoints) { + final RaceColumn raceColumn = raceColumnAndNetPoints.getA(); + final boolean result; + if (raceColumn instanceof RaceColumnInSeries) { + final Series series = ((RaceColumnInSeries) raceColumn).getSeries(); + final Regatta regatta = series.getRegatta(); + boolean found = false; + boolean lastNonMedal = true; + for (final Series seriesInRegatta : regatta.getSeries()) { + if (!found) { + if (seriesInRegatta == series) { + found = true; + } + } else if (!seriesInRegatta.isMedal()) { + lastNonMedal = false; // already found, and seriesInRegatta is a non-medal series that comes after series + } + } + assert found; + result = lastNonMedal; + } else { + result = !raceColumn.isMedalRace(); + } + return result; + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverdetection/impl/ManeuverDetectorImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverdetection/impl/ManeuverDetectorImpl.java old mode 100644 new mode 100755 index fd6580496be..88e0fe6ae2f --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverdetection/impl/ManeuverDetectorImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/maneuverdetection/impl/ManeuverDetectorImpl.java @@ -941,32 +941,32 @@ public class ManeuverDetectorImpl extends AbstractManeuverDetectorImpl { } TimePoint stableBearingAnalysisUntil = maneuverStart == null ? maneuverMainCurveDetails.getTimePointBefore() : maneuverStart.getExtensionTimePoint(); - Speed lowestSpeed = maneuverStart == null ? null : maneuverStart.getLowestSpeedWithinExtensionArea(); - double courseChangeSinceManeuverMainCurveInDegrees = maneuverStart == null ? 0 - : maneuverStart.getCourseChangeInDegreesWithinExtensionArea(); stepsToAnalyze = getSpeedWithBearingStepsWithinTimeRange(stepsToAnalyze, earliestTimePointForSpeedTrendAnalysis, stableBearingAnalysisUntil); ManeuverCurveBoundaryExtension stableBearingExtension = findStableBearingWithMaxAbsCourseChangeSpeed( stepsToAnalyze, true, MAX_TURNING_RATE_IN_DEG_PER_SECOND_FOR_STABLE_COURSE_ANALYSIS); - if (stableBearingExtension != null - && !isCourseChangeLimitExceededForCurveExtension(maneuverMainCurveDetails, stableBearingExtension)) { - maneuverStart = stableBearingExtension; - courseChangeSinceManeuverMainCurveInDegrees += stableBearingExtension - .getCourseChangeInDegreesWithinExtensionArea(); - if (lowestSpeed == null - || lowestSpeed.compareTo(stableBearingExtension.getLowestSpeedWithinExtensionArea()) > 0) { - lowestSpeed = stableBearingExtension.getLowestSpeedWithinExtensionArea(); - } + ManeuverCurveBoundaryExtension mergedExtension = extendManeuverCurveBoundaryExtension(maneuverStart, stableBearingExtension); + if(!isCourseChangeLimitExceededForCurveExtension(maneuverMainCurveDetails, mergedExtension)) { + maneuverStart = mergedExtension; } return maneuverStart != null - ? new ManeuverCurveBoundaryExtension(maneuverStart.getExtensionTimePoint(), - maneuverStart.getSpeedWithBearingAtExtensionTimePoint(), - courseChangeSinceManeuverMainCurveInDegrees - + maneuverStart.getCourseChangeInDegreesWithinExtensionArea(), - lowestSpeed) + ? maneuverStart : new ManeuverCurveBoundaryExtension(maneuverMainCurveDetails.getTimePointBefore(), maneuverMainCurveDetails.getSpeedWithBearingBefore(), 0, null); } + + private ManeuverCurveBoundaryExtension extendManeuverCurveBoundaryExtension(ManeuverCurveBoundaryExtension previousExtension, ManeuverCurveBoundaryExtension newExtension) { + if(previousExtension == null) { + return newExtension; + } + if(newExtension == null) { + return previousExtension; + } + Speed lowestSpeed = previousExtension.getLowestSpeedWithinExtensionArea().compareTo(newExtension.getLowestSpeedWithinExtensionArea()) > 0 ? newExtension.getLowestSpeedWithinExtensionArea() : previousExtension.getLowestSpeedWithinExtensionArea(); + double courseChangeInDegrees = previousExtension.getCourseChangeInDegreesWithinExtensionArea() + newExtension.getCourseChangeInDegreesWithinExtensionArea(); + ManeuverCurveBoundaryExtension mergedExtension = new ManeuverCurveBoundaryExtension(newExtension.getExtensionTimePoint(), newExtension.getSpeedWithBearingAtExtensionTimePoint(), courseChangeInDegrees, lowestSpeed); + return mergedExtension; + } private boolean isCourseChangeLimitExceededForCurveExtension( ManeuverMainCurveDetailsWithBearingSteps maneuverMainCurveDetails, @@ -1025,27 +1025,16 @@ public class ManeuverDetectorImpl extends AbstractManeuverDetectorImpl { } TimePoint stableBearingAnalysisFrom = maneuverEnd == null ? maneuverMainCurveDetails.getTimePointAfter() : maneuverEnd.getExtensionTimePoint(); - Speed lowestSpeed = maneuverEnd == null ? null : maneuverEnd.getLowestSpeedWithinExtensionArea(); - double courseChangeSinceManeuverMainCurveInDegrees = maneuverEnd == null ? 0 - : maneuverEnd.getCourseChangeInDegreesWithinExtensionArea(); stepsToAnalyze = getSpeedWithBearingStepsWithinTimeRange(stepsToAnalyze, stableBearingAnalysisFrom, latestTimePointForSpeedTrendAnalysis); ManeuverCurveBoundaryExtension stableBearingExtension = findStableBearingWithMaxAbsCourseChangeSpeed( stepsToAnalyze, false, MAX_TURNING_RATE_IN_DEG_PER_SECOND_FOR_STABLE_COURSE_ANALYSIS); - if (stableBearingExtension != null - && !isCourseChangeLimitExceededForCurveExtension(maneuverMainCurveDetails, stableBearingExtension)) { - maneuverEnd = stableBearingExtension; - courseChangeSinceManeuverMainCurveInDegrees += stableBearingExtension - .getCourseChangeInDegreesWithinExtensionArea(); - if (lowestSpeed == null - || lowestSpeed.compareTo(stableBearingExtension.getLowestSpeedWithinExtensionArea()) > 0) { - lowestSpeed = stableBearingExtension.getLowestSpeedWithinExtensionArea(); - } + ManeuverCurveBoundaryExtension mergedExtension = extendManeuverCurveBoundaryExtension(maneuverEnd, stableBearingExtension); + if(!isCourseChangeLimitExceededForCurveExtension(maneuverMainCurveDetails, mergedExtension)) { + maneuverEnd = mergedExtension; } return maneuverEnd != null - ? new ManeuverCurveBoundaryExtension(maneuverEnd.getExtensionTimePoint(), - maneuverEnd.getSpeedWithBearingAtExtensionTimePoint(), - courseChangeSinceManeuverMainCurveInDegrees, lowestSpeed) + ? maneuverEnd : new ManeuverCurveBoundaryExtension(maneuverMainCurveDetails.getTimePointAfter(), maneuverMainCurveDetails.getSpeedWithBearingAfter(), 0, null); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/Candidate.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/Candidate.java index b3e60dec544..a3f12426eb4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/Candidate.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/Candidate.java @@ -20,6 +20,13 @@ import com.sap.sse.common.Timed; public interface Candidate extends Comparable, Timed { Waypoint getWaypoint(); + + /** + * Is this a candidate for a fixed mark passing? + */ + default boolean isFixed() { + return false; + } int getOneBasedIndexOfWaypoint(); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java index d2d364d269a..16c5d08278e 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java @@ -1,5 +1,6 @@ package com.sap.sailing.domain.markpassingcalculation; +import java.lang.Thread.State; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -120,11 +121,8 @@ public class MarkPassingCalculator { return null; }); } - try { - executor.invokeAll(tasks); - } catch (Exception e) { - logger.log(Level.SEVERE, "Error trying to compute initial set of mark passings for race "+race.getRace().getName(), e); - } + ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.SEVERE, + "Error trying to compute initial set of mark passings for race "+race.getRace().getName()+": %s", tasks); if (listener != null) { synchronized (MarkPassingCalculator.this) { if (listenerThread == null) { @@ -290,9 +288,14 @@ public class MarkPassingCalculator { // haven't processed them yet with the CandidateFinder or CandidateChooser. if (!finished && !suspended) { if (smallestChangedWaypointIndex.value != -1) { + logger.finer("Handling course change; smallesChangedWaypointIndex=="+smallestChangedWaypointIndex.value+ + "; waypoints added: "+addedWaypoints+"; waypoints removed: "+removedWaypoints); // there was a course change; finder and chooser need to be updated in this regard; // do this before the other changes such as new / replaced position fixes are considered: final Course course = race.getRace().getCourse(); + logger.finer(()->"Handling course change; smallestChangedWaypointIndex=="+smallestChangedWaypointIndex.value+ + "; waypoints added: "+addedWaypoints+"; waypoints removed: "+removedWaypoints+ + "; current course: "+course); final Map, List>> candidateDeltas; // obtain the course's read lock so that the finder creates the candidate deltas under the // premise of the same course as the chooser updates its end proxy node's index; they have @@ -311,11 +314,19 @@ public class MarkPassingCalculator { for (Entry, List>> entry : candidateDeltas.entrySet()) { tasks.add(()->{ Util.Pair, List> pair = entry.getValue(); - chooser.calculateMarkPassDeltas(entry.getKey(), pair.getA(), pair.getB()); - return null; + try { + chooser.calculateMarkPassDeltas(entry.getKey(), pair.getA(), pair.getB()); + return null; + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception trying to calculate mark passing deltas for competitor "+entry.getKey(), e); + throw e; + } }); } - executor.invokeAll(tasks); + logger.finer(()->"Calculating mark passing deltas after course change in executor"); + ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.SEVERE, + "Error calculating mark passing deltas after course change in executor: %s", tasks); + logger.finer(()->"Done calculating mark passing deltas after course change"); } updateManuallySetMarkPassings(fixedMarkPassings, removedFixedMarkPassings, suppressedMarkPassings, unsuppressedMarkPassings); computeMarkPasses(competitorFixes, competitorFixesThatReplacedExistingOnes, markFixes); @@ -412,11 +423,8 @@ public class MarkPassingCalculator { } }); } - try { - executor.invokeAll(tasks); - } catch (InterruptedException e) { - logger.log(Level.INFO, "mark passing calculation interrupted", e); - } + ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.INFO, + "Error during mark passing calculation: %s", tasks); } private class ComputeMarkPassings implements Runnable { @@ -500,8 +508,13 @@ public class MarkPassingCalculator { queue.add(update); // regardless of whether the queue is empty or not, launch the thread if it doesn't run yet and we are not suspended; // the queue may have filled up while we were suspended - if (!suspended && listenerThread == null) { - listenerThread = createAndStartListenerThread(); + if (!suspended) { + if (listenerThread == null) { + listenerThread = createAndStartListenerThread(); + } else if (listenerThread.getState() == State.TERMINATED) { + logger.severe("Listener thread of MarkPassingCalculator (MPC) for race "+race.getRace().getName()+ + " terminated but not null. Why are we still receiving updates? We must have been stopped before!"); + } } } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java index bf771b3f642..8fd60738224 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateChooserImpl.java @@ -474,7 +474,7 @@ public class CandidateChooserImpl implements CandidateChooser { public void setFixedPassing(Competitor c, Integer zeroBasedIndexOfWaypoint, TimePoint t) { LockUtil.lockForWrite(perCompetitorLocks.get(c)); try { - Candidate fixedCan = new CandidateImpl(zeroBasedIndexOfWaypoint + 1, t, 1, Util.get(race.getRace().getCourse().getWaypoints(), zeroBasedIndexOfWaypoint)); + Candidate fixedCan = new CandidateForFixedMarkPassingImpl(zeroBasedIndexOfWaypoint + 1, t, 1, Util.get(race.getRace().getCourse().getWaypoints(), zeroBasedIndexOfWaypoint)); NavigableSet fixed = fixedPassings.get(c); if (fixed != null) { // can only set the mark passing if the competitor is still part of this race if (!fixed.add(fixedCan)) { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java index 8004f7db6c7..25140af58e7 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java @@ -370,9 +370,11 @@ public class CandidateFinderImpl implements CandidateFinder { } /** - * determines the candidates that are new and those that have been removed, keyed by the competitors; the - * {@link Pair#getA() first} element of each value pair has the candidates that are new, the {@link Pair#getB() - * second} element has those that have been removed. + * For all waypoints starting at {@code zeroBasedIndexOfWaypointChanged-1} in the current course removes all + * candidates for all competitors and starts from scratch to determine the candidates for the waypoints starting at + * {@code zeroBasedIndexOfWaypointChanged-1}. The current and the removed candidates are returned, keyed by the + * competitors; the {@link Pair#getA() first} element of each value pair has the candidates that are new, the + * {@link Pair#getB() second} element has those that have been removed. */ private Map, List>> invalidateAfterCourseChange(int zeroBasedIndexOfWaypointChanged) { ConcurrentMap, List>> result = new ConcurrentHashMap<>(); @@ -393,6 +395,7 @@ public class CandidateFinderImpl implements CandidateFinder { changedWaypoints.add(w); } } + logger.finer(()->"Changed waypoints: "+changedWaypoints); final Set> tasks = new HashSet<>(); final Thread executingThread = Thread.currentThread(); // most likely the MarkPassingCalculator.Listen thread for (Competitor c : race.getRace().getCompetitors()) { @@ -419,10 +422,9 @@ public class CandidateFinderImpl implements CandidateFinder { } }); } - executor.invokeAll(tasks); - } catch (InterruptedException e) { - logger.log(Level.SEVERE, "Problem trying to update competitor candidate sets after waypoints starting at zero-based index "+ - zeroBasedIndexOfWaypointChanged+" have changed", e); + ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.SEVERE, + "Problem trying to update competitor candidate sets after waypoints starting at zero-based index "+ + zeroBasedIndexOfWaypointChanged+" have changed: %s", tasks); } finally { course.unlockAfterRead(); } @@ -431,9 +433,10 @@ public class CandidateFinderImpl implements CandidateFinder { } /** - * determines the candidates that are new and those that have been removed, keyed by the competitors; the - * {@link Pair#getA() first} element of each value pair has the candidates that are new, the {@link Pair#getB() - * second} element has those that have been removed. + * After a course change, described by the waypoints added ({@code addedWaypoints}) and the waypoints removed + * ({@code removedWaypoints}), determines the candidates that are new and those that have been removed, keyed by the + * competitors; the {@link Pair#getA() first} element of each value pair has the candidates that are new, the + * {@link Pair#getB() second} element has those that have been removed. */ @Override public Map, List>> updateWaypoints( @@ -449,6 +452,11 @@ public class CandidateFinderImpl implements CandidateFinder { return newAndUpdatedCandidates; } + /** + * For all {@code waypoints} removed, removes the {@link #passingInstructions} entry and clears all + * {@link #xteCandidates} and {@link #distanceCandidates} for all competitors. The candidates cleared this + * way are returned, keyed by the {@link Competitor} to which they belonged. + */ private Map> removeWaypoints(Iterable waypoints) { Map> result = new HashMap<>(); for (Competitor c : race.getRace().getCompetitors()) { @@ -581,20 +589,14 @@ public class CandidateFinderImpl implements CandidateFinder { // case we can quickly obtain the next "firstFixAfter" by simply calling next() on the iterator. firstFixAfterIterator = track.getFixesIterator(t, /* inclusive */ false); } - boolean firstFixAfterIteratorHasNext; try { - firstFixAfterIteratorHasNext = firstFixAfterIterator.hasNext(); + fixAfter = Util.nextOrNull(firstFixAfterIterator); } catch (ConcurrentModificationException e) { // the iterator may have been obtained in a previous look execution, and another // fix may have been added to the track; let's obtain the iterator again. We're // under the track's read lock here: firstFixAfterIterator = track.getFixesIterator(t, /* inclusive */ false); - firstFixAfterIteratorHasNext = firstFixAfterIterator.hasNext(); - } - if (firstFixAfterIteratorHasNext) { - fixAfter = firstFixAfterIterator.next(); - } else { - fixAfter = null; + fixAfter = Util.nextOrNull(firstFixAfterIterator); } lastIterationFix = fix; lastIterationAfterFix = fixAfter; @@ -636,20 +638,14 @@ public class CandidateFinderImpl implements CandidateFinder { // case we can quickly obtain the next "firstFixAfter" by simply calling next() on the iterator. firstFixAfterIterator = track.getFixesIterator(timePoint, /* inclusive */ false); } - boolean firstFixAfterIteratorHasNext; try { - firstFixAfterIteratorHasNext = firstFixAfterIterator.hasNext(); + fixAfter = Util.nextOrNull(firstFixAfterIterator); } catch (ConcurrentModificationException e) { // the iterator may have been obtained in a previous look execution, and another // fix may have been added to the track; let's obtain the iterator again. We're // under the track's read lock here: firstFixAfterIterator = track.getFixesIterator(timePoint, /* inclusive */ false); - firstFixAfterIteratorHasNext = firstFixAfterIterator.hasNext(); - } - if (firstFixAfterIteratorHasNext) { - fixAfter = firstFixAfterIterator.next(); - } else { - fixAfter = null; + fixAfter = Util.nextOrNull(firstFixAfterIterator); } lastIterationFix = fix; lastIterationAfterFix = fixAfter; @@ -726,7 +722,7 @@ public class CandidateFinderImpl implements CandidateFinder { t, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, distance); getDistanceCandidates(c, w).put(fix, newCan); result.getA().add(newCan); - logger.finest("Added distance candidate " + newCan.toString() + "for " + c); + logger.finest("Added distance candidate " + newCan.toString() + " for " + c); } else if (wasCan && !isCan) { getDistanceCandidates(c, w).remove(fix); result.getB().add(oldCan); @@ -735,7 +731,7 @@ public class CandidateFinderImpl implements CandidateFinder { t, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, distance); getDistanceCandidates(c, w).put(fix, newCan); result.getA().add(newCan); - logger.finest("Added distance candidate " + newCan.toString() + "for " + c); + logger.finest("Added distance candidate " + newCan.toString() + " for " + c); result.getB().add(oldCan); } } @@ -803,10 +799,11 @@ public class CandidateFinderImpl implements CandidateFinder { // case we can quickly obtain the next "firstFixAfter" by simply calling next() on the iterator. firstFixAfterIterator = track.getFixesIterator(t, /* inclusive */ false); } - if (firstFixAfterIterator.hasNext()) { - fixAfter = firstFixAfterIterator.next(); - } else { - fixAfter = null; + try { + fixAfter = Util.nextOrNull(firstFixAfterIterator); + } catch (ConcurrentModificationException e) { + firstFixAfterIterator = track.getFixesIterator(t, /* inclusive */ false); + fixAfter = Util.nextOrNull(firstFixAfterIterator); } lastIterationFix = fix; lastIterationAfterFix = fixAfter; @@ -844,14 +841,14 @@ public class CandidateFinderImpl implements CandidateFinder { if (xte == 0) { newCandidates.put(Arrays.asList(fix, fix), createCandidate(c, 0, 0, t, t, w, true)); } else { - if (fixAfter != null && xtesAfter != null && !xtesAfter.get(w).isEmpty()) { + if (fixAfter != null && xtesAfter != null && xtesAfter.get(w) != null && !xtesAfter.get(w).isEmpty()) { Double xteAfter = xtesAfter.get(w).get(0).getMeters(); if (xteAfter != null && xte < 0 != xteAfter <= 0) { newCandidates.put(Arrays.asList(fix, fixAfter), createCandidate(c, xte, xteAfter, t, tAfter, w, true)); } } - if (fixBefore != null && !xtesBefore.get(w).isEmpty()) { + if (fixBefore != null && xtesBefore.get(w) != null && !xtesBefore.get(w).isEmpty()) { Double xteBefore = xtesBefore.get(w).get(0).getMeters(); if (xte < 0 != xteBefore <= 0) { newCandidates.put(Arrays.asList(fixBefore, fix), @@ -894,14 +891,14 @@ public class CandidateFinderImpl implements CandidateFinder { waypointCandidates.remove(canFixes); if (newCan.getProbability() > penaltyForSkipping) { result.getA().add(newCan); - logger.finest("Added XTE " + newCan.toString() + "for " + c); + logger.finest("Added XTE " + newCan.toString() + " for " + c); waypointCandidates.put(canFixes, newCan); } } } else { if (newCan.getProbability() > penaltyForSkipping) { result.getA().add(newCan); - logger.finest("Added XTE " + newCan.toString() + "for " + c); + logger.finest("Added XTE " + newCan.toString() + " for " + c); waypointCandidates.put(canFixes, newCan); } } @@ -918,6 +915,11 @@ public class CandidateFinderImpl implements CandidateFinder { } /** + * Returns a non-{@code null} mapping from the waypoints currently in the course, telling the {@code fix}'s + * cross-track error for each waypoint. There may be "excess" entries from older waypoints that have already + * been removed from the course which may be reflected only by a later call to {@link #invalidateAfterCourseChange(int)}. + * See also bug 5021. + * * @return if for a waypoint the mark positions are known, the resulting map will contain a non-empty list that for * each way of passing the waypoint (e.g., for a gate the competitor can round the left or the right mark) the * cross track error of the {@code fix} to the virtual line that must be crossed is contained; if the mark @@ -925,15 +927,18 @@ public class CandidateFinderImpl implements CandidateFinder { */ private Map> getXTE(Competitor c, GPSFix fix) { Map> result = xteCache.get(c).get(fix); - if (result == null) { - result = new HashMap<>(); - Position p = fix.getPosition(); - TimePoint t = fix.getTimePoint(); + Course course = race.getRace().getCourse(); + course.lockForRead(); + try { + if (result == null) { + result = new HashMap<>(); + xteCache.get(c).put(fix, result); + } + final Position p = fix.getPosition(); + final TimePoint t = fix.getTimePoint(); final MarkPositionAtTimePointCache markPositionCache = new MarkPositionAtTimePointCacheImpl(race, t); - Course course = race.getRace().getCourse(); - course.lockForRead(); - try { - for (Waypoint w : course.getWaypoints()) { + for (Waypoint w : course.getWaypoints()) { + if (!result.containsKey(w)) { // bug5021: calculate in case waypoint was added since last call List distances = new ArrayList<>(); result.put(w, distances); for (Util.Pair crossingInfo : getCrossingInformation(w, t, markPositionCache)) { @@ -942,10 +947,9 @@ public class CandidateFinderImpl implements CandidateFinder { } } } - } finally { - course.unlockAfterRead(); } - xteCache.get(c).put(fix, result); + } finally { + course.unlockAfterRead(); } return result; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateForFixedMarkPassingImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateForFixedMarkPassingImpl.java new file mode 100644 index 00000000000..7c1f9b3d268 --- /dev/null +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateForFixedMarkPassingImpl.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.markpassingcalculation.impl; + +import com.sap.sailing.domain.base.Waypoint; +import com.sap.sse.common.TimePoint; + +public class CandidateForFixedMarkPassingImpl extends CandidateImpl { + private static final long serialVersionUID = 3657502693032556232L; + + public CandidateForFixedMarkPassingImpl(int oneBasedIndexOfWaypoint, TimePoint p, double probability, Waypoint w) { + super(oneBasedIndexOfWaypoint, p, probability, w); + } + + @Override + public boolean isFixed() { + return true; + } +} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilter.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilter.java index 2d13a3f1c24..10a450605bb 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilter.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/MostProbableCandidatesInSmallTimeRangeFilter.java @@ -41,7 +41,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilter { * path's ends such that each path step spans a duration less than or equal to the duration specified by this field. * In other words, each cluster is the transitive hull of fixes not further than this duration apart. */ - private static final Duration CANDIDATE_FILTER_TIME_WINDOW = Duration.ONE_SECOND.times(5); + public static final Duration CANDIDATE_FILTER_TIME_WINDOW = Duration.ONE_SECOND.times(5); /** * When looking for the most probable candidates within time windows of length {@link #CANDIDATE_FILTER_TIME_WINDOW}, @@ -55,7 +55,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilter { private final Candidate startProxyCandidate; private final Candidate endProxyCandidate; - MostProbableCandidatesInSmallTimeRangeFilter(Comparator candidateComparator, Candidate startProxyCandidate, + public MostProbableCandidatesInSmallTimeRangeFilter(Comparator candidateComparator, Candidate startProxyCandidate, Candidate endProxyCandidate) { this.candidateComparator = candidateComparator; filteredCandidates = new TreeSet<>(candidateComparator); @@ -66,18 +66,18 @@ public class MostProbableCandidatesInSmallTimeRangeFilter { /** * Finds the contiguous (by definition of {@link #CANDIDATE_FILTER_TIME_WINDOW}) sequences of candidates adjacent to * those candidates added / removed. The {@code competitorCandidates} set is expected to already contain the - * {@code newCandidates} and to no longer contain the {@code removedCandidates}. {@link #filteredCandidatesStage1} is + * {@code newCandidates} and to no longer contain the {@code removedCandidates}. {@link #filteredCandidates} is * expected to not yet reflect the changes described by {@code newCandidates} and {@code removedCandidates}. *

* * The following steps are performed: *

    - *
  • All candidates from {@code removedCandidates} are removed from {@link #filteredCandidatesStage1}.
  • + *
  • All candidates from {@code removedCandidates} are removed from {@link #filteredCandidates}.
  • *
  • The disjoint sequences containing all new candidates and adjacent to all removed candidates are computed.
  • *
  • The most probable candidate(s) from each contiguous sequence is/are determined. Those pass the filter and are - * added to {@link #filteredCandidatesStage1}
  • + * added to {@link #filteredCandidates} *
  • All other candidates from those sequences do not pass the filter. All of those that are still in - * {@link #filteredCandidatesStage1} are removed from {@link #filteredCandidatesStage1}.
  • + * {@link #filteredCandidates} are removed from {@link #filteredCandidates}. *
* @return a pair whose first element holds the set of candidates that now pass the filter and didn't before, and * whose second element holds the set of candidates that did pass the filter before but don't anymore @@ -86,13 +86,13 @@ public class MostProbableCandidatesInSmallTimeRangeFilter { Iterable newCandidates, Iterable removedCandidates) { assert Util.containsAll(competitorCandidates, newCandidates); // all new candidates must be in competitorCandidates assert !competitorCandidates.stream().filter(c->Util.contains(removedCandidates, c)).findAny().isPresent(); // no removedCandidate must be in competitorCandidates - final Set candidatesAddedToFiltered = new HashSet<>(); - final Set candidatesRemovedFromFiltered = new HashSet<>(); + final NavigableSet candidatesAddedToFiltered = new TreeSet<>(candidateComparator); + final NavigableSet candidatesRemovedFromFiltered = new TreeSet<>(candidateComparator); // create a copy of newCandidates so we can remove objects from the copy as we add candidates to sequences; // this way we'll obtain disjoint sequences and don't need to consider candidates again which have already // been added to a sequence: - final Set newCandidatesModifiableCopy = new HashSet<>(); - final Set removedCandidatesModifiableCopy = new HashSet<>(); + final NavigableSet newCandidatesModifiableCopy = new TreeSet<>(candidateComparator); + final NavigableSet removedCandidatesModifiableCopy = new TreeSet<>(candidateComparator); Util.addAll(newCandidates, newCandidatesModifiableCopy); Util.addAll(removedCandidates, removedCandidatesModifiableCopy); // If the start/end candidates are part of newCandidates and/or removedCandidates, pass them on as is; @@ -102,22 +102,27 @@ public class MostProbableCandidatesInSmallTimeRangeFilter { while (!newCandidatesModifiableCopy.isEmpty()) { final Candidate nextNewCandidate = newCandidatesModifiableCopy.iterator().next(); newCandidatesModifiableCopy.remove(nextNewCandidate); - final NavigableSet contiguousSequenceForNextNewCandidate = getTimeWiseContiguousDistanceCandidates(competitorCandidates, nextNewCandidate, /* includeStartFrom */ true); + final NavigableSet contiguousSequenceForNextNewCandidate = getTimeWiseContiguousCandidates(competitorCandidates, nextNewCandidate, /* includeStartFrom */ true); disjointSequencesAffectedByNewAndRemovedCandidates.add(contiguousSequenceForNextNewCandidate); // remove the candidate grouped in the sequence from the remaining new candidates to consider // because they have been "scooped up" by this sequence already and need no further consideration: Util.removeAll(contiguousSequenceForNextNewCandidate, newCandidatesModifiableCopy); + // Candidates that were removed but fall into the time range of the contiguous sequence just constructed + // can be considered processed: the probabilities for the sequence will be recalculated anyway, and + // the removal cannot have caused a split. + // The candidates removed are added to candidatesRemovedFromFiltered a few lines below. removeAllRemovedCandidatesInOrNearSequence(contiguousSequenceForNextNewCandidate, removedCandidatesModifiableCopy); } // Those candidates that were not yet removed from removedCandidatesModifiableCopy were not within or near // any of the sequences produced by new candidates. Checking their CANDIDATE_FILTER_TIME_WINDOW-neighborhood - // and constructing the sequences affected by their removal. + // and constructing the sequences affected by their removal. Those sequences' max probabilities need to be + // re-assessed, and the removal may cause a split. Util.addAll(removedCandidates, candidatesRemovedFromFiltered); candidatesRemovedFromFiltered.retainAll(filteredCandidates); // those explicitly removed and previously accepted by the filter go away while (!removedCandidatesModifiableCopy.isEmpty()) { final Candidate nextRemovedCandidate = removedCandidatesModifiableCopy.iterator().next(); removedCandidatesModifiableCopy.remove(nextRemovedCandidate); - final NavigableSet contiguousSequenceForNextRemovedCandidate = getTimeWiseContiguousDistanceCandidates(competitorCandidates, nextRemovedCandidate, /* includeStartFrom */ false); + final NavigableSet contiguousSequenceForNextRemovedCandidate = getTimeWiseContiguousCandidates(competitorCandidates, nextRemovedCandidate, /* includeStartFrom */ false); removeAllRemovedCandidatesInOrNearSequence(contiguousSequenceForNextRemovedCandidate, removedCandidatesModifiableCopy); // If adjacent sequences were added before and after the removed candidate, // we need to distinguish whether the gap around the removed candidate was greater @@ -130,7 +135,13 @@ public class MostProbableCandidatesInSmallTimeRangeFilter { for (final SortedSet contiguousCandidateSequence : disjointSequencesAffectedByNewAndRemovedCandidates) { findNewAndRemovedCandidates(contiguousCandidateSequence, candidatesAddedToFiltered, candidatesRemovedFromFiltered); } + // assert that candidatesAddedToFiltered contains only net new candidates not yet in filteredCandidates: + assert !new HashSet<>(candidatesAddedToFiltered).removeAll(filteredCandidates); filteredCandidates.addAll(candidatesAddedToFiltered); + // assert that candidatesRemovedFromFiltered contains only candidates still in filteredCandidates: + assert !new HashSet<>(candidatesRemovedFromFiltered).retainAll(filteredCandidates); + // also assert that candidatesAddedToFiltered and candidatesRemovedFromFiltered are disjoint: + assert !new HashSet<>(candidatesAddedToFiltered).removeAll(candidatesRemovedFromFiltered); filteredCandidates.removeAll(candidatesRemovedFromFiltered); return new Pair<>(candidatesAddedToFiltered, candidatesRemovedFromFiltered); } @@ -157,7 +168,7 @@ public class MostProbableCandidatesInSmallTimeRangeFilter { ArrayList sortedByProbabilityFromLowToHigh = new ArrayList<>(contiguousCandidateSequence); Collections.sort(sortedByProbabilityFromLowToHigh, (c1, c2)->Double.compare(c1.getProbability(), c2.getProbability())); double maxProbability = sortedByProbabilityFromLowToHigh.get(sortedByProbabilityFromLowToHigh.size()-1).getProbability(); - Set candidatesAcceptedFromSequence = new HashSet<>(); + Set candidatesAcceptedFromSequence = new TreeSet<>(candidateComparator); Candidate currentCandidate; for (int i=sortedByProbabilityFromLowToHigh.size()-1; i>=0 && (currentCandidate=sortedByProbabilityFromLowToHigh.get(i)).getProbability()+MAX_PROBABILITY_DELTA >= maxProbability; i--) { @@ -222,34 +233,32 @@ public class MostProbableCandidatesInSmallTimeRangeFilter { * @param includeStartFrom * whether or not to include {@code startFrom} in the resulting set */ - private NavigableSet getTimeWiseContiguousDistanceCandidates(NavigableSet competitorCandidates, Candidate startFrom, boolean includeStartFrom) { + public NavigableSet getTimeWiseContiguousCandidates(NavigableSet competitorCandidates, Candidate startFrom, boolean includeStartFrom) { final NavigableSet result = new TreeSet<>(candidateComparator); if (includeStartFrom) { result.add(startFrom); } - addContiguousCandidates(competitorCandidates.descendingSet().tailSet(startFrom), result); - addContiguousCandidates(competitorCandidates.tailSet(startFrom), result); + addContiguousCandidates(competitorCandidates.descendingSet().tailSet(startFrom, /* inclusive */ false), startFrom, result); + addContiguousCandidates(competitorCandidates.tailSet(startFrom, /* inclusive */ false), startFrom, result); return result; } /** - * The first element in the iteration order of {@code candidates} will not be added to {@code addTo} - * but serves as the first element to compute distance to. The {@link #start} and {@link #end} proxy - * candidates are ignored by this method and will never be added to {@code addTo}. + * The first element in the iteration order of {@code candidates} will also be added to {@code addTo}. The + * {@code startFrom} candidate serves as the first element to compute distance to. The {@link #start} and {@link #end} + * proxy candidates are ignored by this method and will never be added to {@code addTo}. */ - private void addContiguousCandidates(Iterable candidates, Collection addTo) { + private void addContiguousCandidates(Iterable candidates, Candidate startFrom, Collection addTo) { + Candidate current = startFrom; final Iterator iter = candidates.iterator(); - if (iter.hasNext()) { - Candidate current = iter.next(); - while (iter.hasNext()) { - final Candidate next = iter.next(); - if (next != startProxyCandidate && next != endProxyCandidate) { - if (next.getTimePoint().until(current.getTimePoint()).abs().compareTo(CANDIDATE_FILTER_TIME_WINDOW) <= 0) { - addTo.add(next); - current = next; - } else { - break; // too large a gap; end of contiguous sequence in this direction found - } + while (iter.hasNext()) { + final Candidate next = iter.next(); + if (next != startProxyCandidate && next != endProxyCandidate) { + if (next.getTimePoint().until(current.getTimePoint()).abs().compareTo(CANDIDATE_FILTER_TIME_WINDOW) <= 0) { + addTo.add(next); + current = next; + } else { + break; // too large a gap; end of contiguous sequence in this direction found } } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequence.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequence.java index d2cd3c6ea86..f479930b149 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequence.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequence.java @@ -1,8 +1,8 @@ package com.sap.sailing.domain.markpassingcalculation.impl; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.Iterator; import java.util.NavigableSet; import java.util.Set; @@ -38,9 +38,9 @@ public class StationarySequence { * If we identify several consecutive candidates that all lie in a bounding box with a {@link Bounds#getDiameter() * diameter} less than or equal to this distance, only the first and the last of those candidates pass the filter. */ - private static final Distance CANDIDATE_FILTER_DISTANCE = new MeterDistance(30); + public static final Distance CANDIDATE_FILTER_DISTANCE = new MeterDistance(30); - private static final Duration CANDIDATE_FILTER_TIME_WINDOW = Duration.ONE_SECOND.times(10); + public static final Duration CANDIDATE_FILTER_TIME_WINDOW = Duration.ONE_SECOND.times(10); private final NavigableSet candidates; @@ -55,7 +55,7 @@ public class StationarySequence { * first} and {@link #getLast() last} element at the same time. Therefore, the track segment spanned by the * resulting sequence is empty which trivially fulfills the bounding box criterion. */ - StationarySequence(Candidate seed, Comparator candidateComparator, GPSFixTrack track) { + public StationarySequence(Candidate seed, Comparator candidateComparator, GPSFixTrack track) { this.candidates = new TreeSet<>(candidateComparator); this.candidateComparator = candidateComparator; this.track = track; @@ -75,7 +75,7 @@ public class StationarySequence { * {@code candidatesEffectivelyRemoved}; {@code true} is returned in this case, indicating that the sequence * has been extended successfully. */ - boolean tryToExtendAfterLast(Candidate candidateAfterSequence, + public boolean tryToExtendAfterLast(Candidate candidateAfterSequence, Set candidatesEffectivelyAdded, Set candidatesEffectivelyRemoved) { Bounds bounds = computeExtendedBoundsForFixesBetweenCandidates(getLast(), candidateAfterSequence); if (bounds != null) { @@ -89,7 +89,7 @@ public class StationarySequence { // borders of this sequence: final TimeRange timeRangeNoLongerPassingFilter = getExtendedEndBorderRange(extendedBy); if (timeRangeNoLongerPassingFilter != null) { - removeFromFilterResult(timeRangeNoLongerPassingFilter, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + removeFromFilterResultUnlessFixed(timeRangeNoLongerPassingFilter, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); } } return bounds != null; @@ -108,7 +108,7 @@ public class StationarySequence { if (bounds != null) { track.lockForRead(); try { - final Iterator iter = track.getFixesIterator(start.getTimePoint(), /* inclusive */ true, + final Iterator iter = track.getRawFixesIterator(start.getTimePoint(), /* inclusive */ true, end.getTimePoint(), /* inclusive */ true); while (iter.hasNext()) { final GPSFixMoving fix = iter.next(); @@ -141,7 +141,7 @@ public class StationarySequence { * method maintains the set referenced by this parameter accordingly, assuming that the position in the * set may change, or, if this sequence runs empty, it has to be removed from the set altogether. */ - boolean tryToExtendBeforeFirst(Candidate candidateBeforeSequence, + public boolean tryToExtendBeforeFirst(Candidate candidateBeforeSequence, Set candidatesEffectivelyAdded, Set candidatesEffectivelyRemoved, NavigableSet stationarySequenceSetToUpdate) { Bounds bounds = computeExtendedBoundsForFixesBetweenCandidates(candidateBeforeSequence, getFirst()); @@ -158,7 +158,7 @@ public class StationarySequence { // borders of this sequence: final TimeRange timeRangeNoLongerPassingFilter = getExtendedStartBorderRange(extendedBy); if (timeRangeNoLongerPassingFilter != null) { - removeFromFilterResult(timeRangeNoLongerPassingFilter, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + removeFromFilterResultUnlessFixed(timeRangeNoLongerPassingFilter, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); } } return bounds != null; @@ -204,22 +204,35 @@ public class StationarySequence { return result; } - private void removeFromFilterResult(final TimeRange timeRangeNoLongerPassingFilter, Set candidatesEffectivelyAdded, + private void removeFromFilterResultUnlessFixed(final TimeRange timeRangeNoLongerPassingFilter, Set candidatesEffectivelyAdded, Set candidatesEffectivelyRemoved) { final SortedSet candidatesNoLongerPassingFilter = getCandidatesInTimeRange(timeRangeNoLongerPassingFilter); - candidatesEffectivelyAdded.removeAll(candidatesNoLongerPassingFilter); - candidatesEffectivelyRemoved.addAll(candidatesNoLongerPassingFilter); + for (final Candidate candidate : candidatesNoLongerPassingFilter) { + if (!candidate.isFixed()) { + candidatesEffectivelyAdded.removeAll(candidatesNoLongerPassingFilter); + candidatesEffectivelyRemoved.addAll(candidatesNoLongerPassingFilter); + } + } } private SortedSet getCandidatesInTimeRange(final TimeRange timeRange) { final SortedSet candidatesNoLongerPassingFilter = candidates.subSet( - createDummyCandidate(timeRange.from()), - createDummyCandidate(timeRange.to())); + getCandidateMatchingTimePointOrCreateDummy(timeRange.from()), + getCandidateMatchingTimePointOrCreateDummy(timeRange.to())); return candidatesNoLongerPassingFilter; } - Iterable getValidCandidates() { - return ()->candidates.stream().filter(c->isCloseEnoughToSequenceBorder(c)).iterator(); + public Iterable getValidCandidates() { + return ()->candidates.stream().filter(c->isValidCandidate(c)).iterator(); + } + + /** + * A candidate that is part of this stationary sequence is considered valid if it is time-wise close + * enough to one of the sequence's borders (see {@link #CANDIDATE_FILTER_TIME_WINDOW}) or if it is + * a candidate for a {@link Candidate#isFixed() fixed mark passing}. + */ + private boolean isValidCandidate(Candidate c) { + return c.isFixed() || isCloseEnoughToSequenceBorder(c); } private boolean isCloseEnoughToSequenceBorder(Candidate candidate) { @@ -227,8 +240,12 @@ public class StationarySequence { candidate.getTimePoint().until(getLast().getTimePoint()).abs().compareTo(CANDIDATE_FILTER_TIME_WINDOW) < 0; } - Iterable getAllCandidates() { - return candidates; + public int size() { + return candidates.size(); + } + + public boolean isEmpty() { + return candidates.isEmpty(); } /** @@ -242,7 +259,7 @@ public class StationarySequence { assert !candidates.contains(candidate) && !getFirst().getTimePoint().after(candidate.getTimePoint()) && !getLast().getTimePoint().before(candidate.getTimePoint()); candidates.add(candidate); - if (isCloseEnoughToSequenceBorder(candidate)) { + if (isValidCandidate(candidate)) { candidatesEffectivelyAdded.add(candidate); candidatesEffectivelyRemoved.remove(candidate); } @@ -258,19 +275,21 @@ public class StationarySequence { * @param stationarySequenceSetToUpdate * when this method causes a change in what {@link #getFirst()} returns before and after the call, this * method maintains the set referenced by this parameter accordingly, assuming that the position in the - * set may change, or, if this sequence runs empty, it has to be removed from the set altogether. + * set may change, or, if this sequence runs empty, it has to be removed from the set altogether. Furthermore, + * should removing the {@code candidate} reduce this sequence's size to {@code 1}, this sequence is + * removed from the {@code stationarySequenceSetToUpdate}. */ void remove(Candidate candidate, Set candidatesEffectivelyAdded, Set candidatesEffectivelyRemoved, NavigableSet stationarySequenceSetToUpdate) { assert candidates.contains(candidate); - final boolean wasValidCandidate = isCloseEnoughToSequenceBorder(candidate); + final boolean wasValidCandidate = isValidCandidate(candidate); final boolean wasFirst = candidate == getFirst(); final boolean wasLast = candidate == getLast(); - if (wasFirst) { + if (wasFirst || size() == 2) { // if size() == 2 then it will shrink to 1 and this sequence shall be removed stationarySequenceSetToUpdate.remove(this); } candidates.remove(candidate); - if (wasFirst && !candidates.isEmpty()) { + if (wasFirst && candidates.size() > 1) { stationarySequenceSetToUpdate.add(this); } if (wasValidCandidate) { @@ -302,9 +321,13 @@ public class StationarySequence { } private void refreshBoundingBox() { - boundingBoxOfTrackSpanningCandidates = createNewBounds(getFirst()); - boundingBoxOfTrackSpanningCandidates = computeExtendedBoundsForFixesBetweenCandidates(getFirst(), getLast()); - assert boundingBoxOfTrackSpanningCandidates != null; + if (isEmpty()) { + boundingBoxOfTrackSpanningCandidates = null; + } else { + boundingBoxOfTrackSpanningCandidates = createNewBounds(getFirst()); + boundingBoxOfTrackSpanningCandidates = computeExtendedBoundsForFixesBetweenCandidates(getFirst(), getLast()); + assert boundingBoxOfTrackSpanningCandidates != null; + } } /** @@ -312,7 +335,7 @@ public class StationarySequence { * If no estimated position can be obtained, {@code null} is returned. */ private Bounds createNewBounds(Candidate candidate) { - final Position estimatedPosition = track.getEstimatedPosition(candidate.getTimePoint(), /* extrapolate */ false); + final Position estimatedPosition = track.getEstimatedPosition(candidate.getTimePoint(), /* extrapolate */ true); return estimatedPosition == null ? null : new BoundsImpl(estimatedPosition); } @@ -355,8 +378,9 @@ public class StationarySequence { * no split took place, or the split didn't leave more than one candidate for a second sequence}; the new * sequence created by a split otherwise. */ - StationarySequence tryToAddFix(GPSFixMoving newFix, Set candidatesEffectivelyAdded, - Set candidatesEffectivelyRemoved, NavigableSet stationarySequenceSetToUpdate) { + public StationarySequence tryToAddFix(GPSFixMoving newFix, Set candidatesEffectivelyAdded, + Set candidatesEffectivelyRemoved, NavigableSet stationarySequenceSetToUpdate, + boolean isReplacement) { assert !newFix.getTimePoint().before(getFirst().getTimePoint()); assert !newFix.getTimePoint().after(getLast().getTimePoint()); final Bounds newBounds = boundingBoxOfTrackSpanningCandidates.extend(newFix.getPosition()); @@ -364,47 +388,63 @@ public class StationarySequence { if (newBounds.getDiameter().compareTo(CANDIDATE_FILTER_DISTANCE) < 0) { boundingBoxOfTrackSpanningCandidates = newBounds; // ...and we're done tailSequence = null; + if (isReplacement) { + // the fix could have replaced another one, and the bounding box may shrink now; + // later additions may unnecessarily split, so we should be kind and refresh the bounding + // box in this case: + refreshBoundingBox(); + } } else { // split: - final Set oldValidCandidates = new HashSet<>(); + final Set oldValidCandidates = new TreeSet<>(candidateComparator); Util.addAll(getValidCandidates(), oldValidCandidates); - final Candidate dummyCandidateForFix = createDummyCandidate(newFix.getTimePoint()); - SortedSet tailSet = candidates.tailSet(dummyCandidateForFix); + final Candidate candidateForFixTimePoint = getCandidateMatchingTimePointOrCreateDummy(newFix.getTimePoint()); + SortedSet tailSet = candidates.tailSet(candidateForFixTimePoint); // will contain a candidate for newFix.getTimePoint() if such a candidate exists boolean tryToAddCandidateAtFixLater = false; - if (!tailSet.isEmpty() && tailSet.first().getTimePoint().equals(dummyCandidateForFix.getTimePoint())) { - // new fix is exactly on a candidate in this stationary sequence; construct the tailing stationary sequence + if (!tailSet.isEmpty() && tailSet.first().getTimePoint().equals(candidateForFixTimePoint.getTimePoint())) { + // new fix TimePoint matches exactly with that of a candidate in this stationary sequence; + // construct the tailing stationary sequence // without this candidate to start with, then try whether it can be extended to the left: tryToAddCandidateAtFixLater = true; - tailSet = candidates.tailSet(dummyCandidateForFix, /* inclusive */ false); + tailSet = candidates.tailSet(candidateForFixTimePoint, /* inclusive */ false); + assert !tailSet.contains(candidateForFixTimePoint); } tailSequence = tailSet.isEmpty() ? null : createStationarySequence(tailSet); if (tailSequence != null && tryToAddCandidateAtFixLater) { - tailSequence.tryToExtendBeforeFirst(candidates.floor(dummyCandidateForFix), new HashSet<>(), new HashSet<>(), stationarySequenceSetToUpdate); + tailSequence.tryToExtendBeforeFirst(candidates.floor(candidateForFixTimePoint), + new TreeSet<>(candidateComparator), new TreeSet<>(candidateComparator), stationarySequenceSetToUpdate); } // now remove the tail set candidates from this stationary sequence: - final ArrayList fullTailSet = new ArrayList<>(candidates.tailSet(dummyCandidateForFix)); + final ArrayList fullTailSet = new ArrayList<>(candidates.tailSet(candidateForFixTimePoint)); if (!fullTailSet.isEmpty() && fullTailSet.get(0) == getFirst()) { // all candidates will be removed from this sequence; the sequence must be removed from its containing set before removing the first candidate... stationarySequenceSetToUpdate.remove(this); } candidates.removeAll(fullTailSet); + if (size() == 1) { + stationarySequenceSetToUpdate.remove(this); + } // ...and it doesn't need adding because if it was removed, it's empty now. refreshBoundingBox(); - final Set newValidCandidates = new HashSet<>(); + final Set newValidCandidates = new TreeSet<>(candidateComparator); Util.addAll(getValidCandidates(), newValidCandidates); if (tailSequence != null) { // this includes the possibility of a single candidate being added to the tail set Util.addAll(tailSequence.getValidCandidates(), newValidCandidates); } - final Set candidatesAdded = new HashSet<>(newValidCandidates); - candidatesAdded.removeAll(newValidCandidates); - final Set candidatesRemoved = new HashSet<>(oldValidCandidates); + final Set candidatesAdded = new TreeSet<>(candidateComparator); + candidatesAdded.addAll(newValidCandidates); + candidatesAdded.removeAll(oldValidCandidates); + final Set candidatesRemoved = new TreeSet<>(candidateComparator); + candidatesRemoved.addAll(oldValidCandidates); candidatesRemoved.removeAll(newValidCandidates); candidatesEffectivelyAdded.addAll(candidatesAdded); candidatesEffectivelyRemoved.removeAll(candidatesAdded); candidatesEffectivelyRemoved.addAll(candidatesRemoved); candidatesEffectivelyAdded.removeAll(candidatesRemoved); } - return tailSequence != null && Util.size(tailSequence.getAllCandidates()) >= 2 ? tailSequence : null; + final StationarySequence result = tailSequence != null && tailSequence.size() >= 2 ? tailSequence : null; + assert result == null || !this.getLast().getTimePoint().equals(result.getFirst().getTimePoint()); + return result; } /** @@ -416,8 +456,8 @@ public class StationarySequence { * @return a sequence with the candidate(s) */ private StationarySequence createStationarySequence(SortedSet candidates) { - final Set candidatesEffectivelyAdded = new HashSet<>(); - final Set candidatesEffectivelyRemoved = new HashSet<>(); + final Set candidatesEffectivelyAdded = new TreeSet<>(candidateComparator); + final Set candidatesEffectivelyRemoved = new TreeSet<>(candidateComparator); final Iterator candidateIterator = candidates.iterator(); assert candidateIterator.hasNext(); final StationarySequence result = new StationarySequence(candidateIterator.next(), candidateComparator, track); @@ -428,6 +468,34 @@ public class StationarySequence { return result; } + /** + * If this sequence contains a {@link Candidate} that matches the {@code timePoint} exactly, that candidate is + * returned. Otherwise, a dummy candidate is created that has the {@code timePoint} provided. + *

+ * + * Note that depending on the {@link #candidateComparator} used, two candidates are not necessarily considered equal + * only because they have equal time points. + */ + private Candidate getCandidateMatchingTimePointOrCreateDummy(TimePoint timePoint) { + final Candidate dummy = new CandidateImpl(/* one-based index of waypoint */ 1, timePoint, /* probability */ 0, /* waypoint */ null); + final Candidate floorCandidate = candidates.floor(dummy); + final Candidate ceilingCandidate = candidates.ceiling(dummy); + final Candidate result; + if (floorCandidate != null && floorCandidate.getTimePoint().equals(timePoint)) { + result = floorCandidate; + } else if (ceilingCandidate != null && ceilingCandidate.getTimePoint().equals(timePoint)) { + result = ceilingCandidate; + } else { + result = dummy; + } + return result; + } + + @Override + public String toString() { + return "Stationary Sequence "+candidates+", bounds diameter "+boundingBoxOfTrackSpanningCandidates.getDiameter(); + } + /** * Creates a dummy time point that can be used for searching in {@link #candidates} by time point. It * uses {@code 1} for the one-based waypoint index, {@code null} for the waypoint and {@code 0.0} for its @@ -436,5 +504,17 @@ public class StationarySequence { static Candidate createDummyCandidate(TimePoint timePoint) { return new CandidateImpl(/* one-based index of waypoint */ 1, timePoint, /* probability */ 0, /* waypoint */ null); } + + public Iterable getAllCandidates() { + return Collections.unmodifiableCollection(candidates); + } + + /** + * Tells whether the {@link #candidates} set contains {@code candidate}. Containment is decided based on + * the {@link #candidateComparator}'s notion of equality. + */ + public boolean contains(Candidate candidate) { + return candidates.contains(candidate); + } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilter.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilter.java index bd059ae766e..e2b94245c2b 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilter.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/StationarySequenceBasedFilter.java @@ -5,6 +5,7 @@ import java.util.HashSet; import java.util.NavigableSet; import java.util.Set; import java.util.TreeSet; +import java.util.logging.Logger; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.common.Bounds; @@ -35,10 +36,13 @@ import com.sap.sse.common.Util.Pair; * * The filter is created empty. It keeps track of all stationary sequences found so far and sorts them by time. The * algorithm guarantees that after a call to {@link #updateCandidates(Iterable, Iterable)} as well as after a call to - * {@link #addFix} each stationary sequence has at least two candidates in them which passed the first filter state, and - * no stationary sequence can be extended to the next candidate following it or any previous candidate preceding it - * because the track leading there would extend the stationary sequence's bounding box beyond limits, and no two - * adjacent candidates that are not part of the same {@link StationarySequence} can be joined into a valid such sequence. + * {@link #addFix} each stationary sequence has at least two candidates in them which passed the first filter state. No + * stationary sequence can be extended to the next candidate following it or any previous candidate preceding it because + * the track leading there would extend the stationary sequence's bounding box beyond limits, or the candidate outside + * the sequence already belongs to an adjacent sequence. No two adjacent candidates that are not part of any + * {@link StationarySequence} can be joined into a valid sequence. + *

+ * * Furthermore, all {@link StationarySequence}s maintain their invariant, in particular that the track leading from * their {@link StationarySequence#getFirst() first} to their {@link StationarySequence#getLast() last} candidate * remains within a bounding box whose {@link Bounds#getDiameter() diameter} remains below the threshold. Calling @@ -47,9 +51,9 @@ import com.sap.sse.common.Util.Pair; * {@link #updateCandidates(Iterable, Iterable)} as having been removed and that currently pass this filter. *

* - * The sequences are non-overlapping but may touch each other, meaning that a preceding sequence's - * {@link StationarySequence#getLast() last} candidate is the same as the following sequence's - * {@link StationarySequence#getFirst() first} candidate. + * Two adjacent {@link StationarySequence}s must not overlap. The {@link StationarySequence#getLast() last candidate} of + * a sequence must be truly less---according to the {@link #candidateComparator}---than the + * {@link StationarySequence#getFirst() first candidate} of the next sequence. *

* * All sequences, candidates and the track belong to the same {@link Competitor}. @@ -73,6 +77,8 @@ import com.sap.sse.common.Util.Pair; * */ public class StationarySequenceBasedFilter { + private static final Logger logger = Logger.getLogger(StationarySequenceBasedFilter.class.getName()); + /** * Set of sequences managed here; ordered by their {@link StationarySequence#getFirst() first} candidate, * based on the candidate comparator passed to the constructor. This implies that {@link StationarySequence} objects @@ -195,6 +201,8 @@ public class StationarySequenceBasedFilter { removeCandidate(removedCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); } updateFilteredCandidates(candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + final boolean assertion = isCandidatesConsistent(); + assert assertion; return new Pair<>(candidatesEffectivelyAdded, candidatesEffectivelyRemoved); } @@ -217,7 +225,7 @@ public class StationarySequenceBasedFilter { * be extended up to the new candidate, try to construct a new {@link StationarySequence} from the new candidate to * the neighbor (new candidate and neighbor will still pass the filter if such a sequence can be validly constructed * as they are the only two candidates in the new sequence for now). (Note: It is not possible that a single - * {@link StationarySequence} can be constructed spanning between the two neighbors because it it were possible then + * {@link StationarySequence} can be constructed spanning between the two neighbors because if it were possible then * this would have had to have happened before as it does not depend on the appearance of the new candidate.) *

  • The candidate falls into an existing stationary sequence (at or after first and at or before last candidate * in sequence). In this case the set of fixes on the track considered within the sequence hasn't changed. The @@ -227,7 +235,12 @@ public class StationarySequenceBasedFilter { */ private void addCandidate(Candidate newCandidate, Set candidatesEffectivelyAdded, Set candidatesEffectivelyRemoved) { - assert !candidates.contains(newCandidate); + boolean candidatesContainsNewCandidate = candidates.contains(newCandidate); + if (candidatesContainsNewCandidate) { + logger.severe("Candidates "+candidates+" already contain "+newCandidate+" which is to be added."); + } + final boolean assertion = !candidatesContainsNewCandidate && isCandidatesConsistent(); + assert assertion; if (newCandidate == startProxyCandidate) { candidatesEffectivelyAdded.add(newCandidate); candidatesEffectivelyRemoved.remove(newCandidate); @@ -236,18 +249,69 @@ public class StationarySequenceBasedFilter { candidatesEffectivelyRemoved.remove(newCandidate); } else { candidates.add(newCandidate); - lookLeft(newCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); - lookRight(newCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + // First try adding the new candidate to a sequence starting at or before newCandidate. + // This could either succeed by adding it to an existing sequence, or by creating a new + // sequence with newCandidate and its immediate predecessor candidate that so far was + // not part of a sequence. If this was successful, don't even try to extend any later sequence + // towards newCandidate. But it newCandidate could not be added to a sequence looking into the past ("left"), + // try our luck by looking into the future ("right"). + if (!lookLeft(newCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved)) { + lookRight(newCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + } } + assert isCandidatesConsistent(); } - private void lookLeft(Candidate newCandidate, Set candidatesEffectivelyAdded, + /** + * Checks whether the union of all candidates of all stationary sequences from {@link #stationarySequences} is + * contained in {@link #candidates}, and whether adjacent stationary sequences are truly disjoint, not even + * overlapping by a single candidate considered equal by definition of {@link #candidateComparator}. + */ + private boolean isCandidatesConsistent() { + final TreeSet union = new TreeSet<>(candidateComparator); + Candidate lastCandidateInPreviousSequence = null; + boolean overlappingTimePointFound = false; + for (final StationarySequence stationarySequence : stationarySequences) { + if (lastCandidateInPreviousSequence != null && + candidateComparator.compare(lastCandidateInPreviousSequence, stationarySequence.getFirst()) >= 0) { + overlappingTimePointFound = true; + logger.severe("Last candidate "+lastCandidateInPreviousSequence+ + " in sequence overlaps with first element in next sequence "+stationarySequence); + } + lastCandidateInPreviousSequence = stationarySequence.getLast(); + Util.addAll(stationarySequence.getAllCandidates(), union); + } + final boolean allSequenceCandidatesInCandidates = candidates.containsAll(union); + if (!allSequenceCandidatesInCandidates) { + union.removeAll(candidates); + logger.severe("Candidates "+union+" from sequences missing from full candidates collection"); + } + return allSequenceCandidatesInCandidates && !overlappingTimePointFound; + } + + /** + * Searches the latest {@link StationarySequence} in {@link #stationarySequences} that starts at or before + * {@code newCandidate}. If the {@code newCandidate} is within that sequence's time range, it's simply + * {@link StationarySequence#addWithin(Candidate, Set, Set) added}. Otherwise, it has to be later than the current + * end of that sequence, so {@link StationarySequence#tryToExtendAfterLast(Candidate, Set, Set) extending} the + * sequence is tried. + *

    + * + * If extending the sequence before the {@code newCandidate} does not work because it would exceed the sequence's + * bounding box diameter threshold, an attempt is made to join the {@code newCandidate} with its predecessor + * candidate from {@link #candidates} into a new {@link StationarySequence}. + * + * @return {@code true} in case the {@code newCandidate} was added to a {@link StationarySequence}, either a new one + * because the preceding sequence could not be extended, or as an extension of the preceding sequence. + */ + private boolean lookLeft(Candidate newCandidate, Set candidatesEffectivelyAdded, Set candidatesEffectivelyRemoved) { + final boolean addedToSequence; final StationarySequence latestStationarySequenceStartingAtOrBeforeNewCandidate = stationarySequences.floor(createStationarySequence(newCandidate)); final boolean createNewSequenceFromLowerToNew; if (latestStationarySequenceStartingAtOrBeforeNewCandidate != null) { - if (latestStationarySequenceStartingAtOrBeforeNewCandidate.getLast().getTimePoint().before(newCandidate.getTimePoint())) { + if (candidateComparator.compare(latestStationarySequenceStartingAtOrBeforeNewCandidate.getLast(), newCandidate) < 0) { // earlier sequence ends before newCandidate; try to extend, and if extending doesn't work, // request a new sequence to be constructed: createNewSequenceFromLowerToNew = !latestStationarySequenceStartingAtOrBeforeNewCandidate.tryToExtendAfterLast( @@ -266,15 +330,28 @@ public class StationarySequenceBasedFilter { candidatesEffectivelyRemoved.remove(newCandidate); // neighbour's stationary sequence can't be extended to newCandidate or there was no earlier stationary sequence, // look for an earlier candidate and try to create a new stationary sequence; no candidate from the new - // sequence will be eliminated by this filter for now as they form the sequence's boundaries. + // sequence will be eliminated by this filter for now as they form the sequence's boundaries. Construct such + // a sequence only if the "lower" candidate is not already part of the previous sequence, because sequences + // must not share candidates. final Candidate lower = candidates.lower(newCandidate); - if (lower != null) { + if (lower != null && (latestStationarySequenceStartingAtOrBeforeNewCandidate == null || + candidateComparator.compare(latestStationarySequenceStartingAtOrBeforeNewCandidate.getLast(), lower) < 0)) { final StationarySequence newSequence = tryToConstructStationarySequence(lower, newCandidate); if (newSequence != null) { + addedToSequence = true; stationarySequences.add(newSequence); + } else { + addedToSequence = false; } + } else { + addedToSequence = false; } + } else { + addedToSequence = true; } + final boolean assertion = containsNoEmptyOrSingleCandidateStationarySequence() && isCandidatesConsistent(); + assert assertion; + return addedToSequence; } /** @@ -294,13 +371,18 @@ public class StationarySequenceBasedFilter { candidatesEffectivelyAdded.add(newCandidate); candidatesEffectivelyRemoved.remove(newCandidate); final Candidate higher = candidates.higher(newCandidate); - if (higher != null) { + // Construct such a sequence only if the "higher" candidate is not already part of the next sequence, because sequences + // must not share candidates. + if (higher != null && (earliestStationarySequenceStartingAfterNewCandidate == null || + candidateComparator.compare(higher, earliestStationarySequenceStartingAfterNewCandidate.getFirst()) < 0)) { final StationarySequence newSequence = tryToConstructStationarySequence(newCandidate, higher); if (newSequence != null) { stationarySequences.add(newSequence); } } } + final boolean assertion = containsNoEmptyOrSingleCandidateStationarySequence() && isCandidatesConsistent(); + assert assertion; } private StationarySequence tryToConstructStationarySequence(Candidate start, Candidate end) { @@ -343,20 +425,32 @@ public class StationarySequenceBasedFilter { candidatesEffectivelyRemoved.add(removedCandidate); candidatesEffectivelyAdded.remove(removedCandidate); } else { - assert candidates.contains(removedCandidate); + if (!candidates.contains(removedCandidate)) { + logger.severe("Candidates "+candidates+" does not contain "+removedCandidate+" which is to be removed."); + } + final boolean assertion = candidates.contains(removedCandidate) && isCandidatesConsistent(); + assert assertion; + candidates.remove(removedCandidate); + final StationarySequence searchDummySequence = createStationarySequence(removedCandidate); final StationarySequence latestStationarySequenceStartingAtOrBeforeRemovedCandidate = - stationarySequences.floor(createStationarySequence(removedCandidate)); + stationarySequences.floor(searchDummySequence); final boolean addToEffectivelyRemoved; if (latestStationarySequenceStartingAtOrBeforeRemovedCandidate != null) { - if (!latestStationarySequenceStartingAtOrBeforeRemovedCandidate.getLast().getTimePoint() - .before(removedCandidate.getTimePoint())) { + // Adjacent sequences are disjoint. If we find the candidate, even at the beginning of a sequence, + // there is no need to check the previous sequence for that candidate. + final StationarySequence previousSequence; + if (candidateComparator.compare(removedCandidate, latestStationarySequenceStartingAtOrBeforeRemovedCandidate.getFirst()) == 0 + && (previousSequence=stationarySequences.lower(searchDummySequence)) != null) { + assert !previousSequence.contains(removedCandidate); // it was in the floored sequence; it must not be in any prior sequence + } + if (candidateComparator.compare(latestStationarySequenceStartingAtOrBeforeRemovedCandidate.getLast(), removedCandidate) >= 0) { // within the sequence; remove: latestStationarySequenceStartingAtOrBeforeRemovedCandidate.remove(removedCandidate, candidatesEffectivelyAdded, candidatesEffectivelyRemoved, /* StationarySequence set to update */ stationarySequences); - if (Util.size(latestStationarySequenceStartingAtOrBeforeRemovedCandidate.getAllCandidates()) <= 1) { - stationarySequences.remove(latestStationarySequenceStartingAtOrBeforeRemovedCandidate); - } + assert !stationarySequences.contains(latestStationarySequenceStartingAtOrBeforeRemovedCandidate) || + latestStationarySequenceStartingAtOrBeforeRemovedCandidate.size() > 1; + assert isCandidatesConsistent(); addToEffectivelyRemoved = false; // already taken care of by the remove call above } else { // candidate not in any sequence @@ -371,8 +465,19 @@ public class StationarySequenceBasedFilter { candidatesEffectivelyAdded.remove(removedCandidate); } } + assert containsNoEmptyOrSingleCandidateStationarySequence() && isCandidatesConsistent(); } + private boolean containsNoEmptyOrSingleCandidateStationarySequence() { + for (final StationarySequence ss : stationarySequences) { + if (ss.size() < 2) { + logger.severe("Found stationary sequence "+ss+" with less than two candidates in it."); + return false; + } + } + return true; + } + /** * Informs this filter that for its competitor a new position fix was added or replaced. All necessary updates to * the stationary sequences managed by this filter are carried out, and the changes that this has to the result of @@ -412,6 +517,8 @@ public class StationarySequenceBasedFilter { */ Pair, Iterable> updateFixes(Iterable newFixes, Iterable fixesReplacingExistingOnes) { + final boolean assertion = isCandidatesConsistent(); + assert assertion; final Set candidatesEffectivelyAdded = new HashSet<>(); final Set candidatesEffectivelyRemoved = new HashSet<>(); if (newFixes != null) { @@ -420,48 +527,57 @@ public class StationarySequenceBasedFilter { StationarySequence.createDummyCandidate(newFix.getTimePoint()))); if (lastSequenceStartingAtOrBeforeFix != null && !lastSequenceStartingAtOrBeforeFix.getLast().getTimePoint().before(newFix.getTimePoint())) { // fix falls into the existing StationarySequence; update its bounding box: - final StationarySequence splitResult = lastSequenceStartingAtOrBeforeFix.tryToAddFix(newFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved, /* StationarySequence set to update */ stationarySequences); - if (Util.size(lastSequenceStartingAtOrBeforeFix.getAllCandidates()) <= 1) { - stationarySequences.remove(lastSequenceStartingAtOrBeforeFix); - } + final StationarySequence splitResult = lastSequenceStartingAtOrBeforeFix.tryToAddFix( + newFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved, + /* StationarySequence set to update */ stationarySequences, + /* was replacement */ fixesReplacingExistingOnes != null && Util.contains(fixesReplacingExistingOnes, newFix)); + assert !stationarySequences.contains(lastSequenceStartingAtOrBeforeFix) || lastSequenceStartingAtOrBeforeFix.size() > 1; if (splitResult != null) { + assert splitResult.size() > 1; stationarySequences.add(splitResult); } } } } if (fixesReplacingExistingOnes != null) { + Set newFixesHashedForQuickAssertionCheck = new HashSet<>(); + assert Util.addAll(newFixes, newFixesHashedForQuickAssertionCheck) != null; for (final GPSFixMoving fixReplacingExistingOne : fixesReplacingExistingOnes) { - assert Util.contains(newFixes, fixesReplacingExistingOnes); + assert newFixesHashedForQuickAssertionCheck.contains(fixReplacingExistingOne); final Candidate dummyCandidateForReplacementFix = StationarySequence.createDummyCandidate(fixReplacingExistingOne.getTimePoint()); final StationarySequence dummyStationarySequenceForFix = createStationarySequence(dummyCandidateForReplacementFix); final StationarySequence lastSequenceStartingAtOrBeforeFix = stationarySequences.floor(dummyStationarySequenceForFix); final boolean fixIsInStationarySequence = lastSequenceStartingAtOrBeforeFix != null && !lastSequenceStartingAtOrBeforeFix.getLast().getTimePoint().before(fixReplacingExistingOne.getTimePoint()); if (!fixIsInStationarySequence) { - final StationarySequence lastSequenceEndingBeforeFix = // null in case fix is within a stationary sequence - lastSequenceStartingAtOrBeforeFix != null && fixIsInStationarySequence ? null : lastSequenceStartingAtOrBeforeFix; + final StationarySequence lastSequenceEndingBeforeFix = lastSequenceStartingAtOrBeforeFix; // null in case there is no sequence coming before the fix final Candidate lastCandidateBeforeReplacementFix = candidates.lower(dummyCandidateForReplacementFix); - if (lastCandidateBeforeReplacementFix != null) { + if (lastCandidateBeforeReplacementFix != null) { // no need to try to extend a later sequence to "the left" if there is nothing final Candidate firstCandidateAfterReplacementFix = candidates.higher(dummyCandidateForReplacementFix); - if (firstCandidateAfterReplacementFix != null) { - // the fix is between two candidates, so we may try to extend or create a stationary sequence: - if (lastCandidateBeforeReplacementFix == lastSequenceEndingBeforeFix.getLast()) { - // previous candidate is end of a sequence; try to extend - lastSequenceEndingBeforeFix.tryToExtendAfterLast(firstCandidateAfterReplacementFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + if (firstCandidateAfterReplacementFix != null) { // no need to try to extend an earlier sequence to "the right" if there is nothing + // the fix is between two candidates, so we may try to extend or create a stationary sequence if not the next + // candidate is already part of a sequence. We don't merge sequences as of now, and sequences must be disjoint regarding + // their set of candidates. + final StationarySequence firstSequenceStartingAfterFix = stationarySequences.higher(dummyStationarySequenceForFix); + final boolean lookRight; + final boolean previousCandidateIsLastInSequence = lastSequenceEndingBeforeFix != null && lastCandidateBeforeReplacementFix == lastSequenceEndingBeforeFix.getLast(); + final boolean nextCandidateIsFirstInSequence = firstSequenceStartingAfterFix != null && firstCandidateAfterReplacementFix == firstSequenceStartingAfterFix.getFirst(); + if (previousCandidateIsLastInSequence && !nextCandidateIsFirstInSequence) { + // previous candidate is end of a sequence and next one is not part of a sequence; try to extend previous sequence to "the right": + lookRight = !lastSequenceEndingBeforeFix.tryToExtendAfterLast(firstCandidateAfterReplacementFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved); } else { - final StationarySequence firstSequenceStartingAfterFix = stationarySequences.higher(dummyStationarySequenceForFix); - if (firstCandidateAfterReplacementFix == firstSequenceStartingAfterFix.getFirst()) { - // next candidate is start of a sequence; try to extend - firstSequenceStartingAfterFix.tryToExtendBeforeFirst(lastCandidateBeforeReplacementFix, - candidatesEffectivelyAdded, candidatesEffectivelyRemoved, - /* StationarySequence set to update */ stationarySequences); - } else { - // none of the adjacent candidates is part of a sequence; try to create a new one: - final StationarySequence newSequence = createStationarySequence(lastCandidateBeforeReplacementFix); - if (newSequence.tryToExtendAfterLast(firstCandidateAfterReplacementFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved)) { - stationarySequences.add(newSequence); - } + lookRight = true; + } + if (lookRight && nextCandidateIsFirstInSequence && !previousCandidateIsLastInSequence) { + // next candidate is start of a sequence and previous candidate is not part of a sequence; try to extend next sequence to "the left": + firstSequenceStartingAfterFix.tryToExtendBeforeFirst(lastCandidateBeforeReplacementFix, + candidatesEffectivelyAdded, candidatesEffectivelyRemoved, + /* StationarySequence set to update */ stationarySequences); + } else if (!previousCandidateIsLastInSequence && !nextCandidateIsFirstInSequence) { + // none of the adjacent candidates is part of a sequence; try to create a new one: + final StationarySequence newSequence = createStationarySequence(lastCandidateBeforeReplacementFix); + if (newSequence.tryToExtendAfterLast(firstCandidateAfterReplacementFix, candidatesEffectivelyAdded, candidatesEffectivelyRemoved)) { + stationarySequences.add(newSequence); } } } @@ -470,6 +586,7 @@ public class StationarySequenceBasedFilter { } } updateFilteredCandidates(candidatesEffectivelyAdded, candidatesEffectivelyRemoved); + assert containsNoEmptyOrSingleCandidateStationarySequence() && assertion; return new Pair<>(candidatesEffectivelyAdded, candidatesEffectivelyRemoved); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/AbstractRankingMetric.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/AbstractRankingMetric.java index a5a26187e77..b8b460e45c0 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/AbstractRankingMetric.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/AbstractRankingMetric.java @@ -261,12 +261,15 @@ public abstract class AbstractRankingMetric implements RankingMetric { /** * The time from the {@link TrackedRace#getStartOfRace() race start} until timePoint or until - * the point in time when competitor passed the finish mark, whichever comes first. + * the point in time when competitor passed the finish mark, whichever comes first. If there is + * no mark passing for {@code competitor} for the last waypoint or no {@link TrackedRace#getStartOfRace()} is + * known, {@code null} is returned. */ - protected Duration getActualTimeSinceStartOfRace(Competitor competitor, TimePoint timePoint) { + @Override + public Duration getActualTimeSinceStartOfRace(Competitor competitor, TimePoint timePoint) { final Duration result; final TimePoint startOfRace = getTrackedRace().getStartOfRace(); - if (startOfRace == null) { + if (startOfRace == null || timePoint.before(startOfRace)) { result = null; } else { final Waypoint finish = getTrackedRace().getRace().getCourse().getLastWaypoint(); @@ -277,7 +280,11 @@ public abstract class AbstractRankingMetric implements RankingMetric { if (finishingMarkPassing != null && finishingMarkPassing.getTimePoint().before(timePoint)) { result = startOfRace.until(finishingMarkPassing.getTimePoint()); } else { - result = startOfRace.until(timePoint); + if (trackedRace.getEndOfTracking() != null && timePoint.after(trackedRace.getEndOfTracking())) { + result = null; // race not finished until end of tracking; no reasonable value can be computed for competitor + } else { + result = startOfRace.until(timePoint); + } } } } @@ -435,7 +442,9 @@ public abstract class AbstractRankingMetric implements RankingMetric { getTrackedRace().getRace().getCourse().getIndexOfWaypoint(legWho.getTrackedLeg().getLeg().getFrom()) <= getTrackedRace().getRace().getCourse().getIndexOfWaypoint(legTo.getTrackedLeg().getLeg().getFrom()); final Duration result; - if (legWho == null || legTo == null || !legWho.hasStartedLeg(timePoint) || !legTo.hasStartedLeg(timePoint)) { + if (legWho == null || legTo == null || + !(legWho.hasStartedLeg(timePoint) || legWho.hasFinishedLeg(timePoint)) || + !(legTo.hasStartedLeg(timePoint) || legTo.hasFinishedLeg(timePoint))) { result = null; } else { final Competitor who = legWho.getCompetitor(); @@ -494,18 +503,17 @@ public abstract class AbstractRankingMetric implements RankingMetric { * For the situation at timePoint, determines how long in real, uncorrected time who lags * behind to in the leg identified by legWho. If both are still sailing in the leg at * timePoint, this is the time who needs with constant average VMG to reach - * to's position at timePoint. If only to has already finished the leg and no - * mark passing time is known yet for who for the end of the leg then who is projected to - * the end of the leg using her average VMG on the leg, and the difference between who's projected and - * to's actual mark passing times is returned. - *

    - * - * If leg finish mark passings are available for both, who and to, the difference between - * them is returned. + * to's position at timePoint. If only to has already finished the leg at + * {@code timePoint} then who is projected to the end of the leg using her average VMG on the leg, and + * the difference between who's projected and to's actual mark passing times is returned. + * Note that in this latter case it doesn't matter whether {@code who} already has a mark passing for the end of the + * leg or not; the idea is to not "rewrite history" by letting the mark passing have an impact on the rankings prior + * to the mark passing. *

    * * The result may be a negative duration in case who reached the position in question before - * timePoint.

    + * timePoint. + *

    * * Precondition: who and to have both started sailing the leg at timePoint * and to has sailed a greater or equal windward distance compared to who. If not, the @@ -513,8 +521,8 @@ public abstract class AbstractRankingMetric implements RankingMetric { */ protected Duration getPredictedDurationToEndOfLegOrTo(TimePoint timePoint, final TrackedLegOfCompetitor legWho, final TrackedLegOfCompetitor legTo, WindLegTypeAndLegBearingCache cache) { - assert legWho.hasStartedLeg(timePoint); - assert legTo.hasStartedLeg(timePoint); + assert legWho.hasStartedLeg(timePoint) || legWho.hasFinishedLeg(timePoint); + assert legTo.hasStartedLeg(timePoint) || legTo.hasFinishedLeg(timePoint); final Duration toEndOfLegOrTo; if (legTo.hasFinishedLeg(timePoint)) { // calculate actual time it takes who to reach the end of the leg starting at timePoint: @@ -527,9 +535,8 @@ public abstract class AbstractRankingMetric implements RankingMetric { getWindwardDistanceTraveled(legWho.getCompetitor(), legWho.hasFinishedLeg(timePoint)?legWho.getFinishTime():timePoint, cache)) >= 0; // estimate who's leg finishing time by extrapolating with the average VMG (if available) or the current VMG // (if no average VMG can currently be computed, e.g., because the time point is exactly at the leg start) - final Position windwardPositionToReachInWhosCurrentLeg = - getTrackedRace().getApproximatePosition(legWho.getLeg().getTo(), timePoint); - toEndOfLegOrTo = getDurationToReach(windwardPositionToReachInWhosCurrentLeg, timePoint, legWho, cache); + final Position positionOfEndOfLeg = getTrackedRace().getApproximatePosition(legWho.getLeg().getTo(), timePoint); + toEndOfLegOrTo = getDurationToReach(positionOfEndOfLeg, timePoint, legWho, cache); } } else { // competitor "to" is still in same leg; project "who" to "to"'s position using VMG diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/OneDesignRankingMetric.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/OneDesignRankingMetric.java index 2f38163ebef..dbfdb71615b 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/OneDesignRankingMetric.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/OneDesignRankingMetric.java @@ -197,7 +197,6 @@ public class OneDesignRankingMetric extends AbstractRankingMetric { public Duration getCorrectedTimeAtEstimatedArrivalAtCompetitorFarthestAhead() { return getEstimatedActualDurationFromRaceStartToCompetitorFarthestAhead(); } - }; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/RankingMetric.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/RankingMetric.java index 0dd2269675b..9c9d8208e3a 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/RankingMetric.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/ranking/RankingMetric.java @@ -154,6 +154,14 @@ public interface RankingMetric extends Serializable { Comparator getLegRankingComparator(TrackedLeg trackedLeg, TimePoint timePoint, WindLegTypeAndLegBearingCache cache); + /** + * Determine the time sailed for the {@code competitor} at {@code timePoint} in this race. This ignores whether or not + * the race has recorded a start mark passing for the {@code competitor}. If no finish mark passing is found either, the + * duration between the {@link #getStartOfRace() race start time} and {@code timePoint} is returned; otherwise the duration + * between the {@link #getStartOfRace() race start time} and the time when the {@code competitor} finished the race. + */ + Duration getActualTimeSinceStartOfRace(Competitor competitor, TimePoint timePoint); + /** * How much time did the competitor spend in the {@link TrackedRace#getRace() race} described by * trackedRace at timePoint, corrected by the handicapping rules defined by this ranking diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingHandler.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingHandler.java index 5e3f27fb20e..00b8d9807c4 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingHandler.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/RaceTrackingHandler.java @@ -1,6 +1,7 @@ package com.sap.sailing.domain.tracking; import java.io.Serializable; +import java.net.URI; import java.util.Map; import java.util.Optional; @@ -8,11 +9,18 @@ import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.base.Boat; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorAndBoatStore; import com.sap.sailing.domain.base.Course; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.impl.DynamicBoat; +import com.sap.sailing.domain.base.impl.DynamicCompetitor; +import com.sap.sailing.domain.base.impl.DynamicCompetitorWithBoat; +import com.sap.sailing.domain.base.impl.DynamicTeam; import com.sap.sailing.domain.base.impl.RaceDefinitionImpl; +import com.sap.sse.common.Color; +import com.sap.sse.common.Duration; import com.sap.sse.util.ThreadLocalTransporter; /** @@ -29,9 +37,21 @@ public interface RaceTrackingHandler { DynamicRaceDefinitionSet raceDefinitionSetToUpdate, boolean useMarkPassingCalculator, RaceLogResolver raceLogResolver, Optional threadLocalTransporter); + DynamicCompetitor getOrCreateCompetitor(CompetitorAndBoatStore competitorAndBoatStore, Serializable competitorId, + String name, String shortName, Color displayColor, String email, URI flagImageURI, DynamicTeam team, + Double timeOnTimeFactor, Duration timeOnDistanceAllowancePerNauticalMile, String searchTag); + RaceDefinition createRaceDefinition(Regatta regatta, String name, Course course, BoatClass boatClass, Map competitorsAndTheirBoats, Serializable id); + DynamicCompetitorWithBoat getOrCreateCompetitorWithBoat(CompetitorAndBoatStore competitorStore, + Serializable competitorId, String name, String shortName, Color displayColor, String email, + URI flagImageURI, DynamicTeam team, Double timeOnTimeFactor, + Duration timeOnDistanceAllowancePerNauticalMile, String searchTag, DynamicBoat boat); + + DynamicBoat getOrCreateBoat(CompetitorAndBoatStore competitorAndBoatStore, Serializable id, String name, + BoatClass boatClass, String sailId, Color color); + public class DefaultRaceTrackingHandler implements RaceTrackingHandler { @Override public DynamicTrackedRace createTrackedRace(TrackedRegatta trackedRegatta, RaceDefinition raceDefinition, @@ -49,5 +69,30 @@ public interface RaceTrackingHandler { BoatClass boatClass, Map competitorsAndTheirBoats, Serializable id) { return new RaceDefinitionImpl(name, course, boatClass, competitorsAndTheirBoats, id); } + + @Override + public DynamicCompetitor getOrCreateCompetitor(CompetitorAndBoatStore competitorStore, + Serializable competitorId, String name, String shortName, + Color displayColor, String email, URI flagImageURI, DynamicTeam team, Double timeOnTimeFactor, + Duration timeOnDistanceAllowancePerNauticalMile, String searchTag) { + return competitorStore.getOrCreateCompetitor(competitorId, name, shortName, displayColor, email, + flagImageURI, team, timeOnTimeFactor, timeOnDistanceAllowancePerNauticalMile, + searchTag); + } + + @Override + public DynamicCompetitorWithBoat getOrCreateCompetitorWithBoat(CompetitorAndBoatStore competitorStore, + Serializable competitorId, String name, String shortName, Color displayColor, String email, + URI flagImageURI, DynamicTeam team, Double timeOnTimeFactor, + Duration timeOnDistanceAllowancePerNauticalMile, String searchTag, DynamicBoat boat) { + return competitorStore.getOrCreateCompetitorWithBoat(competitorId, name, shortName, displayColor, email, + flagImageURI, team, timeOnTimeFactor, timeOnDistanceAllowancePerNauticalMile, searchTag, boat); + } + + @Override + public DynamicBoat getOrCreateBoat(CompetitorAndBoatStore competitorAndBoatStore, Serializable id, String name, + BoatClass boatClass, String sailId, Color color) { + return competitorAndBoatStore.getOrCreateBoat(id, name, boatClass, sailId, color); + } } } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java index bde6658fda8..120e12fa6e2 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/TrackedRace.java @@ -999,6 +999,18 @@ public interface TrackedRace */ TargetTimeInfo getEstimatedTimeToComplete(TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException; + /** + * Determine the time sailed for the {@code competitor} at {@code timePoint} in this race. This ignores whether or + * not the race has recorded a start mark passing for the {@code competitor}. If no finish mark passing is found + * either, the duration between the {@link #getStartOfRace() race start time} and {@code timePoint} is returned; + * otherwise the duration between the {@link #getStartOfRace() race start time} and the time when the + * {@code competitor} finished the race. If there is no mark passing for {@code competitor} for the last waypoint or + * no {@link TrackedRace#getStartOfRace()} is known, {@code null} is returned. + */ + default Duration getTimeSailedSinceRaceStart(Competitor competitor, TimePoint timePoint) { + return null; + } + /** * Calculates the estimated distance it takes a competitor to sail the race, from start to finish. * diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java index 25686b0339d..bfce78a01cf 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceImpl.java @@ -1097,8 +1097,9 @@ DynamicTrackedRace, GPSTrackListener { @Override public void attachRaceLog(RaceLog raceLog) { - logListener.addTo(raceLog); + logListener.beforeAttaching(raceLog); super.attachRaceLog(raceLog); + logListener.afterAttaching(raceLog); getRaceState(raceLog).addChangedListener(raceStateBasedStartTimeChangedListener); } @@ -1106,9 +1107,12 @@ DynamicTrackedRace, GPSTrackListener { public RaceLog detachRaceLog(Serializable identifier) { final RaceLog attachedRaceLog = attachedRaceLogs.get(identifier); if (attachedRaceLog != null) { - logListener.removeFrom(attachedRaceLog); + logListener.beforeDetaching(attachedRaceLog); } final RaceLog raceLogDetached = super.detachRaceLog(identifier); + if (attachedRaceLog != null) { + logListener.afterDetaching(attachedRaceLog); + } assert raceLogDetached == attachedRaceLog; synchronized (raceStates) { final ReadonlyRaceState raceState = raceStates.remove(attachedRaceLog); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceLogListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceLogListener.java index bcc66c88fa7..f9ced9a8709 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceLogListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/DynamicTrackedRaceLogListener.java @@ -47,7 +47,7 @@ import com.sap.sse.common.Util.Triple; * Listens for changes on a {@link RaceLog} and forwards the relevant ones to a {@link TrackedRace}. Examples: start time changes; * fixed mark passings changes; course design changes; wind fixes entered by the race committee.

    * - * For each {@link RaceLog} {@link #addTo(RaceLog) attached}, a {@link ReadonlyRaceState} is constructed that is used to observe + * For each {@link RaceLog} {@link #beforeAttaching(RaceLog) attached}, a {@link ReadonlyRaceState} is constructed that is used to observe * some properties in an easier to use form than observing the {@link RaceLog}s directly. For example, the {@link ReadonlyRaceState} * can tell the {@link ReadonlyRaceState#getFinishedTime() finished time} and it is possible to receive change notifications for * this value. @@ -74,7 +74,7 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { raceCommitteeWindSource = new WindSourceImpl(WindSourceType.RACECOMMITTEE); } - public void addTo(RaceLog raceLog) { + public void beforeAttaching(RaceLog raceLog) { if (raceLog == null) { logger.severe("Trying to add " + this + " as listener to a null race log for tracked race " + trackedRace.getRace()); } else { @@ -87,12 +87,15 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { trackedRace.setFinishedTime(state.getFinishedTime()); } }); - trackedRace.invalidateStartTime(); - trackedRace.invalidateEndTime(); - analyze(raceLog); } } + public void afterAttaching(RaceLog raceLog) { + trackedRace.invalidateStartTime(); + trackedRace.invalidateEndTime(); + analyze(raceLog); + } + /** * @return the first non-{@code null} {@link ReadonlyRaceState#getFinishedTime() finished time} that is found in any * of the {@link RaceState}s constructed for all the {@link RaceLog}s observed by this listener, or {@code null} @@ -139,7 +142,7 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { } } - public void removeFrom(RaceLog raceLog) { + public void beforeDetaching(RaceLog raceLog) { // Gets called whenever a RaceColumn was already linked to a TrackedRace on linkage. // Maybe we need to reset the status of the old race somehow? There might be no new // TrackedRace to be linked... @@ -147,13 +150,19 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { // TODO: // ??? trackedRace.setStatus(new TrackedRaceStatusImpl(TrackedRaceStatusEnum.PREPARED, 0.0)); if (raceLog != null) { - trackedRace.invalidateStartTime(); - removeAllWindFixesFromWindTrack(raceLog); raceLog.removeListener(this); raceLogs.remove(raceLog); if (markPassingUpdateListener != null) { removeMarkPassingEvents(); } + } + } + + + public void afterDetaching(RaceLog raceLog) { + if (raceLog != null) { + trackedRace.invalidateStartTime(); + removeAllWindFixesFromWindTrack(raceLog); trackedRace.updateMarkPassingsAfterRaceLogChanges(); } } @@ -356,5 +365,4 @@ public class DynamicTrackedRaceLogListener extends BaseRaceLogEventVisitor { trackedRace.invalidateEndTime(); } } - } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index 012947267cc..239fa7dfc9e 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -557,9 +557,9 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl @Override public void raceRemoved(TrackedRace trackedRace) { if (trackedRace == TrackedRaceImpl.this) { - // stop mark passing calculator when tracked race is removed: - markPassingCalculator.stop(); - } + // stop mark passing calculator when tracked race is removed: + markPassingCalculator.stop(); + } } }, /* Not relevant For replication */ Optional.empty(), /* synchronous */ false); } else { @@ -3548,6 +3548,11 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl return new TargetTimeInfoImpl(legTargetTimes); } + @Override + public Duration getTimeSailedSinceRaceStart(Competitor competitor, TimePoint timePoint) { + return getRankingMetric().getActualTimeSinceStartOfRace(competitor, timePoint); + } + @Override public Distance getEstimatedDistanceToComplete(final TimePoint timepoint) throws NotEnoughDataHasBeenAddedException, NoWindException { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/RaceCommitteeAppNotes.html b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/RaceCommitteeAppNotes.html index c77be6d48bc..54054e529a0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/RaceCommitteeAppNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/RaceCommitteeAppNotes.html @@ -4,6 +4,12 @@

    What's New - SAP Sailing Race Manager

    +
    May 2019
    +
      +
    • The Course editor now works correctly when managing more than one regatta. + The last course design and the defined marks are now cached per race group.
    • +
    +
    February 2019
    • Now we consider the start mode flags created in your device configuration diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html index 7935c90bbea..b046b1848c8 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/desktop/places/whatsnew/resources/SailingAnalyticsNotes.html @@ -4,6 +4,13 @@

      What's New - SAP Sailing Analytics

      +
      May 2019
      +
        +
      • Improvements for handicap regattas: when finishing times have been entered, leading to finish line + mark passings, those times will now be considered during calculating the corrected times. This way + it has become possible to fix results even if tracking was incomplete.
      • +
      +
      April 2019
      • Competitor tails on the map can now be colored according to a metric. This enables quick and intuitive diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorWithBoatDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorWithBoatDialog.java index e06dc9f546a..c54c00f5d9c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorWithBoatDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractCompetitorWithBoatDialog.java @@ -4,9 +4,9 @@ import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.SuggestBox; import com.google.gwt.user.client.ui.TextBox; import com.sap.sailing.domain.common.dto.BoatDTO; +import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.domain.common.dto.CompetitorWithBoatDTO; import com.sap.sailing.domain.common.dto.CompetitorWithBoatDTOImpl; -import com.sap.sailing.domain.common.dto.CompetitorDTO; import com.sap.sailing.gwt.common.client.suggestion.BoatClassMasterdataSuggestOracle; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -23,11 +23,13 @@ public abstract class AbstractCompetitorWithBoatDialog extends CompetitorEditDia protected static class CompetitorWithBoatValidator extends CompetitorWithoutBoatValidator { protected final StringMessages stringMessages; + private final BoatDTO originalBoat; - public CompetitorWithBoatValidator(StringMessages stringMessages) { + public CompetitorWithBoatValidator(StringMessages stringMessages, BoatDTO originalBoat) { super(stringMessages); this.stringMessages = stringMessages; + this.originalBoat = originalBoat; } @Override @@ -35,20 +37,46 @@ public abstract class AbstractCompetitorWithBoatDialog extends CompetitorEditDia String result = super.getErrorMessage(competitorToValidate); if (result == null) { BoatDTO boatToValidate = competitorToValidate.getBoat(); - boolean invalidSailId = boatToValidate.getSailId() == null || boatToValidate.getSailId().isEmpty(); + boolean invalidSailId = boatToValidate.getSailId() == null || boatToValidate.getSailId().isEmpty(); boolean invalidName = boatToValidate.getName() == null || boatToValidate.getName().isEmpty(); if (invalidSailId && invalidName) { result = stringMessages.pleaseEnterASailNumberOrABoatName(); } else if (boatToValidate.getColor() != null && boatToValidate.getColor() instanceof InvalidColor) { result = boatToValidate.getColor().getAsHtml(); - } else if (boatToValidate.getBoatClass().getName() == null || boatToValidate.getBoatClass().getName().isEmpty()) { + } else if (boatClassWasChanged(boatToValidate, originalBoat) && isBoatClassInvalid(boatToValidate)) { + // only validate if boat class has changed and the originalBoat actually exists and is not to be + // created here result = stringMessages.pleaseEnterABoatClass(); } } return result; } + + private boolean isBoatClassInvalid(BoatDTO boatToValidate) { + return boatToValidate.getBoatClass().getName() == null || boatToValidate.getBoatClass().getName().isEmpty(); + } + + private boolean boatClassWasChanged(BoatDTO boatToValidate, BoatDTO originalBoat) { + boolean changed = false; + if (boatToValidate.getBoatClass() == null && originalBoat.getBoatClass() != null) { + // boat class has been added + changed = true; + } else if (boatToValidate.getBoatClass() != null && originalBoat.getBoatClass() == null) { + // + changed = true; + } else if (boatToValidate.getBoatClass() == null && originalBoat.getBoatClass() == null) { + // this is a create dialog, the boat class was just created + changed = true; + } else if (originalBoat.getBoatClass().getName() + .equals(boatToValidate.getBoatClass().getName())) { + // boat class exists and stayed the same + changed = false; + } + return changed; + } } + /** * The class creates the UI-dialog to type in the Data about a competitor. * @@ -60,7 +88,10 @@ public abstract class AbstractCompetitorWithBoatDialog extends CompetitorEditDia */ public AbstractCompetitorWithBoatDialog(String dialogTitle, StringMessages stringMessages, CompetitorWithBoatDTO competitorToEdit, DialogCallback callback, String boatClass) { - super(dialogTitle, stringMessages, competitorToEdit, new AbstractCompetitorWithBoatDialog.CompetitorWithBoatValidator(stringMessages), callback); + super(dialogTitle, stringMessages, competitorToEdit, + new AbstractCompetitorWithBoatDialog.CompetitorWithBoatValidator(stringMessages, + competitorToEdit.getBoat()), + callback); final BoatDTO boatToEdit = competitorToEdit.getBoat(); this.boatClassNameTextBox = createSuggestBox(new BoatClassMasterdataSuggestOracle()); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java index 651424a4878..9a2f2ace2be 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java @@ -104,7 +104,7 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint if (Boolean.TRUE.equals(result.isPublic())) { StrippedUserGroupDTO currentTenant = getUserService().getCurrentTenant(); StrippedUserGroupDTO serverTenant = result.getServerDefaultTenant(); - if (!serverTenant.equals(currentTenant)) { + if (!serverTenant.equals(currentTenant) && getUserService().getCurrentUser() != null) { if (getUserService().getCurrentUser().getUserGroups().contains(serverTenant)) { // The current user is in server tenant group and so his default tenant could be changed. if (Window.confirm(getStringMessages().serverIsPublicButTenantIsNotAndCouldBeChanged())) { @@ -385,7 +385,7 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint masterDataImportPanel.ensureDebugId("MasterDataImport"); panel.addToTabPanel(advancedTabPanel, new DefaultRefreshableAdminConsolePanel(masterDataImportPanel), getStringMessages().masterDataImportPanel(), SecuredSecurityTypes.SERVER.getPermissionForObject( - SecuredSecurityTypes.ServerActions.IMPORT_MASTER_DATA, serverInfo)); + SecuredSecurityTypes.ServerActions.CAN_IMPORT_MASTERDATA, serverInfo)); RemoteServerInstancesManagementPanel remoteServerInstancesManagementPanel = new RemoteServerInstancesManagementPanel(getSailingService(), this, getStringMessages()); panel.addToTabPanel(advancedTabPanel, new DefaultRefreshableAdminConsolePanel(remoteServerInstancesManagementPanel), @@ -416,6 +416,7 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint @Override public void refreshAfterBecomingVisible() { userManagementPanel.updateUsers(); + userManagementPanel.refreshSuggests(); } }, getStringMessages().userManagement(), SecuredSecurityTypes.USER.getPermission(DefaultActions.MUTATION_ACTIONS)); @@ -436,6 +437,7 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint @Override public void refreshAfterBecomingVisible() { userGroupManagementPanel.updateUserGroups(); + userGroupManagementPanel.refreshSuggests(); } }, getStringMessages().userGroupManagement(), SecuredSecurityTypes.USER_GROUP.getPermission(DefaultActions.MUTATION_ACTIONS)); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/BoatEditDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/BoatEditDialog.java index a46692cdaaf..5fa9fafd216 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/BoatEditDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/BoatEditDialog.java @@ -50,11 +50,33 @@ public class BoatEditDialog extends DataEntryDialog { result = stringMessages.pleaseEnterASailNumberOrABoatName(); } else if (valueToValidate.getColor() != null && valueToValidate.getColor() instanceof InvalidColor) { result = valueToValidate.getColor().getAsHtml(); - } else if (valueToValidate.getBoatClass().getName() == null || valueToValidate.getBoatClass().getName().isEmpty()) { + } else if (boatClassWasChanged(valueToValidate, boatToEdit) + && (valueToValidate.getBoatClass().getName() == null + || valueToValidate.getBoatClass().getName().isEmpty())) { + // only validate if boat class changed result = stringMessages.pleaseEnterABoatClass(); } return result; } + + private boolean boatClassWasChanged(BoatDTO boatToValidate, BoatDTO originalBoat) { + boolean changed = false; + if (boatToValidate.getBoatClass() == null && originalBoat.getBoatClass() != null) { + // boat class has been added + changed = true; + } else if (boatToValidate.getBoatClass() != null && originalBoat.getBoatClass() == null) { + // + changed = true; + } else if (boatToValidate.getBoatClass() == null && originalBoat.getBoatClass() == null) { + // this is a create dialog, the boat class was just created + changed = true; + } else if (originalBoat.getBoatClass().getName() + .equals(boatToValidate.getBoatClass().getName())) { + // boat class exists and stayed the same + changed = false; + } + return changed; + } }, /* animationEnabled */true, callback); this.ensureDebugId("BoatEditDialog"); this.stringMessages = stringMessages; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java index f429efd69b1..238be6e8bd4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardGroupConfigPanel.java @@ -1062,7 +1062,7 @@ public class LeaderboardGroupConfigPanel extends AbstractRegattaPanel return; } //setup filter value to name from params - groupsFilterablePanel.getTextBox().setValue(nameLeaderBoardGroup); + groupsFilterablePanel.search(nameLeaderBoardGroup); //deselect all leaderboard groups except one which name is from params for (LeaderboardGroupDTO leaderboardGroupDTO : availableLeaderboardGroups) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java index 192af7277d0..200e6ae1daa 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/MediaPanel.java @@ -618,7 +618,7 @@ public class MediaPanel extends FlowPanel implements MediaTracksRefresher { @Override public void ok(final MediaTrack mediaTrack) { - mediaService.addMediaTrack(mediaTrack, new AsyncCallback() { + mediaService.addMediaTrack(mediaTrack, new AsyncCallback() { @Override public void onFailure(Throwable t) { @@ -626,8 +626,8 @@ public class MediaPanel extends FlowPanel implements MediaTracksRefresher { } @Override - public void onSuccess(String dbId) { - mediaTrack.dbId = dbId; + public void onSuccess(MediaTrackWithSecurityDTO mediaTrackWithSecurity) { + mediaTrack.dbId = mediaTrackWithSecurity.dbId; loadMediaTracks(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsManagementPanel.java index 94c741d5fb4..b41c919c03e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ResultImportUrlsManagementPanel.java @@ -120,7 +120,7 @@ public class ResultImportUrlsManagementPanel extends FlowPanel { public void onSuccess(List result) { urlListBox.clear(); for (String s : result) { - urlListBox.addItem(s); + urlListBox.addItem(s, s); } } }); @@ -133,7 +133,7 @@ public class ResultImportUrlsManagementPanel extends FlowPanel { Set toRemove = new HashSet(); for (int i=urlListBox.getItemCount()-1; i>=0; i--) { if (urlListBox.isItemSelected(i)) { - toRemove.add(urlListBox.getItemText(i)); + toRemove.add(urlListBox.getValue(i)); urlListBox.removeItem(i); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index d881cbbac40..ee8149f72fa 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -452,8 +452,7 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat return result; } - private void createRegattas(final Iterable selectedOriginalRegattasFromXRR, - EventDTO newEvent) { + private void createRegattas(final Iterable selectedOriginalRegattasFromXRR, EventDTO newEvent) { eventManagementPanel.fillEvents(); final Set regattaConfigurationsToCreate = new HashSet<>(); for (RegattaDTO originalRegattaFromXRR : selectedOriginalRegattasFromXRR) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java index 0ad778cf001..4de9e3fd461 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingEventManagementPanel.java @@ -80,13 +80,13 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane connectionsTable = new SwissTimingConnectionTableWrapper(userService, sailingService, stringConstants, errorReporter, true, tableResources, () -> { }); - verticalPanel.add(connectionsTable); connectionsTable.refreshSwissTimingConnectionList(); // Add button panel final AccessControlledButtonPanel buttonPanel = new AccessControlledButtonPanel(userService, SecuredDomainType.TRACKED_RACE); verticalPanel.add(buttonPanel); + verticalPanel.add(connectionsTable); buttonPanel.addUnsecuredAction(stringMessages.refresh(), () -> connectionsTable.refreshSwissTimingConnectionList()); // Add SwissTiming Connection @@ -110,6 +110,7 @@ public class SwissTimingEventManagementPanel extends AbstractEventManagementPane @Override public void onSuccess(Void voidResult) { connectionsTable.refreshSwissTimingConnectionList(); + connectionsTable.getFilterField().search(editedConnection.getJsonUrl()); } })); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java index 93c5a76c0f7..e6e8c5ac587 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java @@ -87,12 +87,12 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane errorReporter, true, tableResources, () -> { }); connectionsTable.refreshConnectionList(); - verticalPanel.add(connectionsTable); // create button UI final AccessControlledButtonPanel buttonPanel = createButtonPanel(sailingService, userService, errorReporter, stringMessages); verticalPanel.add(buttonPanel); + verticalPanel.add(connectionsTable); // Table TextColumn regattaNameColumn = new TextColumn() { @@ -270,6 +270,7 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane @Override public void onSuccess(Void voidResult) { connectionsTable.refreshConnectionList(); + connectionsTable.getFilterField().search(editedConnection.getJsonUrl()); } })); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java index 1323abf49cf..f472a1f5501 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TracTracEventManagementPanel.java @@ -97,15 +97,11 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { protected Widget createContent() { VerticalPanel mainPanel = new VerticalPanel(); mainPanel.setWidth("100%"); - CaptionPanel connectionsPanel = createConnectionsPanel(); mainPanel.add(connectionsPanel); - HorizontalPanel racesPanel = createRacesPanel(); racesPanel.setWidth("100%"); - mainPanel.add(racesPanel); - return mainPanel; } @@ -117,20 +113,18 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { connectionsTable = new TracTracConnectionTableWrapper(userService, sailingService, stringMessages, errorReporter, true, tableResources, () -> {}); connectionsTable.refreshTracTracConnectionList(); - connectionsPanel.setContentWidget(tableAndConfigurationPanel); + final Grid grid = new Grid(1, 2); grid.setWidget(0, 0, new Label(stringMessages.racesWithHiddenState() + ":")); final CheckBox showHiddenRacesCheckbox = new CheckBox(stringMessages.show()); showHiddenRacesCheckbox.ensureDebugId("ShowHiddenRacesCheckBox"); grid.setWidget(0, 1, showHiddenRacesCheckbox); - tableAndConfigurationPanel.add(connectionsTable); - tableAndConfigurationPanel.add(grid); // Add TracTrac Connection final AccessControlledButtonPanel buttonPanel = new AccessControlledButtonPanel(userService, SecuredDomainType.TRACKED_RACE); - tableAndConfigurationPanel.add(buttonPanel); buttonPanel.addUnsecuredAction(stringMessages.refresh(), () -> connectionsTable.refreshTracTracConnectionList()); Button addCreateAction = buttonPanel.addCreateAction(stringMessages.addTracTracConnection(), - () -> new EditTracTracConnectionDialog(new TracTracConfigurationWithSecurityDTO(), + () -> new EditTracTracConnectionDialog(new TracTracConfigurationWithSecurityDTO( + userService.getCurrentUser() == null ? null : userService.getCurrentUser().getName()), new DialogCallback() { @Override public void ok(TracTracConfigurationWithSecurityDTO editedConnection) { @@ -148,7 +142,7 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { @Override public void onSuccess(Void voidResult) { - connectionsTable.refreshTracTracConnectionList(); + connectionsTable.refreshTracTracConnectionList(/* selectWhenDone */ editedConnection); } })); } @@ -187,9 +181,16 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { listRacesButton.setEnabled(objectSelected); removeButton.setEnabled(objectSelected); }); + + tableAndConfigurationPanel.add(buttonPanel); + tableAndConfigurationPanel.add(grid); + tableAndConfigurationPanel.add(connectionsTable); + + connectionsPanel.setContentWidget(tableAndConfigurationPanel); + return connectionsPanel; } - + protected HorizontalPanel createRacesPanel() { HorizontalPanel racesPanel = new HorizontalPanel(); CaptionPanel trackableRacesPanel = createTrackableRacesPanel(); @@ -472,25 +473,23 @@ public class TracTracEventManagementPanel extends AbstractEventManagementPanel { List races = TracTracEventManagementPanel.this.raceList.getList(); races.clear(); races.addAll(TracTracEventManagementPanel.this.availableTracTracRaces); - TracTracEventManagementPanel.this.racesFilterablePanel.getTextBox().setText(""); TracTracEventManagementPanel.this.racesTable.setPageSize(races.size()); loadingMessageLabel.setText(""); - // store a successful configuration in the database for later retrieval - sailingService.updateTracTracConfiguration(updatedConnection, - new MarkedAsyncCallback( - new AsyncCallback() { - @Override - public void onFailure(Throwable caught) { - reportError("Exception trying to store configuration in DB: " + caught.getMessage()); - } - - @Override - public void onSuccess(Void voidResult) { - connectionsTable.refreshTracTracConnectionList(); - } - })); + sailingService.updateTracTracConfiguration(updatedConnection, + new MarkedAsyncCallback( + new AsyncCallback() { + @Override + public void onFailure(Throwable caught) { + reportError("Exception trying to store configuration in DB: " + caught.getMessage()); + } + + @Override + public void onSuccess(Void voidResult) { + connectionsTable.refreshTracTracConnectionList(); + } + })); } })); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/multivideo/MultiVideoDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/multivideo/MultiVideoDialog.java index 8238cda8f2a..d7def8399b3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/multivideo/MultiVideoDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/multivideo/MultiVideoDialog.java @@ -43,6 +43,7 @@ import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.domain.common.dto.VideoMetadataDTO; import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.ui.client.MediaServiceAsync; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; @@ -339,10 +340,10 @@ public class MultiVideoDialog extends DialogBox { MediaTrack mediaTrack = new MediaTrack(remoteFile.url, remoteFile.url, remoteFile.startTime.plus(offsetTimeInMS), remoteFile.duration, remoteFile.mime, selectedCandidates); - mediaService.addMediaTrack(mediaTrack, new AsyncCallback() { + mediaService.addMediaTrack(mediaTrack, new AsyncCallback() { @Override - public void onSuccess(String result) { + public void onSuccess(MediaTrackWithSecurityDTO result) { remoteFile.status = EStatus.DONE; remoteFile.isWorking = false; updateUI(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingConnectionTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingConnectionTableWrapper.java index d96cad870ec..1adafbfc4f4 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingConnectionTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/swisstiming/SwissTimingConnectionTableWrapper.java @@ -155,6 +155,9 @@ public class SwissTimingConnectionTableWrapper extends if (t.getPort() != null) { strings.add(t.getPort().toString()); } + if (t.getJsonUrl() != null) { + strings.add(t.getJsonUrl()); + } return strings; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/tractrac/TracTracConnectionTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/tractrac/TracTracConnectionTableWrapper.java index 25d8613d876..29660d5cca8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/tractrac/TracTracConnectionTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/tractrac/TracTracConnectionTableWrapper.java @@ -5,6 +5,7 @@ import static com.sap.sse.security.ui.client.component.AccessControlledActionsCo import java.util.ArrayList; import java.util.List; +import com.google.gwt.core.client.Scheduler; import com.google.gwt.user.cellview.client.AbstractCellTable; import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler; import com.google.gwt.user.cellview.client.TextColumn; @@ -139,6 +140,7 @@ public class TracTracConnectionTableWrapper extends List strings = new ArrayList(); strings.add(t.getName()); strings.add(t.getCreatorName()); + strings.add(t.getJsonUrl()); return strings; } @@ -167,6 +169,15 @@ public class TracTracConnectionTableWrapper extends } public void refreshTracTracConnectionList() { + refreshTracTracConnectionList(/* selectWhenDone */ null); + } + + /** + * @param selectEntityWhenDone + * if not {@code null}, select an entity in the table that the {@link EntityIdentityComparator} considers + * the same entity if present + */ + public void refreshTracTracConnectionList(final TracTracConfigurationWithSecurityDTO selectEntityWhenDone) { sailingServiceAsync .getPreviousTracTracConfigurations(new AsyncCallback>() { @Override @@ -179,6 +190,17 @@ public class TracTracConnectionTableWrapper extends @Override public void onSuccess(List result) { filterField.updateAll(result); + if (selectEntityWhenDone != null) { + Scheduler.get().scheduleDeferred(()->{ + for (final TracTracConfigurationWithSecurityDTO oneResult : result) { + if (getSelectionModel().getEntityIdentityComparator().representSameEntity(oneResult, selectEntityWhenDone)) { + getSelectionModel().setSelected(oneResult, true); + filterField.search(oneResult.getJsonUrl()); + break; + } + } + }); + } } }); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java index 1cfb170489e..b9f3053564c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/DetailTypeFormatter.java @@ -461,9 +461,7 @@ public class DetailTypeFormatter { case RACE_GAP_TO_LEADER_IN_SECONDS: case LEG_ESTIMATED_TIME_TO_NEXT_WAYPOINT_IN_SECONDS: case LEG_CORRECTED_TIME_TRAVELED: - case RACE_CALCULATED_TIME_TRAVELED: case TIME_BETWEEN_RACE_START_AND_COMPETITOR_START: - case RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD: case EXPEDITION_RACE_TIME_TO_BURN_TO_LINE: case EXPEDITION_LEG_TIME_TO_BURN_TO_LINE: case EXPEDITION_RACE_TIME_TO_GUN: @@ -484,6 +482,8 @@ public class DetailTypeFormatter { case RACE_TIME_TRAVELED_DOWNWIND: case RACE_TIME_TRAVELED_REACHING: case RACE_TIME_TRAVELED_UPWIND: + case RACE_CALCULATED_TIME_TRAVELED: + case RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD: case LEG_TIME_TRAVELED: case BRAVOEXTENDED_LEG_CURRENT_DURATION_FOILED_IN_SECONDS: case RACE_CURRENT_DURATION_FOILED_IN_SECONDS: diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/MediaService.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/MediaService.java index 0bd9076d32e..a6c0761cd29 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/MediaService.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/MediaService.java @@ -10,13 +10,13 @@ import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; public interface MediaService extends RemoteService { - Iterable getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier); + Iterable getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier); - Iterable getMediaTracksInTimeRange(RegattaAndRaceIdentifier regattaAndRaceIdentifier); + Iterable getMediaTracksInTimeRange(RegattaAndRaceIdentifier regattaAndRaceIdentifier); Iterable getAllMediaTracks(); - String addMediaTrack(MediaTrack mediaTrack); + MediaTrackWithSecurityDTO addMediaTrack(MediaTrack mediaTrack); void deleteMediaTrack(MediaTrack mediaTrack); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/MediaServiceAsync.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/MediaServiceAsync.java index 83ffc5190fe..42d3dd9b33b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/MediaServiceAsync.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/MediaServiceAsync.java @@ -9,14 +9,14 @@ import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; public interface MediaServiceAsync { void getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier, - AsyncCallback> callback); + AsyncCallback> callback); void getMediaTracksInTimeRange(RegattaAndRaceIdentifier regattaAndRaceIdentifier, - AsyncCallback> callback); + AsyncCallback> callback); void getAllMediaTracks(AsyncCallback> asyncCallback); - void addMediaTrack(MediaTrack mediaTrack, AsyncCallback asyncCallback); + void addMediaTrack(MediaTrack mediaTrack, AsyncCallback asyncCallback); void deleteMediaTrack(MediaTrack mediaTrack, AsyncCallback asyncCallback); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 5a836b6a992..5934b29ae46 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -267,7 +267,7 @@ boatChanges=Boat Changes pairingListTemplate=Pairing list template printHint=Prints the applied version blockedApplyButton=Registered Competitors are unequal to Competitors from Pairinglist! -multiplierInfo=Multipes flights and create them next to each other so that a competition with less boatchanges is possible +multiplierInfo=Multiplies flights and creates them next to each other so that a competition with fewer boat changes is possible noPairingListAvailable=The print function is only available when a pairing list has already been applied to the selected leaderboards race logs. noEventsFound=No events were found noEventSelected=No event selected diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties index 7202c5419b4..b7f87f3120c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_es.properties @@ -526,6 +526,7 @@ longitude=Longitud actionAddEvent=Añadir evento publicationUrl=URL de publicación suppress=Suprimir +isListedOnHomepage=Enumerado en la página de inicio unsuppress=Anular la eliminación suppressedCompetitors=Competidores suprimidos mediaPanel=Audio y vídeo @@ -1026,6 +1027,7 @@ errorGettingIgtimiAuthorizationUrl=Error al obtener URL de autorización de Igti addIgtimiUser=Añadir usuario de Igtimi doYouReallyWantToRemoveLeaderboards=¿Desea realmente eliminar las tablas de clasificación? doYouReallyWantToRemoveNonVisibleLeaderboards=Del mismo modo, está borrando las tablas de clasificación que actualmente no son visibles con el filtro. Se eliminarán las siguientes tablas de clasificación. ¿Desea continuar?\n\n{0} +doYouReallyWantToRemoveLeaderboardGroup=¿Desea realmente eliminar los grupos de la tabla de clasificación "{0}"? doYouReallyWantToRemoveLeaderboardGroups=¿Desea realmente eliminar los grupos de la tabla de clasificación? doYouReallyWantToRemoveNonVisibleLeaderboardGroups=Del mismo modo, está borrando los grupos de la tabla de clasificación que actualmente no son visibles con el filtro. Se eliminarán los siguientes grupos de la tabla de clasificación. ¿Desea continuar?\n\n{0} doYouReallyWantToRemoveTrackedRaces=¿Desea realmente eliminar las carreras rastreadas? @@ -1180,6 +1182,24 @@ controlTrackingFromStartAndFinishTimes=Control de rastreo a partir de la hora de canBoatsOfCompetitorsChangePerRace=¿Pueden modificarse por carrera las embarcaciones de competidores? canBoatsChange=¿Pueden modificarse las embarcaciones? canNotRegisterBoats=Las embarcaciones solo pueden registrarse aquí cuando pueden modificarse por carrera; de otro modo, se registran junto el competidor. +competitorRegistrationType=Tipo de registro de competidor +competitorRegistrationTypeShort=Registro +competitorRegistrationTypeClosed=cerrado +competitorRegistrationTypeOpenModerated=abierto moderado +competitorRegistrationTypeOpenUnmoderated=abierto no moderado +registrationLink=Enlace de registro +registrationLinkConfig=Vista +registrationLinkDialog=Enlace de registro de la regata abierta +registrationLinkDialogQrcode=Código QR +registrationLinkDialogQrcodeExplain=Puede descargar la imagen del código QR y pegarla en su invitación. +registrationLinkSecret=Secreto +registrationLinkSecretExplain=Se añade una cadena secreta al enlace de registro que se requiere para validar una solicitud de registro. Los enlaces públicos y los códigos QR ya no funcionarán si modifica esta cadena. +registrationLinkSecretGenerate=Generar +registrationLinkUrl=URL de enlace de registro +registrationLinkUrlExplain=Puede copiar el URL y pegarlo en su invitación. +registrationLinkUrlCopy=Copiar al portapapeles +registrationLinkShare=Compartir +explainRegistrationLinkDialog=Los competidores de regatas abiertas pueden registrarlas mediante el enlace de registro y el escaneo de código QR siguiente con su smartphone. numberOfQueueMessagesSent=Número de mensajes en cola enviados totalSize=Tamaño total averageMessageSize=Tamaño total por mensaje en bytes @@ -1222,6 +1242,7 @@ enterImageURL=Indicar URL de imagen... enterVideoURL=Indicar URL de vídeo... enterSponsorImageURL=Indicar URL de imagen de patrocinador... enterRaceName=Indicar nombre de carrera... +userGroupManagement=Gestión de grupo de usuarios userManagement=Gestión de usuarios regattaStructureImport=Importación de la estructura de regata filteredBy=Filtrado por @@ -1249,6 +1270,9 @@ noFleetsDefined=Ninguna flota definida successfullyCreatedRegattas=Regatas creadas con éxito errorTryingToRegisterRacesForTracking=Error al intentar registrar carreras {0} para rastreo: {1}. Compruebe sintaxis URI en directo/grabada. errorDeterminingPolarAvailability=Error al determinar la disponibilidad de datos polares / VPP para la carrera {0}: {1} +selectedDetailType=Mostrar métrica +errorLoadingAvailableDetailTypes=Error al cargar las métricas disponibles +tailColor=Color de cola fileStorage=Almacenamiento de archivos active=Activo scoringSchemeHighPointEssOverallDescription=Calificación en puntos. El ganador de una prueba obtiene 10 puntos; el segundo, 9 puntos, etc. Si hay un empate en la serie de Extreme Sailing, el empate se resuelve a favor del competidor que ha ganado más pruebas. Si aún persiste el empate, se utiliza el resultado de la última prueba. @@ -1969,6 +1993,7 @@ errorCalculatingPairingListTemplate=Error al calcular la plantilla de la lista d errorCreatingPairingList=Error al crear la lista de emparejamientos: {0} unexpectedErrorDuringFileImport=Se ha producido un error inesperado durante la carga del fichero. couldNotDownload=No se ha podido descargar el fichero: {0} +roles=Roles sliceRace=Cortar una carrera slicingARaceWasSuccessful=El corte de carrera fue exitoso. enterNameForSlicedRace=Indique un nombre para la carrera a cortar @@ -2111,6 +2136,8 @@ qrCodeUnsecureServerRedirect=El enlace indicado le redireccionará al sitio desc qrCodeUnsecureServerRedirectTitle=Servidor desconocido {0} qrCodeInfoMessage=Escanee este código QR con su dispositivo móvil para continuar con el registro qrCodeErrorMessage=Lo sentimos, esta función no está disponible actualmente. +soundOff=Sin sonido +turnOffAllSoundChannels=Desactive todos los canales de sonido tag=Etiqueta tagButtons=Botones de etiqueta tagPanel=Etiquetas @@ -2196,3 +2223,35 @@ moreLoginInformationSectionSailorProfilesDescription=Después de iniciar sesión moreLoginInformationSectionSailorProfilesHeading=Perfiles de navegante confirmDeletion=Confirmar borrado priorityMustBeANonNegativeNumber=La prioridad debe ser un número entero no negativo +selfServiceServer=El servidor tiene autoservicio +publicServer=El servidor es público +updatedServerSetupError=No se ha podido actualizar la configuración del servidor +updatedServerSetup=Confirmación de servidor actualizada +useExistingUserGroup=Utilizar grupo de usuarios existente +useNewUserGroup=Utilizar grupo de usuarios nuevo +currentGroupOwner=Responsable de grupo actual +migrateCompetitors=Migrar competidores +migrateBoats=Migrar embarcaciones +migrateHierarchyToGroupOwner=Migrar jerarquía a responsable de grupo +serverIsPublicButTenantIsNot=Este servidor está configurado como público, su arrendatario actual no es el servidor. Los objetos que ha creado no serán públicos sin una configuración adicional del servidor. Modifique por defecto el arrendatario mediante el perfil de usuario si desea un escenario de servidor público por defecto. +serverIsPublicButTenantIsNotAndCouldBeChanged=Este servidor está configurado como público, su arrendatario actual no es el servidor. Los objetos que ha creado no serán públicos sin una configuración adicional del servidor. ¿Desea que modifiquemos el arrendatario estándar? +copyMembersAndRoles=Copiar miembros y roles +invalidSecret=Secreto no válido +warningSailInsightVersion=El servidor se ha configurado para el uso con SAP Sail Insight 1. Las regatas abiertas solo son compatibles con SAP Sail Insight 2; se pueden producir problemas de uso. +invalidState=Estado no válido +doYouReallyWantToRemoveDeviceConfiguration=¿Desea realmente eliminar la configuración del dispositivo {0}? +errorFetchingSimulationData=Se ha producido un error al obtener los datos de simulación: {0} +usernameAndPasswordMustBothBeSet=El nombre de usuario y contraseña deben estar completados o en blanco +explainUserName=El nombre de usuario a utilizar para la autenticación en el maestro +explainPassword=La contraseña a utilizar para la autenticación en el maestro +addTracTracConnection=Añadir conexión TracTrac +editTracTracConnection=Tratar conexión TracTrac +filterTracTracConnections=Filtrar conexiones TracTrac +creatorName=Nombre de creador +addSwissTimingAchivedConnection=Añadir conexión SwissTimingArchived +editSwissTimingAchivedConnection=Tratar conexión SwissTimingArchived +filterSwissTimingAchivedConnections=Filtrar conexiones SwissTimingArchived +editSwissTimingConnections=Tratar conexión SwissTiming +filterSwissTimingConnections=Filtrar conexiones SwissTiming +addSwissTimingConnection=Añadir conexión SwissTiming +updateURL=Actualizar URL diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties index fda2f9b1f4f..3ae215520e0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_fr.properties @@ -526,6 +526,7 @@ longitude=Longitude actionAddEvent=Ajouter manifestation publicationUrl=URL de publication suppress=Supprimer +isListedOnHomepage=Est listé sur la page d''accueil unsuppress=Annuler la suppression suppressedCompetitors=Concurrents supprimés mediaPanel=Audio et vidéo @@ -1026,6 +1027,7 @@ errorGettingIgtimiAuthorizationUrl=Erreur lors de l''accès à l''URL d''autoris addIgtimiUser=Ajouter utilisateur Igtimi doYouReallyWantToRemoveLeaderboards=Voulez-vous vraiment supprimer les palmarès ? doYouReallyWantToRemoveNonVisibleLeaderboards=Vous êtes également en train de supprimer des palmarès qui ne sont pas visibles actuellement à cause du filtre. Les palmarès suivants seront supprimés. Voulez-vous poursuivre ?\n\n{0} +doYouReallyWantToRemoveLeaderboardGroup=Voulez-vous vraiment supprimer le groupe de palmarès "{0}" ? doYouReallyWantToRemoveLeaderboardGroups=Voulez-vous vraiment supprimer les groupes de palmarès ? doYouReallyWantToRemoveNonVisibleLeaderboardGroups=Vous êtes également en train de supprimer des groupes de palmarès qui ne sont pas visibles actuellement à cause du filtre. Les groupes de palmarès suivants seront supprimés. Voulez-vous poursuivre ?\n\n{0} doYouReallyWantToRemoveTrackedRaces=Voulez-vous vraiment supprimer les courses suivies ? @@ -1180,6 +1182,24 @@ controlTrackingFromStartAndFinishTimes=Contrôler suivi à partir des heures de canBoatsOfCompetitorsChangePerRace=Les bateaux des concurrents peuvent-ils changer pendant une course canBoatsChange=Les bateaux peuvent-ils changer canNotRegisterBoats=Les bateaux peuvent uniquement être inscrits ici lorsqu''il est possible d''en changer pendant une couse. Sinon, ils sont inscrits avec le concurrent. +competitorRegistrationType=Type d''inscription du concurrent +competitorRegistrationTypeShort=Inscription +competitorRegistrationTypeClosed=clôturée +competitorRegistrationTypeOpenModerated=ouverte et modérée +competitorRegistrationTypeOpenUnmoderated=ouverte et non modérée +registrationLink=Lien d''inscription +registrationLinkConfig=Afficher +registrationLinkDialog=Lien d''inscription pour régate ouverte +registrationLinkDialogQrcode=Code QR +registrationLinkDialogQrcodeExplain=Vous pouvez télécharger l''image du code QR et la coller dans votre invitation. +registrationLinkSecret=Secret +registrationLinkSecretExplain=Une chaîne secrète est ajoutée au lien d''inscription requis pour valider une demande d''inscription. Les liens déjà publics et les codes QR ne fonctionneront plus si vous modifiez ce secret. +registrationLinkSecretGenerate=Générer +registrationLinkUrl=URL du lien d''inscription +registrationLinkUrlExplain=Vous pouvez copier l''URL et la coller dans votre invitation. +registrationLinkUrlCopy=Copier dans le presse-papiers +registrationLinkShare=Partager +explainRegistrationLinkDialog=Les concurrents des régates ouvertes peuvent s''inscrire eux-mêmes en ouvrant le lien d''inscription ou en scannant le code QR ci-dessous avec leur smartphone. numberOfQueueMessagesSent=Nombre de messages en file d''attente envoyés totalSize=Taille totale averageMessageSize=Taille moyenne par message en octets @@ -1222,6 +1242,7 @@ enterImageURL=Saisissez l''URL de l''image... enterVideoURL=Saisissez l''URL de la vidéo... enterSponsorImageURL=Saisissez l''URL de l''image du sponsor... enterRaceName=Saisissez le nom du parcours... +userGroupManagement=Gestion des groupes d''utilisateurs userManagement=Gestion des utilisateurs regattaStructureImport=Importation de la structure de la régate filteredBy=filtré par @@ -1249,6 +1270,9 @@ noFleetsDefined=Aucune flotte définie successfullyCreatedRegattas=Création des régates réussie errorTryingToRegisterRacesForTracking=Erreur lors de la tentative d''enregistrement des courses {0} pour le suivi : {1}. Vérifiez la syntaxe de l''URL stockée/du direct. errorDeterminingPolarAvailability=Erreur lors de la détermination de la disponibilité des données polaires/VPP pour la course {0} : {1} +selectedDetailType=Afficher métrique +errorLoadingAvailableDetailTypes=Erreur lors du chargement des métriques disponibles +tailColor=Couleur du parcours effectué fileStorage=Stockage du fichier active=Actif scoringSchemeHighPointEssOverallDescription=Score en points. Le vainqueur d''un Act totalise 10 points, le 2e - 9 points, etc. En cas d''égalité au classement général des Extreme Sailing Series, la victoire est accordée au concurrent ayant remporté le plus d''Act. Si l''égalité persiste, le résultat obtenu au dernier Act sera utilisé. @@ -1969,6 +1993,7 @@ errorCalculatingPairingListTemplate=Erreur lors du calcul du modèle de la liste errorCreatingPairingList=Erreur lors de la création de la liste d''appariement: {0} unexpectedErrorDuringFileImport=Une erreur inattendue s''est produite lors du chargement du fichier. couldNotDownload=Impossible de télécharger le fichier : {0} +roles=Rôles sliceRace=Segmenter course slicingARaceWasSuccessful=Segmentation de la course réussie enterNameForSlicedRace=Veuillez saisir un nom pour la course à segmenter. @@ -2097,7 +2122,7 @@ swissTimingUpdateUsername=Nom d''utilisateur pour l''envoi des mises à jour swissTimingUpdatePassword=Mot de passe pour l''envoi des mises à jour allowResizing=Autoriser le redimensionnement resizeSuccessfull=Redimensionnement correctement terminé -resizeUnsuccessful=Redimensionnement impossible: {0} +resizeUnsuccessful=Redimensionnement impossible : {0} setUpStorageService=Veuillez d''abord configurer un service de stockage de fichier. automaticResizeNeedsStorageService=Pour pouvoir utiliser le redimensionnement automatique, un service de stockage de fichier doit être configuré. qrCodeBoatInviteTitle=Vous êtes invité à suivre le bateau {0} pour la régate {1}. @@ -2111,6 +2136,8 @@ qrCodeUnsecureServerRedirect=En suivant ce lien, vous allez être redirigé vers qrCodeUnsecureServerRedirectTitle=Serveur inconnu {0} qrCodeInfoMessage=Veuillez scanner ce code QR avec votre appareil mobile pour poursuivre l''inscription. qrCodeErrorMessage=Désolé, cette fonctionnalité n''est pas disponible actuellement. +soundOff=Son désactivé +turnOffAllSoundChannels=Désactiver tous les canaux sonores tag=Balise tagButtons=Boutons de balises tagPanel=Balises @@ -2196,3 +2223,35 @@ moreLoginInformationSectionSailorProfilesDescription=Une fois connecté(e), vous moreLoginInformationSectionSailorProfilesHeading=Profils de sportifs confirmDeletion=Confirmer la suppression priorityMustBeANonNegativeNumber=Pour la priorité, seule un nombre entier non négatif est valide. +selfServiceServer=Le serveur est en libre-service. +publicServer=Le serveur est public. +updatedServerSetupError=Impossible de mettre à jour la configuration du serveur +updatedServerSetup=Configuration du serveur mise à jour +useExistingUserGroup=Utiliser groupe d''utilisateurs existant +useNewUserGroup=Utiliser nouveau groupe d''utilisateurs +currentGroupOwner=Responsable du groupe actuel +migrateCompetitors=Migrer concurrents +migrateBoats=Migrer bateaux +migrateHierarchyToGroupOwner=Migrer hiérarchie vers le responsable du groupe +serverIsPublicButTenantIsNot=Ce serveur est configuré comme étant public, votre locataire ne correspond pas à ceux du serveur. Les objets que vous avez créés ne seront pas publics si le serveur n''est pas davantage configuré. Modifiez votre locataire par défaut via le profil utilisateur si vous souhaitez un scénario de serveur public par défaut. +serverIsPublicButTenantIsNotAndCouldBeChanged=Ce serveur est configuré comme étant public, votre locataire ne correspond pas à ceux du serveur. Les objets que vous avez créés ne seront pas publics si le serveur n''est pas davantage configuré. Devons-nous modifier le locataire par défaut pour vous ? +copyMembersAndRoles=Copier membres et rôles +invalidSecret=Secret non valide +warningSailInsightVersion=Le serveur est configuré pour être utilisé avec SAP Sailing Boat Tracker 1. Les régates ouvertes sont prises en charge uniquement dans SAP Sailing Boat Tracker 2. Cela peut provoquer des problèmes d''utilisation. +invalidState=Statut non valide +doYouReallyWantToRemoveDeviceConfiguration=Voulez-vous vraiment supprimer la configuration de l''appareil "{0}" ? +errorFetchingSimulationData=Une erreur s''est produite lors de l''accès aux données de simulation : {0} +usernameAndPasswordMustBothBeSet=Le nom d''utilisateur et le mot de passe doivent tous les deux être renseignés ou vides. +explainUserName=Nom d''utilisateur à utiliser pour l''authentification dans le système principal +explainPassword=Mot de passe à utiliser pour l''authentification dans le système principal +addTracTracConnection=Ajouter connexion TracTrac +editTracTracConnection=Modifier connexion TracTrac +filterTracTracConnections=Filtrer connexions TracTrac +creatorName=Nom de l''auteur +addSwissTimingAchivedConnection=Ajouter connexion SwissTimingArchived +editSwissTimingAchivedConnection=Modifier connexion SwissTimingArchived +filterSwissTimingAchivedConnections=Filtrer connexions SwissTimingArchived +editSwissTimingConnections=Modifier connexion SwissTiming +filterSwissTimingConnections=Filtrer connexions SwissTiming +addSwissTimingConnection=Ajouter connexion SwissTiming +updateURL=Mettre à jour URL diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties index 385dfe8a518..3c47a985ccf 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties @@ -526,6 +526,7 @@ longitude=経度 actionAddEvent=イベント追加 publicationUrl=公開 URL suppress=非表示 +isListedOnHomepage=ホームページに一覧表示 unsuppress=非表示解除 suppressedCompetitors=非表示競技者 mediaPanel=音声と動画 @@ -1026,6 +1027,7 @@ errorGettingIgtimiAuthorizationUrl=Igtimi 権限 URL 取得でエラー発生: { addIgtimiUser=Igtimi ユーザ追加 doYouReallyWantToRemoveLeaderboards=リーダーボードを削除しますか doYouReallyWantToRemoveNonVisibleLeaderboards=フィルタリングされていて現在表示されていないリーダーボードも削除しようとしています。以下のリーダーボードが削除されます。続行しますか。\n\n{0} +doYouReallyWantToRemoveLeaderboardGroup=リーダーボードグループ "{0}" を削除しますか doYouReallyWantToRemoveLeaderboardGroups=リーダーボードグループを削除しますか doYouReallyWantToRemoveNonVisibleLeaderboardGroups=フィルタリングされていて現在表示されていないリーダーボードグループも削除しようとしています。以下のリーダーボードグループが削除されます。続行しますか。\n\n{0} doYouReallyWantToRemoveTrackedRaces=追跡レースを削除しますか @@ -1180,6 +1182,24 @@ controlTrackingFromStartAndFinishTimes=スタート時刻およびフィニッ canBoatsOfCompetitorsChangePerRace=レースごとに競技者の艇を変更可能 canBoatsChange=艇を変更可能 canNotRegisterBoats=艇はレースごとに変更可能な場合にのみここで登録することができ、可能でない場合は競技者と一緒に登録されます。 +competitorRegistrationType=競技者登録タイプ +competitorRegistrationTypeShort=登録 +competitorRegistrationTypeClosed=終了 +competitorRegistrationTypeOpenModerated=調停済未終了 +competitorRegistrationTypeOpenUnmoderated=未調停未終了 +registrationLink=登録リンク +registrationLinkConfig=表示 +registrationLinkDialog=オープンレガッタの登録リンク +registrationLinkDialogQrcode=QR コード +registrationLinkDialogQrcodeExplain=QR コード画像をダウンロードして、それを招待にペーストできます。 +registrationLinkSecret=シークレット +registrationLinkSecretExplain=シークレット文字列が、登録依頼をチェックするために必要な登録リンクに追加されます。すでにパブリックになっているリンクおよび QR コードは、このシークレットを変更する場合はもう機能しません。 +registrationLinkSecretGenerate=生成 +registrationLinkUrl=登録リンク URL +registrationLinkUrlExplain=URL をコピーして、それを招待にペーストできます。 +registrationLinkUrlCopy=クリップボードにコピー +registrationLinkShare=共有 +explainRegistrationLinkDialog=オープンレガッタの競技者は、登録リンクを開くか、スマートフォンで下記の QR コードをスキャンして、自分自身で登録を行うことができます。 numberOfQueueMessagesSent=送信済キューメッセージ数 totalSize=合計サイズ averageMessageSize=メッセージごとの平均サイズ (バイト) @@ -1222,6 +1242,7 @@ enterImageURL=画像 URL を入力... enterVideoURL=動画 URL を入力... enterSponsorImageURL=スポンサー画像 URL を入力... enterRaceName=レース名を入力... +userGroupManagement=ユーザグループ管理 userManagement=ユーザ管理 regattaStructureImport=レガッタ構成のインポート filteredBy=フィルタキー @@ -1249,6 +1270,9 @@ noFleetsDefined=フリートが定義されていません。 successfullyCreatedRegattas=レガッタが登録されました errorTryingToRegisterRacesForTracking=追跡するレース {0} を登録しようとしてエラーが発生: {1}。ライブ/格納 URI 構文をチェックしてください。 errorDeterminingPolarAvailability=レース {0} のポーラー/VPP データの利用可能性の決定でエラー発生: {1} +selectedDetailType=メトリック表示 +errorLoadingAvailableDetailTypes=利用可能なメトリックのロードでエラー発生 +tailColor=テール色 fileStorage=ファイルストレージ active=有効 scoringSchemeHighPointEssOverallDescription=得点数によるスコアです。アクトの勝者が 10 点、2 位が 9 点などと得点します。Extreme Sailing シリーズ全体で同点となった場合は、より多くのアクトで勝者となった競技者を上位とします。それでも同じ場合には、最終アクトの結果を用います。 @@ -1725,7 +1749,7 @@ sapSailInSight=SAP Sail InSight contentSapSailInSight=iPhone、iPad、および Android フォン向け SAP Sail InSight モバイルアプリにより、ヨット競技者はいつでもどこでもレガッタ参加やパフォーマンスを管理することができます。このアプリは SAP Sailing Analytics ソリューションに接続され、競技者が SAP Sail InSight 対応のレガッタへの受付を行い、自分がどこにいるかを艇速や帆走距離などのパフォーマンス指標とともに追跡し、自らのパフォーマンスを分析することを可能にします。 sapSailInSightReadMore=詳細情報 sapSailingBuoyPinger=SAP Sailing Buoy Pinger -contentSapSailingBuoyPinger=Android フォン向け SAP Sailing Buoy Pinger モバイルアプリにより、ヨットレース運営者はいつでもどこでもレース用のマークの位置決めや、マーク設置担当者とのやり取りを簡略化することができます。このアプリは SAP Sailing Analytics ソリューションに接続され、レガッタ運営者がレース用のマークの地理的位置を追跡することを可能にし、レガッタに参加しているセーラーが SAP Sail InSight モバイルアプリを使用できるようにします。 +contentSapSailingBuoyPinger=Android フォン向け SAP Sailing Buoy Pinger モバイルアプリにより、ヨットレース運営者はいつでもどこでもレース用のマークの位置決めや、マーク設置担当者とのやり取りを簡略化することができます。このアプリは SAP Sailing Analytics ソリューションに接続され、レガッタ運営者がレース用のマークの地理的位置を追跡することを可能にし、レガッタに参加しているセーラーが SAP Sail InSight (SAP Sailing Boat Tracker) モバイルアプリを使用できるようにします。 sapSailingBuoyPingerReadMore=詳細情報 postRaceAnalytics=レース後分析 contentPostRaceAnalytics1=セーリングイベントで収集されたデータへのアクセスにより、戦略の策定や帆走の向上に役立つ、セーラーがそのパフォーマンスを評価および分析する機会が提供されます。 @@ -1969,6 +1993,7 @@ errorCalculatingPairingListTemplate=対戦表テンプレート {0} の計算中 errorCreatingPairingList=対戦表 {0} の作成中にエラーが発生しました unexpectedErrorDuringFileImport=ファイルのアップロード時に予期しないエラーが発生しました。 couldNotDownload={0} ファイルをダウンロードできませんでした。 +roles=ロール sliceRace=レースをスライス slicingARaceWasSuccessful=レースがスライスされました。 enterNameForSlicedRace=スライスするレースの名称を入力してください @@ -2111,6 +2136,8 @@ qrCodeUnsecureServerRedirect=指定されたリンクは不明なサイト {0} qrCodeUnsecureServerRedirectTitle={0} 不明なサーバー qrCodeInfoMessage=モバイルデバイスでこの QR コードをスキャンして登録を続行してください qrCodeErrorMessage=この機能は現在使用できません +soundOff=サウンドオフ +turnOffAllSoundChannels=すべてのサウンドチャネルをオフ tag=タグ tagButtons=タグボタン tagPanel=タグ @@ -2196,3 +2223,35 @@ moreLoginInformationSectionSailorProfilesDescription=ログインすると、セ moreLoginInformationSectionSailorProfilesHeading=セーラープロファイル confirmDeletion=削除確認 priorityMustBeANonNegativeNumber=優先度はマイナスでない整数である必要があります +selfServiceServer=サーバにセルフサービスがあります +publicServer=サーバがパブリックです +updatedServerSetupError=サーバ設定を更新できませんでした +updatedServerSetup=サーバ設定が更新されました +useExistingUserGroup=既存ユーザグループを使用 +useNewUserGroup=既存ユーザグループを使用 +currentGroupOwner=現在のグループオーナー +migrateCompetitors=競技者を移行 +migrateBoats=艇を移行 +migrateHierarchyToGroupOwner=階層をグループオーナーに移行 +serverIsPublicButTenantIsNot=このサーバはパブリックとして設定されており、現在のテナントはサーバのものでありません。ユーザが作成したオブジェクトは追加のサーバ設定なしにはパブリックになりません。デフォルトのパブリックサーバシナリオが必要な場合はユーザプロファイルを介してデフォルトのテナントを変更してください。 +serverIsPublicButTenantIsNotAndCouldBeChanged=このサーバはパブリックとして設定されており、現在のテナントはサーバのものではありません。作成したオブジェクトは追加のサーバ設定なしにはパブリックにはなりません。デフォルトのテナントを変更する必要がありますか。 +copyMembersAndRoles=メンバーとロールをコピー +invalidSecret=無効なシークレット +warningSailInsightVersion=このサーバは SAP Sail Insight 1 で使用するために設定されています。オープンレガッタは SAP Sail Insight 2 でのみサポートされています。これにより使用に問題が生じる可能性があります +invalidState=無効なステータス +doYouReallyWantToRemoveDeviceConfiguration=デバイス設定 {0} を削除しますか +errorFetchingSimulationData=シミュレーションデータのフェッチ時にエラーが発生しました: {0} +usernameAndPasswordMustBothBeSet=ユーザ名とパスワードは両方とも入力するか、両方とも空である必要があります +explainUserName=マスタ上で認証のために使用するユーザ名 +explainPassword=マスタ上で認証のために使用するパスワード +addTracTracConnection=TracTrac 接続の追加 +editTracTracConnection=TracTrac 接続の編集 +filterTracTracConnections=TracTrac 接続のフィルタリング +creatorName=作成者名 +addSwissTimingAchivedConnection=SwissTimingArchived 接続の追加 +editSwissTimingAchivedConnection=SwissTimingArchived 接続の編集 +filterSwissTimingAchivedConnections=SwissTimingArchived 接続のフィルタリング +editSwissTimingConnections=SwissTiming 接続の編集 +filterSwissTimingConnections=SwissTiming 接続のフィルタリング +addSwissTimingConnection=SwissTiming 接続の追加 +updateURL=URL 更新 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties index 334656e9664..15f2e2fb836 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_pt.properties @@ -526,6 +526,7 @@ longitude=Longitude actionAddEvent=Adicionar evento publicationUrl=URL de publicação suppress=Suprimir +isListedOnHomepage=Está listado na home page unsuppress=Anular supressão suppressedCompetitors=Competidores suprimidos mediaPanel=Áudio & Vídeo @@ -1026,6 +1027,7 @@ errorGettingIgtimiAuthorizationUrl=Erro ao obter URL de autorização de Igtimi: addIgtimiUser=Adicionar usuário de Igtimi doYouReallyWantToRemoveLeaderboards=Você tem a certeza que quer remover os painéis de classificação? doYouReallyWantToRemoveNonVisibleLeaderboards=Você está também eliminando painéis de classificação que não estão atualmente visíveis ao filtrar. Os seguintes painéis de classificação serão removidos. Continuar?\n\n{0} +doYouReallyWantToRemoveLeaderboardGroup=Você tem a certeza que quer remover o grupo de painéis de classificação "{0}"? doYouReallyWantToRemoveLeaderboardGroups=Você tem a certeza que quer remover os grupos de painéis de classificação? doYouReallyWantToRemoveNonVisibleLeaderboardGroups=Você está também eliminando grupos de painéis de classificação que não estão atualmente visíveis ao filtrar. Os seguintes grupos de painéis de classificação serão removidos. Continuar?\n\n{0} doYouReallyWantToRemoveTrackedRaces=Você tem a certeza que quer remover as corridas rastreadas? @@ -1180,6 +1182,24 @@ controlTrackingFromStartAndFinishTimes=Rastreamento de controle das horas de par canBoatsOfCompetitorsChangePerRace=Os barcos dos competidores podem mudar por corrida canBoatsChange=Os barcos podem mudar canNotRegisterBoats=Os barcos só podem ser registrados aqui se puderem mudar por corrida, caso contrário, são registrados juntamente com o competidor. +competitorRegistrationType=Tipo de registro de competidor +competitorRegistrationTypeShort=Registro +competitorRegistrationTypeClosed=encerrado +competitorRegistrationTypeOpenModerated=aberto moderado +competitorRegistrationTypeOpenUnmoderated=aberto não moderado +registrationLink=Link de registro +registrationLinkConfig=Visualizar +registrationLinkDialog=Link de registro para regata aberta +registrationLinkDialogQrcode=Código QR +registrationLinkDialogQrcodeExplain=Você pode baixar a imagem do código QR e colá-la em seu convite. +registrationLinkSecret=Chave secreta +registrationLinkSecretExplain=É adicionada uma cadeia secreta ao link de registro, que é necessária para validar uma solicitação de registro. Links já públicos e códigos QR não funcionarão mais se você modificar esta chave secreta. +registrationLinkSecretGenerate=Gerar +registrationLinkUrl=URL de link de registro +registrationLinkUrlExplain=Você pode copiar o URL e colá-lo em seu convite. +registrationLinkUrlCopy=Copiar para o clipboard +registrationLinkShare=Compartilhar +explainRegistrationLinkDialog=Os competidores para regatas abertas podem se registrar a eles próprios abrindo o link de registro ou digitalizando o código QR abaixo com seu smartphone. numberOfQueueMessagesSent=Número de mensagens em fila enviadas totalSize=Tamanho total averageMessageSize=Tamanho médio por mensagem em bytes @@ -1222,6 +1242,7 @@ enterImageURL=Inserir URL de imagem... enterVideoURL=Inserir URL de vídeo... enterSponsorImageURL=Inserir URL de imagem de patrocinador... enterRaceName=Inserir nome da corrida... +userGroupManagement=Administração de grupos de usuários userManagement=Administração de usuários regattaStructureImport=Importação de estrutura da regata filteredBy=filtrado por @@ -1249,6 +1270,9 @@ noFleetsDefined=Nenhuma flotilha definida. successfullyCreatedRegattas=Regatas criadas com êxito errorTryingToRegisterRacesForTracking=Erro ao tentar registrar corridas {0} para rastreamento: {1}. Verificar sintaxe de URI ao vivo/armazenado. errorDeterminingPolarAvailability=Erro ao determinar disponibilidade de dados de carta polar/VPP para corrida {0}: {1} +selectedDetailType=Exibir métrica +errorLoadingAvailableDetailTypes=Erro ao carregar métrica disponível +tailColor=Cor da popa fileStorage=Armazenamento de arquivos active=Ativo scoringSchemeHighPointEssOverallDescription=Pontuação em pontos. O vencedor de uma etapa pontua 10 pontos, o 2º - 9 pontos, .... O desempate na pontuação geral da Extreme Sailing Series é efetuado a favor do competidor com o maior número de vitórias em etapas. Se isso não efetuar o desempate, será utilizado o resultado na última etapa. @@ -1969,6 +1993,7 @@ errorCalculatingPairingListTemplate=Erro ao calcular modelo da lista de pares: { errorCreatingPairingList=Erro ao criar lista de pares: {0} unexpectedErrorDuringFileImport=Ocorreu um erro inesperado durante o carregamento do arquivo. couldNotDownload=Não foi possível baixar o arquivo: {0} +roles=Funções sliceRace=Dividir corrida slicingARaceWasSuccessful=A divisão da corrida foi efetuada com êxito. enterNameForSlicedRace=Inserir um nome para a corrida a ser dividida @@ -2111,6 +2136,8 @@ qrCodeUnsecureServerRedirect=O link indicado redirecionará para o site desconhe qrCodeUnsecureServerRedirectTitle={0} Servidor desconhecido qrCodeInfoMessage=Digitalize este código QR com seu dispositivo móvel para continuar com o registro qrCodeErrorMessage=Esta funcionalidade não está disponível no momento! +soundOff=Som desativado +turnOffAllSoundChannels=Desativar todos os canais de som tag=Etiqueta tagButtons=Botões de etiqueta tagPanel=Etiquetas @@ -2196,3 +2223,35 @@ moreLoginInformationSectionSailorProfilesDescription=Após o logon, você pode a moreLoginInformationSectionSailorProfilesHeading=Perfis de velejadores confirmDeletion=Confirmar eliminação priorityMustBeANonNegativeNumber=A prioridade deve ser um número inteiro não negativo +selfServiceServer=O servidor tem self-service +publicServer=O servidor é público +updatedServerSetupError=Não foi possível atualizar configuração do servidor +updatedServerSetup=Configuração do servidor atualizada +useExistingUserGroup=Utilizar grupo de usuários existente +useNewUserGroup=Utilizar grupo de usuários novo +currentGroupOwner=Proprietário do grupo atual +migrateCompetitors=Migrar competidores +migrateBoats=Migrar barcos +migrateHierarchyToGroupOwner=Migrar hierarquia para proprietário do grupo +serverIsPublicButTenantIsNot=Este servidor está configurado como público, seu inquilino atual não é o do servidor. Os objetos criados por você não serão públicos sem outra configuração do servidor. Modifique seu inquilino padrão mediante o perfil do usuário se pretender um cenário de servidor público padrão. +serverIsPublicButTenantIsNotAndCouldBeChanged=Este servidor está configurado como público, seu inquilino atual não é o do servidor. Os objetos criados por você não serão públicos sem outra configuração do servidor. O inquilino padrão deve ser modificado para você? +copyMembersAndRoles=Copiar membros e funções +invalidSecret=Chave secreta inválida +warningSailInsightVersion=O servidor está configurado para utilização com o SAP Sail Insight 1. As regatas abertas só são suportadas no SAP Sail Insight 2. Isto pode causar problemas na utilização +invalidState=Status inválido +doYouReallyWantToRemoveDeviceConfiguration=Você quer realmente remover a configuração do dispositivo {0}? +errorFetchingSimulationData=Ocorreu um erro ao chamar dados de simulação: {0} +usernameAndPasswordMustBothBeSet=Nome de utilizador e senha devem estar ambos preenchidos ou vazios +explainUserName=Nome de usuário para utilizar para autenticação no mestre +explainPassword=Senha para utilizar para autenticação no mestre +addTracTracConnection=Adicionar conexão TracTrac +editTracTracConnection=Processar conexão TracTrac +filterTracTracConnections=Filtrar conexões TracTrac +creatorName=Nome do autor +addSwissTimingAchivedConnection=Adicionar conexão SwissTimingArchived +editSwissTimingAchivedConnection=Processar conexão SwissTimingArchived +filterSwissTimingAchivedConnections=Filtrar conexões SwissTimingArchived +editSwissTimingConnections=Processar conexão SwissTiming +filterSwissTimingConnections=Filtrar conexões SwissTiming +addSwissTimingConnection=Adicionar conexão SwissTiming +updateURL=Atualizar URL diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index 988b54643c5..0450793c52e 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -526,6 +526,7 @@ longitude=Долгота actionAddEvent=Добавить событие publicationUrl=URL-адрес публикации suppress=Подавлять +isListedOnHomepage=Отображается на начальной странице unsuppress=Отменить подавление suppressedCompetitors=Подавляемые участники mediaPanel=Аудио и видео @@ -1026,6 +1027,7 @@ errorGettingIgtimiAuthorizationUrl=Ошибка при получении URL-а addIgtimiUser=Добавить пользователя Igtimi doYouReallyWantToRemoveLeaderboards=Действительно удалить эти таблицы лидеров? doYouReallyWantToRemoveNonVisibleLeaderboards=Таблицы лидеров, не отображаемые сейчас из-за фильтрации, также удаляются. Будут удалены следующие таблицы лидеров. Продолжить?\n\n{0} +doYouReallyWantToRemoveLeaderboardGroup=Действительно удалить группу таблиц лидеров "{0}"? doYouReallyWantToRemoveLeaderboardGroups=Действительно удалить эти группы таблиц лидеров? doYouReallyWantToRemoveNonVisibleLeaderboardGroups=Группы таблиц лидеров, не отображаемые сейчас из-за фильтрации, также удаляются. Будут удалены следующие группы таблиц лидеров. Продолжить?\n\n{0} doYouReallyWantToRemoveTrackedRaces=Действительно удалить эти отслеживаемые гонки? @@ -1180,6 +1182,24 @@ controlTrackingFromStartAndFinishTimes=Контролировать отслеж canBoatsOfCompetitorsChangePerRace=Участники могут менять лодки в каждой гонке canBoatsChange=Лодки можно менять canNotRegisterBoats=Лодки можно регистрировать здесь, только при наличии возможности менять их в каждой гонке. В противном случае они регистрируются вместе с участником. +competitorRegistrationType=Тип регистрации участника +competitorRegistrationTypeShort=Регистрация +competitorRegistrationTypeClosed=закрыто +competitorRegistrationTypeOpenModerated=открыто с модерацией +competitorRegistrationTypeOpenUnmoderated=открыто без модерации +registrationLink=Ссылка для регистрации +registrationLinkConfig=Просмотр +registrationLinkDialog=Ссылка для регистрации в открытой регате +registrationLinkDialogQrcode=QR-код +registrationLinkDialogQrcodeExplain=Вы можете выгрузить изображение QR-кода и вставить его в приглашение. +registrationLinkSecret=Секрет +registrationLinkSecretExplain=Строка секрета добавляется в ссылку для регистрации и требуется для проверки запроса на регистрацию. Если вы измените этот секрет, то существующие общедоступные ссылки и QR-коды перестанут действовать. +registrationLinkSecretGenerate=Сгенерировать +registrationLinkUrl=URL ссылки для регистрации +registrationLinkUrlExplain=Вы можете скопировать этот URL и вставить его в приглашение. +registrationLinkUrlCopy=Копировать в буфер обмена +registrationLinkShare=Поделиться +explainRegistrationLinkDialog=Участники открытых регат могут зарегистрироваться, открыв ссылку для регистрации или отсканировав QR-код ниже со своего смартфона. numberOfQueueMessagesSent=Число отправленных сообщений очереди totalSize=Общий размер averageMessageSize=Средний размер сообщения в байтах @@ -1222,6 +1242,7 @@ enterImageURL=Ввести URL-адрес изображения... enterVideoURL=Ввести URL-адрес видео... enterSponsorImageURL=Ввести URL-адрес изображения спонсора... enterRaceName=Ввести название гонки... +userGroupManagement=Управление группами пользователей userManagement=Управление пользователями regattaStructureImport=Импорт структуры регаты filteredBy=отфильтровано по @@ -1249,6 +1270,9 @@ noFleetsDefined=Не определены флоты. successfullyCreatedRegattas=Регаты успешно созданы errorTryingToRegisterRacesForTracking=Ошибка при попытке зарегистрировать гонки {0} для отслеживания: {1}. проверьте синтаксис URI оперативных/хранимых данных. errorDeterminingPolarAvailability=Ошибка при определении доступности полярных/VPP данных для гонки {0}: {1} +selectedDetailType=Показать метрику +errorLoadingAvailableDetailTypes=Ошибка при загрузке доступных метрик +tailColor=Цвет хвоста fileStorage=Хранилище файлов active=Активно scoringSchemeHighPointEssOverallDescription=Оценка в баллах. Победитель в акте получает 10 баллов, 2-й — 9 баллов, .... Любое равенство в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. @@ -1969,6 +1993,7 @@ errorCalculatingPairingListTemplate=Ошибка при вычислении ш errorCreatingPairingList=Ошибка при создании списка пар: {0} unexpectedErrorDuringFileImport=При отправке файла возникла непредвиденная ошибка. couldNotDownload=Не удалось загрузить файл: {0} +roles=Роли sliceRace=Сделать срезы гонки slicingARaceWasSuccessful=Срезы гонки сделаны успешно enterNameForSlicedRace=Введите имя для гонки, по которой требуется сделать срезы @@ -2111,6 +2136,8 @@ qrCodeUnsecureServerRedirect=Указанная ссылка открывает qrCodeUnsecureServerRedirectTitle={0} Неизвестный сервер qrCodeInfoMessage=Отсканируйте этот QR-код с помощью мобильного устройства, чтобы продолжить регистрацию qrCodeErrorMessage=Приносим извинения, эта функция сейчас недоступна! +soundOff=Выключить звук +turnOffAllSoundChannels=Выключить все звуковые каналы tag=Тег tagButtons=Кнопки тегирования tagPanel=Теги @@ -2196,3 +2223,35 @@ moreLoginInformationSectionSailorProfilesDescription=После входа в п moreLoginInformationSectionSailorProfilesHeading=Профили участников гонки confirmDeletion=Подтвердить удаление priorityMustBeANonNegativeNumber=Приоритет должен быть не отрицательным целым числом +selfServiceServer=Сервер с самообслуживанием +publicServer=Сервер является общедоступным +updatedServerSetupError=Не удалось обновить настройку сервера +updatedServerSetup=Настройка сервера обновлена +useExistingUserGroup=Используйте существующую группу пользователей +useNewUserGroup=Используйте новую группу пользователей +currentGroupOwner=Владелец текущей группы +migrateCompetitors=Выполнить миграцию участников +migrateBoats=Выполнить миграцию лодок +migrateHierarchyToGroupOwner=Выполнить миграцию иерархии во владельца группы +serverIsPublicButTenantIsNot=Сервер сконфигурирован как общедоступный, ваш текущий тенант не является тенантом сервера. Созданные вами объекты не будут общедоступными без дополнительной настройки конфигурации сервера. Измените тенант по умолчанию через профиль пользователя, если вы хотите использовать сценарий по умолчанию общедоступного сервера. +serverIsPublicButTenantIsNotAndCouldBeChanged=Сервер сконфигурирован как общедоступный, ваш текущий тенант не является тенантом сервера. Созданные вами объекты не будут общедоступными без дополнительной настройки конфигурации сервера. Изменить ваш тенант по умолчанию? +copyMembersAndRoles=Скопировать членов и роли +invalidSecret=Недействительный секрет +warningSailInsightVersion=Сервер сконфигурирован для использования с SAP Sail Insight 1. Открытые регаты поддерживаются только в SAP Sail Insight 2. Это может стать причиной проблем при использовании +invalidState=Недействительный статус +doYouReallyWantToRemoveDeviceConfiguration=Действительно удалить конфигурацию устройства {0}? +errorFetchingSimulationData=Произошла ошибка при вызове данных моделирования: {0} +usernameAndPasswordMustBothBeSet=Имя пользователя и пароль должны быть либо заполненными, либо пустыми +explainUserName=Имя пользователя для аутентификации на главном сервере +explainPassword=Пароль для аутентификации на главном сервере +addTracTracConnection=Добавить соединение TracTrac +editTracTracConnection=Редактировать соединение TracTrac +filterTracTracConnections=Фильтр соединений TracTrac +creatorName=Имя автора +addSwissTimingAchivedConnection=Добавить соединение SwissTimingArchived +editSwissTimingAchivedConnection=Редактировать соединение SwissTimingArchived +filterSwissTimingAchivedConnections=Фильтр соединений SwissTimingArchived +editSwissTimingConnections=Редактировать соединение SwissTiming +filterSwissTimingConnections=Фильтр соединений SwissTiming +addSwissTimingConnection=Добавить соединение SwissTiming +updateURL=Обновить URL diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index 6e6681a16f7..dbeb3ba255f 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -526,6 +526,7 @@ longitude=经度 actionAddEvent=添加活动 publicationUrl=出版 URL suppress=禁止 +isListedOnHomepage=列在主页中 unsuppress=取消禁止 suppressedCompetitors=禁止的参赛队 mediaPanel=音频和视频 @@ -1026,6 +1027,7 @@ errorGettingIgtimiAuthorizationUrl=获取 Igtimi 权限 URL 出错: {0} addIgtimiUser=添加 Igtimi 用户 doYouReallyWantToRemoveLeaderboards=是否确定移除积分榜? doYouReallyWantToRemoveNonVisibleLeaderboards=您还正要删除筛选时目前不可见的积分榜。以下积分榜将移除。是否继续?\n\n{0} +doYouReallyWantToRemoveLeaderboardGroup=是否确定移除积分榜组 "{0}"? doYouReallyWantToRemoveLeaderboardGroups=是否确定移除积分榜组? doYouReallyWantToRemoveNonVisibleLeaderboardGroups=您还正要删除筛选时目前不可见的积分榜组。以下积分榜组将移除。是否继续?\n\n{0} doYouReallyWantToRemoveTrackedRaces=是否确定移除跟踪的比赛轮次? @@ -1180,6 +1182,24 @@ controlTrackingFromStartAndFinishTimes=从开始和结束时间控制跟踪 canBoatsOfCompetitorsChangePerRace=每个比赛轮次可以更改参赛队的船只 canBoatsChange=可以更改船只 canNotRegisterBoats=仅在每个比赛轮次更改船只时,才可以在此处注册船只;否则,与参赛队同时注册。 +competitorRegistrationType=参赛队注册类型 +competitorRegistrationTypeShort=注册 +competitorRegistrationTypeClosed=不公开 +competitorRegistrationTypeOpenModerated=适度公开 +competitorRegistrationTypeOpenUnmoderated=不加节制的公开 +registrationLink=注册链接 +registrationLinkConfig=查看 +registrationLinkDialog=公开赛注册链接 +registrationLinkDialogQrcode=二维码 +registrationLinkDialogQrcodeExplain=您可以下载二维码图像,并将其粘贴到您的邀请中。 +registrationLinkSecret=密钥 +registrationLinkSecretExplain=密钥字符串添加到验证注册申请所需的注册链接。如果您更改此密钥,公共链接和二维码将不再起作用。 +registrationLinkSecretGenerate=生成 +registrationLinkUrl=注册链接 URL +registrationLinkUrlExplain=您可以复制此 URL,并将其粘贴到您的邀请中。 +registrationLinkUrlCopy=复制到剪贴板 +registrationLinkShare=共享 +explainRegistrationLinkDialog=公开赛的参赛队可以通过打开注册链接或使用智能手机扫描下方的二维码来注册。 numberOfQueueMessagesSent=发送的队列消息的数量 totalSize=总大小 averageMessageSize=各消息的平均大小(字节) @@ -1222,6 +1242,7 @@ enterImageURL=输入图像 URL... enterVideoURL=输入视频 URL... enterSponsorImageURL=输入赞助商图像 URL... enterRaceName=输入比赛轮次名称... +userGroupManagement=用户组管理 userManagement=用户管理 regattaStructureImport=比赛结构导入 filteredBy=筛选方式 @@ -1249,6 +1270,9 @@ noFleetsDefined=未定义船队。 successfullyCreatedRegattas=已成功创建比赛 errorTryingToRegisterRacesForTracking=尝试注册比赛轮次{0}用于跟踪出错: {1}。检查实况/存储的 URI 语法。 errorDeterminingPolarAvailability=确定比赛轮次{0}的极坐标/VPP 数据的可用性出错: {1} +selectedDetailType=显示指标 +errorLoadingAvailableDetailTypes=加载可用的指标出错 +tailColor=尾流线颜色 fileStorage=文件存储 active=已激活 scoringSchemeHighPointEssOverallDescription=得分。 分站赛获胜者得 10 分、第 2 名 9 分、.... 在总体极限帆船系列赛中如有平局,以获得最多分站赛名次的参赛队取得胜利。 如果仍有平局,就以最后一个分站赛的名次打破平局。 @@ -1969,6 +1993,7 @@ errorCalculatingPairingListTemplate=计算配对清单模板出错:{0} errorCreatingPairingList=创建配对清单出错:{0} unexpectedErrorDuringFileImport=文件上传期间发生意外错误。 couldNotDownload=无法下载文件:{0} +roles=角色 sliceRace=划分比赛轮次 slicingARaceWasSuccessful=划分比赛轮次成功。 enterNameForSlicedRace=请输入待划分比赛轮次的名称 @@ -2097,7 +2122,7 @@ swissTimingUpdateUsername=用于发送更新的用户名 swissTimingUpdatePassword=用于发送更新的密码 allowResizing=允许重新调整大小 resizeSuccessfull=重新调整大小已成功完成 -resizeUnsuccessful=无法重新调整大小 {0} +resizeUnsuccessful=无法重新调整大小:{0} setUpStorageService=请先设置文件存储服务 automaticResizeNeedsStorageService=自动重新调整大小需要正常运行的 FileStorageService。 qrCodeBoatInviteTitle=您受邀跟踪比赛{1}的船只{0} @@ -2111,6 +2136,8 @@ qrCodeUnsecureServerRedirect=给定的链接将重定向到未知站点{0},请 qrCodeUnsecureServerRedirectTitle={0}未知服务器 qrCodeInfoMessage=请用移动设备扫描此二维码,继续注册 qrCodeErrorMessage=抱歉,此功能目前无法使用! +soundOff=声音关闭 +turnOffAllSoundChannels=关闭所有声道 tag=标记 tagButtons=标记按钮 tagPanel=标记 @@ -2196,3 +2223,35 @@ moreLoginInformationSectionSailorProfilesDescription=登录后,您现在可以 moreLoginInformationSectionSailorProfilesHeading=水手资料 confirmDeletion=确认删除 priorityMustBeANonNegativeNumber=优先级必须是非负整数 +selfServiceServer=服务器具有自助服务 +publicServer=服务器为公共服务器 +updatedServerSetupError=无法更新服务器设置 +updatedServerSetup=服务器设置已更新 +useExistingUserGroup=使用现有用户组 +useNewUserGroup=使用新用户组 +currentGroupOwner=当前组所有者 +migrateCompetitors=迁移参赛队 +migrateBoats=迁移船只 +migrateHierarchyToGroupOwner=迁移层次结构至组所有者 +serverIsPublicButTenantIsNot=此服务器配置为公共服务器,您当前的托管系统不是公共服务器。如果没有进一步的服务器配置,您创建的对象将不会公开。如果需要默认的公共服务器场景,请通过用户参数文件更改默认托管系统。 +serverIsPublicButTenantIsNotAndCouldBeChanged=此服务器配置为公共服务器,您当前的托管系统不是公共服务器。如果没有进一步的服务器配置,您创建的对象将不会公开。是否要为您更改默认托管系统? +copyMembersAndRoles=复制成员和角色 +invalidSecret=密钥无效 +warningSailInsightVersion=此服务器配置为与 SAP Sail Insight 一起使用 1. 仅 SAP Sail Insight 中支持公开赛 2. 这可能会导致使用中出现问题 +invalidState=状态无效 +doYouReallyWantToRemoveDeviceConfiguration=是否确定移除设备配置{0}? +errorFetchingSimulationData=获取模拟数据时发生错误:{0} +usernameAndPasswordMustBothBeSet=用户名和密码必须填写或为空 +explainUserName=用于在主服务器上进行身份验证的用户名 +explainPassword=用于在主服务器上进行身份验证的密码 +addTracTracConnection=添加 TracTrac 连接 +editTracTracConnection=编辑 TracTrac 连接 +filterTracTracConnections=筛选 TracTrac 连接 +creatorName=创建者名称 +addSwissTimingAchivedConnection=添加 SwissTimingArchived 连接 +editSwissTimingAchivedConnection=编辑 SwissTimingArchived 连接 +filterSwissTimingAchivedConnections=筛选 SwissTimingArchived 连接 +editSwissTimingConnections=编辑 SwissTiming 连接 +filterSwissTimingConnections=筛选 SwissTiming 连接 +addSwissTimingConnection=添加 SwissTiming 连接 +updateURL=更新 URL diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/AbstractHtmlMediaPlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/AbstractHtmlMediaPlayer.java index 65174a8db2a..f7b62314c01 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/AbstractHtmlMediaPlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/AbstractHtmlMediaPlayer.java @@ -2,7 +2,7 @@ package com.sap.sailing.gwt.ui.client.media; import com.google.gwt.dom.client.MediaElement; import com.google.gwt.media.client.MediaBase; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.ui.client.media.shared.AbstractMediaPlayer; abstract public class AbstractHtmlMediaPlayer extends AbstractMediaPlayer { @@ -19,7 +19,7 @@ abstract public class AbstractHtmlMediaPlayer extends AbstractMediaPlayer { protected final MediaBase mediaElement; - public AbstractHtmlMediaPlayer(MediaTrack mediaTrack) { + public AbstractHtmlMediaPlayer(MediaTrackWithSecurityDTO mediaTrack) { super(mediaTrack); mediaElement = createMediaElement(); if (mediaElement != null) { @@ -38,12 +38,18 @@ abstract public class AbstractHtmlMediaPlayer extends AbstractMediaPlayer { native void addNativeEventHandlers(MediaElement mediaElement) /*-{ var that = this; - mediaElement.addEventListener('canplay', function() { - that.@com.sap.sailing.gwt.ui.client.media.AbstractHtmlMediaPlayer::initPlayState()(); - }); - mediaElement.addEventListener('timeupdate', function(event) { - that.@com.sap.sailing.gwt.ui.client.media.shared.AbstractMediaPlayer::onMediaTimeUpdate()(); - }); + mediaElement + .addEventListener( + 'canplay', + function() { + that.@com.sap.sailing.gwt.ui.client.media.AbstractHtmlMediaPlayer::initPlayState()(); + }); + mediaElement + .addEventListener( + 'timeupdate', + function(event) { + that.@com.sap.sailing.gwt.ui.client.media.shared.AbstractMediaPlayer::onMediaTimeUpdate()(); + }); }-*/; private void initPlayState() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaManagementControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaManagementControl.java index 5c53d9bbabc..07cbd654bd6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaManagementControl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaManagementControl.java @@ -27,6 +27,7 @@ import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.ui.client.StringMessages; public class MediaManagementControl extends AbstractMediaSelectionControl implements CloseHandler { @@ -44,8 +45,8 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem } public void show() { - Collection videoTracks = new ArrayList<>(); - Collection audioTracks = new ArrayList<>(); + Collection videoTracks = new ArrayList<>(); + Collection audioTracks = new ArrayList<>(); filterGivenMediaTracksTo(videoTracks, audioTracks, mediaPlayerManager.getAssignedMediaTracks()); filterGivenMediaTracksTo(videoTracks, audioTracks, mediaPlayerManager.getOverlappingMediaTracks()); Panel grid = new VerticalPanel(); @@ -80,28 +81,28 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem grid.add(controlButtons); } - private void addVideoTracksToGridPanel(Collection videoTracks, Panel grid) { + private void addVideoTracksToGridPanel(Collection videoTracks, Panel grid) { if (!videoTracks.isEmpty()) { grid.add(new Label(stringMessages.videos())); - for (MediaTrack videoTrack : videoTracks) { + for (MediaTrackWithSecurityDTO videoTrack : videoTracks) { grid.add(createVideoOptions(videoTrack, mediaPlayerManager.getPlayingVideoTracks())); } } } - private void addAudioTracksToGridPanel(Collection audioTracks, Panel grid) { + private void addAudioTracksToGridPanel(Collection audioTracks, Panel grid) { if (!audioTracks.isEmpty()) { grid.add(new Label(stringMessages.audioFiles())); grid.add(createAudioButton(null)); - for (MediaTrack audioTrack : audioTracks) { + for (MediaTrackWithSecurityDTO audioTrack : audioTracks) { grid.add(createAudioButton(audioTrack)); } } } - private void filterGivenMediaTracksTo(Collection videoTracks, - Collection audioTracks, Iterable source) { - for (MediaTrack mediaTrack : source) { + private void filterGivenMediaTracksTo(Collection videoTracks, + Collection audioTracks, Iterable source) { + for (MediaTrackWithSecurityDTO mediaTrack : source) { if (mediaTrack.mimeType == null) { videoTracks.add(mediaTrack); // allow user to remove this strange artifact } else { @@ -119,7 +120,7 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem } } - private Widget createVideoOptions(final MediaTrack videoTrack, Set selectedVideos) { + private Widget createVideoOptions(final MediaTrackWithSecurityDTO videoTrack, Set selectedVideos) { CheckBox videoCheckBox = new CheckBox(videoTrack.title); videoCheckBoxes.put(videoTrack, videoCheckBox); videoCheckBox.setTitle(videoTrack.toString()); @@ -136,7 +137,7 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem } }); - if (mediaPlayerManager.allowsEditing(videoTrack.dbId)) { + if (mediaPlayerManager.allowsEditing(videoTrack)) { HorizontalPanel panel = new HorizontalPanel(); panel.setWidth("100%"); panel.add(videoCheckBox); @@ -156,7 +157,7 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem } - private ToggleButton createConnectButton(final MediaTrack videoTrack) { + private ToggleButton createConnectButton(final MediaTrackWithSecurityDTO videoTrack) { final ToggleButton connectButton = new ToggleButton(); connectButton.setValue(videoTrack.assignedRaces .contains(mediaPlayerManager.getCurrentRace())); @@ -180,7 +181,7 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem videoCheckBox.setEnabled(enable); } - private void disconnectVideoFromRace(final MediaTrack videoTrack, final ToggleButton connectButton) { + private void disconnectVideoFromRace(final MediaTrackWithSecurityDTO videoTrack, final ToggleButton connectButton) { videoTrack.assignedRaces.remove(mediaPlayerManager.getCurrentRace()); mediaPlayerManager.getMediaService().updateRace(videoTrack, new AsyncCallback() { @@ -214,7 +215,7 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem }); } - private Button createDeleteButton(final MediaTrack videoTrack) { + private Button createDeleteButton(final MediaTrackWithSecurityDTO videoTrack) { Button deleteButton = new Button("X", new ClickHandler() { @Override public void onClick(ClickEvent event) { @@ -226,7 +227,7 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem return deleteButton; } - private Widget createAudioButton(final MediaTrack audioTrack) { + private Widget createAudioButton(final MediaTrackWithSecurityDTO audioTrack) { String label = audioTrack != null ? audioTrack.title : stringMessages.soundOff(); String title = audioTrack != null ? audioTrack.toString() : stringMessages.turnOffAllSoundChannels(); RadioButton audioButton = new RadioButton("group-name", label); @@ -236,7 +237,7 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem @Override public void onValueChange(ValueChangeEvent changeEvent) { if (changeEvent.getValue()) { - Set currentlyPlaying = mediaPlayerManager.getPlayingAudioTrack(); + Set currentlyPlaying = mediaPlayerManager.getPlayingAudioTrack(); if (audioTrack == null) { currentlyPlaying.forEach(mediaPlayerManager::closeFloatingPlayer); } else { @@ -246,7 +247,7 @@ public class MediaManagementControl extends AbstractMediaSelectionControl implem } }); - if (mediaPlayerManager.allowsEditing(audioTrack.dbId) && audioTrack != null) { + if (mediaPlayerManager.allowsEditing(audioTrack) && audioTrack != null) { HorizontalPanel panel = new HorizontalPanel(); panel.setWidth("100%"); panel.add(audioButton); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManager.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManager.java index acef53cda1f..03ffdca5e90 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManager.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManager.java @@ -6,6 +6,7 @@ import java.util.Set; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.ui.client.MediaServiceAsync; import com.sap.sse.gwt.client.ErrorReporter; import com.sap.sse.gwt.client.useragent.UserAgentDetails; @@ -38,23 +39,23 @@ public interface MediaPlayerManager { void addPlayerChangeListener(PlayerChangeListener playerChangeListener); - void playAudio(MediaTrack audioTrack); + void playAudio(MediaTrackWithSecurityDTO audioTrack); void muteAudio(); - void playFloatingVideo(MediaTrack videoTrack); + void playFloatingVideo(MediaTrackWithSecurityDTO videoTrack); - void closeFloatingPlayer(MediaTrack videoTrack); + void closeFloatingPlayer(MediaTrackWithSecurityDTO videoTrack); - void playDockedVideo(MediaTrack videoTrack); + void playDockedVideo(MediaTrackWithSecurityDTO videoTrack); void closeDockedVideo(); void addMediaTrack(); - boolean deleteMediaTrack(MediaTrack mediaTrack); + boolean deleteMediaTrack(MediaTrackWithSecurityDTO mediaTrack); - boolean allowsEditing(String mediaTrackDbId); + boolean allowsEditing(MediaTrackWithSecurityDTO mediaTrack); boolean allowsCreating(); @@ -64,13 +65,13 @@ public interface MediaPlayerManager { Boolean isPlaying(); - Set getPlayingAudioTrack(); + Set getPlayingAudioTrack(); Set getPlayingVideoTracks(); - Collection getAssignedMediaTracks(); + Collection getAssignedMediaTracks(); - Collection getOverlappingMediaTracks(); + Collection getOverlappingMediaTracks(); List getAudioTracks(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java index 6364a5e0f37..fbad98ab176 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaPlayerManagerComponent.java @@ -24,7 +24,9 @@ import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; +import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.domain.common.security.SecuredDomainType; import com.sap.sailing.gwt.ui.client.MediaServiceAsync; import com.sap.sailing.gwt.ui.client.RaceTimesInfoProvider; @@ -52,6 +54,7 @@ import com.sap.sse.gwt.client.shared.components.SettingsDialogComponent; import com.sap.sse.gwt.client.shared.settings.ComponentContext; import com.sap.sse.gwt.client.useragent.UserAgentDetails; import com.sap.sse.security.shared.HasPermissions.DefaultActions; +import com.sap.sse.security.shared.dto.SecuredDTO; import com.sap.sse.security.shared.dto.UserDTO; import com.sap.sse.security.ui.client.UserService; import com.sap.sse.security.ui.client.UserStatusEventHandler; @@ -69,8 +72,8 @@ public class MediaPlayerManagerComponent extends AbstractComponent activePlayerContainers = new HashMap(); - private Collection assignedMediaTracks = new ArrayList<>(); - private Collection overlappingMediaTracks = new ArrayList<>(); + private Collection assignedMediaTracks = new ArrayList<>(); + private Collection overlappingMediaTracks = new ArrayList<>(); private Map mediaTrackStatus = new HashMap<>(); private final RegattaAndRaceIdentifier raceIdentifier; @@ -83,6 +86,7 @@ public class MediaPlayerManagerComponent extends AbstractComponent playerChangeListener = new ArrayList<>(); @@ -91,13 +95,15 @@ public class MediaPlayerManagerComponent extends AbstractComponent> getAssignedMediaCallback() { - return new AsyncCallback>() { + private AsyncCallback> getAssignedMediaCallback() { + return new AsyncCallback>() { @Override public void onFailure(Throwable caught) { notifyStateChange(); @@ -266,7 +272,7 @@ public class MediaPlayerManagerComponent extends AbstractComponent mediaTracks) { + public void onSuccess(Iterable mediaTracks) { MediaPlayerManagerComponent.this.assignedMediaTracks.clear(); Util.addAll(mediaTracks, MediaPlayerManagerComponent.this.assignedMediaTracks); for (MediaTrack mediaTrack : MediaPlayerManagerComponent.this.assignedMediaTracks) { @@ -286,8 +292,8 @@ public class MediaPlayerManagerComponent extends AbstractComponent> getOverlappingMediaCallback() { - return new AsyncCallback>() { + private AsyncCallback> getOverlappingMediaCallback() { + return new AsyncCallback>() { @Override public void onFailure(Throwable caught) { notifyStateChange(); @@ -296,7 +302,7 @@ public class MediaPlayerManagerComponent extends AbstractComponent mediaTracks) { + public void onSuccess(Iterable mediaTracks) { MediaPlayerManagerComponent.this.overlappingMediaTracks.clear(); Util.addAll(mediaTracks, MediaPlayerManagerComponent.this.overlappingMediaTracks); for (MediaTrack mediaTrack : MediaPlayerManagerComponent.this.overlappingMediaTracks) { @@ -314,7 +320,7 @@ public class MediaPlayerManagerComponent extends AbstractComponent T createAndWrapVideoPlayer(final MediaTrack videoTrack, VideoContainerFactory videoContainerFactory) { + private T createAndWrapVideoPlayer(final MediaTrackWithSecurityDTO videoTrack, + VideoContainerFactory videoContainerFactory) { final PopoutWindowPlayer.PlayerCloseListener playerCloseListener = new PopoutWindowPlayer.PlayerCloseListener() { private MediaPlayerContainer videoContainer; @@ -408,7 +415,7 @@ public class MediaPlayerManagerComponent extends AbstractComponent() { + new AsyncCallback() { @Override public void onFailure(Throwable t) { @@ -536,8 +543,7 @@ public class MediaPlayerManagerComponent extends AbstractComponent() { @@ -572,15 +578,15 @@ public class MediaPlayerManagerComponent extends AbstractComponent getAssignedMediaTracks() { + public Collection getAssignedMediaTracks() { return Collections.unmodifiableCollection(assignedMediaTracks); } @Override - public Collection getOverlappingMediaTracks() { + public Collection getOverlappingMediaTracks() { removeMediaTracksWhichAreInAssignedMediaTracks(); return Collections.unmodifiableCollection(overlappingMediaTracks); } @@ -711,7 +717,7 @@ public class MediaPlayerManagerComponent extends AbstractComponent getPlayingAudioTrack() { + public Set getPlayingAudioTrack() { return getActiveAudioContainers().stream().map(f -> f.getMediaPlayer().getMediaTrack()) .collect(Collectors.toSet()); } @@ -720,4 +726,8 @@ public class MediaPlayerManagerComponent extends AbstractComponent mediaTracks = new ArrayList<>(mediaPlayerManager.getAssignedMediaTracks()); + final Collection mediaTracks = new ArrayList<>( + mediaPlayerManager.getAssignedMediaTracks()); Panel mediaPanel = new VerticalPanel(); mediaTracks.forEach(track -> mediaPanel.add(createMediaEntry(track))); dialogControl.add(mediaPanel); dialogControl.showRelativeTo(popupLocation); } - private Button createMediaEntry(final MediaTrack mediaTrack) { + private Button createMediaEntry(final MediaTrackWithSecurityDTO mediaTrack) { Button mediaSelectButton = new Button( mediaPlayerManager.getMediaTrackStatus(mediaTrack).toString() + " " + mediaTrack.title); mediaSelectButton.setStyleName("Media-Select-Button"); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaSynchAdapter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaSynchAdapter.java index a99e38bd441..4a7099bea93 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaSynchAdapter.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaSynchAdapter.java @@ -1,6 +1,6 @@ package com.sap.sailing.gwt.ui.client.media; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; public interface MediaSynchAdapter { @@ -20,7 +20,7 @@ public interface MediaSynchAdapter { void updateOffset(); - MediaTrack getMediaTrack(); + MediaTrackWithSecurityDTO getMediaTrack(); void forceAlign(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaSynchControl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaSynchControl.java index 39488edd006..2d1c02c79ca 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaSynchControl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/MediaSynchControl.java @@ -14,7 +14,7 @@ import com.google.gwt.user.client.ui.FocusWidget; import com.google.gwt.user.client.ui.TextBox; import com.google.gwt.user.client.ui.Widget; import com.sap.sailing.domain.common.media.MediaTrack; -import com.sap.sailing.domain.common.security.SecuredDomainType; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.ui.client.MediaServiceAsync; import com.sap.sailing.gwt.ui.client.media.MediaSynchAdapter.EditFlag; import com.sap.sse.gwt.client.ErrorReporter; @@ -247,7 +247,7 @@ public class MediaSynchControl implements EditFlag { } mediaSynchAdapter.setControlsVisible(showEditUI); previewButton.setEnabled(showEditUI); - MediaTrack videoTrack = mediaSynchAdapter.getMediaTrack(); + MediaTrackWithSecurityDTO videoTrack = mediaSynchAdapter.getMediaTrack(); editButton.setEnabled(hasRightToEdit(videoTrack) && !showEditUI); mainPanel.getElement().getStyle() .setDisplay(showEditUI && hasRightToEdit(videoTrack) ? Display.BLOCK : Display.NONE); @@ -256,9 +256,8 @@ public class MediaSynchControl implements EditFlag { discardButton.setEnabled(showEditUI || isDirty); } - private boolean hasRightToEdit(MediaTrack video) { - return userservice.hasPermission( - SecuredDomainType.MEDIA_TRACK.getPermissionForObject(DefaultActions.UPDATE, video)); + private boolean hasRightToEdit(MediaTrackWithSecurityDTO video) { + return userservice.hasPermission(video, DefaultActions.UPDATE); } private boolean isDirty() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/PopoutListener.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/PopoutListener.java index e8042f859b9..c4e96dd7be0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/PopoutListener.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/PopoutListener.java @@ -1,10 +1,10 @@ package com.sap.sailing.gwt.ui.client.media; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; public interface PopoutListener { - void popoutVideo(MediaTrack videoTrack); + void popoutVideo(MediaTrackWithSecurityDTO videoTrack); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSSyncPlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSSyncPlayer.java index a6350155478..3b25cb8960c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSSyncPlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoJSSyncPlayer.java @@ -2,7 +2,7 @@ package com.sap.sailing.gwt.ui.client.media; import com.google.gwt.user.client.ui.RequiresResize; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.ui.client.media.shared.AbstractMediaPlayer; import com.sap.sailing.gwt.ui.client.media.shared.MediaSynchPlayer; import com.sap.sse.common.TimePoint; @@ -16,7 +16,7 @@ public class VideoJSSyncPlayer extends AbstractMediaPlayer implements MediaSynch private final TimePoint raceStartTime; private final Timer raceTimer; - public VideoJSSyncPlayer(MediaTrack mediaTrack, TimePoint raceStartTime, Timer raceTimer) { + public VideoJSSyncPlayer(MediaTrackWithSecurityDTO mediaTrack, TimePoint raceStartTime, Timer raceTimer) { super(mediaTrack); videoJsDelegate = new VideoJSPlayer(true, false); this.raceStartTime = raceStartTime; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoYoutubePlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoYoutubePlayer.java index b33ed10332d..3caedabf3d3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoYoutubePlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/VideoYoutubePlayer.java @@ -9,7 +9,7 @@ import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.Panel; import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.ui.client.media.shared.AbstractMediaPlayer; import com.sap.sailing.gwt.ui.client.media.shared.MediaSynchPlayer; import com.sap.sse.common.TimePoint; @@ -43,7 +43,7 @@ public class VideoYoutubePlayer extends AbstractMediaPlayer implements MediaSync private final List deferredActions = new ArrayList(); - public VideoYoutubePlayer(final MediaTrack videoTrack, TimePoint raceStartTime, Timer raceTimer) { + public VideoYoutubePlayer(final MediaTrackWithSecurityDTO videoTrack, TimePoint raceStartTime, Timer raceTimer) { super(videoTrack); this.raceTimer = raceTimer; this.raceStartTime = raceStartTime; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/PopoutWindowPlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/PopoutWindowPlayer.java index a537da64afa..5603c661773 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/PopoutWindowPlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/PopoutWindowPlayer.java @@ -2,7 +2,7 @@ package com.sap.sailing.gwt.ui.client.media.popup; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.user.client.Window; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.ui.client.media.MediaPlayerContainer; import com.sap.sailing.gwt.ui.client.media.shared.AbstractMediaPlayer; import com.sap.sailing.gwt.ui.client.media.shared.MediaPlayer; @@ -18,7 +18,7 @@ public abstract class PopoutWindowPlayer extends AbstractMediaPlayer implements private final JavaScriptObject playerWindow; private final PlayerCloseListener popupCloseHandler; - protected PopoutWindowPlayer(MediaTrack mediaTrack, PlayerCloseListener popupCloseListener) { + protected PopoutWindowPlayer(MediaTrackWithSecurityDTO mediaTrack, PlayerCloseListener popupCloseListener) { super(mediaTrack); this.popupCloseHandler = popupCloseListener; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/VideoJSWindowPlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/VideoJSWindowPlayer.java index 5f94435ba44..73919cc2c79 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/VideoJSWindowPlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/VideoJSWindowPlayer.java @@ -1,11 +1,11 @@ package com.sap.sailing.gwt.ui.client.media.popup; import com.google.gwt.http.client.URL; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; public class VideoJSWindowPlayer extends PopoutWindowPlayer { - public VideoJSWindowPlayer(MediaTrack mediaTrack, PlayerCloseListener popupCloseListener) { + public VideoJSWindowPlayer(MediaTrackWithSecurityDTO mediaTrack, PlayerCloseListener popupCloseListener) { super(mediaTrack, popupCloseListener); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/YoutubeWindowPlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/YoutubeWindowPlayer.java index cb5102515f5..b90897fd853 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/YoutubeWindowPlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/popup/YoutubeWindowPlayer.java @@ -1,11 +1,11 @@ package com.sap.sailing.gwt.ui.client.media.popup; import com.google.gwt.http.client.URL; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; public class YoutubeWindowPlayer extends PopoutWindowPlayer { - public YoutubeWindowPlayer(MediaTrack mediaTrack, PlayerCloseListener popupCloseListener) { + public YoutubeWindowPlayer(MediaTrackWithSecurityDTO mediaTrack, PlayerCloseListener popupCloseListener) { super(mediaTrack, popupCloseListener); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/shared/AbstractMediaPlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/shared/AbstractMediaPlayer.java index 24fb7d24f59..6fc9be78d9e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/shared/AbstractMediaPlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/shared/AbstractMediaPlayer.java @@ -2,20 +2,20 @@ package com.sap.sailing.gwt.ui.client.media.shared; import java.util.Date; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; public abstract class AbstractMediaPlayer implements MediaPlayer { private static final int TOLERATED_LAG_IN_MILLISECONDS = 4000; - private final MediaTrack mediaTrack; + private final MediaTrackWithSecurityDTO mediaTrack; private long raceTimeInMillis; - protected AbstractMediaPlayer(MediaTrack mediaTrack) { + protected AbstractMediaPlayer(MediaTrackWithSecurityDTO mediaTrack) { this.mediaTrack = mediaTrack; } - public MediaTrack getMediaTrack() { + public MediaTrackWithSecurityDTO getMediaTrack() { return mediaTrack; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/shared/MediaPlayer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/shared/MediaPlayer.java index 0dc7aa496dc..747dadebb75 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/shared/MediaPlayer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/media/shared/MediaPlayer.java @@ -2,7 +2,7 @@ package com.sap.sailing.gwt.ui.client.media.shared; import java.util.Date; -import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; public interface MediaPlayer { @@ -15,7 +15,7 @@ public interface MediaPlayer { void raceTimeChanged(Date raceTime); - MediaTrack getMediaTrack(); + MediaTrackWithSecurityDTO getMediaTrack(); double getDuration(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManeuverMarkersAndLossIndicators.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManeuverMarkersAndLossIndicators.java index c9e99a4ab70..913969b2e86 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManeuverMarkersAndLossIndicators.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/ManeuverMarkersAndLossIndicators.java @@ -165,6 +165,7 @@ public class ManeuverMarkersAndLossIndicators { Marker marker = Marker.newInstance(options); marker.setPosition(latLng); marker.setTitle(ManeuverTypeFormatter.format(maneuver.getType(), stringMessages)); + marker.setZindex(RaceMapOverlaysZIndexes.MANEUVERMARK_ZINDEX); marker.addClickHandler(event -> { LatLng where = raceMap.getCoordinateSystem().toLatLng(maneuver.getPosition()); Widget content = getInfoWindowContent(maneuver, competitor); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapOverlaysZIndexes.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapOverlaysZIndexes.java index 4a3540ccfb8..56a19bc5de5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapOverlaysZIndexes.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/shared/racemap/RaceMapOverlaysZIndexes.java @@ -14,6 +14,7 @@ public interface RaceMapOverlaysZIndexes { public static int WINDSENSOR_ZINDEX = BASE_CANVAS_OVERLAY_ZINDEX + 3; public static int BOATS_ZINDEX = BASE_CANVAS_OVERLAY_ZINDEX + 4; public static int INFO_OVERLAY_ZINDEX = BASE_CANVAS_OVERLAY_ZINDEX + 5; + public static int MANEUVERMARK_ZINDEX = BASE_CANVAS_OVERLAY_ZINDEX + 6; /** the z-index of the info window should always be higher than the rest */ public static int INFO_WINDOW_ZINDEX = BASE_CANVAS_OVERLAY_ZINDEX + 11; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java index 547e5880dbf..e841fe0cdfa 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/LeaderboardPanel.java @@ -1265,17 +1265,17 @@ public abstract class LeaderboardPanel extends A result.put(DetailType.RACE_CURRENT_LEG, new FormattedDoubleLeaderboardRowDTODetailTypeColumn(DetailType.RACE_CURRENT_LEG, new CurrentLeg(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); result.put(DetailType.RACE_TIME_TRAVELED, - new TimeTraveledRaceColumn(getLeaderboardPanel(), this, stringMessages, LEG_COLUMN_HEADER_STYLE, + new TimeTraveledRaceColumnInSeconds(getLeaderboardPanel(), this, stringMessages, LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LEG_DETAIL_COLUMN_HEADER_STYLE, LEG_DETAIL_COLUMN_STYLE)); result.put(DetailType.RACE_CALCULATED_TIME_TRAVELED, - new FormattedDoubleLeaderboardRowDTODetailTypeColumn(DetailType.RACE_CALCULATED_TIME_TRAVELED, + new TotalTimeColumn(DetailType.RACE_CALCULATED_TIME_TRAVELED, new RaceCalculatedTimeTraveledInSeconds(), LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); result.put(DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, - new FormattedDoubleLeaderboardRowDTODetailTypeColumn( - DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, + new TotalTimeColumn(DetailType.RACE_CALCULATED_TIME_AT_ESTIMATED_ARRIVAL_AT_COMPETITOR_FARTHEST_AHEAD, new RaceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds(), - LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, LeaderboardPanel.this)); + LEG_COLUMN_HEADER_STYLE, LEG_COLUMN_STYLE, + LeaderboardPanel.this)); result.put(DetailType.EXPEDITION_RACE_AWA, new FormattedDoubleLeaderboardRowDTODetailTypeColumn(DetailType.EXPEDITION_RACE_AWA, new DoubleTextRaceDetailTypeExtractor(LeaderboardEntryDTO::getExpeditionAWA), @@ -3030,12 +3030,13 @@ public abstract class LeaderboardPanel extends A } /** - * To be expandable, a race column needs to have one or more tracked races associated and needs to have at least - * some GPS data available. Wind data is not required for expandability because several metrics can reasonably be - * determined even without reliable wind information. + * To be expandable, a race column needs to have one or more tracked races associated. No GPS data is required + * (anymore) because there are some interesting columns that may be fed solely based on the Race Log contents, such + * as finishing times and start times, resulting in time sailed and calculated times. Wind data is not required for + * expandability either because several metrics can reasonably be determined even without reliable wind information. */ private boolean shallExpandRaceColumn(RaceColumnDTO raceColumnDTO) { - return showRaceDetails && raceColumnDTO.hasTrackedRaces() && raceColumnDTO.hasGPSData(); + return showRaceDetails && raceColumnDTO.hasTrackedRaces(); } private void removeUnusedRaceColumns(LeaderboardDTO leaderboard) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallTimeTraveledColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallTimeTraveledColumn.java index 955061d4576..32be47e2b1a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallTimeTraveledColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/OverallTimeTraveledColumn.java @@ -50,7 +50,6 @@ public class OverallTimeTraveledColumn extends ExpandableSortableColumn LeaderboardPanel leaderboardPanel, StringMessages stringConstants, String detailHeaderStyle, String detailColumnStyle) { Map> result = new HashMap<>(); - result.put(DetailType.TOTAL_TIME_SAILED_UPWIND_IN_SECONDS, new TotalTimeColumn(DetailType.TOTAL_TIME_SAILED_UPWIND_IN_SECONDS, new TotalTimeSailedUpwindInSeconds(), detailHeaderStyle, detailColumnStyle, leaderboardPanel)); @@ -60,7 +59,6 @@ public class OverallTimeTraveledColumn extends ExpandableSortableColumn result.put(DetailType.TOTAL_TIME_SAILED_REACHING_IN_SECONDS, new TotalTimeColumn(DetailType.TOTAL_TIME_SAILED_REACHING_IN_SECONDS, new TotalTimeSailedReachingInSeconds(), detailHeaderStyle, detailColumnStyle, leaderboardPanel)); - return result; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumnInSeconds.java similarity index 85% rename from java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumn.java rename to java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumnInSeconds.java index 8802fa01d27..9442c5f6bc5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TimeTraveledRaceColumnInSeconds.java @@ -20,7 +20,7 @@ import com.sap.sse.common.InvertibleComparator; import com.sap.sse.common.impl.InvertibleComparatorAdapter; import com.sap.sse.gwt.client.celltable.AbstractSortableColumnWithMinMax; -public class TimeTraveledRaceColumn extends ExpandableSortableColumn implements HasStringAndDoubleValue { +public class TimeTraveledRaceColumnInSeconds extends ExpandableSortableColumn implements HasStringAndDoubleValue { private static final DetailType DETAIL_TYPE = DetailType.RACE_TIME_TRAVELED; @@ -31,7 +31,7 @@ public class TimeTraveledRaceColumn extends ExpandableSortableColumn imp private String headerStyle; private MinMaxRenderer minmaxRenderer; - public TimeTraveledRaceColumn(LeaderboardPanel leaderboardPanel, RaceNameProvider raceNameProvider, StringMessages stringMessages, String headerStyle, String columnStyle, + public TimeTraveledRaceColumnInSeconds(LeaderboardPanel leaderboardPanel, RaceNameProvider raceNameProvider, StringMessages stringMessages, String headerStyle, String columnStyle, String detailHeaderStyle, String detailColumnStyle) { super(leaderboardPanel, /* expandable */true, new TextCell(), DETAIL_TYPE.getDefaultSortingOrder(), stringMessages, detailHeaderStyle, detailColumnStyle, @@ -46,24 +46,17 @@ public class TimeTraveledRaceColumn extends ExpandableSortableColumn imp @Override public Double getDoubleValue(LeaderboardRowDTO row) { - Long result = null; + final Double result; LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); - if (fieldsForRace != null && fieldsForRace.legDetails != null) { - for (LegEntryDTO legDetail : fieldsForRace.legDetails) { - if (legDetail != null) { - if (legDetail.timeInMilliseconds != null) { - if (result == null) { - result = 0l; - } - result += legDetail.timeInMilliseconds; - } - } - } + if (fieldsForRace != null && fieldsForRace.timeSailedSinceRaceStart != null) { + result = fieldsForRace.timeSailedSinceRaceStart.asSeconds(); + } else { + result = null; } - return result == null ? null : new Long(result / 1000).doubleValue(); + return result; } - private Double getTimeTraveledFor(LeaderboardRowDTO row, LegType legType) { + private Double getTimeTraveledForLegTypeInSeconds(LeaderboardRowDTO row, LegType legType) { Long result = null; LeaderboardEntryDTO fieldsForRace = row.fieldsByRaceColumnName.get(getRaceColumnName()); if (fieldsForRace != null && fieldsForRace.legDetails != null) { @@ -101,21 +94,21 @@ public class TimeTraveledRaceColumn extends ExpandableSortableColumn imp private class RaceTimeTraveledUpwindInSeconds implements DataExtractor { @Override public Double get(LeaderboardRowDTO row) { - return getTimeTraveledFor(row, LegType.UPWIND); + return getTimeTraveledForLegTypeInSeconds(row, LegType.UPWIND); } } private class RaceTimeTraveledDownwindInSeconds implements DataExtractor { @Override public Double get(LeaderboardRowDTO row) { - return getTimeTraveledFor(row, LegType.DOWNWIND); + return getTimeTraveledForLegTypeInSeconds(row, LegType.DOWNWIND); } } private class RaceTimeTraveledReachingInSeconds implements DataExtractor { @Override public Double get(LeaderboardRowDTO row) { - return getTimeTraveledFor(row, LegType.REACHING); + return getTimeTraveledForLegTypeInSeconds(row, LegType.REACHING); } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TotalTimeColumn.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TotalTimeColumn.java index c80a6b93e6d..dc1fcc04d3b 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TotalTimeColumn.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboard/TotalTimeColumn.java @@ -12,9 +12,9 @@ import com.sap.sailing.gwt.ui.client.DurationAsHoursMinutesSecondsFormatter; */ public class TotalTimeColumn extends FormattedDoubleLeaderboardRowDTODetailTypeColumn { - protected TotalTimeColumn(DetailType detailType, DataExtractor field, String headerStyle, + protected TotalTimeColumn(DetailType detailType, DataExtractor fieldWithTimeInSeconds, String headerStyle, String columnStyle, DisplayedLeaderboardRowsProvider displayedLeaderboardRowsProvider) { - super(detailType, field, headerStyle, columnStyle, displayedLeaderboardRowsProvider); + super(detailType, fieldWithTimeInSeconds, headerStyle, columnStyle, displayedLeaderboardRowsProvider); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index 51d912d260f..f24d1af6f84 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -43,6 +43,7 @@ import com.sap.sailing.domain.common.dto.RaceColumnDTO; import com.sap.sailing.domain.common.dto.RaceDTO; import com.sap.sailing.domain.common.dto.TagDTO; import com.sap.sailing.domain.common.media.MediaTrack; +import com.sap.sailing.domain.common.media.MediaTrackWithSecurityDTO; import com.sap.sailing.gwt.common.authentication.SailingAuthenticationEntryPointLinkFactory; import com.sap.sailing.gwt.settings.client.leaderboard.SingleRaceLeaderboardSettings; import com.sap.sailing.gwt.settings.client.raceboard.RaceBoardPerspectiveOwnSettings; @@ -458,7 +459,7 @@ public class RaceBoardPanel } mediaPlayerManagerComponent = new MediaPlayerManagerComponent(this, getComponentContext(), mediaPlayerLifecycle, selectedRaceIdentifier, raceTimesInfoProvider, timer, mediaService, userService, stringMessages, - errorReporter, userAgent, this, mediaPlayerSettings); + errorReporter, userAgent, this, mediaPlayerSettings, raceDTO); final LeaderboardWithSecurityFetcher asyncFetcher = new LeaderboardWithSecurityFetcher() { @Override @@ -511,7 +512,7 @@ public class RaceBoardPanel protected void updateRaceTimePanelOverlay() { ArrayList overlays = new ArrayList(); Set videoPlaying = mediaPlayerManagerComponent.getPlayingVideoTracks(); - Set audioPlaying = mediaPlayerManagerComponent.getPlayingAudioTrack(); + Set audioPlaying = mediaPlayerManagerComponent.getPlayingAudioTrack(); for (MediaTrack track : mediaPlayerManagerComponent.getAssignedMediaTracks()) { double start = track.startTime.asMillis(); // do not show bars for very short videos but show for live streaming ones diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java index 7eb5c534cc4..834682f953a 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/SideBySideComponentViewer.java @@ -411,11 +411,10 @@ public class SideBySideComponentViewer implements UserStatusEventHandler { private boolean isMediaManagementVisible(final MediaPlayerManagerComponent mediaPlayerManagerComponent) { boolean canCreateNew = userService.hasCurrentUserPermissionToCreateObjectOfType(SecuredDomainType.MEDIA_TRACK); - boolean canUpdateRace = userService.hasPermission(SecuredDomainType.TRACKED_RACE - .getPermissionForTypeRelativeIdentifier(DefaultActions.UPDATE, mediaPlayerManagerComponent - .getCurrentRace().getIdentifier().getTypeRelativeObjectIdentifier())); + boolean canUpdateRace = userService.hasPermission(mediaPlayerManagerComponent.getCurrentRaceDTO(), + DefaultActions.UPDATE); boolean canDirectlyUpdateAnyExistingTrack = mediaPlayerManagerComponent.getAssignedMediaTracks().stream() - .anyMatch(track -> mediaPlayerManagerComponent.allowsEditing(track.dbId)); + .anyMatch(track -> mediaPlayerManagerComponent.allowsEditing(track)); return canCreateNew && canUpdateRace || canDirectlyUpdateAnyExistingTrack; } } \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/StartAnalysisMode.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/StartAnalysisMode.java index e7bae85ba26..894c7af36c9 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/StartAnalysisMode.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/StartAnalysisMode.java @@ -184,6 +184,7 @@ public class StartAnalysisMode extends RaceBoardModeWithPerRaceCompetitors { raceDetailsToShow.add(DetailType.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_PASSING_START_IN_METERS); raceDetailsToShow.add(DetailType.SPEED_OVER_GROUND_AT_RACE_START); raceDetailsToShow.add(DetailType.SPEED_OVER_GROUND_WHEN_PASSING_START); + raceDetailsToShow.add(DetailType.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START); raceDetailsToShow.add(DetailType.START_TACK); raceDetailsToShow.add(DetailType.RACE_GAP_TO_LEADER_IN_SECONDS); final SingleRaceLeaderboardSettings additiveSettings = SingleRaceLeaderboardSettings diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/MediaServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/MediaServiceImpl.java index 59509aac059..a326149bee5 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/MediaServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/MediaServiceImpl.java @@ -82,13 +82,29 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi } @Override - public Iterable getMediaTracksForRace(RegattaAndRaceIdentifier regattaAndRaceIdentifier) { - return racingEventService().getMediaTracksForRace(regattaAndRaceIdentifier); + public Iterable getMediaTracksForRace( + RegattaAndRaceIdentifier regattaAndRaceIdentifier) { + Collection mediaTracks = new ArrayList<>(); + for (MediaTrack mediaTrack : racingEventService().getMediaTracksForRace(regattaAndRaceIdentifier)) { + MediaTrackWithSecurityDTO securedMediaTrack = new MediaTrackWithSecurityDTO(mediaTrack); + SecurityDTOUtil.addSecurityInformation(racingEventService().getSecurityService(), securedMediaTrack, + mediaTrack.getIdentifier()); + mediaTracks.add(securedMediaTrack); + } + return mediaTracks; } @Override - public Iterable getMediaTracksInTimeRange(RegattaAndRaceIdentifier regattaAndRaceIdentifier) { - return racingEventService().getMediaTracksInTimeRange(regattaAndRaceIdentifier); + public Iterable getMediaTracksInTimeRange( + RegattaAndRaceIdentifier regattaAndRaceIdentifier) { + Collection mediaTracks = new ArrayList<>(); + for (MediaTrack mediaTrack : racingEventService().getMediaTracksInTimeRange(regattaAndRaceIdentifier)) { + MediaTrackWithSecurityDTO securedMediaTrack = new MediaTrackWithSecurityDTO(mediaTrack); + SecurityDTOUtil.addSecurityInformation(racingEventService().getSecurityService(), securedMediaTrack, + mediaTrack.getIdentifier()); + mediaTracks.add(securedMediaTrack); + } + return mediaTracks; } @Override @@ -104,7 +120,7 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi } @Override - public String addMediaTrack(MediaTrack mediaTrack) { + public MediaTrackWithSecurityDTO addMediaTrack(MediaTrack mediaTrack) { SecurityUtils.getSubject().checkPermission(SecuredDomainType.MEDIA_TRACK.getStringPermission(DefaultActions.CREATE)); if (mediaTrack.dbId != null) { throw new IllegalStateException("Property dbId must not be null for newly created media track."); @@ -112,7 +128,10 @@ public class MediaServiceImpl extends RemoteServiceServlet implements MediaServi racingEventService().mediaTrackAdded(mediaTrack); SecurityService securityService = racingEventService().getSecurityService(); securityService.setDefaultOwnershipIfNotSet(mediaTrack.getIdentifier()); - return mediaTrack.dbId; + MediaTrackWithSecurityDTO mediaTrackWithSecurity = new MediaTrackWithSecurityDTO(mediaTrack); + SecurityDTOUtil.addSecurityInformation(racingEventService().getSecurityService(), mediaTrackWithSecurity, + mediaTrackWithSecurity.getIdentifier()); + return mediaTrackWithSecurity; } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index b21c264d134..bc7d7da738d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -2382,7 +2382,6 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet public CoursePositionsDTO getCoursePositions(RegattaAndRaceIdentifier raceIdentifier, Date date) { CoursePositionsDTO result = new CoursePositionsDTO(); TrackedRace trackedRace = getExistingTrackedRace(raceIdentifier); - getSecurityService().checkCurrentUserReadPermission(trackedRace); if (trackedRace != null) { getSecurityService().checkCurrentUserReadPermission(trackedRace); final TimePoint dateAsTimePoint; @@ -5428,9 +5427,7 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet List regattaDTOs = new ArrayList(); Iterable regattas = structureImporter.getRegattas(parsedEvent); for (Regatta regatta : regattas) { - if (getSecurityService().hasCurrentUserReadPermission(regatta)) { - regattaDTOs.add(convertToRegattaDTO(regatta)); - } + regattaDTOs.add(convertToRegattaDTO(regatta)); } return regattaDTOs; } @@ -5946,18 +5943,13 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet final DynamicBoat boat = (DynamicBoat) addOrUpdateBoatInternal(competitor.getBoat()); result = getSecurityService().setOwnershipCheckPermissionForObjectCreationAndRevertOnError( SecuredDomainType.COMPETITOR, CompetitorImpl.getTypeRelativeObjectIdentifier(competitorUUID), - competitor.getName(), new Callable() { - @Override - public CompetitorWithBoat call() throws Exception { - return getBaseDomainFactory().getCompetitorAndBoatStore().getOrCreateCompetitorWithBoat( + competitor.getName(), ()->getBaseDomainFactory().getCompetitorAndBoatStore().getOrCreateCompetitorWithBoat( competitorUUID, competitor.getName(), competitor.getShortName(), competitor.getColor(), competitor.getEmail(), competitor.getFlagImageURL() == null ? null : new URI(competitor.getFlagImageURL()), team, competitor.getTimeOnTimeFactor(), competitor.getTimeOnDistanceAllowancePerNauticalMile(), competitor.getSearchTag(), - boat); - } - }); + boat)); } else { SecurityUtils.getSubject().checkPermission(SecuredDomainType.COMPETITOR.getStringPermissionForTypeRelativeIdentifier( DefaultActions.UPDATE, CompetitorImpl.getTypeRelativeObjectIdentifier(competitor.getId()))); @@ -5990,17 +5982,11 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet DynamicTeam team = new TeamImpl(competitor.getName() + " team", Collections.singleton(sailor), null); result = getSecurityService().setOwnershipCheckPermissionForObjectCreationAndRevertOnError( SecuredDomainType.COMPETITOR, CompetitorImpl.getTypeRelativeObjectIdentifier(competitorUUID), - competitor.getName(), new Callable() { - - @Override - public Competitor call() throws Exception { - return getBaseDomainFactory().getOrCreateCompetitor(competitorUUID, competitor.getName(), + competitor.getName(), ()->getBaseDomainFactory().getOrCreateCompetitor(competitorUUID, competitor.getName(), competitor.getShortName(), competitor.getColor(), competitor.getEmail(), competitor.getFlagImageURL() == null ? null : new URI(competitor.getFlagImageURL()), team, competitor.getTimeOnTimeFactor(), - competitor.getTimeOnDistanceAllowancePerNauticalMile(), competitor.getSearchTag()); - } - }); + competitor.getTimeOnDistanceAllowancePerNauticalMile(), competitor.getSearchTag())); } else { SecurityUtils.getSubject().checkPermission( SecuredDomainType.COMPETITOR.getStringPermissionForTypeRelativeIdentifier(DefaultActions.UPDATE, @@ -6031,7 +6017,31 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet public List addCompetitors(List competitorDescriptors, String searchTag) throws URISyntaxException { List competitorsForSaving = new ArrayList<>(); for (final CompetitorDescriptor competitorDescriptor : competitorDescriptors) { - competitorsForSaving.add(getService().convertCompetitorDescriptorToCompetitorWithBoat(competitorDescriptor, searchTag)); + final Action action = ()->competitorsForSaving.add(getService().convertCompetitorDescriptorToCompetitorWithBoat(competitorDescriptor, searchTag)); + final Boat existingBoat = getService().getCompetitorAndBoatStore().getExistingBoatById(competitorDescriptor.getBoatUUID()); + final Action actionIncludingBoatSecurityCheck; + if (existingBoat == null) { + actionIncludingBoatSecurityCheck = ()->getSecurityService().setOwnershipCheckPermissionForObjectCreationAndRevertOnError( + SecuredDomainType.BOAT, BoatImpl.getTypeRelativeObjectIdentifier(competitorDescriptor.getBoatUUID()), competitorDescriptor.getBoatName(), + action); + } else { + actionIncludingBoatSecurityCheck = action; + } + final Competitor existingCompetitor = getService().getCompetitorAndBoatStore().getExistingCompetitorById(competitorDescriptor.getCompetitorUUID()); + final Action actionIncludingCompetitorAndBoatSecurityCheck; + if (existingCompetitor == null) { + actionIncludingCompetitorAndBoatSecurityCheck = ()->getSecurityService().setOwnershipCheckPermissionForObjectCreationAndRevertOnError( + SecuredDomainType.COMPETITOR, + CompetitorImpl.getTypeRelativeObjectIdentifier(competitorDescriptor.getCompetitorUUID()), competitorDescriptor.getName(), + actionIncludingBoatSecurityCheck); + } else { + actionIncludingCompetitorAndBoatSecurityCheck = actionIncludingBoatSecurityCheck; + } + try { + actionIncludingCompetitorAndBoatSecurityCheck.run(); + } catch (Exception e) { + throw new RuntimeException(e); // this can onlyhave been a RuntimeException in the first place because nothing of the above throws a checked one + } } getBaseDomainFactory().getCompetitorAndBoatStore().addNewCompetitorsWithBoat(competitorsForSaving); return convertToCompetitorWithBoatDTOs(competitorsForSaving); @@ -6080,14 +6090,8 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet UUID boatUUID = UUID.randomUUID(); BoatClass boatClass = getBaseDomainFactory().getOrCreateBoatClass(boat.getBoatClass().getName()); result = getSecurityService().setOwnershipCheckPermissionForObjectCreationAndRevertOnError( - SecuredDomainType.BOAT, BoatImpl.getTypeRelativeObjectIdentifier(boatUUID), boat.getName(), new Callable() { - - @Override - public Boat call() throws Exception { - return getBaseDomainFactory().getOrCreateBoat(boatUUID, boat.getName(), boatClass, - boat.getSailId(), boat.getColor()); - } - }); + SecuredDomainType.BOAT, BoatImpl.getTypeRelativeObjectIdentifier(boatUUID), boat.getName(), + () -> getBaseDomainFactory().getOrCreateBoat(boatUUID, boat.getName(), boatClass, boat.getSailId(), boat.getColor())); } else { SecurityUtils.getSubject().checkPermission( SecuredDomainType.BOAT.getStringPermissionForTypeRelativeIdentifier(DefaultActions.UPDATE, @@ -7611,15 +7615,13 @@ public class SailingServiceImpl extends ResultCachingProxiedRemoteServiceServlet for (CompetitorDTO c : competitorDtos) { competitors.add(getCompetitor(c)); } - MailInvitationType type = MailInvitationType - .valueOf(System.getProperty(MAILTYPE_PROPERTY, MailInvitationType.SailInsight1.name())); Leaderboard leaderboard = getService().getLeaderboardByName(leaderboardName); getSecurityService().checkCurrentUserUpdatePermission(leaderboard); Regatta regatta = getService().getRegattaByName(leaderboardName); getSecurityService().checkCurrentUserUpdatePermission(regatta); getRaceLogTrackingAdapter().inviteCompetitorsForTrackingViaEmail(event, leaderboard, regatta, serverUrlWithoutTrailingSlash, - competitors, iOSAppUrl, androidAppUrl, getLocale(localeInfoName), type); + competitors, iOSAppUrl, androidAppUrl, getLocale(localeInfoName), getMailType()); } @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/TracTracConfigurationWithSecurityDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/TracTracConfigurationWithSecurityDTO.java index 25e6fd35419..03aad78d061 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/TracTracConfigurationWithSecurityDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/TracTracConfigurationWithSecurityDTO.java @@ -26,6 +26,10 @@ public class TracTracConfigurationWithSecurityDTO implements IsSerializable, Sec public TracTracConfigurationWithSecurityDTO() { } + + public TracTracConfigurationWithSecurityDTO(String creatorName) { + this.creatorName = creatorName; + } public TracTracConfigurationWithSecurityDTO(String name, String jsonUrl, String liveDataURI, String storedDataURI, String courseDesignUpdateUrl, String tractracUsername, String tractracPassword, String creatorName) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/BoatMarkVectorGraphics.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/BoatMarkVectorGraphics.java index 8068496973a..ff8fc2174b0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/BoatMarkVectorGraphics.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/BoatMarkVectorGraphics.java @@ -196,10 +196,17 @@ public class BoatMarkVectorGraphics extends AbstractMarkVectorGraphics { private Bearing bearingOrReverseDependingOnXTEToOther(Position pos, Bearing bearing, Position other) { final Bearing result; - if (pos.crossTrackError(other, bearing).compareTo(Distance.NULL) > 0) { - result = bearing.reverse(); + if (bearing == null) { + // no start line bearing is known; assume that the start boat is on the + // starboard side of the line and that the line is approximately + // perpendicular to the bearing to the next mark: + result = pos.getBearingGreatCircle(other).add(new DegreeBearingImpl(-90)); } else { - result = bearing; + if (pos.crossTrackError(other, bearing).compareTo(Distance.NULL) > 0) { + result = bearing.reverse(); + } else { + result = bearing; + } } return result; } diff --git a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java index 2dae30bdcdd..b14b6022dfa 100755 --- a/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java +++ b/java/com.sap.sailing.polars.test/src/com/sap/sailing/polars/windestimation/TestWindEstimationFromManeuversOnAFew505Races.java @@ -90,7 +90,7 @@ public class TestWindEstimationFromManeuversOnAFew505Races extends OnlineTracTra CreateModelException, NotEnoughDataHasBeenAddedException { setUp("event_20110609_KielerWoch-505_Race_2"); Wind average = getManeuverBasedAverageWind(); - assertEquals(235, average.getFrom().getDegrees(), 5.0); + assertEquals(250, average.getFrom().getDegrees(), 10.0); } @Test @@ -108,7 +108,7 @@ public class TestWindEstimationFromManeuversOnAFew505Races extends OnlineTracTra CreateModelException, NotEnoughDataHasBeenAddedException { setUp("event_20110609_KielerWoch-505_race_4"); Wind average = getManeuverBasedAverageWind(); - assertEquals(265, average.getFrom().getDegrees(), 9.0); + assertEquals(265, average.getFrom().getDegrees(), 10.0); } @Test diff --git a/java/com.sap.sailing.selenium.test/com.sap.sailing.selenium.test (Proxy).launch b/java/com.sap.sailing.selenium.test/com.sap.sailing.selenium.test (Proxy).launch deleted file mode 100644 index 3cd0d9e57aa..00000000000 --- a/java/com.sap.sailing.selenium.test/com.sap.sailing.selenium.test (Proxy).launch +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/test/raceboard/SettingsTest.java b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/test/raceboard/SettingsTest.java index 8fbb5972571..f6cfd5937cf 100644 --- a/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/test/raceboard/SettingsTest.java +++ b/java/com.sap.sailing.selenium.test/src/com/sap/sailing/selenium/test/raceboard/SettingsTest.java @@ -161,7 +161,6 @@ public class SettingsTest extends AbstractSeleniumTest { leaderboardSettingsPanelPO.selectDetailsAndDeselectOther(detailsToSelect); leaderboardSettingsPanelPO.setRefreshInterval(1); leaderboardSettingsDialog.pressOk(false, false); - mapSettings = raceboard.openMapSettings(); // verify default mode settings override custom user settings Assert.assertTrue(mapSettings.isWindUp()); @@ -171,12 +170,9 @@ public class SettingsTest extends AbstractSeleniumTest { Assert.assertFalse(mapSettings.isTransparentHoverlines()); mapSettings.setWindUp(false); mapSettings.setTransparentHoverlines(false); - mapSettings.pressOk(false, false); - raceboard = RaceBoardPage.goToRaceboardUrl(getWebDriver(), getContextRoot(), BMW_CUP_REGATTA, BMW_CUP_REGATTA, String.format(BMW_RACE, 1), "WINNING_LANES"); - leaderboardSettingsDialog = raceboard.openLeaderboardSettingsDialog(); leaderboardSettingsPanelPO = leaderboardSettingsDialog.getLeaderboardSettingsPanelPO(); selectedDetails = leaderboardSettingsPanelPO.getSelectedDetails(); @@ -185,38 +181,31 @@ public class SettingsTest extends AbstractSeleniumTest { Assert.assertArrayEquals(detailsToSelect, selectedDetails); // verify that document settings are able to override custom user settings by a system default value Assert.assertEquals(1, leaderboardSettingsPanelPO.getRefreshInterval()); - leaderboardSettingsDialog.pressCancel(); - mapSettings = raceboard.openMapSettings(); // Verify that mode settings are overridden by document settings Assert.assertFalse(mapSettings.isWindUp()); // Verify custom user settings are independent for modes Assert.assertFalse(mapSettings.isTransparentHoverlines()); - - //FIXME uncomment when START_ANALYSIS mode can be handled by remote CI server - // verify that custom document settings override mode settings of other modes + // verify that custom document settings override mode settings of other modes raceboard = RaceBoardPage.goToRaceboardUrl(getWebDriver(), getContextRoot(), BMW_CUP_REGATTA, BMW_CUP_REGATTA, String.format(BMW_RACE, 1), "START_ANALYSIS"); leaderboardSettingsDialog = raceboard.openLeaderboardSettingsDialog(); leaderboardSettingsPanelPO = leaderboardSettingsDialog.getLeaderboardSettingsPanelPO(); selectedDetails = leaderboardSettingsPanelPO.getSelectedDetails(); - detailsToSelect = new DetailCheckboxInfo[] { DetailCheckboxInfo.RACE_GAP_TO_LEADER, // start analysis mode DetailCheckboxInfo.RACE_DISTANCE_TO_START_FIVE_SECONDS_BEFORE_START, // start analysis mode DetailCheckboxInfo.RACE_SPEED_OVER_GROUND_FIVE_SECONDS_BEFORE_START, // start analysis mode DetailCheckboxInfo.DISTANCE_TO_START_AT_RACE_START, // start analysis mode + DetailCheckboxInfo.TIME_BETWEEN_RACE_START_AND_COMPETITOR_START, // start analysis mode DetailCheckboxInfo.SPEED_OVER_GROUND_AT_RACE_START, // start analysis mode DetailCheckboxInfo.SPEED_OVER_GROUND_WHEN_STARTING, // start analysis mode DetailCheckboxInfo.DISTANCE_TO_STARBOARD_END_OF_STARTLINE_WHEN_STARTING, // start analysis mode DetailCheckboxInfo.START_TACK, // start analysis mode - DetailCheckboxInfo.AVERAGE_SPEED_OVER_GROUND, DetailCheckboxInfo.DISTANCE, DetailCheckboxInfo.RANK_GAIN, - DetailCheckboxInfo.TACK, DetailCheckboxInfo.JIBE, DetailCheckboxInfo.PENALTY_CIRCLE }; Assert.assertArrayEquals(detailsToSelect, selectedDetails); leaderboardSettingsDialog.pressCancel(); - mapSettings = raceboard.openMapSettings(); Assert.assertTrue(mapSettings.isWindUp()); } diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/CompetitorAndBoatJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/CompetitorAndBoatJsonSerializer.java index 9f57a1f7fc2..3c8f7c1495e 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/CompetitorAndBoatJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/CompetitorAndBoatJsonSerializer.java @@ -22,8 +22,9 @@ public class CompetitorAndBoatJsonSerializer implements JsonSerializer competitorJsonSerializer, JsonSerializer boatJsonSerializer) { diff --git a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/CompetitorJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/CompetitorJsonSerializer.java index 22693413521..7983eb3413c 100644 --- a/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/CompetitorJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization.shared.android/src/com/sap/sailing/server/gateway/serialization/impl/CompetitorJsonSerializer.java @@ -16,6 +16,8 @@ import com.sap.sailing.domain.common.tracking.impl.CompetitorJsonConstants; import com.sap.sailing.server.gateway.serialization.JsonSerializer; import com.sap.sse.common.Color; import com.sap.sse.common.CountryCode; +import com.sap.sse.security.shared.HasPermissions.DefaultActions; +import com.sap.sse.security.shared.impl.SecuredSecurityTypes.PublicReadableActions; /** * Serializes a {@link Competitor} or {@link CompetitorWithBoat} object. If a {@link CompetitorWithBoat} object, @@ -29,6 +31,13 @@ public class CompetitorJsonSerializer implements JsonSerializer { private final JsonSerializer teamJsonSerializer; private final JsonSerializer boatJsonSerializer; + + /** + * The {@link DefaultActions#READ} permission is required for certain fields such as the e-mail address; + * for other fields the {@link PublicReadableActions#READ_PUBLIC} permission suffices. If this field is set to + * {@code true}, also the non-public fields will be serialized. + */ + private final boolean serializeNonPublicFields; /** * Creates a serializer for {@link Competitor} objects which by default, if the competitor @@ -47,16 +56,28 @@ public class CompetitorJsonSerializer implements JsonSerializer { * {@link CompetitorJsonConstants#FIELD_BOAT} field of the resulting document. */ public static CompetitorJsonSerializer create(boolean serializeBoat) { - return new CompetitorJsonSerializer(TeamJsonSerializer.create(), serializeBoat?BoatJsonSerializer.create():null); + return new CompetitorJsonSerializer(TeamJsonSerializer.create(), serializeBoat?BoatJsonSerializer.create():null, + /* serializeNonPublicFields */ false); + } + + /** + * Creates a serializer for {@link Competitor} objects which if {@code serializeBoat==true} and if the competitor + * {@link Competitor#hasBoat() has a boat attached} will serialize the boat in the + * {@link CompetitorJsonConstants#FIELD_BOAT} field of the resulting document. + */ + public static CompetitorJsonSerializer create(boolean serializeBoat, boolean serializeNonPublicCompetitorFields) { + return new CompetitorJsonSerializer(TeamJsonSerializer.create(), serializeBoat?BoatJsonSerializer.create():null, + serializeNonPublicCompetitorFields); } public CompetitorJsonSerializer() { - this(null, null); + this(null, null, /* serializeNonPublicFields */ false); } - public CompetitorJsonSerializer(JsonSerializer teamJsonSerializer, JsonSerializer boatJsonSerializer) { + public CompetitorJsonSerializer(JsonSerializer teamJsonSerializer, JsonSerializer boatJsonSerializer, boolean serializeNonPublicFields) { this.teamJsonSerializer = teamJsonSerializer; this.boatJsonSerializer = boatJsonSerializer; + this.serializeNonPublicFields = serializeNonPublicFields; } public static JSONObject getCompetitorIdQuery(Competitor competitor) { @@ -82,7 +103,9 @@ public class CompetitorJsonSerializer implements JsonSerializer { result.put(CompetitorJsonConstants.FIELD_SHORT_NAME, competitor.getShortName()); Color color = getColor(competitor); result.put(CompetitorJsonConstants.FIELD_DISPLAY_COLOR, color == null ? null : color.getAsHtml()); - result.put(CompetitorJsonConstants.FIELD_EMAIL, competitor.getEmail()); + if (serializeNonPublicFields) { + result.put(CompetitorJsonConstants.FIELD_EMAIL, competitor.getEmail()); + } result.put(CompetitorJsonConstants.FIELD_SEARCHTAG, competitor.getSearchTag()); final Nationality nationality = competitor.getTeam() == null ? null : competitor.getTeam().getNationality(); result.put(CompetitorJsonConstants.FIELD_NATIONALITY, nationality == null ? "" : nationality.getThreeLetterIOCAcronym()); diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/CompetitorJsonSerializerTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/CompetitorJsonSerializerTest.java index 2b534b75a2b..5c4b16fb0ef 100755 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/CompetitorJsonSerializerTest.java +++ b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/CompetitorJsonSerializerTest.java @@ -114,7 +114,7 @@ public class CompetitorJsonSerializerTest { when(boat.getId()).thenReturn(UUID.randomUUID()); when(boat.getSailID()).thenReturn("1233"); when(boat.getBoatClass()).thenReturn(DomainFactory.INSTANCE.getOrCreateBoatClass("Tornado")); - JSONObject result = CompetitorAndBoatJsonSerializer.create().serialize(new Pair<>(competitorWithBoat, boat)); + JSONObject result = CompetitorAndBoatJsonSerializer.create(/* serializeNonPublicCompetitorFields */ false).serialize(new Pair<>(competitorWithBoat, boat)); final Pair deserializedCompetitorAndBoat = CompetitorAndBoatJsonDeserializer .create(new DomainFactoryImpl(new TransientCompetitorAndBoatStoreImpl(), /* raceLogResolver */ null)) .deserialize((JSONObject) new JSONParser().parse(result.toString())); @@ -135,7 +135,7 @@ public class CompetitorJsonSerializerTest { when(boat.getId()).thenReturn(UUID.randomUUID()); when(boat.getSailID()).thenReturn("12334"); when(boat.getBoatClass()).thenReturn(DomainFactory.INSTANCE.getOrCreateBoatClass("Tornado")); - JSONObject result = CompetitorAndBoatJsonSerializer.create().serialize(new Pair<>(competitor, boat)); + JSONObject result = CompetitorAndBoatJsonSerializer.create(/* serializeNonPublicCompetitorFields */ false).serialize(new Pair<>(competitor, boat)); final Pair deserializedCompetitorAndBoat = CompetitorAndBoatJsonDeserializer .create(new DomainFactoryImpl(new TransientCompetitorAndBoatStoreImpl(), /* raceLogResolver */ null)) .deserialize((JSONObject) new JSONParser().parse(result.toString())); @@ -156,7 +156,7 @@ public class CompetitorJsonSerializerTest { when(boat.getId()).thenReturn(UUID.randomUUID()); when(boat.getSailID()).thenReturn("1233"); when(boat.getBoatClass()).thenReturn(DomainFactory.INSTANCE.getOrCreateBoatClass("Tornado")); - JSONObject result = CompetitorAndBoatJsonSerializer.create().serialize(new Pair<>(competitorWithBoat, boat)); + JSONObject result = CompetitorAndBoatJsonSerializer.create(/* serializeNonPublicCompetitorFields */ false).serialize(new Pair<>(competitorWithBoat, boat)); final Pair deserializedCompetitorAndBoat = CompetitorAndBoatJsonDeserializer .create(new DomainFactoryImpl(new TransientCompetitorAndBoatStoreImpl(), /* raceLogResolver */ null)) .deserialize((JSONObject) new JSONParser().parse(result.toString())); diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogFinishPositioningConfirmedEventSerializerTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogFinishPositioningConfirmedEventSerializerTest.java index d3607f336c2..f69577c0a55 100644 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogFinishPositioningConfirmedEventSerializerTest.java +++ b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogFinishPositioningConfirmedEventSerializerTest.java @@ -54,7 +54,7 @@ public class RaceLogFinishPositioningConfirmedEventSerializerTest { public void setUp() { SharedDomainFactory factory = DomainFactory.INSTANCE; serializer = new RaceLogFinishPositioningConfirmedEventSerializer(new CompetitorJsonSerializer( - new TeamJsonSerializer(new PersonJsonSerializer(new NationalityJsonSerializer())), new BoatJsonSerializer(new BoatClassJsonSerializer()))); + new TeamJsonSerializer(new PersonJsonSerializer(new NationalityJsonSerializer())), new BoatJsonSerializer(new BoatClassJsonSerializer()), /* serializeNonPublicFields */ false)); deserializer = new RaceLogFinishPositioningConfirmedEventDeserializer(new CompetitorJsonDeserializer(factory.getCompetitorAndBoatStore(), /* team deserializer */ null, /* boat deserializer */ null)); now = MillisecondsTimePoint.now(); positioningList = new CompetitorResultsImpl(); diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogWindFixEventSerializerTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogWindFixEventSerializerTest.java index bead4ac462c..a8cfc0190ff 100644 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogWindFixEventSerializerTest.java +++ b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/RaceLogWindFixEventSerializerTest.java @@ -51,7 +51,7 @@ public class RaceLogWindFixEventSerializerTest { public void setUp() { SharedDomainFactory factory = DomainFactory.INSTANCE; serializer = new RaceLogWindFixEventSerializer(new CompetitorJsonSerializer(new TeamJsonSerializer( - new PersonJsonSerializer(new NationalityJsonSerializer())), new BoatJsonSerializer(new BoatClassJsonSerializer())), new WindJsonSerializer( + new PersonJsonSerializer(new NationalityJsonSerializer())), new BoatJsonSerializer(new BoatClassJsonSerializer()), /* serializeNonPublicFields */ false), new WindJsonSerializer( new PositionJsonSerializer())); deserializer = new RaceLogWindFixEventDeserializer(new CompetitorJsonDeserializer(factory.getCompetitorAndBoatStore(), /* team deserializer */ null, /* boat deserializer */ null), new WindJsonDeserializer(new PositionJsonDeserializer())); diff --git a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/TrackingTimesEventSerializerTest.java b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/TrackingTimesEventSerializerTest.java index 1a301e82f29..e910fc88d18 100644 --- a/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/TrackingTimesEventSerializerTest.java +++ b/java/com.sap.sailing.server.gateway.serialization.test/src/com/sap/sailing/server/gateway/serialization/test/racelog/TrackingTimesEventSerializerTest.java @@ -54,7 +54,7 @@ public class TrackingTimesEventSerializerTest { private CompetitorJsonSerializer createCompetitorSerializer() { return new CompetitorJsonSerializer(new TeamJsonSerializer( - new PersonJsonSerializer(new NationalityJsonSerializer())), new BoatJsonSerializer(new BoatClassJsonSerializer())); + new PersonJsonSerializer(new NationalityJsonSerializer())), new BoatJsonSerializer(new BoatClassJsonSerializer()), /* serializeNonPublicFields */ false); } @Test diff --git a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/MarkPassingsJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/MarkPassingsJsonSerializer.java index ce590c01ef0..864fe2cd962 100755 --- a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/MarkPassingsJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/MarkPassingsJsonSerializer.java @@ -26,7 +26,7 @@ public class MarkPassingsJsonSerializer extends AbstractTrackedRaceDataJsonSeria public JSONObject serialize(TrackedRace trackedRace) { final Course course = trackedRace.getRace().getCourse(); JSONObject result = new JSONObject(); - CompetitorAndBoatJsonSerializer competitorWithBoatSerializer = CompetitorAndBoatJsonSerializer.create(); + CompetitorAndBoatJsonSerializer competitorWithBoatSerializer = CompetitorAndBoatJsonSerializer.create(/* serializeNonPublicCompetitorFields */ false); CompetitorJsonSerializer competitorSerializer = CompetitorJsonSerializer.create(); JSONArray byCompetitorJson = new JSONArray(); result.put(BYCOMPETITOR, byCompetitorJson); diff --git a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/RaceEntriesJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/RaceEntriesJsonSerializer.java index 5f3e4327fe9..c9b7d4a1c4d 100644 --- a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/RaceEntriesJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/RaceEntriesJsonSerializer.java @@ -13,37 +13,33 @@ import com.sap.sailing.server.gateway.serialization.JsonSerializer; import com.sap.sse.common.Util.Pair; import com.sap.sse.security.SecurityService; import com.sap.sse.security.shared.HasPermissions.DefaultActions; +import com.sap.sse.security.shared.impl.SecuredSecurityTypes.PublicReadableActions; public class RaceEntriesJsonSerializer implements JsonSerializer { public static final String FIELD_NAME = "name"; public static final String FIELD_COMPETITORS = "competitors"; - private final CompetitorAndBoatJsonSerializer competitorAndBoatSerializer; private final SecurityService securityService; public RaceEntriesJsonSerializer(final SecurityService securityService) { - this(null, securityService); - } - - public RaceEntriesJsonSerializer(CompetitorAndBoatJsonSerializer competitorAndBoatSerializer, final SecurityService securityService) { - this.competitorAndBoatSerializer = competitorAndBoatSerializer; this.securityService = securityService; } public JSONObject serialize(RaceDefinition race) { JSONObject result = new JSONObject(); result.put(FIELD_NAME, race.getName()); - if(competitorAndBoatSerializer != null) { - JSONArray competitorsJson = new JSONArray(); - for (Entry competitorAndBoatEntry: race.getCompetitorsAndTheirBoats().entrySet()) { - Competitor competitor = competitorAndBoatEntry.getKey(); - competitor = (securityService.hasCurrentUserExplicitPermissions(competitor, DefaultActions.READ)) ? competitor : null; - DynamicBoat boat = (DynamicBoat) competitorAndBoatEntry.getValue(); - boat = (securityService.hasCurrentUserExplicitPermissions(boat, DefaultActions.READ)) ? boat : null; - competitorsJson.add(competitorAndBoatSerializer.serialize(new Pair<>(competitor, boat))); - } - result.put(FIELD_COMPETITORS, competitorsJson); + JSONArray competitorsJson = new JSONArray(); + for (Entry competitorAndBoatEntry: race.getCompetitorsAndTheirBoats().entrySet()) { + Competitor competitor = competitorAndBoatEntry.getKey(); + DynamicBoat boat = (DynamicBoat) competitorAndBoatEntry.getValue(); + securityService.checkCurrentUserExplicitPermissions(competitor, PublicReadableActions.READ_PUBLIC); + securityService.checkCurrentUserExplicitPermissions(boat, PublicReadableActions.READ_PUBLIC); + final CompetitorAndBoatJsonSerializer competitorAndBoatJsonSerializer = CompetitorAndBoatJsonSerializer.create( + /* serialize non-public competitor fields: */ + securityService.hasCurrentUserExplicitPermissions(competitor, DefaultActions.READ)); + competitorsJson.add(competitorAndBoatJsonSerializer.serialize(new Pair<>(competitor, boat))); } + result.put(FIELD_COMPETITORS, competitorsJson); return result; } } diff --git a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/RegattaJsonSerializer.java b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/RegattaJsonSerializer.java index a1fca0bd109..c8e9358fced 100644 --- a/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/RegattaJsonSerializer.java +++ b/java/com.sap.sailing.server.gateway.serialization/src/com/sap/sailing/server/gateway/serialization/impl/RegattaJsonSerializer.java @@ -17,6 +17,7 @@ public class RegattaJsonSerializer implements JsonSerializer { public static final String FIELD_START_DATE = "startDate"; public static final String FIELD_END_DATE = "endDate"; public static final String FIELD_SCORINGSYSTEM = "scoringSystem"; + public static final String FIELD_RANKINGMETRIC = "rankingMetric"; public static final String FIELD_SERIES = "series"; public static final String FIELD_COMPETITORS = "competitors"; public static final String FIELD_BOATS = "boats"; @@ -49,6 +50,7 @@ public class RegattaJsonSerializer implements JsonSerializer { result.put(FIELD_START_DATE, regatta.getStartDate() != null ? regatta.getStartDate().asMillis() : null); result.put(FIELD_END_DATE, regatta.getEndDate() != null ? regatta.getEndDate().asMillis() : null); result.put(FIELD_SCORINGSYSTEM, regatta.getScoringScheme().getType().name()); + result.put(FIELD_RANKINGMETRIC, regatta.getRankingMetricType().name()); result.put(FIELD_BOATCLASS, regatta.getBoatClass() != null ? regatta.getBoatClass().getName() : null); result.put(FIELD_COURSE_AREA_ID, regatta.getDefaultCourseArea() != null ? regatta.getDefaultCourseArea().getId().toString() : null); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/CompetitorsAndBoatsJsonExportServlet.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/CompetitorsAndBoatsJsonExportServlet.java index 6ce508ee463..bac0c8afd8e 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/CompetitorsAndBoatsJsonExportServlet.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/impl/rc/CompetitorsAndBoatsJsonExportServlet.java @@ -22,6 +22,7 @@ import com.sap.sailing.server.gateway.serialization.impl.CompetitorAndBoatJsonSe import com.sap.sailing.server.interfaces.RacingEventService; import com.sap.sse.common.Util.Pair; import com.sap.sse.security.shared.HasPermissions.DefaultActions; +import com.sap.sse.security.shared.impl.SecuredSecurityTypes.PublicReadableActions; public class CompetitorsAndBoatsJsonExportServlet extends AbstractJsonHttpServlet { private static final long serialVersionUID = 4510175441769759252L; @@ -52,10 +53,7 @@ public class CompetitorsAndBoatsJsonExportServlet extends AbstractJsonHttpServle response.sendError(HttpServletResponse.SC_NOT_FOUND, "No such leaderboard found."); return; } - - SecurityUtils.getSubject() - .checkPermission(leaderboard.getIdentifier().getStringPermission(DefaultActions.READ)); - + SecurityUtils.getSubject().checkPermission(leaderboard.getIdentifier().getStringPermission(DefaultActions.READ)); RaceColumn raceColumn = leaderboard.getRaceColumnByName(raceColumnName); if (raceColumn == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "No such race column found."); @@ -66,19 +64,21 @@ public class CompetitorsAndBoatsJsonExportServlet extends AbstractJsonHttpServle response.sendError(HttpServletResponse.SC_NOT_FOUND, "No such fleet found."); return; } - CompetitorAndBoatJsonSerializer competitorsAndBoatsSerializer = CompetitorAndBoatJsonSerializer.create(); JSONArray result = new JSONArray(); final TrackedRace trackedRace = raceColumn.getTrackedRace(fleet); - if (trackedRace != null) { - SecurityUtils.getSubject() - .checkPermission(trackedRace.getIdentifier().getStringPermission(DefaultActions.READ)); + SecurityUtils.getSubject().checkPermission(trackedRace.getIdentifier().getStringPermission(DefaultActions.READ)); } - for (Competitor competitor : leaderboard.getCompetitors(raceColumn, fleet)) { Boat boat = leaderboard.getBoatOfCompetitor(competitor, raceColumn, fleet); - JSONObject serializeCompetitorAndBoat = competitorsAndBoatsSerializer.serialize(new Pair(competitor, boat)); - result.add(serializeCompetitorAndBoat); + if (getSecurityService().hasCurrentUserExplicitPermissions(competitor, PublicReadableActions.READ_PUBLIC) && + getSecurityService().hasCurrentUserExplicitPermissions(boat, PublicReadableActions.READ_PUBLIC)) { + // suppress those competitors / boats from the result for which the current user does not even have READ_PUBLIC permissions + CompetitorAndBoatJsonSerializer competitorsAndBoatsSerializer = CompetitorAndBoatJsonSerializer.create( + getSecurityService().hasCurrentUserReadPermission(competitor)); + JSONObject serializeCompetitorAndBoat = competitorsAndBoatsSerializer.serialize(new Pair(competitor, boat)); + result.add(serializeCompetitorAndBoat); + } } setJsonResponseHeader(response); result.writeJSONString(response.getWriter()); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java index 49e9cdef207..bc4e269648d 100755 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResource.java @@ -1194,7 +1194,7 @@ public class LeaderboardsResource extends AbstractLeaderboardsResource { return result; }); } - CompetitorAndBoatJsonSerializer serializer = CompetitorAndBoatJsonSerializer.create(); + CompetitorAndBoatJsonSerializer serializer = CompetitorAndBoatJsonSerializer.create(/* serializeNonPublicCompetitorFields */ false); JSONArray result = new JSONArray(); for (final Competitor c : competitors) { Boat boat = trackedRace.getBoatOfCompetitor(c); diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResourceV2.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResourceV2.java index 73e29d1bb42..e97e2fc54ed 100644 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResourceV2.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/LeaderboardsResourceV2.java @@ -316,7 +316,7 @@ public class LeaderboardsResourceV2 extends AbstractLeaderboardsResource { name = "averageSpeedOverGround-kts"; Distance distance = entry.getDistanceTraveled(); Duration timeSailed = entry.getTimeSailed(); - if (distance != null && timeSailed != null) { + if (distance != null && timeSailed != null && timeSailed.compareTo(Duration.NULL)>0) { value = roundDouble(distance.inTime(timeSailed).getKnots(), 2); } break; diff --git a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java index c83936f7205..9c4d4653eb5 100755 --- a/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java +++ b/java/com.sap.sailing.server.gateway/src/com/sap/sailing/server/gateway/jaxrs/api/RegattasResource.java @@ -101,7 +101,6 @@ import com.sap.sailing.server.gateway.serialization.impl.AbstractTrackedRaceData import com.sap.sailing.server.gateway.serialization.impl.BoatClassJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.BoatJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.ColorJsonSerializer; -import com.sap.sailing.server.gateway.serialization.impl.CompetitorAndBoatJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.CompetitorJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.CompetitorTrackWithEstimationDataJsonSerializer; import com.sap.sailing.server.gateway.serialization.impl.CompleteManeuverCurveWithEstimationDataJsonSerializer; @@ -283,7 +282,7 @@ public class RegattasResource extends AbstractSailingServerResource { NationalityJsonSerializer nationalityJsonSerializer = new NationalityJsonSerializer(); BoatJsonSerializer boatJsonSerializer = new BoatJsonSerializer(new BoatClassJsonSerializer()); CompetitorJsonSerializer competitorJsonSerializer = new CompetitorJsonSerializer(new TeamJsonSerializer( - new PersonJsonSerializer(nationalityJsonSerializer)), boatJsonSerializer); + new PersonJsonSerializer(nationalityJsonSerializer)), boatJsonSerializer, /* serializeNonPublicFields */ false); JsonSerializer regattaSerializer = new RegattaJsonSerializer(null, competitorJsonSerializer, boatJsonSerializer, getSecurityService()); JSONObject serializedRegatta = regattaSerializer.serialize(regatta); String json = serializedRegatta.toJSONString(); @@ -313,8 +312,7 @@ public class RegattasResource extends AbstractSailingServerResource { if (race == null) { response = getBadRaceErrorResponse(regattaName, raceName); } else { - CompetitorAndBoatJsonSerializer competitorAndBoatJsonSerializer = CompetitorAndBoatJsonSerializer.create(); - JsonSerializer raceEntriesSerializer = new RaceEntriesJsonSerializer(competitorAndBoatJsonSerializer, getSecurityService()); + JsonSerializer raceEntriesSerializer = new RaceEntriesJsonSerializer(getSecurityService()); JSONObject serializedRaceEntries = raceEntriesSerializer.serialize(race); String json = serializedRaceEntries.toJSONString(); response = Response.ok(json).header("Content-Type", MediaType.APPLICATION_JSON + ";charset=UTF-8").build(); diff --git a/java/com.sap.sailing.server.interface/src/com/sap/sailing/server/interfaces/RacingEventService.java b/java/com.sap.sailing.server.interface/src/com/sap/sailing/server/interfaces/RacingEventService.java index 94f28689894..48d483b9221 100755 --- a/java/com.sap.sailing.server.interface/src/com/sap/sailing/server/interfaces/RacingEventService.java +++ b/java/com.sap.sailing.server.interface/src/com/sap/sailing/server/interfaces/RacingEventService.java @@ -884,10 +884,12 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch result = false; } else { Regatta regatta = getRegattaByName(leaderboardName); - if (regatta == null && secret != null) { - logger.warning( - "Attempt to skip security checks using regatta secret \"" + secret + "\" for leaderboard \"" - + leaderboardName + "\", but a regatta with the same name could not be resolved"); + if (regatta == null) { + if (secret != null) { + logger.warning( + "Attempt to skip security checks using regatta secret \"" + secret + "\" for leaderboard \"" + + leaderboardName + "\", but a regatta with the same name could not be resolved"); + } // else regatta not found, but no secret specified either; checks won't be skipped, but no skipping was really requested result = false; } else { result = Util.equalStringsWithEmptyIsNull(regatta.getRegistrationLinkSecret(), secret); diff --git a/java/com.sap.sailing.server.replication.test/ReplicationTests (Proxy).launch b/java/com.sap.sailing.server.replication.test/ReplicationTests (Proxy).launch deleted file mode 100644 index a152a7fd218..00000000000 --- a/java/com.sap.sailing.server.replication.test/ReplicationTests (Proxy).launch +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.server.trackfiles.test/com.sap.sailing.server.trackfiles.test (Proxy).launch b/java/com.sap.sailing.server.trackfiles.test/com.sap.sailing.server.trackfiles.test (Proxy).launch deleted file mode 100644 index 375504975e1..00000000000 --- a/java/com.sap.sailing.server.trackfiles.test/com.sap.sailing.server.trackfiles.test (Proxy).launch +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/TrackFileExporterImpl.java b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/TrackFileExporterImpl.java index 0c11fe98d55..046c09b75b8 100644 --- a/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/TrackFileExporterImpl.java +++ b/java/com.sap.sailing.server.trackfiles/src/com/sap/sailing/server/trackfiles/impl/TrackFileExporterImpl.java @@ -2,11 +2,8 @@ package com.sap.sailing.server.trackfiles.impl; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.ZipEntry; @@ -44,7 +41,6 @@ public class TrackFileExporterImpl implements TrackFileExporter { @Override public void writeAllData(List data, TrackFilesFormat format, List races, boolean dataBeforeAfter, boolean rawFixes, final ZipOutputStream out) throws IOException { - WriteZipCallback callback = new WriteZipCallback() { @Override public synchronized void write(ZipEntry entry, byte[] data) throws IOException { @@ -53,7 +49,6 @@ public class TrackFileExporterImpl implements TrackFileExporter { out.closeEntry(); } }; - List callables = new ArrayList<>(); ExecutorService executor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor(); List errors = new ArrayList<>(); @@ -64,32 +59,7 @@ public class TrackFileExporterImpl implements TrackFileExporter { callables.add(callable); } } - - List> results = Collections.> emptyList(); - - try { - results = executor.invokeAll(callables); - } catch (InterruptedException e) { - errors.add(e.getMessage()); - log.log(Level.WARNING, "Error exporting race: " + e.getMessage()); - e.printStackTrace(); - } - - for (Future result : results) { - try { - result.get(); - } catch (ExecutionException e) { - Throwable t = e.getCause(); - errors.add(t.getMessage()); - log.log(Level.WARNING, "Error exporting race: " + t.getMessage()); - t.printStackTrace(); - } catch (InterruptedException e) { - errors.add(e.getMessage()); - log.log(Level.WARNING, "Error exporting race: " + e.getMessage()); - e.printStackTrace(); - } - } - + ThreadPoolUtil.INSTANCE.invokeAllAndLogExceptions(executor, Level.WARNING, "Error exporting race: %s", callables); if (errors.size() > 0) { StringBuilder sb = new StringBuilder(); for (String error : errors) { @@ -100,7 +70,6 @@ public class TrackFileExporterImpl implements TrackFileExporter { callback.write(new ZipEntry("ERRORS"), sb.toString().getBytes()); } catch (Exception e) { log.log(Level.WARNING, "Error exporting race: " + e.getMessage()); - e.printStackTrace(); } } diff --git a/java/com.sap.sailing.server/SailingServer (No Proxy).launch b/java/com.sap.sailing.server/SailingServer (No Proxy).launch index 183250fecdf..7093aba42f1 100755 --- a/java/com.sap.sailing.server/SailingServer (No Proxy).launch +++ b/java/com.sap.sailing.server/SailingServer (No Proxy).launch @@ -1,40 +1,40 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java index 38ec6fb2f9b..3e59c3e8796 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RaceLogScoringReplicator.java @@ -16,7 +16,9 @@ import com.sap.sailing.domain.base.RaceColumnListener; import com.sap.sailing.domain.base.impl.RaceColumnListenerWithDefaultAction; import com.sap.sailing.domain.common.LeaderboardNameConstants; import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.domain.common.RankingMetrics; import com.sap.sailing.domain.leaderboard.Leaderboard; +import com.sap.sailing.domain.leaderboard.RegattaLeaderboard; import com.sap.sailing.domain.leaderboard.ScoringScheme; import com.sap.sailing.domain.racelog.RaceLogIdentifier; import com.sap.sailing.server.interfaces.RacingEventService; @@ -65,8 +67,8 @@ public class RaceLogScoringReplicator implements RaceColumnListenerWithDefaultAc /** * Called when a {@link RaceLogFinishPositioningConfirmedEvent} was received by the {@link RaceLog}. Retrieves the - * last {@link RaceLogFinishPositioningConfirmedEvent} from the racelog and applies the ranks and disqualifications - * entered by the race committee to the score correstions as follows: + * last {@link RaceLogFinishPositioningConfirmedEvent} from the {@link RaceLog} and applies the ranks and + * disqualifications entered by the race committee to the score corrections as follows: *
          *
        • When no {@link CompetitorResult} object is received for a competitor, no change is applied to that * competitor's score corrections.
        • @@ -75,11 +77,13 @@ public class RaceLogScoringReplicator implements RaceColumnListenerWithDefaultAc * {@link MaxPointsReason}, any existing score correction (points) is removed for that competitor, otherwise, the * score correction (points) from the CompetitorResult object is applied if provided (in this case a rank provided * in the {@link CompetitorResult} object is ignored); if no {@link MaxPointsReason} is provided and only a rank but - * no score is provided in the {@link CompetitorResult}, the scoring scheme will compute the score from the rank, - * considering any {@link MaxPointsReason} provided in the result, and that score will be applied as a score - * correction. If a {@link MaxPointsReason} but no explicit score is provided, the rank is ignored because - * the scoring scheme is expected to compute the score from the {@link MaxPointsReason}; therefore, the score - * correction (points) is cleared in this case so that the scoring scheme will be used for score calculation. + * no score is provided in the {@link CompetitorResult}, the scoring scheme will + * {@link ScoringScheme#getScoreForRank(Leaderboard, RaceColumn, Competitor, int, java.util.concurrent.Callable, com.sap.sailing.domain.leaderboard.NumberOfCompetitorsInLeaderboardFetcher, TimePoint) + * compute} the score from the rank, considering any {@link MaxPointsReason} provided in the result, and that score + * will be applied as a score correction. If a {@link MaxPointsReason} but no explicit score is provided, the rank + * is ignored because the scoring scheme is expected to compute the score from the {@link MaxPointsReason}; + * therefore, the score correction (points) is cleared in this case so that the scoring scheme will be used for + * score calculation. *
        • When a {@link CompetitorResult} object is received for a competitor, if it contains {@code null} for the * {@link MaxPointsReason}, any existing {@link MaxPointsReason} is removed for that competitor; otherwise, the * {@link MaxPointsReason} from the {@link CompetitorResult} object is applied, and if no explicit score is provided @@ -174,8 +178,17 @@ public class RaceLogScoringReplicator implements RaceColumnListenerWithDefaultAc // let scoring scheme calculate the penalty score dynamically scoreByRaceCommittee = null; } else { - scoreByRaceCommittee = leaderboard.getScoringScheme().getScoreForRank(leaderboard, raceColumn, competitor, - oneBasedRankByRaceCommittee, ()->numberOfCompetitorsInRace, leaderboard.getNumberOfCompetitorsInLeaderboardFetcher(), timePoint); + // Check whether it's a non-one-design regatta. In that case, "rank by race committee" just means finish line + // crossing order, but that's not relevant for the actual *ranking* in a handicapped regatta. Corrected/calculated + // times are. Therefore, for handicap regattas, don't ask the scoring scheme to map the finishing order + // to a score. + if (leaderboard instanceof RegattaLeaderboard && + ((RegattaLeaderboard) leaderboard).getRegatta().getRankingMetricType() != RankingMetrics.ONE_DESIGN) { + scoreByRaceCommittee = null; + } else { + scoreByRaceCommittee = leaderboard.getScoringScheme().getScoreForRank(leaderboard, raceColumn, competitor, + oneBasedRankByRaceCommittee, ()->numberOfCompetitorsInRace, leaderboard.getNumberOfCompetitorsInLeaderboardFetcher(), timePoint); + } } } else { scoreByRaceCommittee = optionalExplicitScore; diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/security/PermissionAwareRaceTrackingHandler.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/security/PermissionAwareRaceTrackingHandler.java index 1967222be13..df04558958e 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/security/PermissionAwareRaceTrackingHandler.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/security/PermissionAwareRaceTrackingHandler.java @@ -1,8 +1,8 @@ package com.sap.sailing.server.security; import java.io.Serializable; +import java.net.URI; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.Callable; @@ -14,10 +14,15 @@ import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; import com.sap.sailing.domain.base.Boat; import com.sap.sailing.domain.base.BoatClass; import com.sap.sailing.domain.base.Competitor; +import com.sap.sailing.domain.base.CompetitorAndBoatStore; import com.sap.sailing.domain.base.Course; import com.sap.sailing.domain.base.RaceDefinition; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.Sideline; +import com.sap.sailing.domain.base.impl.DynamicBoat; +import com.sap.sailing.domain.base.impl.DynamicCompetitor; +import com.sap.sailing.domain.base.impl.DynamicCompetitorWithBoat; +import com.sap.sailing.domain.base.impl.DynamicTeam; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.RegattaNameAndRaceName; import com.sap.sailing.domain.common.security.SecuredDomainType; @@ -28,7 +33,10 @@ import com.sap.sailing.domain.tracking.RaceTrackingHandler.DefaultRaceTrackingHa import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.TrackedRegatta; import com.sap.sailing.domain.tracking.WindStore; +import com.sap.sse.common.Color; +import com.sap.sse.common.Duration; import com.sap.sse.security.SecurityService; +import com.sap.sse.security.shared.QualifiedObjectIdentifier; import com.sap.sse.security.shared.impl.Ownership; import com.sap.sse.security.shared.impl.UserGroup; import com.sap.sse.util.ThreadLocalTransporter; @@ -73,6 +81,23 @@ public class PermissionAwareRaceTrackingHandler extends DefaultRaceTrackingHandl } } + /** + * Check if there is already an ownership asssociated to the object identified by the + * {@link QualifiedObjectIdentifier}. If no ownership exists, one will be created in the context of the current + * user. + */ + private void createOwnershipIfMissing(final QualifiedObjectIdentifier identifier) { + if (securityService.getOwnership(identifier) == null) { + SubjectThreadState subjectThreadState = new SubjectThreadState(subject); + subjectThreadState.bind(); + try { + securityService.setOwnership(identifier, securityService.getCurrentUser(), defaultTenant); + } finally { + subjectThreadState.restore(); + } + } + } + @Override public DynamicTrackedRace createTrackedRace(TrackedRegatta trackedRegatta, RaceDefinition raceDefinition, Iterable sidelines, WindStore windStore, long delayToLiveInMillis, @@ -88,22 +113,40 @@ public class PermissionAwareRaceTrackingHandler extends DefaultRaceTrackingHandl @Override public RaceDefinition createRaceDefinition(Regatta regatta, String name, Course course, BoatClass boatClass, Map competitorsAndTheirBoats, Serializable id) { - // TODO bug 5015: this is just a very basic hack that is not checking for competitor creation permission but for now only establishes an ownership when none exists - SubjectThreadState subjectThreadState = new SubjectThreadState(subject); - subjectThreadState.bind(); - try { - for (final Entry e : competitorsAndTheirBoats.entrySet()) { - if (securityService.getOwnership(e.getKey().getIdentifier()) == null) { - securityService.setOwnership(e.getKey().getIdentifier(), securityService.getCurrentUser(), defaultTenant); - } - if (securityService.getOwnership(e.getValue().getIdentifier()) == null) { - securityService.setOwnership(e.getValue().getIdentifier(), securityService.getCurrentUser(), defaultTenant); - } - } - } finally { - subjectThreadState.restore(); - } return setOwnershipForRace(new RegattaNameAndRaceName(regatta.getName(), name), () -> super.createRaceDefinition(regatta, name, course, boatClass, competitorsAndTheirBoats, id)); } + + /** Gets or creates the competitor and sets the ownership correctly. */ + @Override + public DynamicCompetitor getOrCreateCompetitor(CompetitorAndBoatStore competitorStore, Serializable competitorId, + String name, String shortName, Color displayColor, String email, URI flagImageURI, DynamicTeam team, + Double timeOnTimeFactor, Duration timeOnDistanceAllowancePerNauticalMile, String searchTag) { + final DynamicCompetitor competitor = competitorStore.getOrCreateCompetitor(competitorId, name, shortName, + displayColor, email, flagImageURI, team, timeOnTimeFactor, timeOnDistanceAllowancePerNauticalMile, + searchTag); + createOwnershipIfMissing(competitor.getIdentifier()); + return competitor; + } + + @Override + public DynamicCompetitorWithBoat getOrCreateCompetitorWithBoat(CompetitorAndBoatStore competitorStore, + Serializable competitorId, String name, String shortName, Color displayColor, String email, + URI flagImageURI, DynamicTeam team, Double timeOnTimeFactor, + Duration timeOnDistanceAllowancePerNauticalMile, String searchTag, DynamicBoat boat) { + final DynamicCompetitorWithBoat competitorWithBoat = competitorStore.getOrCreateCompetitorWithBoat(competitorId, + name, shortName, displayColor, email, flagImageURI, + team, timeOnTimeFactor, timeOnDistanceAllowancePerNauticalMile, searchTag, boat); + createOwnershipIfMissing(competitorWithBoat.getIdentifier()); + createOwnershipIfMissing(competitorWithBoat.getBoat().getIdentifier()); + return competitorWithBoat; + } + + @Override + public DynamicBoat getOrCreateBoat(CompetitorAndBoatStore competitorAndBoatStore, Serializable id, String name, + BoatClass boatClass, String sailId, Color color) { + final DynamicBoat boat = competitorAndBoatStore.getOrCreateBoat(id, name, boatClass, sailId, color); + createOwnershipIfMissing(boat.getIdentifier()); + return boat; + } } diff --git a/java/com.sap.sailing.www/release_notes_admin.html b/java/com.sap.sailing.www/release_notes_admin.html index dccb8b9490f..14e23fab71c 100755 --- a/java/com.sap.sailing.www/release_notes_admin.html +++ b/java/com.sap.sailing.www/release_notes_admin.html @@ -21,6 +21,32 @@

          Release Notes - Administration Console

          +

          May 2019

          +
            +
          • Ranks (finishing orders) entered by the Race Manager app user are no longer + considered for handicap regattas. Only an explicit score will be accepted, + or a finishing time can be provided which will then let the handicap ranking + metric infer the correct rank from that. +
          • +
          • Maneuver markers that appear when clicking on a boat while the RaceBoard is paused + were sometimes hard to click because they disappeared behind the boat's tail. + This behavior is now fixed. +
          • +
          • Fixed an issue with result import URL removal in case the URL erroneously contains + more than one space character. +
          • +
          • Fixed a problem with competitor import: competitors imported now correctly obtain + the current session's user as their owner, and the current session's user's + default creation group as their owning group. +
          • +
          • Fixed issues with Manage2Sail regatta structure import: Imports of regattas with + no "ClassName" property, such as handicap / Yardstick regattas, will now create + regattas with "?" as boat class name to avoid other problems; furthermore, + due to a permission check that was too restrictive, regattas to import could not + be seen unless the user had "super-admin" power. +
          • +
          +

          April 2019

          • Filtering the leaderboard groups during a Master Data Import now also diff --git a/java/com.sap.sailing.xmlexport.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.xmlexport.test/META-INF/MANIFEST.MF index 5a9ec00238f..e48edecc6c6 100644 --- a/java/com.sap.sailing.xmlexport.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.xmlexport.test/META-INF/MANIFEST.MF @@ -9,5 +9,8 @@ Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Require-Bundle: org.junit;bundle-version="4.8.2", com.sap.sse.common, com.sap.sailing.domain.test;bundle-version="1.0.0", - com.tractrac.clientmodule;bundle-version="3.0.7" + com.tractrac.clientmodule;bundle-version="3.0.7", + org.mockito.mockito-core, + org.objenesis;bundle-version="2.1.0", + org.hamcrest;bundle-version="1.1.0" Automatic-Module-Name: com.sap.sailing.xmlexport.test diff --git a/java/com.sap.sailing.xmlexport.test/src/com/sap/sailing/xmlexport/test/SecurityServiceMock.java b/java/com.sap.sailing.xmlexport.test/src/com/sap/sailing/xmlexport/test/SecurityServiceMock.java deleted file mode 100644 index 3e54a8365b7..00000000000 --- a/java/com.sap.sailing.xmlexport.test/src/com/sap/sailing/xmlexport/test/SecurityServiceMock.java +++ /dev/null @@ -1,906 +0,0 @@ -package com.sap.sailing.xmlexport.test; - -import java.io.IOException; -import java.io.InputStream; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.net.MalformedURLException; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.Callable; -import java.util.function.Consumer; -import java.util.function.Function; - -import javax.servlet.ServletContext; - -import org.apache.shiro.cache.CacheManager; -import org.apache.shiro.mgt.SecurityManager; - -import com.sap.sse.common.mail.MailException; -import com.sap.sse.replication.OperationExecutionListener; -import com.sap.sse.replication.OperationWithResult; -import com.sap.sse.replication.OperationWithResultWithIdWrapper; -import com.sap.sse.replication.OperationsToMasterSender; -import com.sap.sse.replication.OperationsToMasterSendingQueue; -import com.sap.sse.replication.ReplicationMasterDescriptor; -import com.sap.sse.security.Action; -import com.sap.sse.security.SecurityService; -import com.sap.sse.security.impl.ReplicableSecurityService; -import com.sap.sse.security.interfaces.Credential; -import com.sap.sse.security.shared.AccessControlListAnnotation; -import com.sap.sse.security.shared.HasPermissions; -import com.sap.sse.security.shared.OwnershipAnnotation; -import com.sap.sse.security.shared.QualifiedObjectIdentifier; -import com.sap.sse.security.shared.RoleDefinition; -import com.sap.sse.security.shared.RolePrototype; -import com.sap.sse.security.shared.SocialUserAccount; -import com.sap.sse.security.shared.TypeRelativeObjectIdentifier; -import com.sap.sse.security.shared.UserGroupManagementException; -import com.sap.sse.security.shared.UserManagementException; -import com.sap.sse.security.shared.WildcardPermission; -import com.sap.sse.security.shared.WithQualifiedObjectIdentifier; -import com.sap.sse.security.shared.impl.AccessControlList; -import com.sap.sse.security.shared.impl.Ownership; -import com.sap.sse.security.shared.impl.Role; -import com.sap.sse.security.shared.impl.User; -import com.sap.sse.security.shared.impl.UserGroup; - -/** - * Mock of SecurityService which does not throw exception on - * checkCurrentUserReadPermission(WithQualifiedObjectIdentifier) and returns true for - * hasCurrentUserReadPermission(WithQualifiedObjectIdentifier). - */ -@SuppressWarnings("restriction") -public class SecurityServiceMock implements SecurityService { - - @Override - public ObjectInputStream createObjectInputStreamResolvingAgainstCache(InputStream is) throws IOException { - - return null; - } - - @Override - public void initiallyFillFromInternal(ObjectInputStream is) - throws IOException, ClassNotFoundException, InterruptedException { - - } - - @Override - public void serializeForInitialReplicationInternal(ObjectOutputStream objectOutputStream) throws IOException { - - } - - @Override - public Iterable> getOperationExecutionListeners() { - - return null; - } - - @Override - public void startedReplicatingFrom(ReplicationMasterDescriptor master) { - - } - - @Override - public void stoppedReplicatingFrom(ReplicationMasterDescriptor master) { - - } - - @Override - public void addOperationExecutionListener(OperationExecutionListener listener) { - - } - - @Override - public void removeOperationExecutionListener(OperationExecutionListener listener) { - - } - - @Override - public void clearReplicaState() throws MalformedURLException, IOException, InterruptedException { - - } - - @Override - public boolean isCurrentlyFillingFromInitialLoad() { - - return false; - } - - @Override - public boolean isCurrentlyApplyingOperationReceivedFromMaster() { - - return false; - } - - @Override - public void setCurrentlyFillingFromInitialLoad(boolean b) { - - } - - @Override - public void setCurrentlyApplyingOperationReceivedFromMaster(boolean b) { - - } - - @Override - public void setUnsentOperationToMasterSender(OperationsToMasterSendingQueue service) { - - } - - @Override - public ReplicationMasterDescriptor getMasterDescriptor() { - - return null; - } - - @Override - public void addOperationSentToMasterForReplication( - OperationWithResultWithIdWrapper operationWithResultWithIdWrapper) { - - } - - @Override - public boolean hasSentOperationToMaster(OperationWithResult operation) { - - return false; - } - - @Override - public , T> void scheduleForSending( - OperationWithResult operationWithResult, OperationsToMasterSender sender) { - - } - - @Override - public Serializable getId() { - - return null; - } - - @Override - public SecurityManager getSecurityManager() { - - return null; - } - - @Override - public OwnershipAnnotation getOwnership(QualifiedObjectIdentifier idOfOwnedObject) { - - return null; - } - - @Override - public OwnershipAnnotation createDefaultOwnershipForNewObject(QualifiedObjectIdentifier idOfNewObject) { - - return null; - } - - @Override - public Iterable getAccessControlLists() { - - return null; - } - - @Override - public AccessControlListAnnotation getAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject) { - - return null; - } - - @Override - public SecurityService setEmptyAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject) { - - return null; - } - - @Override - public SecurityService setEmptyAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, - String displayNameOfAccessControlledObject) { - - return null; - } - - @Override - public AccessControlList updateAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, - Map> permissionMap) { - - return null; - } - - @Override - public AccessControlList overrideAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, - Map> permissionMap) { - - return null; - } - - @Override - public AccessControlList addToAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, - UserGroup userGroup, String action) { - - return null; - } - - @Override - public AccessControlList removeFromAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, - UserGroup group, String action) { - - return null; - } - - @Override - public void deleteAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject) { - - } - - @Override - public Ownership setOwnership(QualifiedObjectIdentifier idOfOwnedObject, User userOwner, UserGroup tenantOwner) { - - return null; - } - - @Override - public Ownership setOwnership(QualifiedObjectIdentifier idOfOwnedObject, User userOwner, UserGroup tenantOwner, - String displayNameOfOwnedObject) { - - return null; - } - - @Override - public void deleteOwnership(QualifiedObjectIdentifier idOfOwnedObject) { - - } - - @Override - public Iterable getUserGroupList() { - - return null; - } - - @Override - public UserGroup getUserGroup(UUID id) { - - return null; - } - - @Override - public UserGroup getUserGroupByName(String name) { - - return null; - } - - @Override - public Iterable getUserGroupsOfUser(User user) { - - return null; - } - - @Override - public UserGroup createUserGroup(UUID id, String name) throws UserGroupManagementException { - - return null; - } - - @Override - public void addUserToUserGroup(UserGroup group, User user) { - - } - - @Override - public void removeUserFromUserGroup(UserGroup group, User user) { - - } - - @Override - public void putRoleDefinitionToUserGroup(UserGroup group, RoleDefinition roleDefinition, boolean forAll) { - - } - - @Override - public void removeRoleDefintionFromUserGroup(UserGroup group, RoleDefinition roleDefinition) { - - } - - @Override - public void deleteUserGroup(UserGroup userGroup) throws UserGroupManagementException { - - } - - @Override - public Iterable getUserList() { - - return null; - } - - @Override - public User getUserByName(String username) { - - return null; - } - - @Override - public User getUserByEmail(String email) { - - return null; - } - - @Override - public User getCurrentUser() { - - return null; - } - - @Override - public String login(String username, String password) throws UserManagementException { - - return null; - } - - @Override - public String getAuthenticationUrl(Credential credential) throws UserManagementException { - - return null; - } - - @Override - public User verifySocialUser(Credential credential) throws UserManagementException { - - return null; - } - - @Override - public void logout() { - - } - - @Override - public User createSimpleUser(String username, String email, String password, String fullName, String company, - Locale locale, String validationBaseURL, UserGroup userOwner) - throws UserManagementException, MailException, UserGroupManagementException { - - return null; - } - - @Override - public void updateSimpleUserPassword(String name, String newPassword) throws UserManagementException { - - } - - @Override - public void updateSimpleUserEmail(String username, String newEmail, String validationBaseURL) - throws UserManagementException { - - } - - @Override - public void updateUserProperties(String username, String fullName, String company, Locale locale) - throws UserManagementException { - - } - - @Override - public User createSocialUser(String username, SocialUserAccount socialUserAccount) - throws UserManagementException, UserGroupManagementException { - - return null; - } - - @Override - public void deleteUser(String username) throws UserManagementException { - - } - - @Override - public RoleDefinition createRoleDefinition(UUID id, String name) { - - return null; - } - - @Override - public void deleteRoleDefinition(RoleDefinition roleDefinition) { - - } - - @Override - public void updateRoleDefinition(RoleDefinition roleDefinitionWithNewProperties) { - - } - - @Override - public Iterable getRoleDefinitions() { - - return null; - } - - @Override - public RoleDefinition getRoleDefinition(UUID idOfRoleDefinition) { - - return null; - } - - @Override - public void addRoleForUser(User user, Role role) { - - } - - @Override - public void addRoleForUser(String username, Role role) { - - } - - @Override - public void removeRoleFromUser(User user, Role role) { - - } - - @Override - public void removeRoleFromUser(String username, Role role) { - - } - - @Override - public Iterable getPermissionsFromUser(String username) throws UserManagementException { - - return null; - } - - @Override - public void removePermissionFromUser(String username, WildcardPermission permissionToRemove) { - - } - - @Override - public void addPermissionForUser(String username, WildcardPermission permissionToAdd) { - - } - - @Override - public void addSetting(String key, Class clazz) throws UserManagementException { - - } - - @Override - public boolean setSetting(String key, Object setting) { - - return false; - } - - @Override - public T getSetting(String key, Class clazz) { - - return null; - } - - @Override - public Map getAllSettings() { - - return null; - } - - @Override - public Map> getAllSettingTypes() { - - return null; - } - - @Override - public void refreshSecurityConfig(ServletContext context) { - - } - - @Override - public CacheManager getCacheManager() { - - return null; - } - - @Override - public void sendMail(String username, String subject, String body) throws MailException { - - } - - @Override - public boolean checkPassword(String username, String password) throws UserManagementException { - - return false; - } - - @Override - public boolean checkPasswordResetSecret(String username, String passwordResetSecret) - throws UserManagementException { - - return false; - } - - @Override - public void resetPassword(String username, String baseURL) throws UserManagementException, MailException { - - } - - @Override - public boolean validateEmail(String username, String validationSecret) throws UserManagementException { - - return false; - } - - @Override - public void setPreference(String username, String key, String value) { - - } - - @Override - public void setPreferenceObject(String name, String preferenceKey, Object preference) { - - } - - @Override - public void unsetPreference(String username, String key) { - - } - - @Override - public String getPreference(String username, String key) { - - return null; - } - - @Override - public T getPreferenceObject(String username, String key) { - - return null; - } - - @Override - public Map getAllPreferences(String username) { - - return null; - } - - @Override - public String createAccessToken(String username) { - - return null; - } - - @Override - public String getAccessToken(String username) { - - return null; - } - - @Override - public String getOrCreateAccessToken(String username) { - - return null; - } - - @Override - public User getUserByAccessToken(String accessToken) { - - return null; - } - - @Override - public void removeAccessToken(String username) { - - } - - @Override - public User loginByAccessToken(String accessToken) { - - return null; - } - - @Override - public UserGroup getDefaultTenant() { - - return null; - } - - @Override - public T setOwnershipCheckPermissionForObjectCreationAndRevertOnError(HasPermissions type, - TypeRelativeObjectIdentifier typeRelativeObjectIdentifier, String securityDisplayName, - Callable createActionReturningCreatedObject) { - - return null; - } - - @Override - public void setOwnershipCheckPermissionForObjectCreationAndRevertOnError(HasPermissions type, - TypeRelativeObjectIdentifier typeRelativeObjectIdentifier, String securityDisplayName, - Action actionToCreateObject) { - - } - - @Override - public User getAllUser() { - - return null; - } - - @Override - public void checkPermissionAndDeleteOwnershipForObjectRemoval(WithQualifiedObjectIdentifier object, - Action actionToDeleteObject) { - - } - - @Override - public T checkPermissionAndDeleteOwnershipForObjectRemoval(WithQualifiedObjectIdentifier object, - Callable actionToDeleteObject) { - - return null; - } - - @Override - public void deleteAllDataForRemovedObject(QualifiedObjectIdentifier identifier) { - - } - - @Override - public void filterObjectsWithPermissionForCurrentUser( - HasPermissions permittedObject, com.sap.sse.security.shared.HasPermissions.Action action, - Iterable objectsToFilter, Consumer filteredObjectsConsumer) { - - } - - @Override - public void filterObjectsWithPermissionForCurrentUser( - HasPermissions permittedObject, com.sap.sse.security.shared.HasPermissions.Action[] actions, - Iterable objectsToFilter, Consumer filteredObjectsConsumer) { - - } - - @Override - public void filterObjectsWithAnyPermissionForCurrentUser( - HasPermissions permittedObject, com.sap.sse.security.shared.HasPermissions.Action[] actions, - Iterable objectsToFilter, Consumer filteredObjectsConsumer) { - - } - - @Override - public List mapAndFilterByReadPermissionForCurrentUser( - HasPermissions permittedObject, Iterable objectsToFilter, Function filteredObjectsMapper) { - - return null; - } - - @Override - public List mapAndFilterByExplicitPermissionForCurrentUser( - HasPermissions permittedObject, com.sap.sse.security.shared.HasPermissions.Action[] actions, - Iterable objectsToFilter, Function filteredObjectsMapper) { - - return null; - } - - @Override - public List mapAndFilterByAnyExplicitPermissionForCurrentUser( - HasPermissions permittedObject, com.sap.sse.security.shared.HasPermissions.Action[] actions, - Iterable objectsToFilter, Function filteredObjectsMapper) { - - return null; - } - - @Override - public boolean hasCurrentUserReadPermission(WithQualifiedObjectIdentifier object) { - return true; - } - - @Override - public boolean hasCurrentUserUpdatePermission(WithQualifiedObjectIdentifier object) { - - return false; - } - - @Override - public boolean hasCurrentUserDeletePermission(WithQualifiedObjectIdentifier object) { - - return false; - } - - @Override - public boolean hasCurrentUserExplicitPermissions(WithQualifiedObjectIdentifier object, - com.sap.sse.security.shared.HasPermissions.Action... actions) { - - return false; - } - - @Override - public boolean hasCurrentUserOneOfExplicitPermissions(WithQualifiedObjectIdentifier object, - com.sap.sse.security.shared.HasPermissions.Action... actions) { - - return false; - } - - @Override - public void checkCurrentUserReadPermission(WithQualifiedObjectIdentifier object) { - - } - - @Override - public void checkCurrentUserUpdatePermission(WithQualifiedObjectIdentifier object) { - - } - - @Override - public void checkCurrentUserDeletePermission(WithQualifiedObjectIdentifier object) { - - } - - @Override - public void checkCurrentUserDeletePermission(QualifiedObjectIdentifier object) { - - } - - @Override - public void checkCurrentUserExplicitPermissions(WithQualifiedObjectIdentifier object, - com.sap.sse.security.shared.HasPermissions.Action... actions) { - - } - - @Override - public void checkCurrentUserHasOneOfExplicitPermissions(WithQualifiedObjectIdentifier object, - com.sap.sse.security.shared.HasPermissions.Action... actions) { - - } - - @Override - public void assumeOwnershipMigrated(String typeName) { - - } - - @Override - public void migrateOwnership(WithQualifiedObjectIdentifier object) { - - } - - @Override - public void migrateOwnership(QualifiedObjectIdentifier object, String displayName) { - - } - - @Override - public void migrateUser(User user) { - - } - - @Override - public void migratePermission(User user, WildcardPermission permissionToMigrate, - Function permissionReplacement) { - - } - - @Override - public void checkMigration(Iterable allInstances) { - - } - - @Override - public boolean hasCurrentUserRoleForOwnedObject(HasPermissions type, - T object, RoleDefinition roleToCheck) { - - return false; - } - - @Override - public boolean hasCurrentUserMetaPermission(WildcardPermission permissionToCheck, Ownership ownership) { - return false; - } - - @Override - public boolean hasCurrentUserMetaPermissionWithOwnershipLookup(WildcardPermission permissionToCheck) { - return false; - } - - @Override - public void setOwnershipIfNotSet(QualifiedObjectIdentifier identifier, User userOwner, UserGroup defaultTenant) { - - } - - @Override - public UserGroup getDefaultTenantForCurrentUser() { - - return null; - } - - @Override - public boolean hasUserAllWildcardPermissionsForAlreadyRealizedQualifications(RoleDefinition role, - Iterable permissionsToCheck) { - - return false; - } - - @Override - public void setDefaultTenantForCurrentServerForUser(String username, UUID defaultTenantId) { - - } - - @Override - public void copyUsersAndRoleAssociations(UserGroup source, UserGroup destination, RoleCopyListener callback) { - - } - - @Override - public User checkPermissionForObjectCreationAndRevertOnErrorForUserCreation(String username, - Callable createActionReturningCreatedObject) { - - return null; - } - - @Override - public T checkPermissionAndDeleteOwnershipForObjectRemoval(QualifiedObjectIdentifier identifier, - Callable actionToDeleteObject) { - - return null; - } - - @Override - public void checkPermissionAndDeleteOwnershipForObjectRemoval(QualifiedObjectIdentifier identifier, - Action actionToDeleteObject) { - - } - - @Override - public T doWithTemporaryDefaultTenant(UserGroup tenant, Callable action) { - - return null; - } - - @Override - public void initialize() { - - } - - @Override - public boolean hasCurrentUserMetaPermissionsOfRoleDefinitionWithQualification(RoleDefinition roleDefinition, - Ownership qualificationForGrantedPermissions) { - - return false; - } - - @Override - public boolean isInitialOrMigration() { - - return false; - } - - @Override - public RoleDefinition getOrCreateRoleDefinitionFromPrototype(RolePrototype rolePrototype) { - - return null; - } - - @Override - public void setDefaultOwnership(QualifiedObjectIdentifier identifier, String description) { - - } - - @Override - public boolean hasCurrentUserAnyPermission(WildcardPermission permissionToCheck) { - - return false; - } - - @Override - public T setOwnershipWithoutCheckPermissionForObjectCreationAndRevertOnError(HasPermissions type, - TypeRelativeObjectIdentifier typeIdentifier, String securityDisplayName, Callable actionWithResult) { - return null; - } - - @Override - public void setOwnershipWithoutCheckPermissionForObjectCreationAndRevertOnError(HasPermissions type, - TypeRelativeObjectIdentifier typeRelativeObjectIdentifier, String securityDisplayName, - Action actionToCreateObject) { - // TODO Auto-generated method stub - - } - - @Override - public void setDefaultOwnershipIfNotSet(QualifiedObjectIdentifier identifier) { - } - -} diff --git a/java/com.sap.sailing.xmlexport.test/src/com/sap/sailing/xmlexport/test/XMLExportTest.java b/java/com.sap.sailing.xmlexport.test/src/com/sap/sailing/xmlexport/test/XMLExportTest.java index aa20e2e0043..4018a8cd1d2 100644 --- a/java/com.sap.sailing.xmlexport.test/src/com/sap/sailing/xmlexport/test/XMLExportTest.java +++ b/java/com.sap.sailing.xmlexport.test/src/com/sap/sailing/xmlexport/test/XMLExportTest.java @@ -11,6 +11,7 @@ import java.net.URISyntaxException; import java.net.URL; import org.junit.Test; +import org.mockito.Mockito; import com.sap.sailing.domain.common.WindSourceType; import com.sap.sailing.domain.common.impl.KnotSpeedWithBearingImpl; @@ -24,6 +25,7 @@ import com.sap.sailing.domain.test.OnlineTracTracBasedTest; import com.sap.sailing.domain.tractracadapter.ReceiverType; import com.sap.sailing.xmlexport.LeaderboardData; import com.sap.sse.common.impl.DegreeBearingImpl; +import com.sap.sse.security.SecurityService; public class XMLExportTest extends OnlineTracTracBasedTest { @@ -60,7 +62,9 @@ public class XMLExportTest extends OnlineTracTracBasedTest { FlexibleLeaderboard leaderboard = new FlexibleLeaderboardImpl("ESS 2014 Nice", new ThresholdBasedResultDiscardingRuleImpl(new int[] {0}), new HighPointFirstGets10LastBreaksTie(), null); leaderboard.addRace(getTrackedRace(), "R1", false); - LeaderboardData leaderboardData = new LeaderboardData(leaderboard, new SecurityServiceMock()); + SecurityService securityService = Mockito.mock(SecurityService.class); + Mockito.when(securityService.hasCurrentUserReadPermission(Mockito.any())).thenReturn(true); + LeaderboardData leaderboardData = new LeaderboardData(leaderboard, securityService); try { leaderboardData.perform(); } catch (Exception e) { diff --git a/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java b/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java index f5f77368012..cb0c8483d56 100755 --- a/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java +++ b/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java @@ -30,6 +30,7 @@ import com.sap.sailing.domain.base.impl.PersonImpl; import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.base.impl.SeriesImpl; import com.sap.sailing.domain.base.impl.TeamImpl; +import com.sap.sailing.domain.common.BoatClassMasterdata; import com.sap.sailing.domain.common.CompetitorRegistrationType; import com.sap.sailing.domain.common.FleetColors; import com.sap.sailing.domain.common.LeaderboardNameConstants; @@ -64,6 +65,7 @@ public class StructureImporter { private LinkedHashMap boatForPerson; private final DomainFactory baseDomainFactory; private final SetRacenumberStrategy setRacenumberStrategy; + private final static String UNKNOWN_BOATCLASS_NAME = "?"; public StructureImporter(SetRacenumberStrategy setRacenumber, DomainFactory baseDomainFactory) { this.setRacenumberStrategy = setRacenumber; @@ -98,9 +100,18 @@ public class StructureImporter { BuildStructure buildStructure = new BuildStructure(races); final TimePoint startDate = null; // TODO can regatta start time be inferred from XRR document? final TimePoint endDate = null; // TODO can regatta end time be inferred from XRR document? - RegattaImpl regatta = new RegattaImpl(RegattaImpl.getDefaultName(event.getTitle(), ((Division) event - .getRaceOrDivisionOrRegattaSeriesResult().get(0)).getTitle()), - baseDomainFactory.getOrCreateBoatClass(result.getA().getBoatClass()), + final String regattaName = RegattaImpl.getDefaultName(event.getTitle(), ((Division) event + .getRaceOrDivisionOrRegattaSeriesResult().get(0)).getTitle()); + final String boatClassName; + if (result.getA().getBoatClass() != null) { + boatClassName = result.getA().getBoatClass(); + } else { + // we need a boat class; use some default + logger.warning("No boat class set in regatta to import: "+regattaName+". Setting to \""+UNKNOWN_BOATCLASS_NAME+"\""); + boatClassName = UNKNOWN_BOATCLASS_NAME; + } + RegattaImpl regatta = new RegattaImpl(regattaName, + baseDomainFactory.getOrCreateBoatClass(boatClassName), /* canBoatsOfCompetitorsChangePerRace */ true, CompetitorRegistrationType.CLOSED, startDate, endDate, getSeries(buildStructure), false, this.baseDomainFactory.createScoringScheme(ScoringSchemeType.LOW_POINT), event.getEventID(), null, diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java index 7a52666c847..b3211af4c69 100755 --- a/java/com.sap.sse.common/src/com/sap/sse/common/Util.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/Util.java @@ -232,6 +232,21 @@ public class Util { return result; } } + + /** + * If {@code iter} {@link Iterator#hasNext has a next element}, that element is returned. Otherwise, {@code null} is + * the result of this method. If a next element exists, the attempt to fetch it may, as usual, throw a + * {@link ConcurrentModificationException}. + */ + public static T nextOrNull(Iterator iter) { + final T result; + if (iter.hasNext()) { + result = iter.next(); + } else { + result = null; + } + return result; + } public static int indexOf(Iterable i, T t) { int result; diff --git a/java/com.sap.sse.filestorage.test/src/com/sap/sse/filestorage/services/test/AmazonS3Test.java b/java/com.sap.sse.filestorage.test/src/com/sap/sse/filestorage/services/test/AmazonS3Test.java index bfc5a8b9955..40e6021016b 100644 --- a/java/com.sap.sse.filestorage.test/src/com/sap/sse/filestorage/services/test/AmazonS3Test.java +++ b/java/com.sap.sse.filestorage.test/src/com/sap/sse/filestorage/services/test/AmazonS3Test.java @@ -21,21 +21,21 @@ import com.sap.sse.filestorage.testsupport.AmazonS3TestSupport; import com.sap.sse.security.testsupport.SecurityServiceMockFactory; public class AmazonS3Test { - @Before - public void setup() throws InvalidPropertiesException { - setUpSecurity(); + @Before + public void setup() throws InvalidPropertiesException, IOException { + setUpSecurity(); storageService = AmazonS3TestSupport.createService(SecurityServiceMockFactory.mockSecurityService()); } - - private void setUpSecurity() { - org.apache.shiro.mgt.SecurityManager securityManager = Mockito.mock(org.apache.shiro.mgt.SecurityManager.class); - Subject fakeSubject = Mockito.mock(Subject.class); - SecurityUtils.setSecurityManager(securityManager); - Mockito.doReturn(fakeSubject).when(securityManager).createSubject(Mockito.any()); - Mockito.doReturn(true).when(fakeSubject).isAuthenticated(); - Mockito.doNothing().when(fakeSubject).checkPermission(Mockito.anyString()); - } - + + private void setUpSecurity() { + org.apache.shiro.mgt.SecurityManager securityManager = Mockito.mock(org.apache.shiro.mgt.SecurityManager.class); + Subject fakeSubject = Mockito.mock(Subject.class); + SecurityUtils.setSecurityManager(securityManager); + Mockito.doReturn(fakeSubject).when(securityManager).createSubject(Mockito.any()); + Mockito.doReturn(true).when(fakeSubject).isAuthenticated(); + Mockito.doNothing().when(fakeSubject).checkPermission(Mockito.anyString()); + } + private static final String teamImageFile = "Bundesliga2014_Regatta6_eventteaser.jpg"; private FileStorageService storageService; diff --git a/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/AmazonS3FileStorageServiceImpl.java b/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/AmazonS3FileStorageServiceImpl.java index 1debbbb8c1d..3216816e4d3 100644 --- a/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/AmazonS3FileStorageServiceImpl.java +++ b/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/AmazonS3FileStorageServiceImpl.java @@ -3,7 +3,6 @@ package com.sap.sse.filestorage.impl; import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; -import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -78,12 +77,6 @@ public class AmazonS3FileStorageServiceImpl extends BaseFileStorageServiceImpl i return new AmazonS3Client(creds); } - private static String getKey(String fileExtension) { - String key = UUID.randomUUID().toString(); - key += fileExtension; - return key; - } - private URI getUri(String key) { try { return new URI(retrievalProtocol, regionRetrievalHost, "/" + bucketName.getValue() + "/" + key, null); diff --git a/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/BaseFileStorageServiceImpl.java b/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/BaseFileStorageServiceImpl.java index c7738c063ba..117a01b95e1 100644 --- a/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/BaseFileStorageServiceImpl.java +++ b/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/BaseFileStorageServiceImpl.java @@ -3,6 +3,7 @@ package com.sap.sse.filestorage.impl; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; +import java.util.UUID; import org.osgi.framework.BundleContext; import org.osgi.util.tracker.ServiceTracker; @@ -46,6 +47,12 @@ public abstract class BaseFileStorageServiceImpl implements FileStorageService { return name; } + protected static String getKey(String fileExtension) { + String key = UUID.randomUUID().toString(); + key += "."+fileExtension; + return key; + } + @Override public String getDescription(Locale locale) { return FileStorageI18n.STRING_MESSAGES.get(locale, descriptionKey); diff --git a/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/LocalFileStorageServiceImpl.java b/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/LocalFileStorageServiceImpl.java index c05a8e97a3a..61b4429260a 100644 --- a/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/LocalFileStorageServiceImpl.java +++ b/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/impl/LocalFileStorageServiceImpl.java @@ -8,7 +8,6 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; -import java.util.UUID; import java.util.logging.Level; import java.util.logging.Logger; @@ -89,12 +88,6 @@ public class LocalFileStorageServiceImpl extends BaseFileStorageServiceImpl impl }); } - private static String getKey(String fileEnding) { - String key = UUID.randomUUID().toString(); - key += fileEnding; - return key; - } - private URI getUri(String pathToFile) { try { return new URI(baseURL.getValue() + "/" + pathToFile); diff --git a/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/testsupport/AmazonS3TestSupport.java b/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/testsupport/AmazonS3TestSupport.java index 1df45bfc84d..6c91265f691 100644 --- a/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/testsupport/AmazonS3TestSupport.java +++ b/java/com.sap.sse.filestorage/src/com/sap/sse/filestorage/testsupport/AmazonS3TestSupport.java @@ -1,7 +1,10 @@ package com.sap.sse.filestorage.testsupport; +import java.io.IOException; + import com.sap.sse.filestorage.InvalidPropertiesException; import com.sap.sse.filestorage.impl.AmazonS3FileStorageServiceImpl; +import com.sap.sse.filestorage.impl.BaseFileStorageServiceImpl; import com.sap.sse.security.SecurityService; public class AmazonS3TestSupport { @@ -9,8 +12,8 @@ public class AmazonS3TestSupport { public static final String s3AccessKey = "NkijH2DfhWgb9fmESPjpeIbpUF+tC220KyTOfvGJ"; private static final String s3BucketName = "sapsailing-automatic-upload-test"; - public static AmazonS3FileStorageServiceImpl createService(final SecurityService securityService) throws InvalidPropertiesException { - AmazonS3FileStorageServiceImpl service = new AmazonS3FileStorageServiceImpl(/* bundleContext */ null) { + public static BaseFileStorageServiceImpl createService(final SecurityService securityService) throws InvalidPropertiesException, IOException { + BaseFileStorageServiceImpl service = new AmazonS3FileStorageServiceImpl(/* bundleContext */ null) { private static final long serialVersionUID = 6887160074291578082L; @Override diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java index e8e856d6db4..c2f95573c89 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/celltable/RefreshableSingleSelectionModel.java @@ -63,10 +63,10 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel */ @Override public void setSelected(T item, boolean selected) { - if (comp == null || dontcheckSelectionState || item == null || getSelectedObject() == null) { + if (getEntityIdentityComparator() == null || dontcheckSelectionState || item == null || getSelectedObject() == null) { super.setSelected(item, selected); } else { - if (comp.representSameEntity(getSelectedObject(), item)) { + if (getEntityIdentityComparator().representSameEntity(getSelectedObject(), item)) { super.setSelected(getSelectedObject(), false); // This old version of item will be deleted with the next clear() } super.setSelected(item, selected); @@ -112,7 +112,7 @@ public class RefreshableSingleSelectionModel extends SingleSelectionModel if (selected != null) { clear(); for (final T it : newObjects) { - boolean isEqual = comp == null ? selected.equals(it) : comp.representSameEntity(selected, it); + boolean isEqual = getEntityIdentityComparator() == null ? selected.equals(it) : getEntityIdentityComparator().representSameEntity(selected, it); if (isEqual) { setSelected(it, true); break; diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_es.properties b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_es.properties index cd8352da30b..1232371c1bb 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_es.properties +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_es.properties @@ -34,3 +34,7 @@ resetToDefault=Reinicializar los valores predeterminados resetToDefaultInProgress=Reinicializando... settingsRemoved=Parametrizaciones por defecto restablecidas settingsRemovedError=No se han podido restablecer las parametrizaciones por defecto +yes=Sí +no=No +pagerStateInfo={0,number,#,###}-{1,number,#,###} de {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} de más de {2,number,#,###} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_fr.properties b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_fr.properties index 31d36d1750c..e0dd8330a38 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_fr.properties +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_fr.properties @@ -34,3 +34,7 @@ resetToDefault=Réinitialiser sur les valeurs par défaut resetToDefaultInProgress=Réinitialisation en cours... settingsRemoved=Options par défaut restaurées settingsRemovedError=Impossible de restaurer les options par défaut +yes=Oui +no=Non +pagerStateInfo={0,number,#,###}-{1,number,#,###} sur {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} sur plus de {2,number,#,###} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ja.properties b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ja.properties index 529bd6aa94f..952c464fe6a 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ja.properties +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ja.properties @@ -34,3 +34,7 @@ resetToDefault=デフォルトに戻す resetToDefaultInProgress=リセット中です... settingsRemoved=デフォルト設定が復元されました settingsRemovedError=デフォルト設定を復元できませんでした +yes=はい +no=いいえ +pagerStateInfo={2,number,#,###} の {0,number,#,###} ~ {1,number,#,###} +pagerStateInfo[false]={2,number,#,###} 上の {0,number,#,###} ~ {1,number,#,###} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_pt.properties b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_pt.properties index 0f74ec6e316..61e670b26d9 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_pt.properties +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_pt.properties @@ -34,3 +34,7 @@ resetToDefault=Reinicializar para padrão resetToDefaultInProgress=Na reinicialização... settingsRemoved=Configurações padrão restauradas settingsRemovedError=Não foi possível restaurar configurações padrão +yes=Sim +no=Não +pagerStateInfo={0,number,#,###}-{1,number,#,###} de {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} de mais de {2,number,#,###} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ru.properties b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ru.properties index da6b238232a..b854f534369 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ru.properties +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ru.properties @@ -34,3 +34,7 @@ resetToDefault=Сбросить на значения по умолчанию resetToDefaultInProgress=Выполняется сброс... settingsRemoved=Настройки по умолчанию восстановлены settingsRemovedError=Не удалось восстановить настройки по умолчанию +yes=Да +no=Нет +pagerStateInfo={0,number,#,###}-{1,number,#,###} из {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} из более чем {2,number,#,###} diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_zh.properties b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_zh.properties index b2cc143f5da..a6bb6e6076f 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_zh.properties +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_zh.properties @@ -34,3 +34,7 @@ resetToDefault=重置为默认值 resetToDefaultInProgress=正在重置... settingsRemoved=默认设置已恢复 settingsRemovedError=无法恢复默认设置 +yes=是 +no=否 +pagerStateInfo={0,number,#,###}-{1,number,#,###}/{2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###}(超过{2,number,#,###}) diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/dialog/DataEntryDialog.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/dialog/DataEntryDialog.java index 6cb90dbd90b..4f882864a33 100755 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/dialog/DataEntryDialog.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/dialog/DataEntryDialog.java @@ -187,7 +187,7 @@ public abstract class DataEntryDialog { } /** - * Allows subcasses to listen to changes of the data shown in the dialog. + * Allows subclasses to listen to changes of the data shown in the dialog. */ protected void onChange(T result) { } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/panels/AbstractFilterablePanel.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/panels/AbstractFilterablePanel.java index 043d370b4eb..c188f0b4fa7 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/panels/AbstractFilterablePanel.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/panels/AbstractFilterablePanel.java @@ -218,6 +218,12 @@ public abstract class AbstractFilterablePanel extends HorizontalPanel { }); } + public void search(String searchString) { + getTextBox().setText(searchString); + filterer.setKeywords(Util.splitAlongWhitespaceRespectingDoubleQuotedPhrases(searchString)); + filter(); + } + public TextBox getTextBox() { return textBox; } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/server/DelegatingProxiedRemoteServiceServlet.java b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/server/DelegatingProxiedRemoteServiceServlet.java index 662eabba162..358c62965a7 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/server/DelegatingProxiedRemoteServiceServlet.java +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/server/DelegatingProxiedRemoteServiceServlet.java @@ -3,6 +3,8 @@ package com.sap.sse.gwt.server; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; import com.google.gwt.user.client.rpc.IncompatibleRemoteServiceException; import com.google.gwt.user.client.rpc.RpcTokenException; @@ -14,6 +16,8 @@ import com.google.gwt.user.server.rpc.SerializationPolicy; import com.sap.sse.common.TimePoint; public abstract class DelegatingProxiedRemoteServiceServlet extends ProxiedRemoteServiceServlet { + private static final Logger logger = Logger.getLogger(DelegatingProxiedRemoteServiceServlet.class.getName()) + ; private static final long serialVersionUID = -5543378343472849437L; /** @@ -73,6 +77,7 @@ public abstract class DelegatingProxiedRemoteServiceServlet extends ProxiedRemot } catch (InvocationTargetException e) { // Try to encode the caught exception Throwable cause = e.getCause(); + logger.log(Level.SEVERE, "Uncaught exception, forwarded to client", cause); responsePayload = RPC.encodeResponseForFailure(serviceMethod, cause, serializationPolicy, flags); } return responsePayload; diff --git a/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/impl/SecuredSecurityTypes.java b/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/impl/SecuredSecurityTypes.java index a4540e93b40..d693a0ba890 100644 --- a/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/impl/SecuredSecurityTypes.java +++ b/java/com.sap.sse.security.common/src/com/sap/sse/security/shared/impl/SecuredSecurityTypes.java @@ -73,7 +73,6 @@ public class SecuredSecurityTypes extends HasPermissionsImpl { CONFIGURE_FILE_STORAGE, CONFIGURE_LOCAL_SERVER, CONFIGURE_REMOTE_INSTANCES, - IMPORT_MASTER_DATA, CREATE_OBJECT, /** @@ -98,7 +97,7 @@ public class SecuredSecurityTypes extends HasPermissionsImpl { READ_REPLICATOR; private static final Action[] ALL_ACTIONS = new Action[] { CONFIGURE_FILE_STORAGE, CONFIGURE_LOCAL_SERVER, - CONFIGURE_REMOTE_INSTANCES, IMPORT_MASTER_DATA, CREATE_OBJECT, CAN_IMPORT_MASTERDATA, DATA_MINING, + CONFIGURE_REMOTE_INSTANCES, CREATE_OBJECT, CAN_IMPORT_MASTERDATA, DATA_MINING, REPLICATE, START_REPLICATION, READ_REPLICATOR, DefaultActions.CHANGE_OWNERSHIP, DefaultActions.CHANGE_ACL, DefaultActions.UPDATE }; } diff --git a/java/com.sap.sse.security.test/src/com/sap/sse/security/test/InheritableThreadLocalAndThreadPoolTest.java b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/InheritableThreadLocalAndThreadPoolTest.java new file mode 100755 index 00000000000..63ae8fdc1dd --- /dev/null +++ b/java/com.sap.sse.security.test/src/com/sap/sse/security/test/InheritableThreadLocalAndThreadPoolTest.java @@ -0,0 +1,55 @@ +package com.sap.sse.security.test; + +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import org.junit.Before; +import org.junit.Test; + +import com.sap.sse.util.impl.NamedTracingScheduledThreadPoolExecutor; + +public class InheritableThreadLocalAndThreadPoolTest { + private static InheritableThreadLocal defaultThreadLocal; + private static InheritableThreadLocal threadLocalWithChildValueInit; + private NamedTracingScheduledThreadPoolExecutor executor; + + @Before + public void setUp() { + defaultThreadLocal = new InheritableThreadLocal<>(); + threadLocalWithChildValueInit = new InheritableThreadLocal() { + @Override + protected Integer childValue(Integer parentValue) { + return 42; + } + }; + executor = new NamedTracingScheduledThreadPoolExecutor("Test Executor", /* corePoolSize */ 1); + } + + @Test + public void testSimpleValue() throws InterruptedException, ExecutionException { + defaultThreadLocal.set(14); + final Callable reader1 = ()->defaultThreadLocal.get(); + final Future future1 = executor.submit(reader1); + defaultThreadLocal.set(15); + final Callable reader2 = ()->defaultThreadLocal.get(); + final Future future2 = executor.submit(reader2); + assertEquals(14, future1.get().intValue()); + assertEquals(15, future2.get().intValue()); + } + + @Test + public void testMissingInitialization() throws InterruptedException, ExecutionException { + threadLocalWithChildValueInit.set(123); + final Callable reader1 = ()->threadLocalWithChildValueInit.get(); + final Future future1 = executor.submit(reader1); + final Runnable setter = ()->threadLocalWithChildValueInit.set(43); + executor.execute(setter); + final Callable reader2 = ()->threadLocalWithChildValueInit.get(); + final Future future2 = executor.submit(reader2); + assertEquals(42, future1.get().intValue()); + assertEquals(42, future2.get().intValue()); + } +} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java index d4f1d1c44e4..511527ca4dc 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/UserService.java @@ -392,23 +392,6 @@ public class UserService { } } - public boolean hasPermission(String permission) { - return hasPermission(new WildcardPermission(permission)); - } - - public boolean hasAllPermissions(WildcardPermission... permissions) { - for (WildcardPermission permission : permissions) { - if (!hasPermission(permission)) { - return false; - } - } - return true; - } - - public boolean hasPermission(WildcardPermission permission) { - return hasPermission(permission, /* ownership */ null, /* acl */ null); - } - /** * Checks whether the user has the permission to the given action for the given object. */ diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java index e9c026057f1..d478a690e30 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/RoleDefinitionsPanel.java @@ -268,8 +268,10 @@ public class RoleDefinitionsPanel extends VerticalPanel { private Set getAllPermissions() { Set allPermissionsAsStrings = new HashSet<>(); - for (final RoleDefinition roleFromAllRoles : getAllRoleDefinitions()) { - Util.addAll(roleFromAllRoles.getPermissions(), allPermissionsAsStrings); + for (HasPermissions permission : userService.getAllKnownPermissions()) { + for (HasPermissions.Action action : permission.getAvailableActions()) { + allPermissionsAsStrings.add(permission.getPermission(action)); + } } return allPermissionsAsStrings; } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserGroupManagementPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserGroupManagementPanel.java index 0b606dedf8f..deff635a994 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserGroupManagementPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/UserGroupManagementPanel.java @@ -127,4 +127,9 @@ public class UserGroupManagementPanel extends Composite { userGroupDetailPanel.updateUserList(); userGroupRoleDefinitionPanel.updateUserGroups(); } + + public void refreshSuggests() { + userGroupDetailPanel.refreshSuggest(); + userGroupRoleDefinitionPanel.refreshSuggest(); + } } \ No newline at end of file diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/roles/RoleDefinitionSuggestOracle.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/roles/RoleDefinitionSuggestOracle.java index f22d20adb36..b9f99fdaac5 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/roles/RoleDefinitionSuggestOracle.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/roles/RoleDefinitionSuggestOracle.java @@ -24,12 +24,22 @@ public class RoleDefinitionSuggestOracle extends MultiWordSuggestOracle { private final Map allRoles = new HashMap<>(); private boolean initialized = false; + private final UserManagementServiceAsync userManagementService; + private final StringMessages stringMessages; public RoleDefinitionSuggestOracle(final UserManagementServiceAsync userManagementService, final StringMessages stringMessages) { + this.userManagementService = userManagementService; + this.stringMessages = stringMessages; + refresh(); + } + + public void refresh() { + initialized = false; userManagementService.getRoleDefinitions(new AsyncCallback>() { @Override public void onSuccess(ArrayList result) { + allRoles.clear(); for (RoleDefinitionDTO role : result) { allRoles.put(role.getName(), new StrippedRoleDefinitionDTO(role.getId(), role.getName(), role.getPermissions())); diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/roles/UserGroupRoleDefinitionPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/roles/UserGroupRoleDefinitionPanel.java index a5767c94392..bb7dbaa32f9 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/roles/UserGroupRoleDefinitionPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/roles/UserGroupRoleDefinitionPanel.java @@ -40,6 +40,7 @@ public class UserGroupRoleDefinitionPanel extends Composite private final SingleSelectionModel userGroupSelectionModel; private final SuggestBox suggestRole; private final UserGroupRoleResources userGroupRoleResources = GWT.create(UserGroupRoleResources.class); + private final RoleDefinitionSuggestOracle oracle; public UserGroupRoleDefinitionPanel(final UserService userService, final StringMessages stringMessages, Iterable additionalPermissions, ErrorReporter errorReporter, @@ -49,8 +50,9 @@ public class UserGroupRoleDefinitionPanel extends Composite this.userGroupSelectionModel = userGroupSelectionModel; final VerticalPanel mainPanel = new VerticalPanel(); + oracle = new RoleDefinitionSuggestOracle(userService.getUserManagementService(), stringMessages); suggestRole = new SuggestBox( - new RoleDefinitionSuggestOracle(userService.getUserManagementService(), stringMessages)); + oracle); userGroupRoleResources.css().ensureInjected(); suggestRole.addStyleName(userGroupRoleResources.css().roleDefinitionSuggest()); suggestRole.getElement().setPropertyString("placeholder", stringMessages.enterRoleName()); @@ -95,6 +97,10 @@ public class UserGroupRoleDefinitionPanel extends Composite } } + public void refreshSuggest() { + oracle.refresh(); + } + /** Creates the button bar with add/remove/refresh buttons and the SuggestBox. */ private Widget createButtonPanel(final UserService userService, final StringMessages stringMessages) { final AccessControlledButtonPanel buttonPanel = new AccessControlledButtonPanel(userService, USER_GROUP); diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/users/UserGroupDetailPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/users/UserGroupDetailPanel.java index 064f0dd51f0..cadf6964469 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/users/UserGroupDetailPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/users/UserGroupDetailPanel.java @@ -235,6 +235,10 @@ public class UserGroupDetailPanel extends Composite oracle.resetAndRemoveExistingUsers(getSelectedUserGroupUsernames()); } + public void refreshSuggest() { + oracle.refresh(); + } + private List getSelectedUserGroupUsernames() { final UserGroupDTO tenant = userGroupSelectionModel.getSelectedObject(); return tenant == null ? Collections.emptyList() diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/users/UserGroupSuggestOracle.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/users/UserGroupSuggestOracle.java index 2c45fde034f..0102a926b8f 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/users/UserGroupSuggestOracle.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/component/usergroup/users/UserGroupSuggestOracle.java @@ -18,10 +18,17 @@ import com.sap.sse.security.ui.client.i18n.StringMessages; public class UserGroupSuggestOracle extends AbstractListSuggestOracle { private final Collection allUsers = new ArrayList<>(); + private final UserManagementServiceAsync userManagementService; + private final StringMessages stringMessages; public UserGroupSuggestOracle(final UserManagementServiceAsync userManagementService, final StringMessages stringMessages) { + this.userManagementService = userManagementService; + this.stringMessages = stringMessages; + refresh(); + } + public void refresh() { userManagementService.getUserList(new AsyncCallback>() { @Override public void onFailure(Throwable caught) { @@ -30,6 +37,7 @@ public class UserGroupSuggestOracle extends AbstractListSuggestOracle { @Override public void onSuccess(Collection result) { + allUsers.clear(); allUsers.addAll(result); setSelectableValues(result); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties index 676b428b026..7dce1118776 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties @@ -148,7 +148,6 @@ editUser=Edit User validated=Validated groups=Groups group=Group -validated=Validated errorDeletingUser=Error deleting user {0}: {1} doYouReallyWantToRemoveUser=Really remove user {0}? errorTryingToUpdateUser=Error trying to update user {0}: {1} diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_es.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_es.properties index 4995ccbac9c..199ad3c6819 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_es.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_es.properties @@ -34,6 +34,7 @@ userDetails=Detalles de usuario deleteUser=Borrar usuario deleteUserQuestion=¿Desea borrar el usuario? doYouReallyWantToDeleteUser=¿Está seguro de que desea borrar el usuario {0}? +doYouReallyWantToDeleteNUsers=¿Está seguro de que desea borrar los usuarios {0}? account={0}-Cuenta save=Grabar remove=Eliminar @@ -107,5 +108,85 @@ settingsRemovedError=No se han podido restablecer las parametrizaciones por defe showMoreInfoLogin=Regístrese para acceder a más funciones dismiss=Más tarde moreInfo=Más info +editACL=Editar ACL +errorEditingACL=Error al editar la lista de control de accesos (ACL). +editAnACL=Editar una lista de control de accesos (ACL). +createUserGroup=Crear grupo de usuarios +removeUserGroup=Eliminar grupo de usuarios +youHaveToSelectAUserGroup=Debe seleccionar un grupo de usuarios. +doYouReallyWantToRemoveUserGroup=¿Está seguro de que desea eliminar el grupo de usuarios {0}? +couldNotDeleteUserGroup=No se ha podido borrar el grupo de usuarios. +filterUserGroups=Filtrar grupos de usuarios... +id=ID +displayName=Visualizar nombre +userGroups=Grupos de usuarios +errorTryingToLoadRoles=Error al intentar cargar roles: {0} +filterRoles=Filtrar roles +actions=Acciones +actionEdit=Editar +actionRemove=Eliminar +actionChangeOwnership=Modificar propiedad +errorUpdatingOwnership=Error al actualizar la propiedad del objeto {0} +doYouReallyWantToRemoveRole=¿Está seguro de que desea eliminar el rol o los roles "{0}"? +errorTryingToDeleteRole=Error al intentar eliminar el rol "{0}": {1} +pleaseEnterARoleName=Indique un nombre de rol +editRoles=Editar roles +otherRoleWithNameAlreadyExists=Ya existe otro rol con el nombre "{0}". +errorCreatingRole=Error al crear el rol "{0}": {1} +errorEditingRoles=Error al editar el rol "{0}": {1} +usersInUserGroup=Usuarios en el grupo de usuarios +allUsers=Todos los usuarios +couldNotAddUserToUserGroup=No se ha podido añadir el usuario {0} al grupo de usuarios {1}: {2} +couldNotRemoveUserFromUserGroup=No se ha podido eliminar el usuario {0} del grupo de usuarios {1}: {2} +enterUserGroupName=Indicar nombre de grupo de usuarios +defaultTenant=Grupo de usuarios que debe utilizarse para la creación de objetos pagerStateInfo={0,number,#,###}-{1,number,#,###} de {2,number,#,###} -pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} de más de {2,number,#,###} \ No newline at end of file +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} de más de {2,number,#,###} +yes=Sí +no=No +editUser=Editar usuario +validated=Validado +groups=Grupos +group=Grupo +errorDeletingUser=Error al borrar el usuario {0}: {1} +doYouReallyWantToRemoveUser=¿Está seguro de que desea eliminar el usuario {0}? +errorTryingToUpdateUser=Error al intentar actualizar el usuario {0}: {1} +ownership=Propiedad +editObjectOwnership=Editar propiedad de objeto +errorObtainingUser=Error al obtener el usuario: {0} +errorObtainingUserGroup=Error al obtener el grupo de usuarios: {0} +pleaseWaitUntilUsernameIsResolved=Espere hasta que el nombre del usuario se haya resuelto +pleaseWaitUntilUserGroupNameIsResolved=Espere hasta que el nombre del grupo de usuarios se haya resuelto +userNotFound=El usuario {0} no existe +usergroupNotFound=El grupo de usuarios {0} no existe +actionChangeACL=Modificar ACL +editACLForObject=Editar ACL para ''{0}'' +acl=ACL +errorUpdatingAcl=Error al actualizar ACL: {0}. +allowedActionName=Nombre de acción permitido +deniedActionName=Nombre de acción denegado +errorMessageUserGroupNameNotFound=No se ha encontrado el grupo de usuarios por nombre {0}. +successMessageAddedUserGroup=Grupo de usuarios añadido {0}. +successMessageRemovedUserGroup=Grupo de usuarios eliminado {0}. +allowedActions=Acciones permitidas +deniedActions=Acciones denegadas +migrateGroupOwner=Migrar propietario de grupo para jerarquía +errorDeletingUserGroup=Error al borrar el grupo de usuarios {0}: {1} +enterAtLeastOneOwner=Indique como mínimo un propietario +removeRole=Eliminar rol +addRole=Añadir rol +couldNotAddRoleToGroup=El rol {0} no se ha podido añadir al grupo {1}. +roleName=Nombre de rol +enabledForAllUsers=Activado para todos los usuarios +couldNotDeleteRole=No se ha podido borrar el rol {0}. +couldNotLoadRoles=No se han podido cargar los roles. +enterUsername=Indicar nombre de usuario +addUser=Añadir usuario +couldNotLoadUsers=No se han podido cargar los usuarios. +couldNotRemoveRoleFromUser=No se ha podido eliminar el rol ''{1}'' del usuario ''{0}''. +couldNotRemovePermissionFromUser=No se ha podido eliminar el permiso ''{1}'' del usuario ''{0}''. +filterPermission=Autorización de filtro +permission=Autorización +nullUserGroup= +editRolesAndPermissionsForUser=Editar roles y autorizaciones para el usuario {0} +permissionType:Tipo de autorización diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_fr.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_fr.properties index e601e7d120b..1f9d3054654 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_fr.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_fr.properties @@ -34,6 +34,7 @@ userDetails=Détails utilisateur deleteUser=Supprimer utilisateur deleteUserQuestion=Supprimer l''utilisateur ? doYouReallyWantToDeleteUser=Souhaitez-vous vraiment supprimer l''utilisateur {0} ? +doYouReallyWantToDeleteNUsers=Voulez-vous vraiment supprimer {0} utilisateur(s) ? account=Compte {0} save=Enregistrer remove=Supprimer @@ -107,5 +108,85 @@ settingsRemovedError=Impossible de restaurer les options par défaut showMoreInfoLogin=Connectez-vous pour accéder à davantage de fonctionnalités dismiss=Plus tard moreInfo=Plus d''infos -pagerStateInfo={0,number,#,###}-{1,number,#,###} de {2,number,#,###} -pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} de sur {2,number,#,###} \ No newline at end of file +editACL=Modifier la liste de contrôle d''accès (ACL) +errorEditingACL=Erreur lors de la modification de la liste de contrôle d''accès (ACL) +editAnACL=Modifier une liste de contrôle d''accès (ACL) +createUserGroup=Créer groupe d''utilisateurs +removeUserGroup=Supprimer groupe d''utilisateurs +youHaveToSelectAUserGroup=Vous devez sélectionner un groupe d''utilisateurs. +doYouReallyWantToRemoveUserGroup=Souhaitez-vous vraiment supprimer le groupe d''utilisateurs {0} ? +couldNotDeleteUserGroup=Impossible de supprimer le groupe d''utilisateurs +filterUserGroups=Filtrer les groupes d''utilisateurs... +id=ID +displayName=Nom d''affichage +userGroups=Groupes d''utilisateurs +errorTryingToLoadRoles=Erreur lors du chargement de rôles : {0} +filterRoles=Filtrer rôles +actions=Actions +actionEdit=Modifier +actionRemove=Supprimer +actionChangeOwnership=Modifier détention +errorUpdatingOwnership=Erreur lors de la mise à jour de la détention de l''objet {0}. +doYouReallyWantToRemoveRole=Souhaitez-vous vraiment supprimer le(s) rôle(s) "{0}" ? +errorTryingToDeleteRole=Erreur lors de la suppression du rôle"{0}" : {1} +pleaseEnterARoleName=Saisissez un nom de rôle. +editRoles=Modifier rôles +otherRoleWithNameAlreadyExists=Un rôle du nom "{0}" existe déjà. +errorCreatingRole=Erreur lors de la création du rôle "{0}" : {1} +errorEditingRoles=Erreur lors de la modification du rôle "{0}" : {1} +usersInUserGroup=Utilisateurs dans le groupe d''utilisateurs +allUsers=Tous les utilisateurs +couldNotAddUserToUserGroup=Impossible d''ajouter l''utilisateur {0} au groupe d''utilisateurs {1} : {2} +couldNotRemoveUserFromUserGroup=Impossible de supprimer l''utilisateur {0} du groupe d''utilisateurs {1} : {2} +enterUserGroupName=Saisissez le nom du groupe d''utilisateurs. +defaultTenant=Groupe d''utilisateurs à utiliser pour la création d''objet +pagerStateInfo={0,number,#,###}-{1,number,#,###} sur {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} sur plus de {2,number,#,###} +yes=Oui +no=Non +editUser=Modifier utilisateur +validated=Validé +groups=Groupes +group=Groupe +errorDeletingUser=Erreur lors de la suppression de l''utilisateur {0} : {1} +doYouReallyWantToRemoveUser=Supprimer vraiment l''utilisateur {0} ? +errorTryingToUpdateUser=Erreur lors de la mise à jour de l''utilisateur {0} : {1} +ownership=Détention +editObjectOwnership=Modifier la détention de l''objet +errorObtainingUser=Erreur lors de l''accès à l''utilisateur : {0} +errorObtainingUserGroup=Erreur lors de l''accès au groupe d''utilisateurs : {0} +pleaseWaitUntilUsernameIsResolved=Veuillez patienter jusqu''à la résolution du nom d''utilisateur. +pleaseWaitUntilUserGroupNameIsResolved=Veuillez patienter jusqu''à la résolution du nom du groupe d''utilisateurs. +userNotFound=Utilisateur {0} introuvable +usergroupNotFound=Groupe d''utilisateurs {0} introuvable +actionChangeACL=Modifier l''ACL +editACLForObject=Modifier l''ACL en ''{0}'' +acl=ACL +errorUpdatingAcl=Erreur lors de la mise à jour de l''ACL : {0} +allowedActionName=Nom de l''action autorisée +deniedActionName=Nom de l''action refusée +errorMessageUserGroupNameNotFound=La recherche du groupe d''utilisateurs par le nom {0} a échoué. +successMessageAddedUserGroup=Groupe d''utilisateurs {0} ajouté +successMessageRemovedUserGroup=Groupe d''utilisateurs {0} supprimé +allowedActions=Actions autorisées +deniedActions=Actions refusées +migrateGroupOwner=Migrer détenteur de groupe pour hiérarchie +errorDeletingUserGroup=Erreur lors de la suppression du groupe d''utilisateurs {0} : {1} +enterAtLeastOneOwner=Veuillez saisir au moins un détenteur. +removeRole=Supprimer rôle +addRole=Ajouter rôle +couldNotAddRoleToGroup=Le rôle {0} n''a pas pu être ajouté au groupe {1}. +roleName=Nom du rôle +enabledForAllUsers=Activé pour tous les utilisateurs +couldNotDeleteRole=Impossible de supprimer le rôle {0} +couldNotLoadRoles=Impossible de charger les rôles +enterUsername=Saisissez le nom d''utilisateur. +addUser=Ajouter utilisateur +couldNotLoadUsers=Impossible de charger les utilisateurs +couldNotRemoveRoleFromUser=Impossible de supprimer le rôle ''{1}'' de l''utilisateur ''{0}''. +couldNotRemovePermissionFromUser=Impossible de supprimer l''autorisation ''{1}'' de l''utilisateur ''{0}''. +filterPermission=Filtrer autorisations +permission=Autorisation +nullUserGroup= +editRolesAndPermissionsForUser=Modifier les rôles et autorisations pour l''utilisateur {0} +permissionType:Type d''autorisation diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties index ee84f8f0656..9ed1601907d 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties @@ -34,6 +34,7 @@ userDetails=ユーザ詳細 deleteUser=ユーザ削除 deleteUserQuestion=ユーザを削除しますか doYouReallyWantToDeleteUser=ユーザ {0} を削除しますか +doYouReallyWantToDeleteNUsers={0} ユーザを削除しますか account={0} アカウント save=保存 remove=削除 @@ -107,5 +108,85 @@ settingsRemovedError=デフォルト設定を復元できませんでした showMoreInfoLogin=サインインして追加機能へのアクセス権限を取得してください dismiss=後で moreInfo=追加情報 -pagerStateInfo={2,number,#,###} の {0,number,#,###}-{1,number,#,###} -pagerStateInfo[false]={2,number,#,###} 超の {0,number,#,###}-{1,number,#,###} \ No newline at end of file +editACL=ACL 編集 +errorEditingACL=アクセスコントロールリスト (ACL) の編集でエラーが発生しました。 +editAnACL=アクセスコントロールリスト (ACL) を編集 +createUserGroup=ユーザグループを作成 +removeUserGroup=ユーザグループを削除 +youHaveToSelectAUserGroup=ユーザグループを選択する必要があります。 +doYouReallyWantToRemoveUserGroup=ユーザグループ {0} を削除しますか。 +couldNotDeleteUserGroup=ユーザグループを削除できませんでした。 +filterUserGroups=ユーザグループのフィルタリング... +id=ID +displayName=表示名 +userGroups=ユーザグループ +errorTryingToLoadRoles=ロールをロードしようとしてエラーが発生: {0} +filterRoles=ロールのフィルタリング +actions=アクション +actionEdit=編集 +actionRemove=削除 +actionChangeOwnership=オーナーシップ変更 +errorUpdatingOwnership=オブジェクト {0} のオーナーシップ更新でエラーが発生しました +doYouReallyWantToRemoveRole=ロール "{0}" を削除しますか +errorTryingToDeleteRole=ロール "{0}" を削除しようとしてエラーが発生: {1} +pleaseEnterARoleName=ロール名を入力してください +editRoles=ロール編集 +otherRoleWithNameAlreadyExists="{0}" という名前の別のロールはすでに存在しています。 +errorCreatingRole=ロール "{0}" の作成でエラー発生: {1} +errorEditingRoles=ロール "{0}" の編集でエラー発生: {1} +usersInUserGroup=ユーザグループにあるユーザ +allUsers=すべてのユーザ +couldNotAddUserToUserGroup=ユーザ {0} をユーザグループ {1} に追加できませんでした: {2} +couldNotRemoveUserFromUserGroup=ユーザ {0} をユーザグループ {1} から削除できませんでした: {2} +enterUserGroupName=ユーザグループ名を入力 +defaultTenant=オブジェクト作成に使用するユーザグループ +pagerStateInfo={2,number,#,###} の {0,number,#,###} ~ {1,number,#,###} +pagerStateInfo[false]={2,number,#,###} 上の {0,number,#,###} ~ {1,number,#,###} +yes=はい +no=いいえ +editUser=ユーザ編集 +validated=チェック済 +groups=グループ +group=グループ +errorDeletingUser=ユーザ {0} の削除でエラー発生: {1} +doYouReallyWantToRemoveUser=ユーザ {0} を削除しますか +errorTryingToUpdateUser=ユーザ {0} を更新しようとしてエラーが発生: {1} +ownership=オーナーシップ +editObjectOwnership=オブジェクトオーナーシップ編集 +errorObtainingUser=ユーザの取得でエラーが発生: {0} +errorObtainingUserGroup=ユーザグループの取得でエラーが発生: {0} +pleaseWaitUntilUsernameIsResolved=ユーザ名が削除されるまで待ってください +pleaseWaitUntilUserGroupNameIsResolved=ユーザグループ名が解決されるまで待ってください +userNotFound=ユーザ {0} は見つかりませんでした +usergroupNotFound=ユーザグループ {0} は見つかりませんでした +actionChangeACL=ACL 変更 +editACLForObject=''{0}'' の ACL を編集 +acl=ACL +errorUpdatingAcl=ACL の更新でエラー発生: {0} +allowedActionName=許可されたアクション名 +deniedActionName=拒否されたアクション名 +errorMessageUserGroupNameNotFound={0} という名前でユーザグループが見つかりませんでした。 +successMessageAddedUserGroup=ユーザグループ {0} が追加されました。 +successMessageRemovedUserGroup=ユーザグループ {0} が削除されました。 +allowedActions=許可されたアクション +deniedActions=拒否されたアクション +migrateGroupOwner=階層のグループオーナーを移行 +errorDeletingUserGroup=ユーザグループ {0} の削除でエラーが発生: {1} +enterAtLeastOneOwner=少なくとも 1 名のオーナーを入力してください +removeRole=ロール削除 +addRole=ロール追加 +couldNotAddRoleToGroup=ロール {0} をグループ {1} に追加できませんでした。 +roleName=ロール名 +enabledForAllUsers=すべてのユーザに対して有効化されました +couldNotDeleteRole=ロール {0} を削除できませんでした。 +couldNotLoadRoles=ロールをロードできませんでした。 +enterUsername=ユーザ名を入力してください +addUser=ユーザ追加 +couldNotLoadUsers=ユーザをロードできませんでした。 +couldNotRemoveRoleFromUser=ロール ''{1}'' をユーザ ''{0}'' から削除できませんでした。 +couldNotRemovePermissionFromUser=権限 ''{1}'' をユーザ ''{0}'' から削除できませんでした。 +filterPermission=権限のフィルタリング +permission=権限 +nullUserGroup= +editRolesAndPermissionsForUser=ユーザ {0} のロールと権限を編集 +permissionType:権限タイプ diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_pt.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_pt.properties index 18ff4684cd6..019347dd719 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_pt.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_pt.properties @@ -34,6 +34,7 @@ userDetails=Detalhes do usuário deleteUser=Eliminar usuário deleteUserQuestion=Eliminar usuário? doYouReallyWantToDeleteUser=Eliminar o usuário {0}? +doYouReallyWantToDeleteNUsers=Eliminar {0} usuários? account=Conta {0} save=Gravar remove=Eliminar @@ -107,5 +108,85 @@ settingsRemovedError=Não foi possível restaurar configurações padrão showMoreInfoLogin=Efetue o logon para obter acesso a mais funcionalidades dismiss=Mais tarde moreInfo=Mais informação +editACL=Processar ACL +errorEditingACL=Erro ao processar lista de controle de acesso (ACL). +editAnACL=Processar uma lista de controle de acesso (ACL) +createUserGroup=Criar grupo de usuários +removeUserGroup=Remover grupo de usuários +youHaveToSelectAUserGroup=Você precisa selecionar um grupo de usuários. +doYouReallyWantToRemoveUserGroup=Remover o grupo de usuários {0}? +couldNotDeleteUserGroup=Não foi possível eliminar grupo de usuários. +filterUserGroups=Filtrar grupos de usuários... +id=ID +displayName=Nome de exibição +userGroups=Grupos de usuários +errorTryingToLoadRoles=Erro ao tentar carregar funções: {0} +filterRoles=Filtrar funções +actions=Ações +actionEdit=Processar +actionRemove=Remover +actionChangeOwnership=Modificar propriedade +errorUpdatingOwnership=Erro ao atualizar propriedade de objeto {0} +doYouReallyWantToRemoveRole=Remover funções "{0}"? +errorTryingToDeleteRole=Erro ao tentar eliminar função "{0}": {1} +pleaseEnterARoleName=Inserir um nome de função +editRoles=Processar funções +otherRoleWithNameAlreadyExists=Já existe outra função com o nome "{0}". +errorCreatingRole=Erro ao criar função "{0}": {1} +errorEditingRoles=Erro ao processar função "{0}": {1} +usersInUserGroup=Usuários no grupo de usuários +allUsers=Todos os usuários +couldNotAddUserToUserGroup=Não foi possível adicionar o usuário {0} ao grupo de usuários {1}: {2} +couldNotRemoveUserFromUserGroup=Não foi possível remover o usuário {0} do grupo de usuários {1}: {2} +enterUserGroupName=Inserir nome do grupo de usuários +defaultTenant=Grupo de usuários para utilizar para criação de objeto pagerStateInfo={0,number,#,###}-{1,number,#,###} de {2,number,#,###} -pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} de mais de {2,number,#,###} \ No newline at end of file +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} de mais de {2,number,#,###} +yes=Sim +no=Não +editUser=Processar usuário +validated=Validado +groups=Grupos +group=Grupo +errorDeletingUser=Erro ao eliminar usuário {0}: {1} +doYouReallyWantToRemoveUser=Remover usuário {0}? +errorTryingToUpdateUser=Erro ao tentar atualizar usuário {0}: {1} +ownership=Propriedade +editObjectOwnership=Processar propriedade de objeto +errorObtainingUser=Erro ao obter usuário: {0} +errorObtainingUserGroup=Erro ao obter grupo de usuários: {0} +pleaseWaitUntilUsernameIsResolved=Aguardar até nome de usuário estar resolvido +pleaseWaitUntilUserGroupNameIsResolved=Aguardar até nome de grupo de usuários estar resolvido +userNotFound=Usuário {0} não encontrado +usergroupNotFound=Grupo de usuários {0} não encontrado +actionChangeACL=Modificar ACL +editACLForObject=Processar ACL para ''{0}'' +acl=ACL +errorUpdatingAcl=Erro ao atualizar ACL: {0}. +allowedActionName=Nome de ação permitido +deniedActionName=Nome de ação recusado +errorMessageUserGroupNameNotFound=Grupo de usuários não encontrado por nome {0}. +successMessageAddedUserGroup=Grupo de usuários adicionado {0}. +successMessageRemovedUserGroup=Grupo de usuários removido {0}. +allowedActions=Ações permitidas +deniedActions=Ações recusadas +migrateGroupOwner=Migrar proprietário do objeto para hierarquia +errorDeletingUserGroup=Erro ao eliminar grupo de usuários {0}: {1} +enterAtLeastOneOwner=Inserir pelo menos um proprietário +removeRole=Remover função +addRole=Adicionar função +couldNotAddRoleToGroup=Não foi possível adicionar função {0} ao grupo {1}. +roleName=Nome da função +enabledForAllUsers=Ativado para todos os usuários +couldNotDeleteRole=Não foi possível eliminar função {0}. +couldNotLoadRoles=Não foi possível carregar funções. +enterUsername=Inserir nome do usuário +addUser=Adicionar usuário +couldNotLoadUsers=Não foi possível carregar usuários. +couldNotRemoveRoleFromUser=Não foi possível remover função ''{1}'' de usuário ''{0}''. +couldNotRemovePermissionFromUser=Não foi possível remover autorização ''{1}'' do usuário ''{0}''. +filterPermission=Filtrar autorização +permission=Autorização +nullUserGroup= +editRolesAndPermissionsForUser=Processar funções e autorizações para usuário {0} +permissionType:Tipo de autorização diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties index 1d22671e9d9..8d76edd19ee 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties @@ -34,6 +34,7 @@ userDetails=Данные пользователя deleteUser=Удалить пользователя deleteUserQuestion=Удалить пользователя? doYouReallyWantToDeleteUser=Действительно удалить пользователя {0}? +doYouReallyWantToDeleteNUsers=Удалить {0} пользователей? account={0}-учетная запись save=Сохранить remove=Удалить @@ -107,5 +108,85 @@ settingsRemovedError=Не удалось восстановить настрой showMoreInfoLogin=Выполните вход, чтобы получить доступ к дополнительным функциям dismiss=Позже moreInfo=Дополнительная информация +editACL=Редактировать ACL +errorEditingACL=Ошибка при редактировании списка контроля доступа (ACL) +editAnACL=Редактировать список контроля доступа (ACL) +createUserGroup=Создать группу пользователей +removeUserGroup=Удалить группу пользователей +youHaveToSelectAUserGroup=Необходимо выбрать группу пользователей. +doYouReallyWantToRemoveUserGroup=Действительно удалить группу пользователей {0}? +couldNotDeleteUserGroup=Не удалось удалить группу пользователей. +filterUserGroups=Фильтровать группы пользователей... +id=Ид. +displayName=Отображаемое имя +userGroups=Группы пользователей +errorTryingToLoadRoles=Ошибка при попытке загрузки ролей: {0} +filterRoles=Фильтровать роли +actions=Операции +actionEdit=Редактировать +actionRemove=Удалить +actionChangeOwnership=Изменить владение +errorUpdatingOwnership=Ошибка при обновлении владения для объекта {0} +doYouReallyWantToRemoveRole=Действительно удалить роль/роли "{0}"? +errorTryingToDeleteRole=Ошибка при попытке удаления роли "{0}": {1} +pleaseEnterARoleName=Введите имя роли +editRoles=Редактировать роли +otherRoleWithNameAlreadyExists=Уже существует другая роль с именем "{0}". +errorCreatingRole=Ошибка при создании роли "{0}": {1} +errorEditingRoles=Ошибка при редактировании роли "{0}": {1} +usersInUserGroup=Пользователи в группе пользователей +allUsers=Все пользователи +couldNotAddUserToUserGroup=Не удалось добавить пользователя {0} в группу пользователей {1}: {2} +couldNotRemoveUserFromUserGroup=Не удалось удалить пользователя {0} из группы пользователей {1}: {2} +enterUserGroupName=Введите имя группы пользователей +defaultTenant=Группа пользователей для создания объекта pagerStateInfo={0,number,#,###}-{1,number,#,###} из {2,number,#,###} -pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} из {2,number,#,###} \ No newline at end of file +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} из более чем {2,number,#,###} +yes=Да +no=Нет +editUser=Редактировать пользователя +validated=Проверено +groups=Группы +group=Группа +errorDeletingUser=Ошибка при удалении пользователя {0}: {1} +doYouReallyWantToRemoveUser=Действительно удалить пользователя {0}? +errorTryingToUpdateUser=Ошибка при попытке обновления пользователя {0}: {1} +ownership=Владение +editObjectOwnership=Редактировать владение объектом +errorObtainingUser=Ошибка при получении пользователя: {0} +errorObtainingUserGroup=Ошибка при получении группы пользователей: {0} +pleaseWaitUntilUsernameIsResolved=Дождитесь разрешения имени пользователя +pleaseWaitUntilUserGroupNameIsResolved=Дождитесь разрешения имени группы пользователей +userNotFound=Пользователь {0} не найден +usergroupNotFound=Группа пользователей {0} не найдена +actionChangeACL=Изменить ACL +editACLForObject=Редактировать ACL для ''{0}'' +acl=ACL +errorUpdatingAcl=Ошибка при обновлении ACL: {0}. +allowedActionName=Имя разрешенной операции +deniedActionName=Имя отклоненной операции +errorMessageUserGroupNameNotFound=Не удалось найти группу пользователей пл имени {0}. +successMessageAddedUserGroup=Группа пользователей {0} добавлена. +successMessageRemovedUserGroup=Группа пользователей {0} удалена. +allowedActions=Разрешенные операции +deniedActions=Отклоненные операции +migrateGroupOwner=Выполнить миграцию владельца группы для иерархии +errorDeletingUserGroup=Ошибка при удалении группы пользователей {0}: {1} +enterAtLeastOneOwner=Введите хотя бы одного владельца +removeRole=Удалить роль +addRole=Добавить роль +couldNotAddRoleToGroup=Роль {0} не удалось добавить в группу {1} +roleName=Имя роли +enabledForAllUsers=Активировано для всех пользователей +couldNotDeleteRole=Не удалось удалить роль {0}. +couldNotLoadRoles=Не удалось загрузить роли. +enterUsername=Введите имя пользователя +addUser=Добавить пользователя +couldNotLoadUsers=Не удалось загрузить пользователей. +couldNotRemoveRoleFromUser=Не удалось удалить роль ''{1}'' у пользователя ''{0}''. +couldNotRemovePermissionFromUser=Не удалось удалить полномочия ''{1}'' у пользователя ''{0}''. +filterPermission=Фильтр полномочий +permission=Полномочия +nullUserGroup= +editRolesAndPermissionsForUser=Редактировать роли и полномочия для пользователя {0} +permissionType:Тип полномочий diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties index 0141edd0b0a..fc6e820456d 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties @@ -34,6 +34,7 @@ userDetails=用户详细信息 deleteUser=删除用户 deleteUserQuestion=是否删除用户? doYouReallyWantToDeleteUser=是否确定删除用户{0}? +doYouReallyWantToDeleteNUsers=是否确定删除{0}个用户? account={0} 账户 save=保存 remove=移除 @@ -107,5 +108,85 @@ settingsRemovedError=无法恢复默认设置 showMoreInfoLogin=请登录,以访问更多功能 dismiss=稍后 moreInfo=更多信息 +editACL=编辑 ACL +errorEditingACL=编辑访问控制清单 (ACL) 出错。 +editAnACL=编辑访问控制清单 (ACL) +createUserGroup=创建用户组 +removeUserGroup=移除用户组 +youHaveToSelectAUserGroup=需要选择用户组。 +doYouReallyWantToRemoveUserGroup=是否确定移除用户组{0}? +couldNotDeleteUserGroup=无法删除用户组。 +filterUserGroups=筛选用户组... +id=编号 +displayName=显示名称 +userGroups=用户组 +errorTryingToLoadRoles=尝试加载角色出错:{0} +filterRoles=筛选角色 +actions=操作 +actionEdit=编辑 +actionRemove=移除 +actionChangeOwnership=更改所有权 +errorUpdatingOwnership=更新对象{0}的所有权出错 +doYouReallyWantToRemoveRole=是否确定移除角色 "{0}"? +errorTryingToDeleteRole=尝试删除角色 "{0}" 出错:{1} +pleaseEnterARoleName=请输入角色名称 +editRoles=编辑角色 +otherRoleWithNameAlreadyExists=名称为 "{0}" 的另一个角色已存在。 +errorCreatingRole=创建角色 "{0}" 出错:{1} +errorEditingRoles=编辑角色 "{0}" 出错:{1} +usersInUserGroup=用户组中的用户 +allUsers=所有用户 +couldNotAddUserToUserGroup=无法将用户{0}添加到用户组{1}:{2} +couldNotRemoveUserFromUserGroup=无法将用户{0}从用户组{1}中移除:{2} +enterUserGroupName=输入用户组名称 +defaultTenant=用户对象创建的用户组 pagerStateInfo={0,number,#,###}-{1,number,#,###}/{2,number,#,###} -pagerStateInfo[false]={0,number,#,###}-{1,number,#,###}/{2,number,#,###} 以上 \ No newline at end of file +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###}(超过{2,number,#,###}) +yes=是 +no=否 +editUser=编辑用户 +validated=已验证 +groups=组 +group=组 +errorDeletingUser=删除用户{0}时出错:{1} +doYouReallyWantToRemoveUser=是否确定移除用户{0}? +errorTryingToUpdateUser=尝试更新用户{0}时出错:{1} +ownership=所有权 +editObjectOwnership=编辑对象所有权 +errorObtainingUser=获取用户出错:{0} +errorObtainingUserGroup=获取用户组出错:{0} +pleaseWaitUntilUsernameIsResolved=请等待解析用户名称 +pleaseWaitUntilUserGroupNameIsResolved=请等待解析用户组名称 +userNotFound=未找到用户{0} +usergroupNotFound=未找到用户组{0} +actionChangeACL=更改 ACL +editACLForObject=为 ''{0}'' 编辑 ACL +acl=ACL +errorUpdatingAcl=更新 ACL 出错:{0}。 +allowedActionName=允许的操作名称 +deniedActionName=拒绝的操作名称 +errorMessageUserGroupNameNotFound=按名称{0}未找到用户组。 +successMessageAddedUserGroup=已添加用户组{0}。 +successMessageRemovedUserGroup=已移除用户组{0}。 +allowedActions=允许的操作 +deniedActions=拒绝的操作 +migrateGroupOwner=为层次结构迁移组所有者 +errorDeletingUserGroup=删除用户组{0}出错:{1} +enterAtLeastOneOwner=请至少输入一个所有者 +removeRole=移除角色 +addRole=添加角色 +couldNotAddRoleToGroup=无法将角色{0}添加到组{1}。 +roleName=角色名称 +enabledForAllUsers=已为所有用户启用 +couldNotDeleteRole=无法删除角色{0}。 +couldNotLoadRoles=无法加载角色。 +enterUsername=输入用户名称 +addUser=添加用户 +couldNotLoadUsers=无法加载用户。 +couldNotRemoveRoleFromUser=无法将角色 ''{1}'' 从用户 ''{0}'' 中移除。 +couldNotRemovePermissionFromUser=无法将权限 ''{1}'' 从用户 ''{0}'' 中移除。 +filterPermission=筛选权限 +permission=权限 +nullUserGroup= +editRolesAndPermissionsForUser=为用户{0}编辑角色和权限 +permissionType:权限类型 diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserManagementPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserManagementPanel.java index 73cebaf40d7..85aca163136 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserManagementPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserManagementPanel.java @@ -44,6 +44,7 @@ public class UserManagementPanel exte private final UserTableWrapper, TR> userList; private final RefreshableMultiSelectionModel userSelectionModel; private final TextBox userNameTextbox; + private final UserRoleDefinitionPanel userRoleDefinitionPanel; public UserManagementPanel(final UserService userService, final StringMessages stringMessages, ErrorReporter errorReporter, TR tableResources) { @@ -56,7 +57,10 @@ public class UserManagementPanel exte final VerticalPanel west = new VerticalPanel(); final AccessControlledButtonPanel buttonPanel = new AccessControlledButtonPanel(userService, USER); west.add(buttonPanel); - buttonPanel.addUnsecuredAction(stringMessages.refresh(), this::updateUsers); + userList = new UserTableWrapper<>(userService, additionalPermissions, stringMessages, errorReporter, + /* multiSelection */ true, /* enablePager */ true, tableResources); + buttonPanel.addUnsecuredAction(stringMessages.refresh(), + () -> userList.refreshUserList((Callback, Throwable>) null, false)); buttonPanel.addCreateActionWithoutServerCreateObjectPermissionCheck(stringMessages.createUser(), () -> new CreateUserDialog(stringMessages, userManagementService, userCreatedHandlers, userService) .show()); @@ -67,8 +71,6 @@ public class UserManagementPanel exte userNameTextbox.addKeyUpHandler( e -> editRolesAndPermissionsForUserButton.setEnabled(!userNameTextbox.getText().isEmpty())); editRolesAndPermissionsForUserButton.setEnabled(false); - userList = new UserTableWrapper<>(userService, additionalPermissions, stringMessages, errorReporter, - /* multiSelection */ true, /* enablePager */ true, tableResources); userSelectionModel = userList.getSelectionModel(); final Button deleteButton = buttonPanel.addRemoveAction(stringMessages.remove(), () -> { assert userSelectionModel.getSelectedSet().size() > 0; @@ -134,7 +136,7 @@ public class UserManagementPanel exte final HorizontalPanel detailsPanel = new HorizontalPanel(); // add details panel for user roles - final UserRoleDefinitionPanel userRoleDefinitionPanel = new UserRoleDefinitionPanel(userService, stringMessages, + userRoleDefinitionPanel = new UserRoleDefinitionPanel(userService, stringMessages, errorReporter, tableResources, userList.getSelectionModel(), () -> updateUsers()); detailsPanel.add(userRoleDefinitionPanel); @@ -180,6 +182,10 @@ public class UserManagementPanel exte }); } + public void refreshSuggests() { + userRoleDefinitionPanel.refreshSuggest(); + } + public void updateUsers() { userList.refreshUserList((Callback, Throwable>) null); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserTableWrapper.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserTableWrapper.java index fbfa5605a92..a18f6cbf456 100755 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserTableWrapper.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/UserTableWrapper.java @@ -244,7 +244,10 @@ extends TableWrapper { /** * @param callback optional; may be {@code null} */ - public void refreshUserList(final Callback, Throwable> callback) { + public void refreshUserList(final Callback, Throwable> callback, boolean clearSelection) { + if (clearSelection) { + getSelectionModel().clear(); + } final AsyncCallback> myCallback = new AsyncCallback>() { @Override public void onFailure(Throwable caught) { @@ -266,6 +269,10 @@ extends TableWrapper { getUserManagementService().getUserList(myCallback); } + public void refreshUserList(final Callback, Throwable> callback) { + refreshUserList(callback, true); + } + private void getFilteredUsers(Iterable result) { filterField.updateAll(result); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/roles/UserRoleDefinitionPanel.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/roles/UserRoleDefinitionPanel.java index 0020b33f9d5..a9204465401 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/roles/UserRoleDefinitionPanel.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/usermanagement/roles/UserRoleDefinitionPanel.java @@ -49,6 +49,7 @@ public class UserRoleDefinitionPanel extends HorizontalPanel private final SuggestBox suggestRole; private final RoleAndPermissionDetailsResources roleAndPermissionDetailsResources = GWT .create(RoleAndPermissionDetailsResources.class); + private final RoleDefinitionSuggestOracle oracle; public UserRoleDefinitionPanel(final UserService userService, final StringMessages stringMessages, final ErrorReporter errorReporter, final CellTableWithCheckboxResources tableResources, @@ -72,7 +73,7 @@ public class UserRoleDefinitionPanel extends HorizontalPanel this.userSelectionModel = multiToSingleSelectionModelAdapter; // create role suggest - final RoleDefinitionSuggestOracle oracle = new RoleDefinitionSuggestOracle( + oracle = new RoleDefinitionSuggestOracle( userService.getUserManagementService(), stringMessages); suggestRole = new SuggestBox(oracle); roleAndPermissionDetailsResources.css().ensureInjected(); @@ -148,6 +149,10 @@ public class UserRoleDefinitionPanel extends HorizontalPanel add(captionPanel); } + public void refreshSuggest() { + oracle.refresh(); + } + private void initPlaceholder(final UIObject target, final String placeholder) { target.getElement().setAttribute("placeholder", placeholder); } diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java index 0919e2ffec4..16628d1e23c 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/server/UserManagementServiceImpl.java @@ -525,15 +525,12 @@ public class UserManagementServiceImpl extends RemoteServiceServlet implements U @Override public void updateSimpleUserPassword(final String username, String oldPassword, String passwordResetSecret, String newPassword) throws UserManagementException { final User user = getSecurityService().getUserByName(username); - getSecurityService().checkCurrentUserUpdatePermission(user); - if (getSecurityService().hasCurrentUserOneOfExplicitPermissions(user, UserActions.FORCE_OVERWRITE_PASSWORD)) { - // e.g. admin is allowed to update the password without knowing the old password and/or secret - getSecurityService().updateSimpleUserPassword(username, newPassword); - sendPasswordChangedMailAsync(username); - } else if (// someone knew a username and the correct password for that user + // Is, e.g., admin is allowed to update the password without knowing the old password and/or secret? + if (getSecurityService().hasCurrentUserOneOfExplicitPermissions(user, UserActions.FORCE_OVERWRITE_PASSWORD) + || // someone knew a username and the correct password for that user (oldPassword != null && getSecurityService().checkPassword(username, oldPassword)) - // someone provided the correct password reset secret for the correct username - || (passwordResetSecret != null && getSecurityService().checkPasswordResetSecret(username, passwordResetSecret))) { + || // someone provided the correct password reset secret for the correct username + (passwordResetSecret != null && getSecurityService().checkPasswordResetSecret(username, passwordResetSecret))) { getSecurityService().updateSimpleUserPassword(username, newPassword); sendPasswordChangedMailAsync(username); } else { @@ -881,6 +878,11 @@ public class UserManagementServiceImpl extends RemoteServiceServlet implements U @Override public void run() throws Exception { + final QualifiedObjectIdentifier qualifiedObjectAssociationIdentifier = SecuredSecurityTypes.ROLE_ASSOCIATION + .getQualifiedObjectIdentifier(associationTypeIdentifier); + getSecurityService().addToAccessControlList(qualifiedObjectAssociationIdentifier, + null, DefaultActions.READ.name()); + getSecurityService().addRoleForUser(user, role); logger.info(message); } @@ -1026,6 +1028,10 @@ public class UserManagementServiceImpl extends RemoteServiceServlet implements U @Override public void run() throws Exception { + final QualifiedObjectIdentifier qualifiedObjectAssociationIdentifier = SecuredSecurityTypes.PERMISSION_ASSOCIATION + .getQualifiedObjectIdentifier(associationTypeIdentifier); + getSecurityService().addToAccessControlList(qualifiedObjectAssociationIdentifier, + null, DefaultActions.READ.name()); getSecurityService().addPermissionForUser(username, permission); logger.info(message); } diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityWebSessionManager.java b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityWebSessionManager.java index ee10cc17df9..e13eecb32b6 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/SecurityWebSessionManager.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/SecurityWebSessionManager.java @@ -1,7 +1,5 @@ package com.sap.sse.security; -import java.util.WeakHashMap; - import org.apache.shiro.session.InvalidSessionException; import org.apache.shiro.session.Session; import org.apache.shiro.session.SessionException; @@ -13,6 +11,7 @@ import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import com.sap.sse.common.Duration; import com.sap.sse.common.impl.MillisecondsDurationImpl; import com.sap.sse.common.impl.MillisecondsTimePoint; +import com.sap.sse.concurrent.ConcurrentWeakHashMap; import com.sap.sse.util.TimerWithRunnable; /** @@ -39,7 +38,9 @@ public class SecurityWebSessionManager extends DefaultWebSessionManager { */ private static final Duration MAX_DURATION_AFTER_WHICH_TO_PING_SESSION = Duration.ONE_HOUR; - private static final WeakHashMap sessionsAlreadyScheduledForOnChange = new WeakHashMap<>(); + private static final Object DUMMY = new Object(); // for use as dummy value in sessionsAlreadyScheduledForOnChange + + private static final ConcurrentWeakHashMap sessionsAlreadyScheduledForOnChange = new ConcurrentWeakHashMap<>(); public SecurityWebSessionManager() { super(); @@ -56,7 +57,7 @@ public class SecurityWebSessionManager extends DefaultWebSessionManager { private void triggerOnChangeLatestInHalfTimeoutPeriod(Session s) { Duration sendDurationLatestIn = new MillisecondsDurationImpl(s.getTimeout()).minus(MAX_DURATION_ASSUMED_FOR_MESSAGE_DELIVERY); if (!sessionsAlreadyScheduledForOnChange.containsKey(s)) { - sessionsAlreadyScheduledForOnChange.put(s, null); + sessionsAlreadyScheduledForOnChange.put(s, DUMMY); timer.schedule(()->{ onChange(s); sessionsAlreadyScheduledForOnChange.remove(s); }, MillisecondsTimePoint.now().plus( sendDurationLatestIn.compareTo(MAX_DURATION_AFTER_WHICH_TO_PING_SESSION) > 0 ? diff --git a/java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java b/java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java index bdf0cd99d9b..b4567fdf97d 100644 --- a/java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java +++ b/java/com.sap.sse.security/src/com/sap/sse/security/impl/Activator.java @@ -23,6 +23,7 @@ import com.sap.sse.security.SecurityService; import com.sap.sse.security.UsernamePasswordRealm; import com.sap.sse.security.interfaces.AccessControlStore; import com.sap.sse.security.interfaces.UserStore; +import com.sap.sse.security.shared.HasPermissions.DefaultActions; import com.sap.sse.security.shared.HasPermissionsProvider; import com.sap.sse.security.shared.QualifiedObjectIdentifier; import com.sap.sse.security.shared.RoleDefinition; @@ -223,6 +224,10 @@ public class Activator implements BundleActivator { QualifiedObjectIdentifier associationQualifiedIdentifier = SecuredSecurityTypes.ROLE_ASSOCIATION .getQualifiedObjectIdentifier(associationTypeIdentifier); securityService.migrateOwnership(associationQualifiedIdentifier, associationTypeIdentifier.toString()); + if (securityService.isInitialOrMigration()) { + securityService.addToAccessControlList(associationQualifiedIdentifier, null, + DefaultActions.READ.name()); + } } for (WildcardPermission permission : user.getPermissions()) { securityService.migratePermission(user, permission, this::getPermissionReplacement); @@ -231,6 +236,10 @@ public class Activator implements BundleActivator { QualifiedObjectIdentifier associationQualifiedIdentifier = SecuredSecurityTypes.PERMISSION_ASSOCIATION .getQualifiedObjectIdentifier(associationTypeIdentifier); securityService.migrateOwnership(associationQualifiedIdentifier, associationTypeIdentifier.toString()); + if (securityService.isInitialOrMigration()) { + securityService.addToAccessControlList(associationQualifiedIdentifier, null, + DefaultActions.READ.name()); + } } } diff --git a/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java b/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java index 7c30332086f..68c451cfe91 100755 --- a/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java +++ b/java/com.sap.sse/src/com/sap/sse/util/SmartFutureCache.java @@ -249,6 +249,11 @@ public class SmartFutureCache> { this.callerThread = callerThread; } + @Override + public Map, Object> getThreadLocalValuesToInherit() { + return tracingGetHelper.getThreadLocalValuesToInherit(); + } + /** * Tries to update this task's update interval so as to re-used an already scheduled task for a given key. * The update can only be performed if the task hasn't read the update interval after it has been started. @@ -292,6 +297,7 @@ public class SmartFutureCache> { @Override public V call() { + tracingGetHelper.setInheritableThreadLocalValues(); try { final U updateInterval; final Set locksPropagatedFromGettingThreads; @@ -365,6 +371,7 @@ public class SmartFutureCache> { LockUtil.unpropagateLockSetFrom(callerThread); } gettingThreads.clear(); + tracingGetHelper.removeInheritableThreadLocalValues(); } } catch (Exception e) { // cache won't be updated diff --git a/java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java b/java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java index d257f585c2c..0427222e25d 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java +++ b/java/com.sap.sse/src/com/sap/sse/util/ThreadPoolUtil.java @@ -1,6 +1,11 @@ package com.sap.sse.util; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; +import java.util.logging.Level; import com.sap.sse.util.impl.ThreadPoolUtilImpl; @@ -79,4 +84,24 @@ public interface ThreadPoolUtil { ScheduledExecutorService createForegroundTaskThreadPoolExecutor(String name); ScheduledExecutorService createForegroundTaskThreadPoolExecutor(int size, String name); + + /** + * Logs all exceptions that occurred during the execution of the {@code futures} provided. + * + * @param messageTemplate + * a message string that must contain a single {@code %s} parameter placeholder that will be substituted + * by the exception's message. + */ + void logExceptionsFromFutures(Level logLevel, String messageTemplate, Iterable> futures); + + /** + * Uses the {@code executor}'s {@link ExecutorService#invokeAll(java.util.Collection) invokeAll} method to execute + * all {@code tasks}. All exceptions that occur are logged, using the {@code logLevel} provided, using as the log + * message the message template, parameterized with the exception message. + * + * @param messageTemplate + * a message string that must contain a single {@code %s} parameter placeholder that will be substituted + * by the exception's message. + */ + List> invokeAllAndLogExceptions(ExecutorService executor, Level logLevel, String messageTemplate, Iterable> tasks); } diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutor.java b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutor.java index eed0129dbfd..3ac51c0774c 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutor.java +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutor.java @@ -1,5 +1,6 @@ package com.sap.sse.util.impl; +import java.util.Map; import java.util.concurrent.ThreadPoolExecutor; /** @@ -11,4 +12,5 @@ import java.util.concurrent.ThreadPoolExecutor; */ public interface KnowsExecutor { void setExecutorThisTaskIsScheduledFor(ThreadPoolExecutor executorThisTaskIsScheduledFor); + Map, Object> getThreadLocalValuesToInherit(); } diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGet.java b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGet.java index e091e87135c..09d8bb10111 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGet.java +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGet.java @@ -14,4 +14,17 @@ import com.sap.sse.util.ThreadPoolUtil; * @param */ public interface KnowsExecutorAndTracingGet extends KnowsExecutor, HasTracingGet { + /** + * Assuming that the thread pool's thread has taken over and is about to run this task, + * sets all inheritable thread local values for the executing thread. The values + * are obtained through {@link #getThreadLocalValuesToInherit()}. + */ + void setInheritableThreadLocalValues(); + + /** + * Removes / "unsets" the values for all thread locals whose values were memorized by + * {@link #getThreadLocalValuesToInherit()}. Call this method at the end of the + * call/run method of this task. + */ + void removeInheritableThreadLocalValues(); } diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGetImpl.java b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGetImpl.java index 1da1ed2591d..43fa6d73ba6 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGetImpl.java +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/KnowsExecutorAndTracingGetImpl.java @@ -1,7 +1,16 @@ package com.sap.sse.util.impl; +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.Map.Entry; +import java.util.WeakHashMap; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; +import java.util.logging.Level; +import java.util.logging.Logger; import com.sap.sse.util.ThreadPoolUtil; @@ -16,7 +25,77 @@ import com.sap.sse.util.ThreadPoolUtil; * @param */ public class KnowsExecutorAndTracingGetImpl extends HasTracingGetImpl implements KnowsExecutorAndTracingGet { + private static final Logger logger = Logger.getLogger(KnowsExecutorAndTracingGetImpl.class.getName()); + private static Field inheritableThreadLocalsField; + private static Method childValueMethod; private ThreadPoolExecutor executorThisTaskIsScheduledFor; + private final WeakHashMap, Object> threadLocalValuesToInherit; + + static { + try { + inheritableThreadLocalsField = Thread.class.getDeclaredField("inheritableThreadLocals"); + inheritableThreadLocalsField.setAccessible(true); + childValueMethod = InheritableThreadLocal.class.getDeclaredMethod("childValue", Object.class); + childValueMethod.setAccessible(true); + } catch (NoSuchFieldException | SecurityException | NoSuchMethodException e) { + logger.log(Level.SEVERE, "Problem finding field inheritableThreadLocals; will be unable to forward InheritableThreadLocal values to thread pool threads", e); + } + } + + /** + * Copies the values of all {@link InheritableThreadLocal} to the {@link #threadLocalValuesToInherit} map so + * that they can be set just before this task is run. + */ + public KnowsExecutorAndTracingGetImpl() { + WeakHashMap, Object> threadLocalValuesToInheritValue = null; + final Thread currentThread = Thread.currentThread(); + try { + final Object inheritableThreadLocals = inheritableThreadLocalsField.get(currentThread); + if (inheritableThreadLocals != null) { + threadLocalValuesToInheritValue = new WeakHashMap<>(); + final Method expungeStaleEntries = inheritableThreadLocals.getClass().getDeclaredMethod("expungeStaleEntries"); + expungeStaleEntries.setAccessible(true); + expungeStaleEntries.invoke(inheritableThreadLocals); + final Field tableField = inheritableThreadLocals.getClass().getDeclaredField("table"); + tableField.setAccessible(true); + final Object[] table = (Object[]) tableField.get(inheritableThreadLocals); + for (int j = 0; j < table.length; j++) { + final WeakReference entry = (WeakReference) table[j]; + if (entry != null) { + @SuppressWarnings("unchecked") + final InheritableThreadLocal key = (InheritableThreadLocal) entry.get(); + if (key != null) { + final Field valueField = entry.getClass().getDeclaredField("value"); + valueField.setAccessible(true); + final Object value = childValueMethod.invoke(key, valueField.get(entry)); + threadLocalValuesToInheritValue.put(key, value); + } + } + } + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Problem accessing field inheritableThreadLocals; will be unable to forward InheritableThreadLocal values to thread pool threads", e); + } + threadLocalValuesToInherit = threadLocalValuesToInheritValue; + } + + @Override + public void setInheritableThreadLocalValues() { + for (Entry, Object> e : threadLocalValuesToInherit.entrySet()) { + e.getKey().set(e.getValue()); + } + } + + @Override + public void removeInheritableThreadLocalValues() { + for (Entry, Object> e : threadLocalValuesToInherit.entrySet()) { + e.getKey().remove(); + } + } + + public Map, Object> getThreadLocalValuesToInherit() { + return Collections.unmodifiableMap(threadLocalValuesToInherit); + } @Override public void setExecutorThisTaskIsScheduledFor(ThreadPoolExecutor executorThisTaskIsScheduledFor) { diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareFutureTask.java b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareFutureTask.java index 037da45e3dd..a8347715e10 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareFutureTask.java +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareFutureTask.java @@ -1,5 +1,6 @@ package com.sap.sse.util.impl; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; @@ -24,32 +25,53 @@ public class ThreadPoolAwareFutureTask extends FutureTask implements Knows private static final Logger logger = Logger.getLogger(ThreadPoolAwareFutureTask.class.getName()); private final KnowsExecutorAndTracingGet getHelper = new KnowsExecutorAndTracingGetImpl(); private final Object callableOrRunnableIfLoggingFine; + private final Object delegate; // either a Callable or a Runnable whose type may or may not conform to the KnowsExecutor interface public ThreadPoolAwareFutureTask(ThreadPoolExecutor executor, Callable callable) { super(callable); + delegate = callable; if (logger.isLoggable(Level.FINE)) { callableOrRunnableIfLoggingFine = callable; } else { callableOrRunnableIfLoggingFine = null; } - getHelper.setExecutorThisTaskIsScheduledFor(executor); + setExecutorThisTaskIsScheduledFor(executor); } public ThreadPoolAwareFutureTask(ThreadPoolExecutor executor, Runnable runnable, V result) { super(runnable, result); + delegate = runnable; if (logger.isLoggable(Level.FINE)) { callableOrRunnableIfLoggingFine = runnable; } else { callableOrRunnableIfLoggingFine = null; } - getHelper.setExecutorThisTaskIsScheduledFor(executor); + setExecutorThisTaskIsScheduledFor(executor); + } + + @Override + public void run() { + getHelper.setInheritableThreadLocalValues(); + try { + super.run(); + } finally { + getHelper.removeInheritableThreadLocalValues(); + } } @Override public void setExecutorThisTaskIsScheduledFor(ThreadPoolExecutor executorThisTaskIsScheduledFor) { + if (delegate instanceof KnowsExecutor) { + ((KnowsExecutor) delegate).setExecutorThisTaskIsScheduledFor(executorThisTaskIsScheduledFor); + } this.getHelper.setExecutorThisTaskIsScheduledFor(executorThisTaskIsScheduledFor); } + @Override + public Map, Object> getThreadLocalValuesToInherit() { + return getHelper.getThreadLocalValuesToInherit(); + } + @Override public V get() throws InterruptedException, ExecutionException { return getHelper.callGetAndTraceAfterEachTimeout(this); diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareRunnableScheduledFutureDelegate.java b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareRunnableScheduledFutureDelegate.java index 935d3a1ff2a..5dfc9ba461c 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareRunnableScheduledFutureDelegate.java +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolAwareRunnableScheduledFutureDelegate.java @@ -20,7 +20,12 @@ public class ThreadPoolAwareRunnableScheduledFutureDelegate extends KnowsExec } public void run() { - future.run(); + setInheritableThreadLocalValues(); + try { + future.run(); + } finally { + removeInheritableThreadLocalValues(); + } } public boolean isPeriodic() { @@ -50,4 +55,13 @@ public class ThreadPoolAwareRunnableScheduledFutureDelegate extends KnowsExec public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return future.get(timeout, unit); } + + @Override + public void setExecutorThisTaskIsScheduledFor(ThreadPoolExecutor executorThisTaskIsScheduledFor) { + if (future instanceof KnowsExecutor) { + // transitively announce the executor to the contained future: + ((KnowsExecutor) future).setExecutorThisTaskIsScheduledFor(executorThisTaskIsScheduledFor); + } + super.setExecutorThisTaskIsScheduledFor(executorThisTaskIsScheduledFor); + } } diff --git a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java index 4c6b046a7a3..0fb175c8495 100644 --- a/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java +++ b/java/com.sap.sse/src/com/sap/sse/util/impl/ThreadPoolUtilImpl.java @@ -1,10 +1,20 @@ package com.sap.sse.util.impl; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; +import java.util.logging.Level; +import java.util.logging.Logger; +import com.sap.sse.common.Util; import com.sap.sse.util.ThreadPoolUtil; public class ThreadPoolUtilImpl implements ThreadPoolUtil { + private static final Logger logger = Logger.getLogger(ThreadPoolUtilImpl.class.getName()); private static final int REASONABLE_THREAD_POOL_SIZE = Math.max(Runtime.getRuntime().availableProcessors()-1, 3); private final ScheduledExecutorService defaultBackgroundTaskThreadPoolExecutor; @@ -58,4 +68,33 @@ public class ThreadPoolUtilImpl implements ThreadPoolUtil { public int getReasonableThreadPoolSize() { return REASONABLE_THREAD_POOL_SIZE; } + + @Override + public void logExceptionsFromFutures(Level logLevel, String messageTemplate, Iterable> futures) { + for (final Future result : futures) { + try { + result.get(); + } catch (ExecutionException e) { + Throwable t = e.getCause(); + logger.log(logLevel, String.format(messageTemplate, t.getMessage()), t); + } catch (InterruptedException e) { + logger.log(logLevel, String.format(messageTemplate, e.getMessage()), e); + } + } + } + + @Override + public List> invokeAllAndLogExceptions(ExecutorService executor, Level logLevel, String messageTemplate, + Iterable> tasks) { + final List> tasksAsList = new ArrayList<>(); + Util.addAll(tasks, tasksAsList); + List> result = null; + try { + result = executor.invokeAll(tasksAsList); + logExceptionsFromFutures(logLevel, messageTemplate, result); + } catch (InterruptedException e) { + logger.log(logLevel, String.format(messageTemplate, e.getMessage()), e); + } + return result; + } } diff --git a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF index d1eeca6b532..709332dc766 100644 --- a/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF +++ b/java/com.tractrac.clientmodule/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: TracTrac Client module Bundle-SymbolicName: com.tractrac.clientmodule -Bundle-Version: 3.12.0 +Bundle-Version: 3.12.1 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ClassPath: ., lib/TracAPI.jar diff --git a/java/com.tractrac.clientmodule/README.txt b/java/com.tractrac.clientmodule/README.txt index eaa8b8c8cd0..c2d59ac4116 100644 --- a/java/com.tractrac.clientmodule/README.txt +++ b/java/com.tractrac.clientmodule/README.txt @@ -16,6 +16,26 @@ It contains also some files: - test.sh -> script that compiles the code in the src folder, creates the test.jar library and execute the code of the example. - Manifest.txt -> manifest used to create the test.jar file +******************************************** +************* TracAPI 3.12.1 *************** +******************************************** +This is a final version. It fixes bugs in the implementation and it adds a some features. +It keeps the backward compatibility. + +It keeps the backward compatibility: + +Release date: 10/05/2019 +Build number: c547c9b + + 1) Features + + - When a race is loaded from a file, the percentage of loaded data was inaccurate + and to fix this issue the MTB reader has been reimplemented. Now the reading time + is faster (Requested by André Borud, 07/05/2019) + + 2) Bugs + + ******************************************** ************* TracAPI 3.12.0 *************** ******************************************** diff --git a/java/com.tractrac.clientmodule/lib/TracAPI.jar b/java/com.tractrac.clientmodule/lib/TracAPI.jar index 959fec81fe2..48ccc3afcf2 100644 Binary files a/java/com.tractrac.clientmodule/lib/TracAPI.jar and b/java/com.tractrac.clientmodule/lib/TracAPI.jar differ diff --git a/java/com.tractrac.clientmodule/pom.xml b/java/com.tractrac.clientmodule/pom.xml index 9533e4e5f4e..1689cc7afdc 100755 --- a/java/com.tractrac.clientmodule/pom.xml +++ b/java/com.tractrac.clientmodule/pom.xml @@ -8,6 +8,6 @@ 1.0.0-SNAPSHOT com.tractrac.clientmodule - 3.12.0 + 3.12.1 eclipse-plugin diff --git a/java/target/configuration/logging_debug.properties b/java/target/configuration/logging_debug.properties index c2d7c796ebb..3bde9fe7a84 100644 --- a/java/target/configuration/logging_debug.properties +++ b/java/target/configuration/logging_debug.properties @@ -18,7 +18,7 @@ java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter # Overwrite the log leves for specific loggers here com.sap.sailing.level = FINE com.sap.sse.level = FINE -com.sap.sailing.domain.markpassingcalculation.level = FINE +com.sap.sailing.domain.markpassingcalculation.level = FINEST # log at level FINEST for TransientCompetitorStoreIMpl #com.sap.sailing.domain.base.impl.TransientCompetitorStoreImpl.level = FINEST #com.sap.sailing.domain.base.impl.SharedDomainFactoryImpl.level = FINEST diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessagePersistenceManager.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessagePersistenceManager.java index 7083d50131c..982cc39d352 100644 --- a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessagePersistenceManager.java +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/services/sending/MessagePersistenceManager.java @@ -1,9 +1,18 @@ package com.sap.sailing.android.shared.services.sending; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +import com.sap.sailing.android.shared.logging.ExLog; +import com.sap.sailing.android.shared.util.FileHandlerUtils; + import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; @@ -13,13 +22,6 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Set; -import com.sap.sailing.android.shared.logging.ExLog; -import com.sap.sailing.android.shared.util.FileHandlerUtils; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - public class MessagePersistenceManager { private final static String TAG = MessagePersistenceManager.class.getSimpleName(); @@ -167,16 +169,10 @@ public class MessagePersistenceManager { return messageIntent; } - /** - * @return - * @throws FileNotFoundException - */ - private String getFileContent() throws FileNotFoundException { + private String getFileContent(InputStream inputStream) { String fileContent = ""; - FileInputStream inputStream; try { - inputStream = context.openFileInput(delayedMessagesFileName); - + ExLog.i(context, TAG, "Convert stream to string..."); fileContent = FileHandlerUtils.convertStreamToString(inputStream, context); inputStream.close(); } catch (IOException e) { @@ -204,7 +200,9 @@ public class MessagePersistenceManager { private void initializeFileAndPersistedMessages() { try { - String fileContent = getFileContent(); + ExLog.i(context, TAG, "Open file for reading..."); + FileInputStream inputStream = context.openFileInput(delayedMessagesFileName); + String fileContent = getFileContent(inputStream); String[] messageLines = fileContent.split("\n"); for (String messageLine : messageLines) { if (!messageLine.isEmpty()) { @@ -225,6 +223,7 @@ public class MessagePersistenceManager { private void writeToFile(String content, int mode) { try { + Log.wtf(TAG, "Create and write file"); FileOutputStream outputStream = context.openFileOutput(delayedMessagesFileName, mode); outputStream.write(content.getBytes()); outputStream.close(); diff --git a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/activities/RegattaActivity.java b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/activities/RegattaActivity.java index eada3b81b06..0b9604760d0 100755 --- a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/activities/RegattaActivity.java +++ b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/ui/activities/RegattaActivity.java @@ -66,6 +66,7 @@ import java.net.URL; import java.util.Locale; import static com.sap.sailing.domain.common.racelog.tracking.DeviceMappingConstants.URL_LEADERBOARD_NAME; +import static com.sap.sailing.domain.common.racelog.tracking.DeviceMappingConstants.URL_LEADERBOARD_NAME_FOR_SECRET; import static com.sap.sailing.domain.common.racelog.tracking.DeviceMappingConstants.URL_SECRET; public class RegattaActivity extends AbstractRegattaActivity @@ -277,8 +278,8 @@ public class RegattaActivity extends AbstractRegattaActivity String secret = uri.getQueryParameter(URL_SECRET); if (secret != null) { builder.appendQueryParameter(URL_SECRET, secret); + builder.appendQueryParameter(URL_LEADERBOARD_NAME_FOR_SECRET, uri.getQueryParameter(URL_LEADERBOARD_NAME)); } - builder.appendQueryParameter(URL_LEADERBOARD_NAME, uri.getQueryParameter(URL_LEADERBOARD_NAME)); return new URL(builder.build().toString()); } @@ -385,8 +386,8 @@ public class RegattaActivity extends AbstractRegattaActivity String secret = uri.getQueryParameter(URL_SECRET); if (secret != null) { builder.appendQueryParameter(URL_SECRET, secret); + builder.appendQueryParameter(URL_LEADERBOARD_NAME_FOR_SECRET, uri.getQueryParameter(URL_LEADERBOARD_NAME)); } - builder.appendQueryParameter(URL_LEADERBOARD_NAME, uri.getQueryParameter(URL_LEADERBOARD_NAME)); new UploadTeamImageTask(this, imageFile, this).execute(builder.build().toString()); } catch (UnsupportedEncodingException e) { ExLog.e(this, TAG, "Failed to encode competitor ID: " + e.getMessage()); diff --git a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/utils/CheckinHelper.java b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/utils/CheckinHelper.java index 6129efb0837..e916ef93c5b 100644 --- a/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/utils/CheckinHelper.java +++ b/mobile/com.sap.sailing.android.tracking.app/src/com/sap/sailing/android/tracking/app/utils/CheckinHelper.java @@ -11,7 +11,7 @@ import org.json.JSONObject; public class CheckinHelper { @NonNull - private static JSONObject getBaseCheckinJsom(String deviceUuid, String pushDeviceId, + private static JSONObject getBaseCheckinJson(String deviceUuid, String pushDeviceId, long fromMillis, JSONObject jsonObject) throws JSONException { jsonObject.put(DeviceMappingConstants.JSON_DEVICE_TYPE, "android"); @@ -28,7 +28,7 @@ public class CheckinHelper { JSONObject jsonObject = new JSONObject(); jsonObject.putOpt(DeviceMappingConstants.JSON_REGISTER_SECRET, secret); jsonObject.put(DeviceMappingConstants.JSON_COMPETITOR_ID_AS_STRING, competitorId); - return getBaseCheckinJsom(deviceUuid, pushDeviceId, fromMillis, jsonObject); + return getBaseCheckinJson(deviceUuid, pushDeviceId, fromMillis, jsonObject); } @NonNull @@ -38,7 +38,7 @@ public class CheckinHelper { JSONObject jsonObject = new JSONObject(); jsonObject.putOpt(DeviceMappingConstants.JSON_REGISTER_SECRET, secret); jsonObject.put(DeviceMappingConstants.JSON_MARK_ID_AS_STRING, markId); - return getBaseCheckinJsom(deviceUuid, pushDeviceId, fromMillis, jsonObject); + return getBaseCheckinJson(deviceUuid, pushDeviceId, fromMillis, jsonObject); } @NonNull @@ -48,7 +48,7 @@ public class CheckinHelper { JSONObject jsonObject = new JSONObject(); jsonObject.putOpt(DeviceMappingConstants.JSON_REGISTER_SECRET, secret); jsonObject.put(DeviceMappingConstants.JSON_BOAT_ID_AS_STRING, boatId); - return getBaseCheckinJsom(deviceUuid, pushDeviceId, fromMillis, jsonObject); + return getBaseCheckinJson(deviceUuid, pushDeviceId, fromMillis, jsonObject); } } diff --git a/mobile/com.sap.sailing.racecommittee.app/res/values-de/strings.xml b/mobile/com.sap.sailing.racecommittee.app/res/values-de/strings.xml old mode 100644 new mode 100755 index 5606dc94af4..7eb68a68717 --- a/mobile/com.sap.sailing.racecommittee.app/res/values-de/strings.xml +++ b/mobile/com.sap.sailing.racecommittee.app/res/values-de/strings.xml @@ -266,13 +266,11 @@ Gestartet %s Beendet %s Wind - dd Abbrechen Rückruf Kurs Zieleinlauf Verschieben - von abhängigen Rennen %2$s / %1$f kn http://maps.googleapis.com/maps/api/geocode/json?address= \?sensor=true diff --git a/mobile/com.sap.sailing.racecommittee.app/res/values/strings.xml b/mobile/com.sap.sailing.racecommittee.app/res/values/strings.xml old mode 100644 new mode 100755 index 415847248a3..60538f6a632 --- a/mobile/com.sap.sailing.racecommittee.app/res/values/strings.xml +++ b/mobile/com.sap.sailing.racecommittee.app/res/values/strings.xml @@ -307,13 +307,11 @@ Please select a regatta Onboarding Sign Me Up - dd Abandon Recall Course Finish Postpone - of dependent races Select a Race Pathfinder diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/DataStore.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/DataStore.java index 66079cb523d..c7763694a1b 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/DataStore.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/DataStore.java @@ -58,17 +58,17 @@ public interface DataStore { void registerRaces(Collection races); - Collection getMarks(); + Collection getMarks(RaceGroup raceGroup); - Mark getMark(Serializable id); + Mark getMark(RaceGroup raceGroup, Serializable id); - boolean hasMark(Serializable id); + boolean hasMark(RaceGroup raceGroup, Serializable id); - void addMark(Mark mark); + void addMark(RaceGroup raceGroup, Mark mark); - CourseBase getLastPublishedCourseDesign(); + CourseBase getLastPublishedCourseDesign(RaceGroup raceGroup); - void setLastPublishedCourseDesign(CourseBase courseData); + void setLastPublishedCourseDesign(RaceGroup raceGroup, CourseBase courseData); Set getRaceGroups(); diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/InMemoryDataStore.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/InMemoryDataStore.java index a0da74e67d6..73263c481b0 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/InMemoryDataStore.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/InMemoryDataStore.java @@ -36,8 +36,8 @@ public enum InMemoryDataStore implements DataStore { private Context mContext; private LinkedHashMap eventsById; private LinkedHashMap managedRaceById; - private LinkedHashMap marksById; - private CourseBase courseData; + private LinkedHashMap> marksData; + private LinkedHashMap courseData; private SharedDomainFactory domainFactory; private Serializable eventUUID; @@ -63,8 +63,8 @@ public enum InMemoryDataStore implements DataStore { public void reset() { eventsById = new LinkedHashMap<>(); managedRaceById = new LinkedHashMap<>(); - marksById = new LinkedHashMap<>(); - courseData = null; + marksData = new LinkedHashMap<>(); + courseData = new LinkedHashMap<>(); domainFactory = new SharedDomainFactoryImpl(new AndroidRaceLogResolver()); eventUUID = null; @@ -255,34 +255,47 @@ public enum InMemoryDataStore implements DataStore { * * * * * * MARKS * * * * * * */ - @Override - public Collection getMarks() { - return marksById.values(); + private LinkedHashMap getMarksByRaceGroup(RaceGroup raceGroup) { + LinkedHashMap marks = marksData.get(raceGroup); + if (marks == null) { + marks = new LinkedHashMap<>(); + marksData.put(raceGroup, marks); + } + return marks; + } + + private void setMarksByRaceGroup(RaceGroup raceGroup, LinkedHashMap marks) { + marksData.put(raceGroup, marks); } @Override - public Mark getMark(Serializable id) { - return marksById.get(id); + public Collection getMarks(RaceGroup raceGroup) { + return getMarksByRaceGroup(raceGroup).values(); } @Override - public boolean hasMark(Serializable id) { - return marksById.containsKey(id); + public Mark getMark(RaceGroup raceGroup, Serializable id) { + return getMarksByRaceGroup(raceGroup).get(id); } @Override - public void addMark(Mark mark) { - marksById.put(mark.getId(), mark); + public boolean hasMark(RaceGroup raceGroup, Serializable id) { + return getMarksByRaceGroup(raceGroup).containsKey(id); } @Override - public CourseBase getLastPublishedCourseDesign() { - return courseData; + public void addMark(RaceGroup raceGroup, Mark mark) { + getMarksByRaceGroup(raceGroup).put(mark.getId(), mark); } @Override - public void setLastPublishedCourseDesign(CourseBase courseData) { - this.courseData = courseData; + public CourseBase getLastPublishedCourseDesign(RaceGroup raceGroup) { + return courseData.get(raceGroup); + } + + @Override + public void setLastPublishedCourseDesign(RaceGroup raceGroup, CourseBase courseData) { + this.courseData.put(raceGroup, courseData); } @Override diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OfflineDataManager.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OfflineDataManager.java index 2086b6719f7..b2542d3f007 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OfflineDataManager.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OfflineDataManager.java @@ -164,9 +164,9 @@ public class OfflineDataManager extends DataManager { Mark m2 = new MarkImpl("Green"); Mark m3 = new MarkImpl("White"); - dataStore.addMark(m1); - dataStore.addMark(m2); - dataStore.addMark(m3); + dataStore.addMark(raceGroup, m1); + dataStore.addMark(raceGroup, m2); + dataStore.addMark(raceGroup, m3); } @Override @@ -224,7 +224,7 @@ public class OfflineDataManager extends DataManager { return new ImmediateDataLoaderCallbacks>(context, callback, new Callable>() { @Override public Collection call() throws Exception { - return dataStore.getMarks(); + return dataStore.getMarks(managedRace.getRaceGroup()); } }); } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OnlineDataManager.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OnlineDataManager.java index 8484138d974..773dd8eff46 100755 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OnlineDataManager.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/OnlineDataManager.java @@ -25,6 +25,7 @@ import com.sap.sailing.domain.base.configuration.ConfigurationLoader; import com.sap.sailing.domain.base.configuration.DeviceConfiguration; import com.sap.sailing.domain.base.configuration.RegattaConfiguration; import com.sap.sailing.domain.base.impl.RaceColumnFactorImpl; +import com.sap.sailing.domain.base.racegroup.RaceGroup; import com.sap.sailing.domain.common.racelog.RaceLogServletConstants; import com.sap.sailing.racecommittee.app.AppConstants; import com.sap.sailing.racecommittee.app.AppPreferences; @@ -110,9 +111,9 @@ public class OnlineDataManager extends DataManager { } } - public void addMarks(Collection marks) { + public void addMarks(RaceGroup raceGroup, Collection marks) { for (Mark mark : marks) { - dataStore.addMark(mark); + dataStore.addMark(raceGroup, mark); } } @@ -204,7 +205,7 @@ public class OnlineDataManager extends DataManager { ExLog.i(context, TAG, "Creating marks-OnlineDataLoader " + id); JsonDeserializer markDeserializer = new MarkDeserializer(domainFactory); DataParser> parser = new MarksDataParser(markDeserializer); - DataHandler> handler = new MarksDataHandler(OnlineDataManager.this); + DataHandler> handler = new MarksDataHandler(OnlineDataManager.this, managedRace.getRaceGroup()); ManagedRaceIdentifier identifier = managedRace.getIdentifier(); // no parameter encoding required here; the UrlHelper.generateUrl call uses Url.Builder which handles diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/handlers/MarksDataHandler.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/handlers/MarksDataHandler.java index 49d5ad05882..6a6e31727a5 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/handlers/MarksDataHandler.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/data/handlers/MarksDataHandler.java @@ -1,29 +1,33 @@ package com.sap.sailing.racecommittee.app.data.handlers; +import com.sap.sailing.domain.base.Mark; +import com.sap.sailing.domain.base.racegroup.RaceGroup; +import com.sap.sailing.racecommittee.app.data.OnlineDataManager; + import java.util.Collection; -import com.sap.sailing.domain.base.Mark; -import com.sap.sailing.racecommittee.app.data.OnlineDataManager; - public class MarksDataHandler extends DataHandler> { - public MarksDataHandler(OnlineDataManager manager) { + private final RaceGroup raceGroup; + + public MarksDataHandler(OnlineDataManager manager, RaceGroup raceGroup) { super(manager); + this.raceGroup = raceGroup; } @Override public boolean hasCachedResults() { - return !manager.getDataStore().getMarks().isEmpty(); + return !manager.getDataStore().getMarks(raceGroup).isEmpty(); } @Override public Collection getCachedResults() { - return manager.getDataStore().getMarks(); + return manager.getDataStore().getMarks(raceGroup); } @Override public void onResult(Collection data, boolean isCached) { - manager.addMarks(data); + manager.addMarks(raceGroup, data); } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/CourseFragmentMarks.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/CourseFragmentMarks.java index f363cf0258d..bd4600308f4 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/CourseFragmentMarks.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/fragments/raceinfo/CourseFragmentMarks.java @@ -23,6 +23,7 @@ import com.sap.sailing.domain.base.Waypoint; import com.sap.sailing.domain.base.impl.ControlPointWithTwoMarksImpl; import com.sap.sailing.domain.base.impl.CourseDataImpl; import com.sap.sailing.domain.base.impl.WaypointImpl; +import com.sap.sailing.domain.base.racegroup.RaceGroup; import com.sap.sailing.domain.common.CourseDesignerMode; import com.sap.sailing.domain.common.PassingInstruction; import com.sap.sailing.racecommittee.app.AppConstants; @@ -458,7 +459,8 @@ public class CourseFragmentMarks extends CourseFragment } private void fillPreviousCourseElementsWithLastPublishedCourseDesign() { - CourseBase lastPublishedCourseDesign = InMemoryDataStore.INSTANCE.getLastPublishedCourseDesign(); + CourseBase lastPublishedCourseDesign = InMemoryDataStore.INSTANCE + .getLastPublishedCourseDesign(getRace().getIdentifier().getRaceGroup()); if (lastPublishedCourseDesign != null) { fillPreviousCourseElementsInList(lastPublishedCourseDesign); } @@ -546,7 +548,8 @@ public class CourseFragmentMarks extends CourseFragment private void saveChangedCourseDesignInCache(CourseBase courseDesign) { if (!Util.isEmpty(courseDesign.getWaypoints())) { - InMemoryDataStore.INSTANCE.setLastPublishedCourseDesign(courseDesign); + RaceGroup raceGroup = getRace().getRaceGroup(); + InMemoryDataStore.INSTANCE.setLastPublishedCourseDesign(raceGroup, courseDesign); } } diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/layouts/CompetitorEditLayout.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/layouts/CompetitorEditLayout.java index 48c9f01c402..517bdb7d1ff 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/layouts/CompetitorEditLayout.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/layouts/CompetitorEditLayout.java @@ -1,12 +1,21 @@ package com.sap.sailing.racecommittee.app.ui.layouts; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.List; -import java.util.Locale; +import android.annotation.SuppressLint; +import android.app.DatePickerDialog; +import android.content.Context; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.DatePicker; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ScrollView; +import android.widget.Spinner; +import android.widget.TextView; +import com.sap.sailing.android.shared.logging.ExLog; import com.sap.sailing.android.shared.util.AppUtils; import com.sap.sailing.android.shared.util.ViewHelper; import com.sap.sailing.domain.abstractlog.race.CompetitorResult; @@ -17,26 +26,26 @@ import com.sap.sailing.racecommittee.app.R; import com.sap.sailing.racecommittee.app.domain.impl.CompetitorResultWithIdImpl; import com.sap.sailing.racecommittee.app.ui.adapters.StringArraySpinnerAdapter; import com.sap.sailing.racecommittee.app.ui.fragments.dialogs.FullTimePickerDialog; +import com.sap.sailing.racecommittee.app.ui.utils.DecimalKeyListener; import com.sap.sse.common.TimePoint; import com.sap.sse.common.impl.MillisecondsTimePoint; -import android.annotation.SuppressLint; -import android.app.DatePickerDialog; -import android.content.Context; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.DatePicker; -import android.widget.EditText; -import android.widget.FrameLayout; -import android.widget.ScrollView; -import android.widget.Spinner; -import android.widget.TextView; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.Locale; @SuppressLint("ViewConstructor") public class CompetitorEditLayout extends ScrollView implements DatePickerDialog.OnDateSetListener, FullTimePickerDialog.OnTimeSetListener { + private static final String TAG = CompetitorEditLayout.class.getName(); + private CompetitorResultWithIdImpl mCompetitor; private GregorianCalendar mCalendar; @@ -50,13 +59,15 @@ public class CompetitorEditLayout extends ScrollView private boolean mRestricted; + private DecimalFormat format = new DecimalFormat(); + public CompetitorEditLayout(Context context, CompetitorResultWithIdImpl competitor, int maxPos, - boolean hasWarning) { + boolean hasWarning) { this(context, null, competitor, maxPos, true, hasWarning); } public CompetitorEditLayout(final Context context, final TimePoint startTime, CompetitorResultWithIdImpl competitor, - int maxPos, boolean restrictedView, boolean hasWarning) { + int maxPos, boolean restrictedView, boolean hasWarning) { super(context); mRestricted = restrictedView; @@ -168,8 +179,34 @@ public class CompetitorEditLayout extends ScrollView mPenalty.setSelection(penaltyAdapter.getPosition(mCompetitor.getMaxPointsReason().toString())); } mScore = ViewHelper.get(layout, R.id.competitor_score); - if (mScore != null && mCompetitor.getScore() != null) { - mScore.setText(String.format(Locale.getDefault(), "%f", mCompetitor.getScore())); + if (mScore != null) { + mScore.setKeyListener(new DecimalKeyListener()); + final char separator = DecimalFormatSymbols.getInstance().getDecimalSeparator(); + mScore.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + @Override + public void afterTextChanged(Editable s) { + mScore.removeTextChangedListener(this); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // If necessary replace '.' with ',' + if (c == '.' && separator != '.') { + s.replace(i, i + 1, Character.toString(separator)); + } + } + mScore.addTextChangedListener(this); + } + }); + if (mCompetitor.getScore() != null) { + mScore.setText(format.format(mCompetitor.getScore())); + } } mComment = ViewHelper.get(layout, R.id.competitor_comment); if (mComment != null) { @@ -209,7 +246,11 @@ public class CompetitorEditLayout extends ScrollView } Double score = null; if (mScore != null && !TextUtils.isEmpty(mScore.getText())) { - score = Double.valueOf(mScore.getText().toString()); + try { + score = format.parse(mScore.getText().toString()).doubleValue(); + } catch (ParseException e) { + ExLog.w(getContext(), TAG, "Parsing score failed: " + mScore.getText().toString()); + } } TimePoint finishingTime = null; if (!mRestricted) { diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/utils/DecimalKeyListener.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/utils/DecimalKeyListener.java new file mode 100644 index 00000000000..2df5d95b54d --- /dev/null +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/utils/DecimalKeyListener.java @@ -0,0 +1,98 @@ +package com.sap.sailing.racecommittee.app.ui.utils; + +import android.support.annotation.NonNull; +import android.text.InputType; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.method.NumberKeyListener; + +public class DecimalKeyListener extends NumberKeyListener { + + @NonNull + @Override + protected char[] getAcceptedChars() { + return new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ',', '.'}; + } + + @Override + public int getInputType() { + return InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL; + } + + @Override + public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) { + CharSequence out = super.filter(source, start, end, dest, dstart, dend); + + if (out != null) { + source = out; + start = 0; + end = out.length(); + } + + int decimal = -1; + int dlen = dest.length(); + + /* + * Find out if the existing text has a decimal point characters. + */ + + for (int i = 0; i < dstart; i++) { + char c = dest.charAt(i); + + if (isDecimalPointChar(c)) { + decimal = i; + } + } + for (int i = dend; i < dlen; i++) { + char c = dest.charAt(i); + + if (isDecimalPointChar(c)) { + decimal = i; + } + } + + /* + * If it does, we must strip them out from the source. + * Go in reverse order so the offsets are stable. + */ + + SpannableStringBuilder stripped = null; + + for (int i = end - 1; i >= start; i--) { + char c = source.charAt(i); + boolean strip = false; + + if (isDecimalPointChar(c)) { + if (decimal >= 0) { + strip = true; + } else { + decimal = i; + } + } + + if (strip) { + if (end == start + 1) { + return ""; // Only one character, and it was stripped. + } + + if (stripped == null) { + stripped = new SpannableStringBuilder(source, start, end); + } + + stripped.delete(i - start, i + 1 - start); + } + } + + if (stripped != null) { + return stripped; + } else if (out != null) { + return out; + } else { + return null; + } + } + + private boolean isDecimalPointChar(final char c) { + return c == ',' || c == '.'; + } +} diff --git a/toolchains.xml b/toolchains.xml index 13caba14dbf..15fc236d91b 100644 --- a/toolchains.xml +++ b/toolchains.xml @@ -4,28 +4,6 @@ --> - - jdk - - 1.6 - JavaSE-1.6 - sun - - - /opt/jdk1.6.0_25/jre - - - - jdk - - 1.7 - JavaSE-1.7 - sun - - - /opt/jdk1.7.0_02/jre - - jdk diff --git a/translation_v2.json b/translation_v2.json old mode 100644 new mode 100755 index d4c505cba7f..406daf32288 --- a/translation_v2.json +++ b/translation_v2.json @@ -18,6 +18,23 @@ } ] }, + { + "collectionName":"DataMiningGwtUi", + "folders":[ + { + "startingFolderPath":"java/com.sap.sse.datamining.ui/src/main/java/com/sap/sse/datamining/ui/client", + "sourceFilters":[ + "**/StringMessages*.properties" + ], + "excludedSourceFilters":[ + "**/*_zh.properties" + ], + "xlfFolderPath":"/XLF/[fullPath]/[targetFile]", + "targetFolderPath":".", + "pseudoLocFolderPath":"." + } + ] + }, { "collectionName":"RacelogTracking", "folders":[ @@ -85,7 +102,7 @@ "collectionName":"FileStorage", "folders":[ { - "startingFolderPath":"java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage", + "startingFolderPath":"java/com.sap.sse.gwt/src/com/sap/sse/gwt/client", "sourceFilters":[ "**/StringMessages*.properties" ], @@ -177,6 +194,72 @@ "pseudoLocFolderPath":"." } ] + }, + { + "collectionName":"BuoyPingerAppAndroid", + "folders":[ + { + "startingFolderPath": "mobile/com.sap.sailing.android.buoy.positioning.app/res/values/**", + "sourceFilters":["strings.xml"], + "targetFolderPath":"../values-[langCode]", + "targetFileNamingConvention":"[filename].[extension]" + } + ] + }, + { + "collectionName":"BuoyPingerAppAndroid", + "folders":[ + { + "startingFolderPath": "mobile/com.sap.sailing.android.buoy.positioning.app/res/values-land/**", + "sourceFilters":["strings.xml"], + "targetFolderPath":"../values-[langCode]-land", + "targetFileNamingConvention":"[filename].[extension]" + } + ] + }, + { + "collectionName":"RaceManagerAppAndroid", + "folders":[ + { + "startingFolderPath": "mobile/com.sap.sailing.racecommittee.app/res/**", + "sourceFilters":["strings.xml"], + "targetFolderPath":"../values-[langCode]", + "targetFileNamingConvention":"[filename].[extension]" + } + ] + }, + { + "collectionName":"SailInsightAppAndroid", + "folders":[ + { + "startingFolderPath": "mobile/com.sap.sailing.android.tracking.app/res/**", + "sourceFilters":["strings.xml"], + "targetFolderPath":"../values-[langCode]", + "targetFileNamingConvention":"[filename].[extension]" + } + ] + }, + { + "collectionName":"AndroidAppsShared", + "folders":[ + { + "startingFolderPath": "mobile/com.sap.sailing.android.shared/res/**", + "sourceFilters":["strings.xml"], + "targetFolderPath":"../values-[langCode]", + "targetFileNamingConvention":"[filename].[extension]" + } + ] + }, + { + "collectionName":"SailInsightAppIOS", + "folders":[ + { + "startingFolderPath": "ios/SAPTracker/src/xcode/SAPTracker/**", + "sourceFilters":["*.strings"], + "targetFolderPath":"../[langCode].lproj", + "targetFileNamingConvention":"[filename].[extension]" + } + ] } ], "defaultConfiguration":{ diff --git a/wiki/howto/ignore-missing-gwt-sdk-warning.png b/wiki/howto/ignore-missing-gwt-sdk-warning.png new file mode 100755 index 00000000000..a7ffdcd56ec Binary files /dev/null and b/wiki/howto/ignore-missing-gwt-sdk-warning.png differ diff --git a/wiki/howto/ignore-package-not-exported-warning.png b/wiki/howto/ignore-package-not-exported-warning.png new file mode 100755 index 00000000000..5de05de1b3d Binary files /dev/null and b/wiki/howto/ignore-package-not-exported-warning.png differ diff --git a/wiki/howto/onboarding.md b/wiki/howto/onboarding.md index d282aceff63..77e69dd8c78 100644 --- a/wiki/howto/onboarding.md +++ b/wiki/howto/onboarding.md @@ -11,17 +11,16 @@ First of all, make sure you've looked at [http://www.amazon.de/Patterns-Elements 1. Eclipse (Eclipse IDE for Eclipse Committers, version 4.11.0 ["2019-03"]https://www.eclipse.org/downloads/packages/release/2019-03/r/eclipse-ide-eclipse-committers)), [http://www.eclipse.org](http://www.eclipse.org) 2. Install the eclipse plugins (see Automatic Eclipse plugin installation below) 3. Git (e.g. Git for Windows v2.18), [http://git-scm.com](http://git-scm.com) / [https://git-for-windows.github.io](https://git-for-windows.github.io) -4. MongoDB (e.g. Production Release 2.6.7), download: [https://www.mongodb.com/](https://www.mongodb.com/) +4. MongoDB (e.g. Production Release 3.6.12), download: [https://www.mongodb.com/](https://www.mongodb.com/) 5. RabbitMQ, download from [http://www.rabbitmq.com](http://www.rabbitmq.com). Requires Erlang to be installed. RabbitMQ installer will assist in installing Erlang. Some sources report that there may be trouble with latest versions of RabbitMQ. In some cases, McAffee seems to block the installation of the latest version on SAP hardware; in other cases connection problems to newest versions have been reported. We know that version 3.6.8 works well. [https://github.com/rabbitmq/rabbitmq-server/releases/tag/rabbitmq_v3_6_8](https://github.com/rabbitmq/rabbitmq-server/releases/tag/rabbitmq_v3_6_8) is the link. -6. JDK 1.7 (Java SE 7), [http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html](http://www.oracle.com/technetwork/java/javase/downloads/java-archive-downloads-javase7-521261.html) --- Alternatively you can use the SAPJVM 1.7: Go to [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.7, extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM. -7. JDK 1.8 (Java SE 8), [http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) --- Alternatively you can use the SAPJVM 1.8: Go to [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.8, extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM. -8. Maven 3.1.1 (or higher), [http://maven.apache.org](http://maven.apache.org) -9. GWT SDK 2.8.2 ([http://www.gwtproject.org/download.html](http://www.gwtproject.org/download.html)) -10. Standalone Android SDK (see section "Additional steps required for Android projects"). OPTIONALLY: You may additionally install Android Studio ([https://developer.android.com/tools/studio/index.html](https://developer.android.com/tools/studio/index.html)) or IntelliJ IDEA ([https://www.jetbrains.com/idea/download/](https://www.jetbrains.com/idea/download/)). +6. JDK 1.8 (Java SE 8), [http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) --- Alternatively you can use the SAPJVM 1.8: Go to [http://sapjvm.wdf.sap.corp:1080/downloads](http://sapjvm.wdf.sap.corp:1080/downloads), select JVM 1.8, extract the downloaded .zip into desired location (e.g. C:\Program Files\Java), then Go to Window -> Preferences -> Java -> Installed JREs and add the VM. +7. Maven 3.1.1 (or higher), [http://maven.apache.org](http://maven.apache.org) +8. GWT SDK 2.8.2 ([http://www.gwtproject.org/download.html](http://www.gwtproject.org/download.html)) +9. Standalone Android SDK (see section "Additional steps required for Android projects"). OPTIONALLY: You may additionally install Android Studio ([https://developer.android.com/tools/studio/index.html](https://developer.android.com/tools/studio/index.html)) or IntelliJ IDEA ([https://www.jetbrains.com/idea/download/](https://www.jetbrains.com/idea/download/)). #### Automatic Eclipse plugin installation -The necessary Eclipse plugins described above can be automatically be installed into a newly unzipped version of [Eclipse IDE for Eclipse Committers 4.11.0 "2019-03"](http://www.eclipse.org/downloads/packages/eclipse-ide-eclipse-committers/photonr) by using the script "configuration/installPluginsForEclipse2019-03.sh". In addition, the script applies some updates to plugins packaged with Eclipse itself. To start the plugin installation, run the following command using your Eclipse installation directory as command line parameter for the script: +The necessary Eclipse plugins described above can be automatically be installed into a newly unzipped version of [Eclipse IDE for Eclipse Committers 4.11.0 "2019-03"](https://www.eclipse.org/downloads/packages/release/2019-03/r/eclipse-ide-eclipse-committers) by using the script "configuration/installPluginsForEclipse2019-03.sh". In addition, the script applies some updates to plugins packaged with Eclipse itself. To start the plugin installation, run the following command using your Eclipse installation directory as command line parameter for the script: ./installPluginsForEclipse2019-03.sh "/some/path/on/my/computer/eclipse" @@ -37,12 +36,24 @@ The script will install the following plugins for your convenience: * GWT Plugin ([https://github.com/gwt-plugins/gwt-eclipse-plugin](https://github.com/gwt-plugins/gwt-eclipse-plugin)) * GWT SDM Debug Bridge ([http://sdbg.github.io/](http://sdbg.github.io/)) -* Android Tools ([https://marketplace.eclipse.org/content/android-development-tools-eclipse](https://marketplace.eclipse.org/content/android-development-tools-eclipse)) * Easy Shell ([https://anb0s.github.io/EasyShell/](https://anb0s.github.io/EasyShell/)) * Memory Analyzer ([https://www.eclipse.org/mat/](https://www.eclipse.org/mat/)) * SAP JVM Profiler ([https://tools.hana.ondemand.com](https://tools.hana.ondemand.com)) +* UMLet ([https://www.umlet.com/](https://www.umlet.com/)) * various updates to preinstalled plugins +#### Tuning the Eclipse Installation + +Out of the box, two settings in Eclipse avoid a clean workspace. Go to Window - Preferences and change the following two settings: + +* Set the "Missing GWT SDK" warning to "Ignore". See screenshot below. + +![](ignore-missing-gwt-sdk-warning.png) + +* Set the "Plug-in does not export all packages" warning to "Ignore". See screenshot below. + +![](ignore-package-not-exported-warning.png) + #### Further optional but recommended installations 1. Cygwin, [http://www.cygwin.com/](http://www.cygwin.com/) @@ -87,9 +98,8 @@ The primary Git repository for the project is hosted on sapsailing.com. It is mi * Install Eclipse debugger for GWT SuperDevMode * Install Eclipse eGit (optional) * Check that JDK 1.8 is available and has been set for compilation in Eclipse - * Check that the both JDKs are available (Windows->Preferences->Java->Installed JREs) - * Check that JDK 1.7 has been matched to JavaSE-1.7 and that JDK 1.8 has been matched to JavaSE-1.8 (...>Installed JREs>Execution Environments) - * It is also possible to match the SAPJVM 7 or 8 to JavaSE-1.7 / JavaSE-1.8 (for profiling purposes) + * Check that JDK 1.8 has been matched to JavaSE-1.8 (...>Installed JREs>Execution Environments) + * It is also possible to match the SAPJVM 8 to JavaSE-1.8 (for profiling purposes) * Import all Race Analysis projects from the `java/` subdirectory of the git main folder (make sure to import via the wizard "Git->Projects from Git" "Import->General->Projects from Folder or Archive" in Eclipse, and additionally make sure to scan for nested projects!) * Import all projects from the `mobile/` subdirectory of the git main folder; this in particular contains the race committee app projects * Set the Eclipse target platform to race-analysis-p2-remote.target (located in com.sap.sailing.targetplatform/definitions) @@ -135,40 +145,11 @@ When doing a merge in Eclipse, you can tell the merge editor to do a similar thi When a file has "wrong line endings" (line endings are different to what is configured in ".gitattributes" file) and you touch those files without changing the contents, git will potentially show that these files are changed. To get rid of those phantom changes, you can do a "git checkout HEAD path/toFile/in/repository". -#### Additional steps required for Android projects - -To ensure that all components of the Analysis Suite are working, you should also import all Android projects (mobile/) into your workspace. There are some additional requirements to enable the build process of these projects. - -1. Add the Android Development Tools (ADT) plugin to your Eclipse IDE - - In Eclipse click Help -> Install New Software -> Add and enter [https://dl-ssl.google.com/android/eclipse/](https://dl-ssl.google.com/android/eclipse/) - - Select the Developer Tools and install - - After restarting Eclipse the "Welcome to Android Development"-window should help you with installing the Android SDK - - It is also possible to download the Android SDK separately from the official Google download website. However, as of Revision 25.0.0 of the Android SDK Tools, the SDK Manager became an integrated part of Android Studio. Therefore, Revisions newer than 24.4.1 will not come with a standalone SDK Manager. Since it is absolutely essential if you want to use Eclipse, please download the Android SDK from the following link: Windows: [https://dl.google.com/android/installer_r24.4.1-windows.exe](https://dl.google.com/android/installer_r24.4.1-windows.exe) MacOS: [https://dl.google.com/android/android-sdk_r24.4.1-macosx.zip](https://dl.google.com/android/android-sdk_r24.4.1-macosx.zip) Linux: [https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz](https://dl.google.com/android/android-sdk_r24.4.1-linux.tgz) -2. Setup the Android SDK - * In Eclipse press Window -> Android SDK Manager - * Install most recent version of "Android SDK Platform Tools" under "Tools" (hint: watch out not to update Android SDK Tools, see note below) - * Install everything of "Android 3.2 API 13" - * Install everything of "Android 5.1.1 API 22" - * From the package "Extras", install "Android Support Repository" **or** "Android Support Library". Additionally install "Google Play Services", "Google Repository" **and** "Google USB Driver". - * Please note that versions of the Android SDK Build Tools from 24.0.0 onwards might cause problems for your Eclipse IDE. The same goes for API versions from 24 onwards. If you get errors containing the message "refusing to generate code from aidl file defining parcelable", a workaround for now is to uninstall these newer versions of the Build Tools and the API. In the future however, this temporary solution may not be available anymore for compability reasons. -3. Import the Android projects into your workspace - * Android projects can be found in the /mobile subdirectory - -To deploy an Android project (for example com.sap.sailing.racecommittee.app) to a real device: - -1. Plug-in the device - * Development mode must be enabled on the device - * For certain device/OS combinations additional device drivers are needed - * You can check if the device is detected correctly by checking the "Devices" tab of the "DDMS" Eclipse perspective. -2. Start a run configuration of the project -3. Select your attached device in the device selection screen -4. The app should be started after deployment - #### Further hints If you are working with a linux-system and you get the error message `error while loading shared libraries: libz.so.1: cannot open shared object file: No such file or directory` try to install lib32z1 and lib32stdc++6. -####Steps to consider for using other modules +#### Steps to consider for using other modules 1. For Eclipse Build * MANIFEST.MF , add module names unter dependencies * *.gwt.xml , add ``