Skip to content

Commit

Permalink
Merge pull request #33 from skedgo/emm/deep-linking
Browse files Browse the repository at this point in the history
Add deep linking for Uber, Lyft and FlitWays
  • Loading branch information
Thuy Trinh committed Mar 18, 2016
2 parents e79c4a9 + 7a47b79 commit dcbd1e9
Show file tree
Hide file tree
Showing 23 changed files with 1,609 additions and 0 deletions.
Binary file added trip-kit/IOStoAndroidStringsConverter.jar
Binary file not shown.
8 changes: 8 additions & 0 deletions trip-kit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ android {
}
}

// Generation relative to tripgo! Add translations repo to tripkit?
task(generateTripkitStrings, type: org.gradle.api.tasks.JavaExec) {
classpath(files('IOStoAndroidStringsConverter.jar'))
main('com.skedgo.tripgo.tools.android.Main')
args(['./src/main/res', '../../libraries/translations', 'void',
'en#es#de#fi#pt#zh-Hant#zh-Hans', 'Tripkit.strings'])
}

dependencies {
testCompile "junit:junit:4.12"
testCompile "org.assertj:assertj-core:1.7.0"
Expand Down
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");
}
}
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);
}
}
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();
}
}
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 {}
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);
}
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;
}
}
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();
}
}
Loading

0 comments on commit dcbd1e9

Please sign in to comment.