-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #33 from skedgo/emm/deep-linking
Add deep linking for Uber, Lyft and FlitWays
- Loading branch information
Showing
23 changed files
with
1,609 additions
and
0 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
...kit/src/androidTest/java/com/skedgo/android/tripkit/SingleReverseGeocoderFactoryTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package com.skedgo.android.tripkit; | ||
|
||
import android.support.test.InstrumentationRegistry; | ||
import android.support.test.runner.AndroidJUnit4; | ||
|
||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
|
||
import rx.observers.TestSubscriber; | ||
|
||
@RunWith(AndroidJUnit4.class) | ||
public class SingleReverseGeocoderFactoryTest { | ||
private SingleReverseGeocoderFactory factory; | ||
|
||
@Before public void before() { | ||
factory = new SingleReverseGeocoderFactory(InstrumentationRegistry.getInstrumentation().getTargetContext()); | ||
} | ||
|
||
/* This test may fail if devices don't have network. */ | ||
@Test public void reverseGeocodeInCA() { | ||
final TestSubscriber<String> subscriber = new TestSubscriber<>(); | ||
factory.call( | ||
ImmutableReverseGeocodingParams.builder() | ||
.lat(33.956252) | ||
.lng(-118.217896) | ||
.maxResults(1) | ||
.build() | ||
).subscribe(subscriber); | ||
subscriber.awaitTerminalEvent(); | ||
subscriber.assertNoErrors(); | ||
subscriber.assertValue("8677 Evergreen Ave\nSouth Gate, CA 90280"); | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
trip-kit/src/androidTest/java/com/skedgo/android/tripkit/TripKitImplTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package com.skedgo.android.tripkit; | ||
|
||
import android.support.test.InstrumentationRegistry; | ||
import android.support.test.runner.AndroidJUnit4; | ||
|
||
import com.skedgo.android.common.model.Location; | ||
import com.skedgo.android.common.model.TripSegment; | ||
|
||
import org.junit.Before; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
|
||
import java.util.List; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import rx.observers.TestSubscriber; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
@RunWith(AndroidJUnit4.class) | ||
public class TripKitImplTest { | ||
private TripKit kit; | ||
|
||
@Before public void before() { | ||
kit = new TripKitImpl( | ||
Configs.builder() | ||
.context(InstrumentationRegistry.getInstrumentation().getTargetContext()) | ||
.regionEligibility("") | ||
.debuggable(true) | ||
.build() | ||
); | ||
} | ||
|
||
@Test public void resolveFlitWaysBooking() { | ||
final TripSegment segment = new TripSegment(); | ||
segment.setFrom(new Location(33.956252, -118.217896)); | ||
segment.setTo(new Location(33.962775, -118.202395)); | ||
segment.setStartTimeInSecs(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())); | ||
|
||
final TestSubscriber<BookingAction> subscriber = new TestSubscriber<>(); | ||
kit.getBookingResolver().performExternalActionAsync( | ||
ExternalActionParams.builder() | ||
.flitWaysPartnerKey("25251325") | ||
.action("flitways") | ||
.segment(segment) | ||
.build() | ||
).subscribe(subscriber); | ||
subscriber.awaitTerminalEvent(); | ||
subscriber.assertNoErrors(); | ||
final List<BookingAction> events = subscriber.getOnNextEvents(); | ||
assertThat(events).hasSize(1); | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
trip-kit/src/main/java/com/skedgo/android/tripkit/BookingAction.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.skedgo.android.tripkit; | ||
|
||
import android.content.Intent; | ||
|
||
import org.immutables.value.Value; | ||
|
||
import static org.immutables.value.Value.Style.BuilderVisibility.PACKAGE; | ||
import static org.immutables.value.Value.Style.ImplementationVisibility.PRIVATE; | ||
|
||
@Value.Immutable | ||
@Value.Style(visibility = PRIVATE, builderVisibility = PACKAGE) | ||
public abstract class BookingAction { | ||
public static Builder builder() { | ||
return new BookingActionBuilder(); | ||
} | ||
|
||
@BookingProvider public abstract int bookingProvider(); | ||
public abstract boolean hasApp(); | ||
public abstract Intent data(); | ||
|
||
public interface Builder { | ||
Builder bookingProvider(@BookingProvider int bookingProvider); | ||
Builder hasApp(boolean hasApp); | ||
Builder data(Intent data); | ||
BookingAction build(); | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
trip-kit/src/main/java/com/skedgo/android/tripkit/BookingProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
package com.skedgo.android.tripkit; | ||
|
||
import android.support.annotation.IntDef; | ||
|
||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
|
||
@IntDef({ | ||
BookingResolver.UBER, | ||
BookingResolver.LYFT, | ||
BookingResolver.FLITWAYS, | ||
BookingResolver.OTHERS | ||
}) | ||
@Retention(RetentionPolicy.SOURCE) | ||
public @interface BookingProvider {} |
13 changes: 13 additions & 0 deletions
13
trip-kit/src/main/java/com/skedgo/android/tripkit/BookingResolver.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.skedgo.android.tripkit; | ||
|
||
import rx.Observable; | ||
|
||
public interface BookingResolver { | ||
int UBER = 0; | ||
int LYFT = UBER + 1; | ||
int FLITWAYS = LYFT + 1; | ||
int OTHERS = FLITWAYS + 1; | ||
|
||
Observable<BookingAction> performExternalActionAsync(ExternalActionParams params); | ||
String getTitleForExternalAction(String externalAction); | ||
} |
195 changes: 195 additions & 0 deletions
195
trip-kit/src/main/java/com/skedgo/android/tripkit/BookingResolverImpl.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package com.skedgo.android.tripkit; | ||
|
||
import android.content.Intent; | ||
import android.content.pm.PackageManager; | ||
import android.content.res.Resources; | ||
import android.net.Uri; | ||
import android.support.annotation.NonNull; | ||
|
||
import com.skedgo.android.common.model.Location; | ||
import com.skedgo.android.common.model.TripSegment; | ||
|
||
import java.io.UnsupportedEncodingException; | ||
import java.net.URLDecoder; | ||
import java.text.SimpleDateFormat; | ||
import java.util.Date; | ||
import java.util.Locale; | ||
import java.util.TimeZone; | ||
|
||
import okhttp3.HttpUrl; | ||
import rx.Observable; | ||
import rx.functions.Func0; | ||
import rx.functions.Func1; | ||
import rx.functions.Func2; | ||
import rx.schedulers.Schedulers; | ||
|
||
public final class BookingResolverImpl implements BookingResolver { | ||
private static final String UBER_PACKAGE = "com.ubercab"; | ||
private static final String LYFT_PACKAGE = "me.lyft.android"; | ||
private final Resources resources; | ||
private final PackageManager packageManager; | ||
private final Func1<ReverseGeocodingParams, Observable<String>> reverseGeocoderFactory; | ||
|
||
public BookingResolverImpl( | ||
Resources resources, PackageManager packageManager, | ||
@NonNull Func1<ReverseGeocodingParams, Observable<String>> reverseGeocoderFactory) { | ||
this.resources = resources; | ||
this.packageManager = packageManager; | ||
this.reverseGeocoderFactory = reverseGeocoderFactory; | ||
} | ||
|
||
@Override public Observable<BookingAction> performExternalActionAsync( | ||
ExternalActionParams params) { | ||
final BookingAction.Builder actionBuilder = BookingAction.builder(); | ||
final String externalAction = params.action(); | ||
if (externalAction.equals("uber")) { | ||
actionBuilder.bookingProvider(UBER); | ||
if (isPackageInstalled(UBER_PACKAGE)) { | ||
final BookingAction action = actionBuilder.hasApp(true).data( | ||
new Intent(Intent.ACTION_VIEW).setData(Uri.parse("uber://")) | ||
).build(); | ||
return Observable.just(action); | ||
} else { | ||
final BookingAction action = actionBuilder.hasApp(false).data( | ||
new Intent(Intent.ACTION_VIEW).setData(Uri.parse("https://m.uber.com/sign-up")) | ||
).build(); | ||
return Observable.just(action); | ||
} | ||
} else if (externalAction.startsWith("lyft")) { | ||
actionBuilder.bookingProvider(LYFT); | ||
if (isPackageInstalled(LYFT_PACKAGE)) { | ||
final BookingAction action = actionBuilder.hasApp(true).data( | ||
new Intent(Intent.ACTION_VIEW).setData(Uri.parse("lyft://")) | ||
).build(); | ||
return Observable.just(action); | ||
} else { | ||
final Intent data = new Intent(Intent.ACTION_VIEW) | ||
.setData(Uri.parse("https://play.google.com/store/apps/details?id=" + LYFT_PACKAGE)); | ||
final BookingAction action = actionBuilder | ||
.hasApp(false) | ||
.data(data) | ||
.build(); | ||
return Observable.just(action); | ||
} | ||
} else if (externalAction.equals("flitways")) { | ||
actionBuilder.bookingProvider(FLITWAYS); | ||
final String flitWaysPartnerKey = params.flitWaysPartnerKey(); | ||
if (flitWaysPartnerKey == null) { | ||
final Intent data = new Intent(Intent.ACTION_VIEW) | ||
.setData(Uri.parse("https://flitways.com")); | ||
final BookingAction action = actionBuilder | ||
.hasApp(false) | ||
.data(data) | ||
.build(); | ||
return Observable.just(action); | ||
} else { | ||
final TripSegment segment = params.segment(); | ||
final Location departure = segment.getFrom(); | ||
final Location arrival = segment.getTo(); | ||
final long startTimeInSecs = segment.getStartTimeInSecs(); | ||
final String timeZone = segment.getTimeZone(); | ||
return Observable | ||
.fromCallable(new Func0<HttpUrl.Builder>() { | ||
@Override public HttpUrl.Builder call() { | ||
final SimpleDateFormat dateFormat = new SimpleDateFormat("dd/MM/yyyy hh:mm a", Locale.US); | ||
if (timeZone != null) { | ||
dateFormat.setTimeZone(TimeZone.getTimeZone(timeZone)); | ||
} | ||
|
||
final String tripDate = dateFormat.format(new Date(startTimeInSecs * 1000)); | ||
return HttpUrl.parse("https://flitways.com/api/link") | ||
.newBuilder() | ||
.addQueryParameter("trip_date", tripDate) | ||
.addQueryParameter("partner_key", flitWaysPartnerKey); | ||
} | ||
}) | ||
.flatMap(new Func1<HttpUrl.Builder, Observable<BookingAction>>() { | ||
@Override public Observable<BookingAction> call(final HttpUrl.Builder builder) { | ||
return Observable.combineLatest( | ||
reverseGeocoderFactory.call( | ||
ImmutableReverseGeocodingParams.builder() | ||
.lat(departure.getLat()) | ||
.lng(departure.getLon()) | ||
.maxResults(1) | ||
.build()), | ||
reverseGeocoderFactory.call( | ||
ImmutableReverseGeocodingParams.builder() | ||
.lat(arrival.getLat()) | ||
.lng(arrival.getLon()) | ||
.maxResults(1) | ||
.build()), | ||
new Func2<String, String, BookingAction>() { | ||
@Override public BookingAction call(String departureAddress, String arrivalAddress) { | ||
final String url = builder | ||
.addQueryParameter("pick", departureAddress) | ||
.addQueryParameter("destination", arrivalAddress) | ||
.build() | ||
.toString(); | ||
return actionBuilder | ||
.hasApp(false) | ||
.data(new Intent(Intent.ACTION_VIEW, Uri.parse(url))) | ||
.build(); | ||
} | ||
} | ||
); | ||
} | ||
}) | ||
.subscribeOn(Schedulers.io()); | ||
} | ||
} else if (externalAction.startsWith("http")) { | ||
final BookingAction action = actionBuilder | ||
.bookingProvider(OTHERS) | ||
.hasApp(false) | ||
.data(new Intent(Intent.ACTION_VIEW, Uri.parse(externalAction))) | ||
.build(); | ||
return Observable.just(action); | ||
} else { | ||
return Observable.error(new UnsupportedOperationException()); | ||
} | ||
} | ||
|
||
@Override public String getTitleForExternalAction(String externalAction) { | ||
if (externalAction.equals("gocatch")) { | ||
return resources.getString(R.string.gocatch_a_taxi); | ||
} else if (externalAction.equals("uber")) { | ||
return isPackageInstalled(UBER_PACKAGE) | ||
? resources.getString(R.string.open_uber) | ||
: resources.getString(R.string.get_uber); | ||
} else if (externalAction.startsWith("lyft")) { | ||
// Also 'lyft_line', etc. | ||
return isPackageInstalled(LYFT_PACKAGE) | ||
? resources.getString(R.string.open_lyft) | ||
: resources.getString(R.string.get_lyft); | ||
} else if (externalAction.equals("flitways")) { | ||
return "Book with FlitWays"; // TODO: i18n. | ||
} else if (externalAction.startsWith("tel:")) { | ||
if (externalAction.contains("name=")) { | ||
String name = externalAction.substring(externalAction.indexOf("name=") + "name=".length()); | ||
try { | ||
name = URLDecoder.decode(name, "UTF-8"); | ||
} catch (UnsupportedEncodingException e) { | ||
} | ||
return resources.getString(R.string.calltaxiformat, name); | ||
} else { | ||
return resources.getString(R.string.call); | ||
} | ||
} else if (externalAction.startsWith("sms:")) { | ||
return "Send SMS"; // TODO: i18n. | ||
} else if (externalAction.startsWith("http:") || externalAction.startsWith("https:")) { | ||
return resources.getString(R.string.show_website); | ||
} else { | ||
return ""; | ||
} | ||
} | ||
|
||
private boolean isPackageInstalled(String packageName) { | ||
try { | ||
packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES); | ||
return true; | ||
} catch (PackageManager.NameNotFoundException e) { | ||
// Ignored. | ||
} | ||
|
||
return false; | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
trip-kit/src/main/java/com/skedgo/android/tripkit/ExternalActionParams.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.skedgo.android.tripkit; | ||
|
||
import android.support.annotation.Nullable; | ||
|
||
import com.skedgo.android.common.model.TripSegment; | ||
|
||
import org.immutables.value.Value; | ||
|
||
import static org.immutables.value.Value.Style.BuilderVisibility.PACKAGE; | ||
import static org.immutables.value.Value.Style.ImplementationVisibility.PRIVATE; | ||
|
||
@Value.Immutable | ||
@Value.Style(visibility = PRIVATE, builderVisibility = PACKAGE) | ||
public abstract class ExternalActionParams { | ||
public static Builder builder() { | ||
return new ExternalActionParamsBuilder(); | ||
} | ||
|
||
public abstract String action(); | ||
public abstract TripSegment segment(); | ||
@Nullable public abstract String flitWaysPartnerKey(); | ||
|
||
public interface Builder { | ||
Builder action(String action); | ||
Builder segment(TripSegment segment); | ||
Builder flitWaysPartnerKey(String flitWaysPartnerKey); | ||
ExternalActionParams build(); | ||
} | ||
} |
Oops, something went wrong.