From dcdbb4e52ec30764f13890b337481cf6373781c1 Mon Sep 17 00:00:00 2001 From: Alex Texter Date: Mon, 23 Nov 2015 14:00:13 -0800 Subject: [PATCH] [Gradle Release Plugin] - pre tag commit: 'v0.1.0'. --- .gitignore | 25 ++ CHANGELOG.md | 4 + LICENSE | 19 ++ README.md | 121 +++++++++- build.gradle | 175 ++++++++++++++ gradle.properties | 5 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 +++++++++++++ gradlew.bat | 90 +++++++ releasenotes.gtpl | 6 + samples/build.gradle | 36 +++ samples/request-button-sample/build.gradle | 39 +++ .../request-button-sample/gradle.properties | 1 + .../src/main/AndroidManifest.xml | 43 ++++ .../android/rides/samples/SampleActivity.java | 68 ++++++ .../drawable-hdpi/uber_sample_ic_launcher.png | Bin 0 -> 1169 bytes .../drawable-mdpi/uber_sample_ic_launcher.png | Bin 0 -> 777 bytes .../uber_sample_ic_launcher.png | Bin 0 -> 1560 bytes .../uber_sample_ic_launcher.png | Bin 0 -> 2323 bytes .../uber_sample_ic_launcher.png | Bin 0 -> 3460 bytes .../src/main/res/layout/activity_sample.xml | 67 ++++++ .../src/main/res/values/dimens.xml | 26 ++ .../src/main/res/values/strings.xml | 27 +++ sdk/build.gradle | 115 +++++++++ sdk/proguard.txt | 0 sdk/src/main/AndroidManifest.xml | 27 +++ .../uber/sdk/android/rides/RequestButton.java | 159 ++++++++++++ .../sdk/android/rides/RequestDeeplink.java | 189 +++++++++++++++ .../sdk/android/rides/RideParameters.java | 227 ++++++++++++++++++ .../uber/sdk/android/rides/UberButton.java | 215 +++++++++++++++++ sdk/src/main/res/drawable-hdpi/uber_badge.png | Bin 0 -> 1187 bytes sdk/src/main/res/drawable-mdpi/uber_badge.png | Bin 0 -> 874 bytes .../main/res/drawable-xhdpi/uber_badge.png | Bin 0 -> 1665 bytes .../main/res/drawable-xxhdpi/uber_badge.png | Bin 0 -> 2428 bytes .../main/res/drawable-xxxhdpi/uber_badge.png | Bin 0 -> 3297 bytes .../uber_button_background_black_100.xml | 27 +++ .../uber_button_background_black_90.xml | 27 +++ .../uber_button_background_selector_black.xml | 27 +++ .../uber_button_background_selector_white.xml | 27 +++ .../uber_button_background_white_100.xml | 27 +++ .../uber_button_background_white_80.xml | 27 +++ sdk/src/main/res/values/attrs.xml | 34 +++ sdk/src/main/res/values/colors.xml | 30 +++ sdk/src/main/res/values/dimens.xml | 28 +++ sdk/src/main/res/values/strings.xml | 27 +++ sdk/src/main/res/values/styles.xml | 42 ++++ sdk/src/main/res/values/themes.xml | 28 +++ .../sdk/android/rides/RequestButtonTest.java | 140 +++++++++++ .../android/rides/RequestDeeplinkTest.java | 217 +++++++++++++++++ .../sdk/android/rides/RideParametersTest.java | 132 ++++++++++ .../com/uber/sdk/android/rides/TestUtils.java | 68 ++++++ .../sdk/android/rides/UberButtonTest.java | 216 +++++++++++++++++ .../dropoff_client_and_product_provided | 1 + .../resources/deeplinkuris/full_details_uri | 1 + .../deeplinkuris/just_client_provided | 1 + .../resources/deeplinkuris/no_app_installed | 1 + .../deeplinkuris/no_nickname_or_Address | 1 + .../deeplinkuris/pickup_and_client_provided | 1 + settings.gradle | 3 + 60 files changed, 2986 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 releasenotes.gtpl create mode 100644 samples/build.gradle create mode 100644 samples/request-button-sample/build.gradle create mode 100644 samples/request-button-sample/gradle.properties create mode 100644 samples/request-button-sample/src/main/AndroidManifest.xml create mode 100644 samples/request-button-sample/src/main/java/com/uber/sdk/android/rides/samples/SampleActivity.java create mode 100644 samples/request-button-sample/src/main/res/drawable-hdpi/uber_sample_ic_launcher.png create mode 100644 samples/request-button-sample/src/main/res/drawable-mdpi/uber_sample_ic_launcher.png create mode 100644 samples/request-button-sample/src/main/res/drawable-xhdpi/uber_sample_ic_launcher.png create mode 100644 samples/request-button-sample/src/main/res/drawable-xxhdpi/uber_sample_ic_launcher.png create mode 100644 samples/request-button-sample/src/main/res/drawable-xxxhdpi/uber_sample_ic_launcher.png create mode 100644 samples/request-button-sample/src/main/res/layout/activity_sample.xml create mode 100644 samples/request-button-sample/src/main/res/values/dimens.xml create mode 100644 samples/request-button-sample/src/main/res/values/strings.xml create mode 100644 sdk/build.gradle create mode 100644 sdk/proguard.txt create mode 100644 sdk/src/main/AndroidManifest.xml create mode 100644 sdk/src/main/java/com/uber/sdk/android/rides/RequestButton.java create mode 100644 sdk/src/main/java/com/uber/sdk/android/rides/RequestDeeplink.java create mode 100644 sdk/src/main/java/com/uber/sdk/android/rides/RideParameters.java create mode 100644 sdk/src/main/java/com/uber/sdk/android/rides/UberButton.java create mode 100644 sdk/src/main/res/drawable-hdpi/uber_badge.png create mode 100644 sdk/src/main/res/drawable-mdpi/uber_badge.png create mode 100644 sdk/src/main/res/drawable-xhdpi/uber_badge.png create mode 100644 sdk/src/main/res/drawable-xxhdpi/uber_badge.png create mode 100644 sdk/src/main/res/drawable-xxxhdpi/uber_badge.png create mode 100644 sdk/src/main/res/drawable/uber_button_background_black_100.xml create mode 100644 sdk/src/main/res/drawable/uber_button_background_black_90.xml create mode 100644 sdk/src/main/res/drawable/uber_button_background_selector_black.xml create mode 100644 sdk/src/main/res/drawable/uber_button_background_selector_white.xml create mode 100644 sdk/src/main/res/drawable/uber_button_background_white_100.xml create mode 100644 sdk/src/main/res/drawable/uber_button_background_white_80.xml create mode 100644 sdk/src/main/res/values/attrs.xml create mode 100644 sdk/src/main/res/values/colors.xml create mode 100644 sdk/src/main/res/values/dimens.xml create mode 100644 sdk/src/main/res/values/strings.xml create mode 100644 sdk/src/main/res/values/styles.xml create mode 100644 sdk/src/main/res/values/themes.xml create mode 100644 sdk/src/test/java/com/uber/sdk/android/rides/RequestButtonTest.java create mode 100644 sdk/src/test/java/com/uber/sdk/android/rides/RequestDeeplinkTest.java create mode 100644 sdk/src/test/java/com/uber/sdk/android/rides/RideParametersTest.java create mode 100644 sdk/src/test/java/com/uber/sdk/android/rides/TestUtils.java create mode 100644 sdk/src/test/java/com/uber/sdk/android/rides/UberButtonTest.java create mode 100644 sdk/src/test/resources/deeplinkuris/dropoff_client_and_product_provided create mode 100644 sdk/src/test/resources/deeplinkuris/full_details_uri create mode 100644 sdk/src/test/resources/deeplinkuris/just_client_provided create mode 100644 sdk/src/test/resources/deeplinkuris/no_app_installed create mode 100644 sdk/src/test/resources/deeplinkuris/no_nickname_or_Address create mode 100644 sdk/src/test/resources/deeplinkuris/pickup_and_client_provided create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b5f68e08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +.classpath +.project +.settings +eclipsebin + +bin +gen +build +out +lib + +target +pom.xml.* +release.properties +local.properties +.gradle + +.idea +*.iml +classes + +obj + +.DS_Store +log.txt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..b0131780 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +v0.1.0 - 11/24/2015 +------------------ + - Initial version. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..b090ca4f --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015 Uber Technologies, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 304360ca..ac5362f9 100644 --- a/README.md +++ b/README.md @@ -1 +1,120 @@ -Readme +# Uber Rides Android SDK + +Official Android SDK (beta) to support Uber’s deeplinks. + +This library allows you to integrate Uber into your Android app. + +At a minimum, this SDK is designed to work with Android SDK 16. + +## Before you begin + +Before using this SDK, register your application on the [Uber Developer Site](https://developer.uber.com/). + +## Installation + +To use the Uber Rides Android SDK, add the compile dependency with the latest version of the Uber SDK. + +### Gradle + +Add the Uber Rides Android SDK to your `build.gradle`: +```gradle +dependencies { + compile 'com.uber.sdk:rides-android:0.1.0' +} +``` + +### Maven + +In the `pom.xml` file: +```xml + + com.uber.sdk + rides-android + 0.1.0/version> + +``` + +## How to use + +You can add a Ride Request Button to your View like you would any other View: +```java +RequestButton requestButton = new RequestButton(context); +requestButton.setClientId("your_client_id"); +layout.addView(requestButton); +``` + +This will create a request button with default behavior, with pickup pin set to the user’s current location. The user will need to select a product and input additional information when they are switched over to the Uber application. + +You can also add your button through XML: +```xml + + + + + +``` + +To use the `uber` custom attribute be sure to add `xmlns:uber="http://schemas.android.com/apk/res-auto"` to your root view element. + +### Adding Parameters + +We suggest passing additional parameters to make the Uber experience even more seamless for your users. For example, dropoff location parameters can be used to automatically pass the user’s destination information over to the driver: +```java +RequestButton requestButton = RequestButton(context); +requestButton.setClientId(“your_client_id”); +RideParameters rideParams = new RideParameters.Builder() + .setProductID(“abc123-productID”) + .setPickupLocation(latitude: “37.770”, longitude: “-122.466”, nickname: “California Academy of Sciences”) + .setDropoffLocation(latitude: “37.791”, longitude: “-122.405”, nickname: “Pier 39”) + .build(); +requestButton.setRideParameters(rideParams); +layout.addView(requestButton); +``` +With all the necessary parameters set, pressing the button will seamlessly prompt a ride request confirmation screen. + +### Color Style + +The default color has a black background with white text: +```xml + +``` +For a button with a white background and black text: +```xml + +``` + +## Sample Apps + + +A sample app can be found in the `samples` folder. Alternatively, you can also download a sample from the [releases page](https://github.com/uber/rides-android-sdk/releases/tag/v0.1.0). + +Don’t forget to configure the appropriate `res/values/strings.xml` file and add your client ID. + +To install the sample app from your IDE, File > New > Import Project and select the extracted folder from the downloaded sample. + +## Getting help + +Uber developers actively monitor the Uber Tag on StackOverflow. If you need help installing or using the library, you can ask a question there. Make sure to tag your question with `uber-api` and `android`! + +For full documentation about our API, visit our Developer Site. + +## Contributing + +We love contributions. If you’ve found a bug in the library or would like new features added, go ahead and open issues or pull requests against this repo. Write a test to show your bug was fixed or the feature works as expected. + +## MIT Licensed diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..efaa696c --- /dev/null +++ b/build.gradle @@ -0,0 +1,175 @@ +apply plugin: 'distribution' +apply plugin: 'net.researchgate.release' +apply plugin: 'co.riiid.gradle' + +import groovy.text.GStringTemplateEngine +import org.codehaus.groovy.runtime.DateGroovyMethods + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + classpath 'net.researchgate:gradle-release:2.2.2' + classpath 'co.riiid:gradle-github-plugin:0.3.1' + } +} + +allprojects { + apply plugin: 'checkstyle' + apply plugin: 'maven' + + ["githubToken", "ossrhUsername", "ossrhPassword", + "signing.keyId", "signing.password", "signing.secretKeyRingFile",].each {checkAndDefaultProperty(it)} + + ext.set("unsnapshottedVersion", version.replaceAll("-SNAPSHOT", "")) + ext.set("samples", project(":samples").subprojects.collect {it.path}) + ext.set("isReleaseVersion", !version.endsWith("SNAPSHOT")) + + repositories { + jcenter() + } + + checkstyle { + toolVersion = "6.11.2" + } + + task checkstyleMain(type: Checkstyle, overwrite: true) { + configFile = new File("{$project.projectDir}/config/checkstyle/checkstyle-main.xml") + } + + task checkstyleTest(type: Checkstyle, overwrite: true) { + configFile = new File("{$project.projectDir}/config/checkstyle/checkstyle-test.xml") + } +} + +def generateReleaseNotes() { + def changelogSnippet = generateChangelogSnippet() + def model = [title: "Uber Rides Android SDK (Beta) v${unsnapshottedVersion}", + date: DateGroovyMethods.format(new Date(), 'MM/dd/yyyy'), + snippet: changelogSnippet, + assets: project.samples.collect {[ + title: project(it).name, + download: githubDownloadPrefix + "v${unsnapshottedVersion}/" + + project(it).name + "-v${unsnapshottedVersion}.zip", + description: project(it).description, + ]}] + def engine = new GStringTemplateEngine() + def template = engine.createTemplate(rootProject.file('releasenotes.gtpl')).make(model) + return template.toString() +} + +def generateChangelogSnippet() { + def changelog = rootProject.file('CHANGELOG.md').text + def snippet = "" + def stop = false + changelog.eachLine {line, count -> + if (count >= 2) { + stop = stop || line.startsWith("v"); + if (!stop) { + snippet += line + "\n"; + } + } + } + return " " + snippet.trim(); +} + +def checkAndDefaultProperty(prop) { + if (!project.hasProperty(prop)) { + logger.warn("Add " + prop + " to your ~/.gradle/gradle.properties file.") + rootProject.ext.set(prop, prop) + } +} + +def checkForChangelogUpdates(task) { + def changelogtext = rootProject.file('CHANGELOG.md').text + if (!changelogtext.startsWith("v${unsnapshottedVersion} -")) { + throw new AssertionError( + "Changelog must be updated with v{$unsnapshottedVersion} before release. Please check " + + rootProject.file('CHANGELOG.md').absolutePath) + } +} + +gradle.taskGraph.afterTask { Task task, TaskState state -> + if (task.path.endsWith("release") || task.path.endsWith("githubReleaseZip") + || task.path.endsWith("publicrepoDistZip")) { + checkForChangelogUpdates(task) + } +} + +// Skip signing archives on Jenkins when -SNAPSHOT is being checked in. +gradle.taskGraph.beforeTask { Task task -> + if (task.path.contains("sign") && !ext.isReleaseVersion) { + task.enabled = false + } +} + +afterReleaseBuild.dependsOn ":sdk:uploadArchives" +updateVersion.dependsOn ":githubRelease" +githubRelease.dependsOn project(":samples").subprojects.collect {it.path + ":githubReleaseZip"} + +release { + failOnCommitNeeded = false + failOnPublishNeeded = false + failOnSnapshotDependencies = false + revertOnFail = true + tagTemplate = "v${unsnapshottedVersion}" +} + +github { + owner = 'uber' + repo = 'rides-android-sdk' + token = "${githubToken}" + tagName = "v${unsnapshottedVersion}" + targetCommitish = 'master' + name = "v${unsnapshottedVersion}" + body = generateReleaseNotes() + assets = project.samples.collect { + project(it).buildDir.absolutePath + "/distributions/" + project(it).name + + "-v${unsnapshottedVersion}.zip" + } +} + +distributions { + publicrepo { + baseName = 'publicrepo' + contents { + from(rootDir) { + include 'build.gradle' + include 'CHANGELOG.md' + include 'gradle.properties' + include 'gradlew' + include 'gradlew.bat' + include 'LICENSE' + include 'releasenotes.gtpl' + include 'settings.gradle' + include 'gradle/' + } + + from(rootDir) { + include 'README.md' + filter { String line -> + line.replaceAll("_version_", unsnapshottedVersion) + } + } + + from('sdk') { + filesNotMatching("**/*.png") { + filter { String line -> + line.replaceAll("_version_", unsnapshottedVersion) + } + } + exclude 'build' + exclude '*.iml' + into 'sdk' + } + + from('samples') { + exclude '**/build' + exclude '**/*.iml' + into 'samples' + } + } + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..183a8e55 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,5 @@ +group=com.uber.sdk +groupId=com.uber.sdk +artifactId=rides-android +githubDownloadPrefix=https://github.com/uber/rides-android-sdk/releases/download/ +version=0.1.0 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8c0fb64a8698b08ecc4158d828ca593c4928e9dd GIT binary patch literal 49896 zcmagFb986H(k`5d^NVfUwr$(C?M#x1ZQHiZiEVpg+jrjgoQrerx!>1o_ul)D>ebz~ zs=Mmxr&>W81QY-S1PKWQ%N-;H^tS;2*XwVA`dej1RRn1z<;3VgfE4~kaG`A%QSPsR z#ovnZe+tS9%1MfeDyz`RirvdjPRK~p(#^q2(^5@O&NM19EHdvN-A&StN>0g6QA^VN z0Gx%Gq#PD$QMRFzmK+utjS^Y1F0e8&u&^=w5K<;4Rz|i3A=o|IKLY+g`iK6vfr9?+ z-`>gmU&i?FGSL5&F?TXFu`&Js6h;15QFkXp2M1H9|Eq~bpov-GU(uz%mH0n55wUl- zv#~ccAz`F5wlQ>e_KlJS3@{)B?^v*EQM=IxLa&76^y51a((wq|2-`qON>+4dLc{Oo z51}}o^Zen(oAjxDK7b++9_Yg`67p$bPo3~BCpGM7uAWmvIhWc5Gi+gQZ|Pwa-Gll@<1xmcPy z|NZmu6m)g5Ftu~BG&Xdxclw7Cij{xbBMBn-LMII#Slp`AElb&2^Hw+w>(3crLH!;I zN+Vk$D+wP1#^!MDCiad@vM>H#6+`Ct#~6VHL4lzmy;lSdk>`z6)=>Wh15Q2)dQtGqvn0vJU@+(B5{MUc*qs4!T+V=q=wy)<6$~ z!G>e_4dN@lGeF_$q9`Ju6Ncb*x?O7=l{anm7Eahuj_6lA{*#Gv*TaJclevPVbbVYu z(NY?5q+xxbO6%g1xF0r@Ix8fJ~u)VRUp`S%&rN$&e!Od`~s+64J z5*)*WSi*i{k%JjMSIN#X;jC{HG$-^iX+5f5BGOIHWAl*%15Z#!xntpk($-EGKCzKa zT7{siZ9;4TICsWQ$pu&wKZQTCvpI$Xvzwxoi+XkkpeE&&kFb!B?h2hi%^YlXt|-@5 zHJ~%AN!g_^tmn1?HSm^|gCE#!GRtK2(L{9pL#hp0xh zME}|DB>(5)`iE7CM)&_+S}-Bslc#@B5W4_+k4Cp$l>iVyg$KP>CN?SVGZ(&02>iZK zB<^HP$g$Lq*L$BWd?2(F?-MUbNWTJVQdW7$#8a|k_30#vHAD1Z{c#p;bETk0VnU5A zBgLe2HFJ3032$G<`m*OB!KM$*sdM20jm)It5OSru@tXpK5LT>#8)N!*skNu1$TpIw zufjjdp#lyH5bZ%|Iuo|iu9vG1HrIVWLH>278xo>aVBkPN3V$~!=KnlXQ4eDqS7%E% zQ!z^$Q$b^6Q)g#cLpwur(|<0gWHo6A6jc;n`t(V9T;LzTAU{IAu*uEQ%Ort1k+Kn+f_N`9|bxYC+~Z1 zCC1UCWv*Orx$_@ydv9mIe(liLfOr7mhbV@tKw{6)q^1DH1nmvZ0cj215R<~&I<4S| zgnr;9Cdjqpz#o8i0CQjtl`}{c*P)aSdH|abxGdrR)-3z+02-eX(k*B)Uqv6~^nh** z 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/releasenotes.gtpl b/releasenotes.gtpl new file mode 100644 index 00000000..3a697ef1 --- /dev/null +++ b/releasenotes.gtpl @@ -0,0 +1,6 @@ +${title} - ${date} +${snippet} + +| Download | Description | +| ------------- |-------------| +<% assets.each{ asset -> %>| <%= "[" + asset.title + "](" + asset.download + ")" %> | <%= asset.description %> |\n<%}%> \ No newline at end of file diff --git a/samples/build.gradle b/samples/build.gradle new file mode 100644 index 00000000..9f777b0a --- /dev/null +++ b/samples/build.gradle @@ -0,0 +1,36 @@ +subprojects { + task githubReleaseZip(type: Zip) { + version = "v${unsnapshottedVersion}" + + from('.') { + filesNotMatching("**/*.png") { + filter { String line -> + line.replaceAll("compile project\\(':sdk'\\)", + "compile '${groupId}:${artifactId}:${unsnapshottedVersion}'") + } + } + into '.' + exclude 'build' + exclude '*.iml' + } + + from(rootProject.projectDir.absolutePath) { + include 'gradle/' + include 'gradlew' + include 'gradlew.bat' + include 'LICENSE' + into '.' + } + + from('build/poms') { + include 'pom-default.xml' + rename { String fileName -> + fileName.replaceAll('-default', '') + } + filter { String line -> + line.replaceAll('-SNAPSHOT', '') + } + into '.' + } + } +} \ No newline at end of file diff --git a/samples/request-button-sample/build.gradle b/samples/request-button-sample/build.gradle new file mode 100644 index 00000000..6de1b9e9 --- /dev/null +++ b/samples/request-button-sample/build.gradle @@ -0,0 +1,39 @@ +apply plugin: 'com.android.application' + +repositories { + jcenter() + mavenLocal() +} + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + } +} + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.uber.sdk.android.rides.samples" + minSdkVersion 16 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:23.0.1' + compile project(':sdk') +} diff --git a/samples/request-button-sample/gradle.properties b/samples/request-button-sample/gradle.properties new file mode 100644 index 00000000..1da43d5b --- /dev/null +++ b/samples/request-button-sample/gradle.properties @@ -0,0 +1 @@ +description=Ride Request Button Sample \ No newline at end of file diff --git a/samples/request-button-sample/src/main/AndroidManifest.xml b/samples/request-button-sample/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a28ae321 --- /dev/null +++ b/samples/request-button-sample/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/samples/request-button-sample/src/main/java/com/uber/sdk/android/rides/samples/SampleActivity.java b/samples/request-button-sample/src/main/java/com/uber/sdk/android/rides/samples/SampleActivity.java new file mode 100644 index 00000000..ce046c57 --- /dev/null +++ b/samples/request-button-sample/src/main/java/com/uber/sdk/android/rides/samples/SampleActivity.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides.samples; + +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; + +import com.uber.sdk.android.rides.RideParameters; +import com.uber.sdk.android.rides.RequestButton; + +/** + * Activity that demonstrates how to use a {@link RequestButton}. + */ +public class SampleActivity extends AppCompatActivity { + + private static final String DROPOFF_ADDR = "One Embarcadero Center, San Francisco"; + private static final float DROPOFF_LAT = 37.795079f; + private static final float DROPOFF_LONG = -122.397805f; + private static final String DROPOFF_NICK = "Embarcadero"; + private static final String PICKUP_ADDR = "1455 Market Street, San Francisco"; + private static final float PICKUP_LAT = 37.775304f; + private static final float PICKUP_LONG = -122.417522f; + private static final String PICKUP_NICK = "Uber HQ"; + private static final String UBERX_PRODUCT_ID = "a1111c8c-c720-46c3-8534-2fcdd730040d"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_sample); + + String clientId = getString(R.string.client_id); + if (clientId.equals("insert_your_client_id_here")) { + throw new IllegalArgumentException("Please enter your client ID in client_id in res/values/strings.xml"); + } + + RequestButton uberButtonBlack = (RequestButton) findViewById(R.id.uber_button_black); + RequestButton uberButtonWhite = (RequestButton) findViewById(R.id.uber_button_white); + + RideParameters rideParameters = new RideParameters.Builder() + .setProductId(UBERX_PRODUCT_ID) + .setPickupLocation(PICKUP_LAT, PICKUP_LONG, PICKUP_NICK, PICKUP_ADDR) + .setDropoffLocation(DROPOFF_LAT, DROPOFF_LONG, DROPOFF_NICK, DROPOFF_ADDR) + .build(); + + uberButtonBlack.setRideParameters(rideParameters); + uberButtonWhite.setRideParameters(rideParameters); + } +} diff --git a/samples/request-button-sample/src/main/res/drawable-hdpi/uber_sample_ic_launcher.png b/samples/request-button-sample/src/main/res/drawable-hdpi/uber_sample_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a626ceb44d5d9595558ab5133913a95c88d9fa7d GIT binary patch literal 1169 zcmV;C1aA9@P)Px(OG!jQRCodHoJ~knQ51lW)BGrV zn{)3S=KTP0V**To2{3_UOCTvuW^Rus>!HKpsMqut;@BjLcqtl<3~g`!=+q|kTb$Gx zQGosA(|sjN zthxCPl$KsK1P3#8xm-|O>;oL9x_LYvsHiAY=35D%qM`yz^DHd9LXHcCLZpq%prphn z6PuPP0fgV0m1bl)PELNd;u@^xBnDFgNOY>Q-_!F1Mn>Ml#zsJ;Tf@GTY|^r5H8nS7 zV$-rfAPBv^&&Y9RH*?bAp24xNAfpqp%Z>pNWEl_xA_x#_t{?_P5FpfCK@5l>K&ZKb z7!W~#P;&(_Ac6p)<_cm!1OY&O;1nP$?OuyVg-ZxoUFuG0%&DrO&8ne%h$>563Ak$uIh8L5?=|R`T6g< z*mZTa;B?w{+3jCEjDfMlCd)~*G16B8XnfqSiEx;f7z1O8O_q~rW2CPH z(8Ro|1oyMCdyT`{|-n+(ns~-@ScZrx0}FKP!w~u`(+A(uLdEqx}tM^ z>Hr~}%%_9kNKHEcBNEtvwU#CwBpD#3oAiM!jzI@2M}&iEp$v6KT0#T*yR2j&4%6+o zPiR2#hVQb_{^Pl%4NCO)ci_uSUhKWxBrJo( jGA6(Tm;e(vrUd=~QH?KXBprd|00000NkvXXu0mjfQDzuK literal 0 HcmV?d00001 diff --git a/samples/request-button-sample/src/main/res/drawable-mdpi/uber_sample_ic_launcher.png b/samples/request-button-sample/src/main/res/drawable-mdpi/uber_sample_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ce495910fa91ca25816064988b1b25a5e3a2aa9d GIT binary patch literal 777 zcmV+k1NQuhP)Px%!%0LzRA>e5S>=|w6mD{`^h9%MUokPhL+ z!*)>*<)Qze>w=lZY=J$Q~O<-;k~!-?eNAapYXny_x<_#{(A2+ z1E5Jy;9L{9LPneBpko-uo0jetsaJ^ieBXb1c6L|_YN8rQr-#2PTzpWoSF7#c4g@1$ z{+_B%o-C89k!^r$2Fd12B957?_fIWhwJVhqM221#rV(#GT2 zkGsElPNZ&j_61}zfSx-x{QB&|kNG0_$KOI|vH|HU z6r%h?28&OPXTV6O+b7B*QnCS@JV~rf+LUYnuMJ78Oxl!e0PlQ#s|`dk&=&C zRBWK{_K3Z}N#T>Kk-9?o6S-A5<)Bz2?H}EW)uhY0+!Oc%KT)@yb{4bN00000NkvXX Hu0mjf53z2# literal 0 HcmV?d00001 diff --git a/samples/request-button-sample/src/main/res/drawable-xhdpi/uber_sample_ic_launcher.png b/samples/request-button-sample/src/main/res/drawable-xhdpi/uber_sample_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..24d5dd0072f95463f1c9b21c46ab05bff7694047 GIT binary patch literal 1560 zcmai!c|6k%0LFhiw7Hf!O75c+d)?PsKBYC~NQR-et+pu2G4mp48an2EDan0AQj(A> zo6sul&9x*$uBJk1X1UGM>;3C}Kkq;9AJ6msvlzN;si{xA&j zJa=(VV)o*ZE4g~5Je>2T5?+$nLrfuK@8Cs-ragxSVfXqnw%w`H>9camY0#zg88{wv zIgeFyE|nVd^5|(EurAVPZS?O8pB5IyO$WZA3ziBkr>EDKQ|nPtCV)JauAl>jgZ{zl zGhK!BJ!o5v1peK^!b02J*<{7#Ghj1TON%51^J5Sum+FAjIe1esQcgnMvav3x>+Q^5 ztb{9X0Vhr;Jx`(;Sg!nd*#G%O9b|rC#r1K_BG(q=hPBFbb`>bSpJZgNL_*u3jv7iX zf-`XbszI#`lmV#h!H5n*(SX}4f}Xw9?}&H>5Q6P#o(0DzmO`{~bcl#>bQhRV@`VjU z@&P*)&y;^v`qO20>uGy=nX=#BmB)K8D?pR8$!%0xmgCL|$(n5Mhf6XRdEHxzzT~_s ze}=3uj&yNxp)E`mo0*BMNiAsK^Kdvq@v>aF%!~Aetr%U)1+c->{@-5mPMbRx7gJ& zH%1KXdh-H1oZb2wtr&B>?82k)EZ}L-{u;HX2^^iRvSbISXQBea0D9o(5N?|G&~u0F!=q>Ze`1IWWl&L_U0v@;;uF#CCZ%cvc4sb4 z2EumUvK;Mpc5z{1YE()|IO_+m-+6)Wmn)5GpisqAGcDeX-;lf#JvSd|=0$b4eU{qS zw)zyH?6T|X$Luq^s)!ObLfMQFNMHXzSa|ATeLRu)k{5T6tzou(oEecqhJ;?%WK}{* ztr(e7^(hQyJP`uZYPwb9K};PRALlHku*fWnPF^LR>1>Z_g->%|v0|pCgmj^BYhwhs zZNc1Time{-hI2a1JzB6k^kD;OYN#`=Z~UlS)SSuXwl+7)U^Q52HvbDJ?Et?!51o2_ z1MKN3J+N0c$XUvV$!i}wc6QPe6_j8lX7r-QKMxKMOGgPM#trozb$+NTsV zZ_g`>80IT7Sh3c2zb2*nimDfum%SfUR$AlQVl}lk5V5i1;G%9{FFH^0vmxkk9gk;< zC5{x}34}9@hBhvjiw&I%5O9RrnbQ+4;K!Xbv|L(Rq@Y}jR$k`EM+DGlj307`FjYeO zjitubY(U@Xg4OC2l8_+d>HcTRosve^})=2KHvG?P_2|@x??xHF=%jN zezN#;+C!IpSisxuiG+<*bGPS@2$-ct5 z?HjOAi5H1S=;ZM#?Um!Oo?@}GFkxvj_WMku3g~voAs@@(r0uI*)9BeMSH7Pi=*i_$ zL`dBZ932|S6zKG}Bw`vm{6($)pF#?_<^%EWF%~xd3n_pl!7dXCQlxyM{u%gsD0fW< z+>KwmA=i}89GQ^szvF6?3{SV0Us+oqFBW7_f)6 x#bPb(0QILo(Zn^F_G%5HlkuPX)Pa)h`Q$KaXb$IwLgnVw15OUdaMf5!(qC?s(*ghh literal 0 HcmV?d00001 diff --git a/samples/request-button-sample/src/main/res/drawable-xxhdpi/uber_sample_ic_launcher.png b/samples/request-button-sample/src/main/res/drawable-xxhdpi/uber_sample_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..138ad4fbca6b4f03cc351344f4392539900a1afc GIT binary patch literal 2323 zcmds3`#%#38z0N1qIxAXVyzq^mng+twvbyRVKm7OnsF$(P8=P|vAK-qz8Mn>F>K8$ z=K4;YVsZ(`txVZ;u!G|sUhn(<0q2MJxA*h;KF{~_d7jVr`^)ouKF>qA;cO>=2y_Sl z0LVMo+uYpetbd2B^nMmz6TIFhiHMtaR{^zrwK)Jlw%5T1?v9gKp!#6l?G(FQEX$jY zCWHp7+G<}QxY|f2*hmG@7eEBYj;Md@?dc3UrmS#za*+rz1)J@VbATMAQ|cKNIwK!3RnnPz^bWq_Y*GVD#-<; zvdbQz!FF21?kK~oa|6V;iT{Hrmwf0f!Oa_*ooC?d`0=Bg_ETB$%Jy#oqy!i-hlg;>SBi!}fN8-{!ftGDp;&6N!AoM!ANgOCt{jOAgkhMA%$S>9kKU z7MGdIb-@X%kZ1uC%vKQcaEaa`vjLdE|`sNo$u-ezhjHz-PGUM5MSSOK=5pR9bWr+<}PI+O1g>}r&M;YH{JSvLYr9|8ap!r(Rg z({zVarpdO!Ah(ZIA3%e1Wxqir4YOpGc{&m*_L@nJc!1p54k@N1K>FtYy9}M1xWOTs zX)2}7bwlX=y+LSn+Y4@u(G8>D%cLr5Lb!SQj*!WshYxq_b@Zy`h9;>)NyZQS-u$;5 zd9v`G1xflvH6=7PlX<1lv>j&Ywk&de)i*4p%=Xol3H++Ld+}isyMo(Sw5ZSC+gH>X zUPShGe0|><^p&?R@52_!>xC-X6s0XT--z0G8`l9gaPE}v?j|rMJnIrl1WTflj0Z=G z(wrSCwwqPQeTJTb2KU!#cUP+rx;Gc#i_D`TC?+aReO!J)IjAMZLfH03l*8dTO<9c| zKUH-h!$Vq7<5IHg0o~kO5e_s)Y1j_fgs7`Sg*5N8A(7iX9)Z+EXQ~;($7h|rg5u1F zj%+?Ww8lo$i zy$t3K5_lVr)R%DPy2e{Yj&!`0koxofZTaMV`hHopXWiiE z&$ND~NLtqmI42qIETopUuRKJ>9&S~*zf?f+6LvuAsT|8F(iVk+KM)vUm=$s4 zq-m+Z^Ig#VtO3^j?kfJ7)%_{j6vKDmGqBm^ioJZm&Q$?E;7~k}qQzwNkC><9dppgB zg=KB9O&VEj+5Ar>acpWAaWIurqvTb zqRdZ*GUwpqo1ICC-$h=H8GW3j9NNNnzjjzc!~9z?nZ zLaEpTNl^;oG34yBP2E%`LjIxIuv0BzZdMLBM`B&?8sU&stu{Yd{6ie;RS~Cmo%|dZ zIzF`CQBF{-QTLoV9Q}=PUCncbQKS?jle`XFHkc#um@!Te5#(j$U{IA!Pd*#(UAZ|*uiPC48&4s|w=U2n=aRTY)h zvh^m5Y_2BWNZf#pKj~DAEoF_$-hOi!)Jqv3lc4+#%Vw7 Qe>ngLTW6cvtLUVE0VO9sApigX literal 0 HcmV?d00001 diff --git a/samples/request-button-sample/src/main/res/drawable-xxxhdpi/uber_sample_ic_launcher.png b/samples/request-button-sample/src/main/res/drawable-xxxhdpi/uber_sample_ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..06aff4218aaabff121cadd1ac4be7efdc4f55ccf GIT binary patch literal 3460 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 literal 0 HcmV?d00001 diff --git a/samples/request-button-sample/src/main/res/layout/activity_sample.xml b/samples/request-button-sample/src/main/res/layout/activity_sample.xml new file mode 100644 index 00000000..b05f44ec --- /dev/null +++ b/samples/request-button-sample/src/main/res/layout/activity_sample.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + diff --git a/samples/request-button-sample/src/main/res/values/dimens.xml b/samples/request-button-sample/src/main/res/values/dimens.xml new file mode 100644 index 00000000..6d8bfe77 --- /dev/null +++ b/samples/request-button-sample/src/main/res/values/dimens.xml @@ -0,0 +1,26 @@ + + + + + 16dp + diff --git a/samples/request-button-sample/src/main/res/values/strings.xml b/samples/request-button-sample/src/main/res/values/strings.xml new file mode 100644 index 00000000..6505cd59 --- /dev/null +++ b/samples/request-button-sample/src/main/res/values/strings.xml @@ -0,0 +1,27 @@ + + + + + Uber SDK + insert_your_client_id_here + diff --git a/sdk/build.gradle b/sdk/build.gradle new file mode 100644 index 00000000..5df47c0c --- /dev/null +++ b/sdk/build.gradle @@ -0,0 +1,115 @@ +apply plugin: 'com.android.library' +apply plugin: 'maven' +apply plugin: 'signing' +apply plugin: 'com.github.dcendents.android-maven' + +buildscript { + repositories { + jcenter() + } + + dependencies{ + classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' + } +} + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + useLibrary 'org.apache.http.legacy' + + defaultConfig { + minSdkVersion 16 + versionName version + consumerProguardFiles 'proguard.txt' + } +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.sourceFiles +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +signing { + sign configurations.archives +} + +project.archivesBaseName = artifactId + +uploadArchives { + repositories { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + + repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + + pom.project { + name 'Uber Rides Android SDK (beta)' + packaging 'aar' + artifactId artifactId + + description 'The official Android SDK (beta) for the Uber Rides API.' + url 'https://developer.uber.com' + + scm { + connection 'scm:git:git@github.com:uber/rides-android-sdk.git' + developerConnection 'scm:git:git@github.com:uber/rides-android-sdk.git' + url 'git@github.com:uber/rides-android-sdk.git' + } + + licenses { + license { + name 'MIT License' + url 'http://www.opensource.org/licenses/mit-license.php' + } + } + + developers { + developer { + id 'arogal' + name 'Adam Rogal' + email 'arogal@uber.com' + } + + developer { + id 'itstexter' + name 'Alex Texter' + email 'texter@uber.com' + } + } + } + } + } +} + +dependencies { + compile 'com.android.support:appcompat-v7:23.0.1' + compile 'com.google.guava:guava:18.0' + compile 'com.google.http-client:google-http-client-jackson2:1.19.0' + + testCompile 'junit:junit:4.12' + testCompile "org.mockito:mockito-core:1.9.5" + testCompile "org.robolectric:robolectric:3.0" +} diff --git a/sdk/proguard.txt b/sdk/proguard.txt new file mode 100644 index 00000000..e69de29b diff --git a/sdk/src/main/AndroidManifest.xml b/sdk/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b6889414 --- /dev/null +++ b/sdk/src/main/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/sdk/src/main/java/com/uber/sdk/android/rides/RequestButton.java b/sdk/src/main/java/com/uber/sdk/android/rides/RequestButton.java new file mode 100644 index 00000000..7f4e0d06 --- /dev/null +++ b/sdk/src/main/java/com/uber/sdk/android/rides/RequestButton.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import android.content.Context; +import android.content.res.TypedArray; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.view.View; + +/** + * An Uber styled button to request rides with specific {@link RideParameters}. Default {@link RideParameters} is + * set to a pickup of the device's location. Requires a client ID to function. + */ +public class RequestButton extends UberButton { + + private static final String USER_AGENT_BUTTON = "rides-button-v0.1.0"; + + @NonNull + private RideParameters mRideParameters = new RideParameters.Builder().build(); + @Nullable + private String mClientId; + + public RequestButton(Context context) { + this(context, null); + } + + public RequestButton(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.uberButtonStyle); + } + + public RequestButton(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr, 0); + } + + /** + * Sets the {@link RideParameters} that will be used to request a ride when the button is clicked. If null will + * use default RideParameters behavior. + */ + public void setRideParameters(@Nullable RideParameters rideParameters) { + if (rideParameters == null) { + rideParameters = new RideParameters.Builder().build(); + } + mRideParameters = rideParameters; + } + + /** + * Sets the client ID that is used to power the ride request. + */ + public void setClientId(@NonNull String clientId) { + this.mClientId = clientId; + } + + @Override + protected void init( + @NonNull Context context, + @Nullable AttributeSet attributeSet, + int defStyleAttrs, + int defStyleRes) { + Style style = Style.DEFAULT; + if (attributeSet != null) { + TypedArray typedArray = context.getTheme().obtainStyledAttributes(attributeSet, + R.styleable.RequestButton, 0, 0); + mClientId = typedArray.getString(R.styleable.RequestButton_client_id); + style = Style.fromInt(typedArray.getInt(R.styleable.RequestButton_style, + Style.DEFAULT.getValue())); + typedArray.recycle(); + } + // If no style specified, or just the default UberButton style, use the style attribute + defStyleRes = defStyleRes == 0 || defStyleRes == R.style.UberButton ? style.getStyleId() : defStyleRes; + + super.init(context, attributeSet, defStyleAttrs, defStyleRes); + + setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + if (mClientId == null) { + throw new IllegalStateException("Client ID required to use RequestButton."); + } + + RequestDeeplink requestDeeplink = new RequestDeeplink.Builder() + .setClientId(mClientId) + .setRideParameters(mRideParameters) + .setUserAgent(USER_AGENT_BUTTON) + .build(); + + requestDeeplink.execute(getContext()); + } + }); + } + + /** + * Encapsulates the valid values for the uber:color_scheme attribute for a {@link RequestButton} + */ + private enum Style { + /** + * Black background, white text. This is the default. + */ + BLACK(0, R.style.UberButton_RideRequest), + + /** + * White background, black text. + */ + WHITE(1, R.style.UberButton_RideRequest_White); + + private static Style DEFAULT = BLACK; + + private int mIntValue; + private int mStyleId; + + Style(int value, int styleId) { + this.mIntValue = value; + this.mStyleId = styleId; + } + + /** + * If the value is not found returns default Style. + */ + @NonNull + static Style fromInt(int enumValue) { + for (Style style : values()) { + if (style.getValue() == enumValue) { + return style; + } + } + + return DEFAULT; + } + + private int getValue() { + return mIntValue; + } + + private int getStyleId() { + return mStyleId; + } + } +} diff --git a/sdk/src/main/java/com/uber/sdk/android/rides/RequestDeeplink.java b/sdk/src/main/java/com/uber/sdk/android/rides/RequestDeeplink.java new file mode 100644 index 00000000..b9e6edfd --- /dev/null +++ b/sdk/src/main/java/com/uber/sdk/android/rides/RequestDeeplink.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import com.google.common.base.Preconditions; + + +/** + * A deeplink for requesting rides in the Uber application. + * + * @see Uber deeplink documentation + */ +public class RequestDeeplink { + + private static final String UBER_PACKAGE_NAME = "com.ubercab"; + private static final String UBER_SDK_LOG_TAG = "UberSDK"; + private static final String USER_AGENT_DEEPLINK = "rides-deeplink-v0.1.0"; + + @NonNull private final Uri mUri; + + private RequestDeeplink(@NonNull Uri uri) { + mUri = uri; + } + + /** + * Executes the deeplink to launch the Uber app. If the app is not installed redirects to the play store. + * + * @param context The {@link Context} the deeplink will be executed from, used to start a new {@link Activity}. + * If not a windowed context will error. + */ + public void execute(@NonNull Context context) { + PackageManager packageManager = context.getPackageManager(); + try { + packageManager.getPackageInfo(UBER_PACKAGE_NAME, PackageManager.GET_ACTIVITIES); + Intent intent = new Intent(Intent.ACTION_VIEW, mUri); + context.startActivity(intent); + } catch (PackageManager.NameNotFoundException e) { + Log.i(UBER_SDK_LOG_TAG, "Uber app not installed, redirecting to mobile sign up."); + String redirect = context.getResources().getString(R.string.mobile_redirect); + String url = String.format(redirect, + mUri.getQueryParameter(Builder.CLIENT_ID), mUri.getQueryParameter(Builder.USER_AGENT)); + context.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); + } + } + + /** + * The {@link Uri} for the deeplink. + */ + @NonNull + public Uri getUri() { + return mUri; + } + + /** + * Builder for {@link RequestDeeplink} objects. + */ + public static class Builder { + + public static final String ACTION = "action"; + public static final String SET_PICKUP = "setPickup"; + public static final String CLIENT_ID = "client_id"; + public static final String PRODUCT_ID = "product_id"; + public static final String MY_LOCATION = "my_location"; + public static final String LATITUDE = "[latitude]"; + public static final String LONGITUDE = "[longitude]"; + public static final String NICKNAME = "[nickname]"; + public static final String FORMATTED_ADDRESS = "[formatted_address]"; + public static final String SCHEME = "uber"; + public static final String USER_AGENT = "user-agent"; + + private String mClientId; + private String mUserAgent = USER_AGENT_DEEPLINK; + private RideParameters mRideParameters; + + /** + * Sets the client ID for the app the deeplink is being started from. + */ + public RequestDeeplink.Builder setClientId(@NonNull String cliendId) { + mClientId = cliendId; + return this; + } + + /** + * Sets the {@link RideParameters} for the deeplink. + */ + public RequestDeeplink.Builder setRideParameters(@NonNull RideParameters rideParameters) { + mRideParameters = rideParameters; + return this; + } + + /** + * Builds an {@link RequestDeeplink} object. + */ + @NonNull + public RequestDeeplink build() { + validate(); + + Uri.Builder builder = new Uri.Builder(); + builder.scheme(SCHEME); + builder.appendQueryParameter(ACTION, SET_PICKUP); + builder.appendQueryParameter(CLIENT_ID, mClientId); + if (mRideParameters.getProductId() != null) { + builder.appendQueryParameter(PRODUCT_ID, mRideParameters.getProductId()); + } + if (mRideParameters.getPickupLatitude() != null && mRideParameters.getPickupLongitude() != null) { + addLocation(LocationType.PICKUP, Float.toString(mRideParameters.getPickupLatitude()), + Float.toString(mRideParameters.getPickupLongitude()), mRideParameters.getPickupNickname(), + mRideParameters.getPickupAddress(), builder); + } + if (mRideParameters.isPickupMyLocation()) { + builder.appendQueryParameter(LocationType.PICKUP.getUriQueryKey(), MY_LOCATION); + } + if (mRideParameters.getDropoffLatitude() != null && mRideParameters.getDropoffLongitude() != null) { + addLocation(LocationType.DROPOFF, Float.toString(mRideParameters.getDropoffLatitude()), + Float.toString(mRideParameters.getDropoffLongitude()), mRideParameters.getDropoffNickname(), + mRideParameters.getDropoffAddress(), builder); + } + if (mUserAgent == null) { + mUserAgent = USER_AGENT_DEEPLINK; + } + builder.appendQueryParameter(USER_AGENT, mUserAgent); + return new RequestDeeplink(builder.build()); + } + + /** + * Sets the user agent, describing where this {@link RequestDeeplink} came from for analytics. + */ + RequestDeeplink.Builder setUserAgent(@NonNull String userAgent) { + mUserAgent = userAgent; + return this; + } + + private void addLocation(@NonNull LocationType locationType, @NonNull String latitude, + @NonNull String longitude, @Nullable String nickname, @Nullable String address, Uri.Builder builder) { + String typeQueryKey = locationType.getUriQueryKey(); + builder.appendQueryParameter(typeQueryKey + LATITUDE, latitude); + builder.appendQueryParameter(typeQueryKey + LONGITUDE, longitude); + if (nickname != null) { + builder.appendQueryParameter(typeQueryKey + NICKNAME, nickname); + } + if (address != null) { + builder.appendQueryParameter(typeQueryKey + FORMATTED_ADDRESS, address); + } + } + + private void validate() { + Preconditions.checkState(mClientId != null, "Must supply a client ID."); + Preconditions.checkState(mRideParameters != null, "Must supply ride parameters."); + } + + private enum LocationType { + PICKUP, + DROPOFF; + + private String getUriQueryKey() { + return name().toLowerCase(); + } + } + } +} diff --git a/sdk/src/main/java/com/uber/sdk/android/rides/RideParameters.java b/sdk/src/main/java/com/uber/sdk/android/rides/RideParameters.java new file mode 100644 index 00000000..5703f60e --- /dev/null +++ b/sdk/src/main/java/com/uber/sdk/android/rides/RideParameters.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import android.support.annotation.Nullable; + +/** + * Represents the parameters for an Uber ride. + */ +public class RideParameters { + + private final boolean mIsPickupMyLocation; + private final String mProductId; + private final Float mPickupLatitude; + private final Float mPickupLongitude; + private final String mPickupNickname; + private final String mPickupAddress; + private final Float mDropoffLatitude; + private final Float mDropoffLongitude; + private final String mDropoffNickname; + private final String mDropoffAddress; + + private RideParameters(boolean isPickupMyLocation, + @Nullable String productId, + @Nullable Float pickupLatitude, + @Nullable Float pickupLongitude, + @Nullable String pickupNickname, + @Nullable String pickupAddress, + @Nullable Float dropoffLatitude, + @Nullable Float dropoffLongitude, + @Nullable String dropoffNickname, + @Nullable String dropoffAddress) { + mIsPickupMyLocation = isPickupMyLocation; + mProductId = productId; + mPickupLatitude = pickupLatitude; + mPickupLongitude = pickupLongitude; + mPickupNickname = pickupNickname; + mPickupAddress = pickupAddress; + mDropoffLatitude = dropoffLatitude; + mDropoffLongitude = dropoffLongitude; + mDropoffNickname = dropoffNickname; + mDropoffAddress = dropoffAddress; + } + + /** + * @return True if the pickup location of the ride is set to be the device's location, false if a + * specific pickup location has been set. + */ + public boolean isPickupMyLocation() { + return mIsPickupMyLocation; + } + + /** + * Gets the product ID for the ride. + */ + @Nullable + public String getProductId() { + return mProductId; + } + + /** + * Gets the latitude of the pickup location of the ride. Null if no pickup location specified. + */ + @Nullable + public Float getPickupLatitude() { + return mPickupLatitude; + } + + /** + * Gets the longitude of the pickup location of the ride. Null if no pickup location specified. + */ + @Nullable + public Float getPickupLongitude() { + return mPickupLongitude; + } + + /** + * Gets the nickname of the pickup location of the ride. Null if no pickup location specified. + */ + @Nullable + public String getPickupNickname() { + return mPickupNickname; + } + + /** + * Gets the address of the pickup location of the ride. Null if no pickup location specified. + */ + @Nullable + public String getPickupAddress() { + return mPickupAddress; + } + + /** + * Gets the latitude of the dropoff location of the ride. Null if no dropoff location specified. + */ + @Nullable + public Float getDropoffLatitude() { + return mDropoffLatitude; + } + + /** + * Gets the longitude of the dropoff location of the ride. Null if no dropoff location specified. + */ + @Nullable + public Float getDropoffLongitude() { + return mDropoffLongitude; + } + + /** + * Gets the nickname of the dropoff location of the ride. Null if no dropoff location specified. + */ + @Nullable + public String getDropoffNickname() { + return mDropoffNickname; + } + + /** + * Gets the address of the dropoff location of the ride. Null if no dropoff location specified. + */ + @Nullable + public String getDropoffAddress() { + return mDropoffAddress; + } + + /** + * Builder for {@link RideParameters} objects. + */ + public static class Builder { + + private boolean mIsPickupMyLocation = true; + private String mProductId; + private Float mPickupLatitude; + private Float mPickupLongitude; + private String mPickupNickname; + private String mPickupAddress; + private Float mDropoffLatitude; + private Float mDropoffLongitude; + private String mDropoffNickname; + private String mDropoffAddress; + + /** + * Sets the product ID for the ride. + */ + public RideParameters.Builder setProductId(String productId) { + mProductId = productId; + return this; + } + + /** + * Sets the pickup location for the ride. If no pickup is supplied then it defaults to the device's location. + * + * @param latitude The latitude of the pickup. + * @param longitude The longitude of the pickup. + * @param nickname This will show up as the text name at the request a ride screen in the Uber app. If not + * supplied will just show address. + * @param address The address of the pickup location. If not supplied the bar will read 'Go to pin'. + */ + public RideParameters.Builder setPickupLocation(float latitude, float longitude, @Nullable String nickname, + @Nullable String address) { + mPickupLatitude = latitude; + mPickupLongitude = longitude; + mPickupNickname = nickname; + mPickupAddress = address; + mIsPickupMyLocation = false; + return this; + } + + /** + * Sets the dropoff location for the ride. + * + * @param latitude The latitude of the dropoff. + * @param longitude The longitude of the dropoff. + * @param nickname This will show up as the text name at the request a ride screen in the Uber app. If not + * supplied will just show address. + * @param address The address of the dropoff location. If not supplied will read 'Destination'. + */ + public RideParameters.Builder setDropoffLocation(float latitude, float longitude, @Nullable String nickname, + @Nullable String address) { + mDropoffLatitude = latitude; + mDropoffLongitude = longitude; + mDropoffNickname = nickname; + mDropoffAddress = address; + return this; + } + + /** + * Sets the pickup location for the ride to be the device's current location. + */ + public RideParameters.Builder setPickupToMyLocation() { + mIsPickupMyLocation = true; + mPickupLatitude = null; + mPickupLongitude = null; + mPickupNickname = null; + mPickupAddress = null; + return this; + } + + /** + * Builds an {@link RideParameters} object. + */ + public RideParameters build() { + return new RideParameters(mIsPickupMyLocation, mProductId, mPickupLatitude, mPickupLongitude, + mPickupNickname, mPickupAddress, mDropoffLatitude, mDropoffLongitude, mDropoffNickname, + mDropoffAddress); + } + } +} diff --git a/sdk/src/main/java/com/uber/sdk/android/rides/UberButton.java b/sdk/src/main/java/com/uber/sdk/android/rides/UberButton.java new file mode 100644 index 00000000..73a8e233 --- /dev/null +++ b/sdk/src/main/java/com/uber/sdk/android/rides/UberButton.java @@ -0,0 +1,215 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.Typeface; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.Button; + +/** + * {@link android.widget.Button} that can be used as a button and provides default Uber styling. + */ +public class UberButton extends Button { + + private static final int DEFAULT_TEXT_SIZE = 18; + + /** + * Constructor. + * + * @param context the context creating the view. + */ + public UberButton(Context context) { + this(context, null); + } + + /** + * Constructor. + * + * @param context the context creating the view. + * @param attrs attributes for the view. + */ + public UberButton(Context context, AttributeSet attrs) { + this(context, attrs, R.attr.uberButtonStyle); + } + + /** + * Constructor. + * + * @param context the context creating the view. + * @param attrs attributes for the view. + * @param defStyleAttr the default attribute to use for a style if none is specified. + */ + public UberButton(Context context, AttributeSet attrs, int defStyleAttr) { + this(context, attrs, defStyleAttr, 0); + } + + /** + * Constructor. + * + * @param context the context creating the view. + * @param attrs attributes for the view. + * @param defStyleAttr the default attribute to use for a style if none is specified. + * @param defStyleRes the default style, used only if defStyleAttr is 0 or can not be found in the theme. + */ + public UberButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr); + defStyleRes = defStyleRes == 0 ? R.style.UberButton : defStyleRes; + init(context, attrs, defStyleAttr, defStyleRes); + } + + protected void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + setBackgroundAttributes(context, attrs, defStyleAttr, defStyleRes); + setDrawableAttributes(context, attrs, defStyleAttr, defStyleRes); + setPaddingAttributes(context, attrs, defStyleAttr, defStyleRes); + setTextAttributes(context, attrs, defStyleAttr, defStyleRes); + } + + private void setBackgroundAttributes( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + int attrsResources[] = { + android.R.attr.background, + }; + TypedArray backgroundAttributes = context.getTheme().obtainStyledAttributes( + attrs, + attrsResources, + defStyleAttr, + defStyleRes); + try { + if (backgroundAttributes.hasValue(0)) { + int backgroundResource = backgroundAttributes.getResourceId(0, 0); + if (backgroundResource != 0) { + setBackgroundResource(backgroundResource); + } else { + setBackgroundColor(backgroundAttributes.getColor(0, Color.BLACK)); + } + } else { + setBackgroundColor(backgroundAttributes.getColor(0, Color.BLACK)); + } + } finally { + backgroundAttributes.recycle(); + } + } + + private void setDrawableAttributes( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + int attrsResources[] = { + android.R.attr.drawableLeft, + android.R.attr.drawableTop, + android.R.attr.drawableRight, + android.R.attr.drawableBottom, + android.R.attr.drawablePadding, + }; + TypedArray drawableAttributes = context.getTheme().obtainStyledAttributes( + attrs, + attrsResources, + defStyleAttr, + defStyleRes); + try { + setCompoundDrawablesWithIntrinsicBounds( + drawableAttributes.getResourceId(0, 0), + drawableAttributes.getResourceId(1, 0), + drawableAttributes.getResourceId(2, 0), + drawableAttributes.getResourceId(3, 0)); + setCompoundDrawablePadding(drawableAttributes.getDimensionPixelSize(4, 0)); + } finally { + drawableAttributes.recycle(); + } + } + + private void setPaddingAttributes( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + int attrsResources[] = { + android.R.attr.padding, + android.R.attr.paddingLeft, + android.R.attr.paddingTop, + android.R.attr.paddingRight, + android.R.attr.paddingBottom, + }; + TypedArray paddingAttributes = context.getTheme().obtainStyledAttributes( + attrs, + attrsResources, + defStyleAttr, + defStyleRes); + try { + int padding = paddingAttributes.getDimensionPixelOffset(0, 0); + int paddingLeft = paddingAttributes.getDimensionPixelSize(1, 0); + paddingLeft = paddingLeft == 0 ? padding : paddingLeft; + int paddingTop = paddingAttributes.getDimensionPixelSize(2, 0); + paddingTop = paddingTop == 0 ? padding : paddingTop; + int paddingRight = paddingAttributes.getDimensionPixelSize(3, 0); + paddingRight = paddingRight == 0 ? padding : paddingRight; + int paddingBottom = paddingAttributes.getDimensionPixelSize(4, 0); + paddingBottom = paddingBottom == 0 ? padding : paddingBottom; + setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); + } finally { + paddingAttributes.recycle(); + } + } + + private void setTextAttributes( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, + int defStyleRes) { + int attrsResources[] = { + android.R.attr.textColor, + android.R.attr.gravity, + android.R.attr.textSize, + android.R.attr.textStyle, + android.R.attr.text, + }; + TypedArray textAttributes = context.getTheme().obtainStyledAttributes( + attrs, + attrsResources, + defStyleAttr, + defStyleRes); + try { + setTextColor(textAttributes.getColor(0, Color.WHITE)); + setGravity(textAttributes.getInt(1, Gravity.CENTER)); + setTextSize(TypedValue.COMPLEX_UNIT_DIP, textAttributes.getDimensionPixelSize(2, DEFAULT_TEXT_SIZE)); + setTypeface(Typeface.defaultFromStyle(textAttributes.getInt(3, Typeface.NORMAL))); + String text = textAttributes.getString(4); + if (text != null) { + setText(textAttributes.getString(4)); + } + } finally { + textAttributes.recycle(); + } + } +} diff --git a/sdk/src/main/res/drawable-hdpi/uber_badge.png b/sdk/src/main/res/drawable-hdpi/uber_badge.png new file mode 100644 index 0000000000000000000000000000000000000000..fd00ad07cd03f93c4d58c1b50fc4465ffdf32910 GIT binary patch literal 1187 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 literal 0 HcmV?d00001 diff --git a/sdk/src/main/res/drawable-xxxhdpi/uber_badge.png b/sdk/src/main/res/drawable-xxxhdpi/uber_badge.png new file mode 100644 index 0000000000000000000000000000000000000000..6b297882d8deb96fc32cdc8252df11a556282641 GIT binary patch literal 3297 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 literal 0 HcmV?d00001 diff --git a/sdk/src/main/res/drawable/uber_button_background_black_100.xml b/sdk/src/main/res/drawable/uber_button_background_black_100.xml new file mode 100644 index 00000000..7483056c --- /dev/null +++ b/sdk/src/main/res/drawable/uber_button_background_black_100.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/sdk/src/main/res/drawable/uber_button_background_black_90.xml b/sdk/src/main/res/drawable/uber_button_background_black_90.xml new file mode 100644 index 00000000..7ec3af6a --- /dev/null +++ b/sdk/src/main/res/drawable/uber_button_background_black_90.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/sdk/src/main/res/drawable/uber_button_background_selector_black.xml b/sdk/src/main/res/drawable/uber_button_background_selector_black.xml new file mode 100644 index 00000000..e6e827e9 --- /dev/null +++ b/sdk/src/main/res/drawable/uber_button_background_selector_black.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/sdk/src/main/res/drawable/uber_button_background_selector_white.xml b/sdk/src/main/res/drawable/uber_button_background_selector_white.xml new file mode 100644 index 00000000..9d8eb7bb --- /dev/null +++ b/sdk/src/main/res/drawable/uber_button_background_selector_white.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/sdk/src/main/res/drawable/uber_button_background_white_100.xml b/sdk/src/main/res/drawable/uber_button_background_white_100.xml new file mode 100644 index 00000000..7a6d2a28 --- /dev/null +++ b/sdk/src/main/res/drawable/uber_button_background_white_100.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/sdk/src/main/res/drawable/uber_button_background_white_80.xml b/sdk/src/main/res/drawable/uber_button_background_white_80.xml new file mode 100644 index 00000000..4734b752 --- /dev/null +++ b/sdk/src/main/res/drawable/uber_button_background_white_80.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/sdk/src/main/res/values/attrs.xml b/sdk/src/main/res/values/attrs.xml new file mode 100644 index 00000000..dd0aa31b --- /dev/null +++ b/sdk/src/main/res/values/attrs.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/sdk/src/main/res/values/colors.xml b/sdk/src/main/res/values/colors.xml new file mode 100644 index 00000000..26c5cb64 --- /dev/null +++ b/sdk/src/main/res/values/colors.xml @@ -0,0 +1,30 @@ + + + + + #09091A + #222231 + + #C0C0C8 + #CDCDD3 + diff --git a/sdk/src/main/res/values/dimens.xml b/sdk/src/main/res/values/dimens.xml new file mode 100644 index 00000000..2a0a9785 --- /dev/null +++ b/sdk/src/main/res/values/dimens.xml @@ -0,0 +1,28 @@ + + + + + 20sp + 8dp + 2dp + diff --git a/sdk/src/main/res/values/strings.xml b/sdk/src/main/res/values/strings.xml new file mode 100644 index 00000000..c543de48 --- /dev/null +++ b/sdk/src/main/res/values/strings.xml @@ -0,0 +1,27 @@ + + + + + Ride there with Uber + https://m.uber.com/sign-up?client_id=%1$s&user-agent=%2$s + diff --git a/sdk/src/main/res/values/styles.xml b/sdk/src/main/res/values/styles.xml new file mode 100644 index 00000000..bda64f49 --- /dev/null +++ b/sdk/src/main/res/values/styles.xml @@ -0,0 +1,42 @@ + + + + + + + + diff --git a/sdk/src/main/res/values/themes.xml b/sdk/src/main/res/values/themes.xml new file mode 100644 index 00000000..3970822d --- /dev/null +++ b/sdk/src/main/res/values/themes.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/sdk/src/test/java/com/uber/sdk/android/rides/RequestButtonTest.java b/sdk/src/test/java/com/uber/sdk/android/rides/RequestButtonTest.java new file mode 100644 index 00000000..cbe32401 --- /dev/null +++ b/sdk/src/test/java/com/uber/sdk/android/rides/RequestButtonTest.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.support.annotation.NonNull; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.res.builder.RobolectricPackageManager; +import org.robolectric.shadows.ShadowActivity; + +import java.io.IOException; + +import static com.uber.sdk.android.rides.TestUtils.readUriResourceWithUserAgentParam; +import static org.junit.Assert.assertEquals; +import static org.robolectric.Shadows.shadowOf; + +/** + * Tests {@link RequestButton} + */ +@RunWith(RobolectricGradleTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21) +public class RequestButtonTest { + + private static final String CLIENT_ID = "clientId"; + private static final float PICKUP_LAT = 32.1234f; + private static final float PICKUP_LONG = -122.3456f; + private static final String PICKUP_NICK = "pickupNick"; + private static final String PICKUP_ADDR = "Pickup Address"; + private static final String UBER_PACKAGE_NAME = "com.ubercab"; + private static final String USER_AGENT_BUTTON = "rides-button-v0.1.0"; + + @Rule public ExpectedException exception = ExpectedException.none(); + + private Activity mActivity; + private RequestButton mRequestButton; + + @Before + public void setup() { + mActivity = Robolectric.setupActivity(Activity.class); + mRequestButton = new RequestButton(mActivity); + } + + @Test + public void onClick_whenNullClientId_shouldThrowException() { + exception.expect(RuntimeException.class); + exception.expectMessage("Client ID required to use RequestButton."); + + mRequestButton.performClick(); + } + + @Test + public void onClick_whenClientIdProvidedAndNoUberApp_shouldStartMobileSite() throws IOException { + String expectedUri = readUriResourceWithUserAgentParam("src/test/resources/deeplinkuris/no_app_installed", + USER_AGENT_BUTTON); + + ShadowActivity shadowActivity = setupShadowActivityWithUber(false); + + mRequestButton.setClientId(CLIENT_ID); + mRequestButton.performClick(); + + Intent shadowedIntent = shadowActivity.getNextStartedActivity(); + assertEquals(expectedUri, shadowedIntent.getData().toString()); + } + + @Test + public void onClick_whenClientIdProvidedAndUberAppInstalled_shouldStartUberApp() throws IOException { + String expectedUri = readUriResourceWithUserAgentParam("src/test/resources/deeplinkuris/just_client_provided", + USER_AGENT_BUTTON); + + ShadowActivity shadowActivity = setupShadowActivityWithUber(true); + + mRequestButton.setClientId(CLIENT_ID); + mRequestButton.performClick(); + + Intent shadowedIntent = shadowActivity.getNextStartedActivity(); + assertEquals(expectedUri, shadowedIntent.getDataString()); + } + + @Test + public void onClick_whenClientIdAndPickupProvidedAndUberAppInstalled_shouldStartUberAppWithParams() + throws IOException { + String path = "src/test/resources/deeplinkuris/pickup_and_client_provided"; + String expectedUri = readUriResourceWithUserAgentParam(path, USER_AGENT_BUTTON); + + ShadowActivity shadowActivity = setupShadowActivityWithUber(true); + + RideParameters rideParameters = new RideParameters.Builder() + .setPickupLocation(PICKUP_LAT, PICKUP_LONG, PICKUP_NICK, PICKUP_ADDR) + .build(); + mRequestButton.setClientId(CLIENT_ID); + mRequestButton.setRideParameters(rideParameters); + mRequestButton.performClick(); + + Intent shadowedIntent = shadowActivity.getNextStartedActivity(); + assertEquals(expectedUri, shadowedIntent.getData().toString()); + } + + @NonNull + private ShadowActivity setupShadowActivityWithUber(boolean isUberInstalled) { + ShadowActivity shadowActivity = shadowOf(mActivity); + if (isUberInstalled) { + RobolectricPackageManager packageManager = (RobolectricPackageManager) shadowActivity.getPackageManager(); + + PackageInfo uberPackage = new PackageInfo(); + uberPackage.packageName = UBER_PACKAGE_NAME; + packageManager.addPackage(uberPackage); + } + return shadowActivity; + } +} diff --git a/sdk/src/test/java/com/uber/sdk/android/rides/RequestDeeplinkTest.java b/sdk/src/test/java/com/uber/sdk/android/rides/RequestDeeplinkTest.java new file mode 100644 index 00000000..42215c95 --- /dev/null +++ b/sdk/src/test/java/com/uber/sdk/android/rides/RequestDeeplinkTest.java @@ -0,0 +1,217 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageInfo; + + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.res.builder.RobolectricPackageManager; +import org.robolectric.shadows.ShadowActivity; + +import java.io.IOException; + +import static com.uber.sdk.android.rides.TestUtils.readUriResourceWithUserAgentParam; +import static org.junit.Assert.assertEquals; +import static org.robolectric.Shadows.shadowOf; + +/** + * Tests {@link RequestDeeplink} + */ +@RunWith(RobolectricGradleTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21) +public class RequestDeeplinkTest { + + private static final String UBER_PACKAGE_NAME = "com.ubercab"; + private static final String CLIENT_ID = "clientId"; + private static final String PRODUCT_ID = "productId"; + private static final float PICKUP_LAT = 32.1234f; + private static final float PICKUP_LONG = -122.3456f; + private static final String PICKUP_NICK = "pickupNick"; + private static final String PICKUP_ADDR = "Pickup Address"; + private static final float DROPOFF_LAT = 32.5678f; + private static final float DROPOFF_LONG = -122.6789f; + private static final String DROPOFF_NICK = "pickupNick"; + private static final String DROPOFF_ADDR = "Dropoff Address"; + private static final String USER_AGENT_DEEPLINK = "rides-deeplink-v0.1.0"; + + @Rule public ExpectedException exception = ExpectedException.none(); + + @Test + public void onBuildDeeplink_whenClientIdAndDefaultRideParamsProvided_shouldHaveDefaults() throws IOException { + String expectedUri = readUriResourceWithUserAgentParam("src/test/resources/deeplinkuris/just_client_provided", + USER_AGENT_DEEPLINK); + + RideParameters rideParameters = new RideParameters.Builder().build(); + RequestDeeplink deeplink = new RequestDeeplink.Builder() + .setRideParameters(rideParameters) + .setClientId(CLIENT_ID) + .build(); + + assertEquals("URI does not match.", expectedUri, deeplink.getUri().toString()); + } + + @Test + public void onBuildDeeplink_whenFullRideParamsProvided_shouldCompleteUri() throws IOException { + String expectedUri = readUriResourceWithUserAgentParam("src/test/resources/deeplinkuris/full_details_uri", + USER_AGENT_DEEPLINK); + + RideParameters rideParameters = new RideParameters.Builder() + .setPickupLocation(PICKUP_LAT, PICKUP_LONG, PICKUP_NICK, PICKUP_ADDR) + .setDropoffLocation(DROPOFF_LAT, DROPOFF_LONG, DROPOFF_NICK, DROPOFF_ADDR) + .setProductId(PRODUCT_ID) + .build(); + RequestDeeplink deeplink = new RequestDeeplink.Builder() + .setRideParameters(rideParameters) + .setClientId(CLIENT_ID) + .build(); + + assertEquals("URI does not match.", expectedUri, deeplink.getUri().toString()); + } + + @Test + public void onBuildDeeplink_whenPickupAndClientIdProvided_shouldNotHaveDropoffOrProduct() + throws IOException { + String path = "src/test/resources/deeplinkuris/pickup_and_client_provided"; + String expectedUri = readUriResourceWithUserAgentParam(path, USER_AGENT_DEEPLINK); + + RideParameters rideParameters = new RideParameters.Builder() + .setPickupLocation(PICKUP_LAT, PICKUP_LONG, PICKUP_NICK, PICKUP_ADDR) + .build(); + RequestDeeplink deeplink = new RequestDeeplink.Builder() + .setRideParameters(rideParameters) + .setClientId(CLIENT_ID) + .build(); + + assertEquals("URI does not match.", expectedUri, deeplink.getUri().toString()); + } + + @Test + public void onBuildDeeplink_whenDropoffClientIdAndProductIdProvided_shouldHaveDefaultPickupAndFullDropoff() + throws IOException { + String path = "src/test/resources/deeplinkuris/dropoff_client_and_product_provided"; + String expectedUri = readUriResourceWithUserAgentParam(path, USER_AGENT_DEEPLINK); + + RideParameters rideParameters = new RideParameters.Builder() + .setProductId(PRODUCT_ID) + .setDropoffLocation(DROPOFF_LAT, DROPOFF_LONG, DROPOFF_NICK, DROPOFF_ADDR) + .build(); + RequestDeeplink deeplink = new RequestDeeplink.Builder() + .setRideParameters(rideParameters) + .setClientId(CLIENT_ID) + .build(); + + assertEquals("URI does not match.", expectedUri, deeplink.getUri().toString()); + } + + @Test + public void onBuildDeeplink_whenNoNicknameOrAddressProvided_shouldNotHaveNicknameAndAddress() + throws IOException { + String expectedUri = readUriResourceWithUserAgentParam("src/test/resources/deeplinkuris/no_nickname_or_address", + USER_AGENT_DEEPLINK); + + RideParameters rideParameters = new RideParameters.Builder() + .setProductId(PRODUCT_ID) + .setPickupLocation(PICKUP_LAT, PICKUP_LONG, null, null) + .setDropoffLocation(DROPOFF_LAT, DROPOFF_LONG, null, null) + .build(); + RequestDeeplink deeplink = new RequestDeeplink.Builder() + .setRideParameters(rideParameters) + .setClientId(CLIENT_ID) + .build(); + + assertEquals("URI does not match.", expectedUri, deeplink.getUri().toString()); + } + + @Test + public void onBuildDeeplink_whenNoClientId_shouldNotBuild() { + exception.expect(IllegalStateException.class); + exception.expectMessage("Must supply a client ID."); + + RideParameters rideParameters = new RideParameters.Builder().build(); + new RequestDeeplink.Builder().setRideParameters(rideParameters).build(); + } + + @Test + public void onBuildDeeplink_whenNoRideParams_shouldNotBuild() { + exception.expect(IllegalStateException.class); + exception.expectMessage("Must supply ride parameters."); + + new RequestDeeplink.Builder().setClientId(CLIENT_ID).build(); + } + + @Test + public void execute_whenNoUberApp_shouldPointToMobileSite() throws IOException { + String expectedUri = readUriResourceWithUserAgentParam("src/test/resources/deeplinkuris/no_app_installed", + USER_AGENT_DEEPLINK); + + Activity activity = Robolectric.setupActivity(Activity.class); + ShadowActivity shadowActivity = shadowOf(activity); + + RideParameters rideParameters = new RideParameters.Builder().build(); + + RequestDeeplink requestDeeplink = new RequestDeeplink.Builder() + .setClientId(CLIENT_ID) + .setRideParameters(rideParameters) + .build(); + requestDeeplink.execute(activity); + + Intent startedIntent = shadowActivity.getNextStartedActivity(); + assertEquals(expectedUri, startedIntent.getData().toString()); + } + + @Test + public void execute_whenUberAppInsalled_shouldPointToUberApp() throws IOException { + String expectedUri = readUriResourceWithUserAgentParam("src/test/resources/deeplinkuris/just_client_provided", + USER_AGENT_DEEPLINK); + + Activity activity = Robolectric.setupActivity(Activity.class); + ShadowActivity shadowActivity = shadowOf(activity); + + RobolectricPackageManager packageManager = (RobolectricPackageManager) shadowActivity.getPackageManager(); + + PackageInfo uberPackage = new PackageInfo(); + uberPackage.packageName = UBER_PACKAGE_NAME; + packageManager.addPackage(uberPackage); + + RideParameters rideParameters = new RideParameters.Builder().build(); + + RequestDeeplink requestDeeplink = new RequestDeeplink.Builder() + .setClientId(CLIENT_ID) + .setRideParameters(rideParameters) + .build(); + + requestDeeplink.execute(activity); + + Intent startedIntent = shadowActivity.getNextStartedActivity(); + assertEquals(expectedUri, startedIntent.getData().toString()); + } +} diff --git a/sdk/src/test/java/com/uber/sdk/android/rides/RideParametersTest.java b/sdk/src/test/java/com/uber/sdk/android/rides/RideParametersTest.java new file mode 100644 index 00000000..9c98ed20 --- /dev/null +++ b/sdk/src/test/java/com/uber/sdk/android/rides/RideParametersTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests {@link RideParameters} + */ +public class RideParametersTest { + + public static final String PRODUCT_ID = "productId"; + private static final float PICKUP_LAT = 32.1234f; + private static final float PICKUP_LONG = -122.3456f; + private static final String PICKUP_NICK = "pickupNick"; + private static final String PICKUP_ADDR = "Pickup Address"; + private static final float DROPOFF_LAT = 32.5678f; + private static final float DROPOFF_LONG = -122.6789f; + private static final String DROPOFF_NICK = "pickupNick"; + private static final String DROPOFF_ADDR = "Dropoff Address"; + + @Test + public void onBuildRideParams_whenNothingSet_shouldHaveDefaults() { + RideParameters rideParameters = new RideParameters.Builder().build(); + + assertDefaults(rideParameters); + } + + @Test + public void onBuildRideParams_whenAllSet_shouldBeFilled() { + RideParameters rideParameters = new RideParameters.Builder() + .setProductId(PRODUCT_ID) + .setPickupLocation(PICKUP_LAT, PICKUP_LONG, PICKUP_NICK, PICKUP_ADDR) + .setDropoffLocation(DROPOFF_LAT, DROPOFF_LONG, DROPOFF_NICK, DROPOFF_ADDR) + .build(); + + assertFalse(rideParameters.isPickupMyLocation()); + assertEquals("Product ID does not match.", PRODUCT_ID, rideParameters.getProductId()); + + assertEquals("Pickup latitude does not match.", Float.valueOf(PICKUP_LAT), rideParameters.getPickupLatitude()); + assertEquals("Pickup longitude does not match.", Float.valueOf(PICKUP_LONG), + rideParameters.getPickupLongitude()); + assertEquals("Pickup nickname does not match.", PICKUP_NICK, rideParameters.getPickupNickname()); + assertEquals("Pickup address does not match.", PICKUP_ADDR, rideParameters.getPickupAddress()); + + assertEquals("Dropoff latitude does not match.", Float.valueOf(DROPOFF_LAT), + rideParameters.getDropoffLatitude()); + assertEquals("Dropoff longitude does not match.", Float.valueOf(DROPOFF_LONG), + rideParameters.getDropoffLongitude()); + assertEquals("Dropoff nickname does not match.", DROPOFF_NICK, rideParameters.getDropoffNickname()); + assertEquals("Dropoff address does not match.", DROPOFF_ADDR, rideParameters.getDropoffAddress()); + } + + @Test + public void onBuildRideParams_whenJustSetToMyLocation_shouldEqualDefaults() { + RideParameters rideParameters = new RideParameters.Builder().setPickupToMyLocation().build(); + + assertDefaults(rideParameters); + } + + @Test + public void onBuildRideParams_whenSetPickupLocationAndThenPickupToMyLocation_shouldHavePickupAsMyLocation() { + RideParameters rideParameters = new RideParameters.Builder() + .setPickupLocation(PICKUP_LAT, PICKUP_LONG, PICKUP_NICK, PICKUP_ADDR) + .setPickupToMyLocation() + .build(); + + assertDefaults(rideParameters); + } + + @Test + public void onBuildRideParams_whenSetPickupToMyLocationAndThenAPickupLocation_shouldHavePickupAsMyLocation() { + RideParameters rideParameters = new RideParameters.Builder() + .setPickupToMyLocation() + .setPickupLocation(PICKUP_LAT, PICKUP_LONG, PICKUP_NICK, PICKUP_ADDR) + .build(); + + assertFalse(rideParameters.isPickupMyLocation()); + assertNull(rideParameters.getProductId()); + + assertEquals("Pickup latitude does not match.", Float.valueOf(PICKUP_LAT), rideParameters.getPickupLatitude()); + assertEquals("Pickup longitude does not match.", Float.valueOf(PICKUP_LONG), + rideParameters.getPickupLongitude()); + assertEquals("Pickup nickname does not match.", PICKUP_NICK, rideParameters.getPickupNickname()); + assertEquals("Pickup address does not match.", PICKUP_ADDR, rideParameters.getPickupAddress()); + + assertNull(rideParameters.getDropoffLatitude()); + assertNull(rideParameters.getDropoffLongitude()); + assertNull(rideParameters.getDropoffNickname()); + assertNull(rideParameters.getDropoffAddress()); + } + + private void assertDefaults(RideParameters rideParameters) { + assertTrue(rideParameters.isPickupMyLocation()); + assertNull(rideParameters.getProductId()); + + assertNull(rideParameters.getPickupLatitude()); + assertNull(rideParameters.getPickupLongitude()); + assertNull(rideParameters.getPickupNickname()); + assertNull(rideParameters.getPickupAddress()); + + assertNull(rideParameters.getDropoffLatitude()); + assertNull(rideParameters.getDropoffLongitude()); + assertNull(rideParameters.getDropoffNickname()); + assertNull(rideParameters.getDropoffAddress()); + } +} diff --git a/sdk/src/test/java/com/uber/sdk/android/rides/TestUtils.java b/sdk/src/test/java/com/uber/sdk/android/rides/TestUtils.java new file mode 100644 index 00000000..11581375 --- /dev/null +++ b/sdk/src/test/java/com/uber/sdk/android/rides/TestUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import android.support.annotation.NonNull; + +import com.google.api.client.repackaged.com.google.common.base.Joiner; +import com.google.common.io.Files; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +/** + * Adds utility methods common amongst multiple test classes. + */ +public final class TestUtils { + + /** + * Reads the file out as a string, trimming the result. + * + * @param path The root path of the File. If unable to access will dig down to module level. + */ + public static String readProjectResource(String path) throws IOException { + File file = new File(path); + if (!file.exists()) { + // If not at full path, check module dir + List list = Arrays.asList(path.split(File.pathSeparator)); + file = new File(Joiner.on(File.pathSeparatorChar).join(list.subList(1, list.size()))); + } + return Files.toString(file, StandardCharsets.UTF_8).trim(); + } + + /** + * Reads a URI stored as a text resource file and then adds the user agent. + * + * @param path The root path of the File. If unable to access will dig down to module level. + * @param userAgent The user-agent to be added as a query parameter to the resulting URI. + */ + @NonNull + public static String readUriResourceWithUserAgentParam(@NonNull String path, @NonNull String userAgent) + throws IOException { + String uri = readProjectResource(path); + return uri.concat(String.format("&user-agent=%s", userAgent)); + } +} diff --git a/sdk/src/test/java/com/uber/sdk/android/rides/UberButtonTest.java b/sdk/src/test/java/com/uber/sdk/android/rides/UberButtonTest.java new file mode 100644 index 00000000..9493b9be --- /dev/null +++ b/sdk/src/test/java/com/uber/sdk/android/rides/UberButtonTest.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2015 Uber Technologies, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.uber.sdk.android.rides; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; + +import org.apache.maven.artifact.ant.shaded.StringUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricGradleTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; +import org.robolectric.res.Attribute; +import org.robolectric.shadows.CoreShadowsAdapter; +import org.robolectric.shadows.RoboAttributeSet; + +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests {@link UberButton} + */ +@RunWith(RobolectricGradleTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 21) +public class UberButtonTest { + + private static final String ANDROID_ATTR_BACKGROUND = "android:attr/background"; + private static final String ANDROID_ATTR_DRAWABLE_LEFT = "android:attr/drawableLeft"; + private static final String ANDROID_ATTR_DRAWABLE_TOP = "android:attr/drawableTop"; + private static final String ANDROID_ATTR_DRAWABLE_RIGHT = "android:attr/drawableRight"; + private static final String ANDROID_ATTR_DRAWABLE_BOTTOM = "android:attr/drawableBottom"; + private static final String ANDROID_ATTR_DRAWABLE_PADDING = "android:attr/drawablePadding"; + private static final String ANDROID_ATTR_GRAVITY = "android:attr/gravity"; + private static final String ANDROID_ATTR_PADDING = "android:attr/padding"; + private static final String ANDROID_ATTR_PADDING_LEFT = "android:attr/paddingLeft"; + private static final String ANDROID_ATTR_PADDING_TOP = "android:attr/paddingTop"; + private static final String ANDROID_ATTR_PADDING_RIGHT = "android:attr/paddingRight"; + private static final String ANDROID_ATTR_PADDING_BOTTOM = "android:attr/paddingBottom"; + private static final String ANDROID_ATTR_TEXT_COLOR = "android:attr/textColor"; + private static final String ANDROID_ATTR_TEXT_SIZE = "android:attr/textSize"; + private static final String ANDROID_ATTR_TEXT_STYLE = "android:attr/textStyle"; + private static final String ANDROID_ATTR_TEXT = "android:attr/text"; + + private static final String ANDROID_COLOR_BLACK = "@android:color/black"; + private static final String ANDROID_COLOR_WHITE = "@android:color/white"; + private static final String DRAWABLE_UBER_BADGE = "@drawable/uber_badge"; + private static final String GRAVITY_END = "end"; + private static final String STYLE_ITALIC = "italic"; + private static final String ONE_SP = "1sp"; + private static final String TWO_SP = "2sp"; + private static final String THREE_SP = "3sp"; + private static final String FOUR_SP = "4sp"; + private static final String TEXT = "test"; + + private static final String UBER_PACKAGE_NAME = "com.uber.sdk.android.rides"; + + private Context mContext; + + @Before + public void setup() { + mContext = RuntimeEnvironment.application; + } + + @Test + public void onCreate_whenBackgroundAttributeSet_shouldSetBackground() { + AttributeSet attributeSet = makeAttributeSet( + makeAttribute(ANDROID_ATTR_BACKGROUND, ANDROID_COLOR_WHITE) + ); + UberButton uberButton = new UberButton(mContext, attributeSet, 0, 0); + assertEquals(Color.WHITE, ((ColorDrawable) uberButton.getBackground()).getColor()); + } + + @Test + public void onCreate_whenCompoundDrawablesAndPaddingSet_shouldSetCompoundDrawableAttributes() { + AttributeSet attributeSet = makeAttributeSet( + makeAttribute(ANDROID_ATTR_DRAWABLE_LEFT, DRAWABLE_UBER_BADGE), + makeAttribute(ANDROID_ATTR_DRAWABLE_TOP, DRAWABLE_UBER_BADGE), + makeAttribute(ANDROID_ATTR_DRAWABLE_RIGHT, DRAWABLE_UBER_BADGE), + makeAttribute(ANDROID_ATTR_DRAWABLE_BOTTOM, DRAWABLE_UBER_BADGE), + makeAttribute(ANDROID_ATTR_DRAWABLE_PADDING, ONE_SP) + ); + + UberButton uberButton = new UberButton(mContext, attributeSet, 0, 0); + Drawable[] drawables = uberButton.getCompoundDrawables(); + assertNotNull(drawables[0]); + assertNotNull(drawables[1]); + assertNotNull(drawables[2]); + assertNotNull(drawables[3]); + assertEquals(1, uberButton.getCompoundDrawablePadding()); + } + + @Test + public void onCreate_whenOverallPaddingSet_shouldAddOverallPadding() { + AttributeSet attributeSet = makeAttributeSet( + makeAttribute(ANDROID_ATTR_PADDING, ONE_SP) + ); + UberButton uberButton = new UberButton(mContext, attributeSet, 0, 0); + assertEquals(1, uberButton.getPaddingLeft()); + assertEquals(1, uberButton.getPaddingTop()); + assertEquals(1, uberButton.getPaddingRight()); + assertEquals(1, uberButton.getPaddingBottom()); + } + + @Test + public void onCreate_whenIndividualPaddingsSet_shouldHaveSeparatePaddings() { + AttributeSet attributeSet = makeAttributeSet( + makeAttribute(ANDROID_ATTR_PADDING_LEFT, ONE_SP), + makeAttribute(ANDROID_ATTR_PADDING_TOP, TWO_SP), + makeAttribute(ANDROID_ATTR_PADDING_RIGHT, THREE_SP), + makeAttribute(ANDROID_ATTR_PADDING_BOTTOM, FOUR_SP) + ); + UberButton uberButton = new UberButton(mContext, attributeSet, 0, 0); + assertEquals(1, uberButton.getPaddingLeft()); + assertEquals(2, uberButton.getPaddingTop()); + assertEquals(3, uberButton.getPaddingRight()); + assertEquals(4, uberButton.getPaddingBottom()); + } + + @Test + public void onCreate_whenIndividualAndOverallPaddingsSet_shouldHaveIndividualPaddingsTrumpOverall() { + AttributeSet attributeSet = makeAttributeSet( + makeAttribute(ANDROID_ATTR_PADDING, ONE_SP), + makeAttribute(ANDROID_ATTR_PADDING_TOP, TWO_SP), + makeAttribute(ANDROID_ATTR_PADDING_BOTTOM, FOUR_SP) + ); + UberButton uberButton = new UberButton(mContext, attributeSet, 0, 0); + assertEquals(1, uberButton.getPaddingLeft()); + assertEquals(2, uberButton.getPaddingTop()); + assertEquals(1, uberButton.getPaddingRight()); + assertEquals(4, uberButton.getPaddingBottom()); + } + + @Test + public void onCreate_whenTextAttributesSet_shouldAddAllAttributes() { + AttributeSet attributeSet = makeAttributeSet( + makeAttribute(ANDROID_ATTR_TEXT_COLOR, ANDROID_COLOR_BLACK), + makeAttribute(ANDROID_ATTR_GRAVITY, GRAVITY_END), + makeAttribute(ANDROID_ATTR_TEXT_SIZE, FOUR_SP), + makeAttribute(ANDROID_ATTR_TEXT_STYLE, STYLE_ITALIC), + makeAttribute(ANDROID_ATTR_TEXT, TEXT) + ); + UberButton uberButton = new UberButton(mContext, attributeSet, 0, 0); + assertEquals(Color.BLACK, uberButton.getCurrentTextColor()); + assertEquals(Typeface.ITALIC, uberButton.getTypeface().getStyle()); + assertEquals(4, uberButton.getTextSize(), 0); + assertEquals(TEXT, uberButton.getText().toString()); + assertTrue(uberButton.getGravity() != 0); + } + + @Test + public void onCreate_whenNoAttributesSet_shouldUseUberButtonDefaults() { + UberButton uberButton = new UberButton(RuntimeEnvironment.application, null, 0, 0); + Resources resources = mContext.getResources(); + + assertEquals(resources.getDrawable(R.drawable.uber_button_background_selector_black), + uberButton.getBackground()); + + assertNull(uberButton.getCompoundDrawables()[0]); + assertNull(uberButton.getCompoundDrawables()[1]); + assertNull(uberButton.getCompoundDrawables()[2]); + assertNull(uberButton.getCompoundDrawables()[3]); + + assertEquals(0, uberButton.getCompoundDrawablePadding()); + + float padding = resources.getDimension(R.dimen.button_padding); + assertEquals(padding, uberButton.getPaddingLeft(), 0); + assertEquals(padding, uberButton.getPaddingTop(), 0); + assertEquals(padding, uberButton.getPaddingRight(), 0); + assertEquals(padding, uberButton.getPaddingBottom(), 0); + + assertEquals(resources.getColor(R.color.uber_white_100), uberButton.getCurrentTextColor()); + assertEquals(Typeface.NORMAL, uberButton.getTypeface().getStyle()); + assertEquals(resources.getDimension(R.dimen.text_size), uberButton.getTextSize(), 0); + assertTrue(uberButton.getGravity() != 0); + assertTrue(StringUtils.isEmpty(uberButton.getText().toString())); + } + + private static AttributeSet makeAttributeSet(Attribute... attributes) { + return new RoboAttributeSet(Arrays.asList(attributes), new CoreShadowsAdapter().getResourceLoader()); + } + + private static Attribute makeAttribute(String fullyQualifiedAttributeName, Object value) { + return new Attribute(fullyQualifiedAttributeName, String.valueOf(value), UBER_PACKAGE_NAME); + } +} diff --git a/sdk/src/test/resources/deeplinkuris/dropoff_client_and_product_provided b/sdk/src/test/resources/deeplinkuris/dropoff_client_and_product_provided new file mode 100644 index 00000000..1c0e84b6 --- /dev/null +++ b/sdk/src/test/resources/deeplinkuris/dropoff_client_and_product_provided @@ -0,0 +1 @@ +uber:?action=setPickup&client_id=clientId&product_id=productId&pickup=my_location&dropoff%5Blatitude%5D=32.5678&dropoff%5Blongitude%5D=-122.6789&dropoff%5Bnickname%5D=pickupNick&dropoff%5Bformatted_address%5D=Dropoff%20Address diff --git a/sdk/src/test/resources/deeplinkuris/full_details_uri b/sdk/src/test/resources/deeplinkuris/full_details_uri new file mode 100644 index 00000000..3ee2554c --- /dev/null +++ b/sdk/src/test/resources/deeplinkuris/full_details_uri @@ -0,0 +1 @@ +uber:?action=setPickup&client_id=clientId&product_id=productId&pickup%5Blatitude%5D=32.1234&pickup%5Blongitude%5D=-122.3456&pickup%5Bnickname%5D=pickupNick&pickup%5Bformatted_address%5D=Pickup%20Address&dropoff%5Blatitude%5D=32.5678&dropoff%5Blongitude%5D=-122.6789&dropoff%5Bnickname%5D=pickupNick&dropoff%5Bformatted_address%5D=Dropoff%20Address diff --git a/sdk/src/test/resources/deeplinkuris/just_client_provided b/sdk/src/test/resources/deeplinkuris/just_client_provided new file mode 100644 index 00000000..d59d917b --- /dev/null +++ b/sdk/src/test/resources/deeplinkuris/just_client_provided @@ -0,0 +1 @@ +uber:?action=setPickup&client_id=clientId&pickup=my_location diff --git a/sdk/src/test/resources/deeplinkuris/no_app_installed b/sdk/src/test/resources/deeplinkuris/no_app_installed new file mode 100644 index 00000000..ebe53eba --- /dev/null +++ b/sdk/src/test/resources/deeplinkuris/no_app_installed @@ -0,0 +1 @@ +https://m.uber.com/sign-up?client_id=clientId diff --git a/sdk/src/test/resources/deeplinkuris/no_nickname_or_Address b/sdk/src/test/resources/deeplinkuris/no_nickname_or_Address new file mode 100644 index 00000000..d8250a17 --- /dev/null +++ b/sdk/src/test/resources/deeplinkuris/no_nickname_or_Address @@ -0,0 +1 @@ +uber:?action=setPickup&client_id=clientId&product_id=productId&pickup%5Blatitude%5D=32.1234&pickup%5Blongitude%5D=-122.3456&dropoff%5Blatitude%5D=32.5678&dropoff%5Blongitude%5D=-122.6789 diff --git a/sdk/src/test/resources/deeplinkuris/pickup_and_client_provided b/sdk/src/test/resources/deeplinkuris/pickup_and_client_provided new file mode 100644 index 00000000..1b953677 --- /dev/null +++ b/sdk/src/test/resources/deeplinkuris/pickup_and_client_provided @@ -0,0 +1 @@ +uber:?action=setPickup&client_id=clientId&pickup%5Blatitude%5D=32.1234&pickup%5Blongitude%5D=-122.3456&pickup%5Bnickname%5D=pickupNick&pickup%5Bformatted_address%5D=Pickup%20Address diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..69b6ac4c --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +include ':sdk' +include ':samples' +include ':samples:request-button-sample'