-
Notifications
You must be signed in to change notification settings - Fork 990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
deep linking first pass #171
Changes from all commits
6adf367
ce8fdcd
bd5b468
95e00a7
4a519c9
af1143a
5ce9242
4e94d69
4e34be8
a328e8f
fa8df14
62bd882
e8b92f1
63ba649
638038e
ec837cf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
package com.kickstarter.ui.activities; | ||
|
||
import android.content.Intent; | ||
import android.net.Uri; | ||
import android.os.Bundle; | ||
import android.os.Parcelable; | ||
import android.support.annotation.NonNull; | ||
import android.support.annotation.Nullable; | ||
|
||
import com.kickstarter.libs.BaseActivity; | ||
import com.kickstarter.libs.RefTag; | ||
import com.kickstarter.libs.qualifiers.RequiresActivityViewModel; | ||
import com.kickstarter.libs.utils.ApplicationUtils; | ||
import com.kickstarter.ui.IntentKey; | ||
import com.kickstarter.viewmodels.DeepLinkViewModel; | ||
|
||
import java.util.List; | ||
|
||
import static com.kickstarter.libs.rx.transformers.Transformers.observeForUI; | ||
|
||
@RequiresActivityViewModel(DeepLinkViewModel.ViewModel.class) | ||
public final class DeepLinkActivity extends BaseActivity<DeepLinkViewModel.ViewModel> { | ||
@Override | ||
protected void onCreate(final @Nullable Bundle savedInstanceState) { | ||
super.onCreate(savedInstanceState); | ||
|
||
this.viewModel.outputs.requestPackageManager() | ||
.compose(bindToLifecycle()) | ||
.compose(observeForUI()) | ||
.subscribe(__ -> inputPackageManager()); | ||
|
||
this.viewModel.outputs.startBrowser() | ||
.compose(bindToLifecycle()) | ||
.compose(observeForUI()) | ||
.subscribe(this::startBrowser); | ||
|
||
this.viewModel.outputs.startDiscoveryActivity() | ||
.compose(bindToLifecycle()) | ||
.compose(observeForUI()) | ||
.subscribe(__ -> startDiscoveryActivity()); | ||
|
||
this.viewModel.outputs.startProjectActivity() | ||
.compose(bindToLifecycle()) | ||
.compose(observeForUI()) | ||
.subscribe(this::startProjectActivity); | ||
} | ||
|
||
private void startDiscoveryActivity() { | ||
ApplicationUtils.startNewDiscoveryActivity(this); | ||
finish(); | ||
} | ||
|
||
private void startProjectActivity(final @NonNull Uri uri) { | ||
final Intent projectIntent = new Intent(this, ProjectActivity.class) | ||
.setData(uri); | ||
final String ref = uri.getQueryParameter("ref"); | ||
if (ref != null) { | ||
projectIntent.putExtra(IntentKey.REF_TAG, RefTag.from(ref)); | ||
} | ||
startActivity(projectIntent); | ||
finish(); | ||
} | ||
|
||
private void startBrowser(final @NonNull List<Intent> targetIntents) { | ||
if (!targetIntents.isEmpty()) { | ||
/* We need to remove the first intent so it's not duplicated | ||
when we add the EXTRA_INITIAL_INTENTS intents. */ | ||
final Intent chooserIntent = Intent.createChooser(targetIntents.remove(0), ""); | ||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, | ||
targetIntents.toArray(new Parcelable[targetIntents.size()])); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could any of this logic get pushed into the ViewModel? not sure if it relies on any context from the activity... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think only |
||
startActivity(chooserIntent); | ||
} | ||
finish(); | ||
} | ||
|
||
private void inputPackageManager() { | ||
this.viewModel.inputs.packageManager(getPackageManager()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
package com.kickstarter.viewmodels; | ||
|
||
import android.content.Intent; | ||
import android.content.pm.PackageManager; | ||
import android.net.Uri; | ||
import android.support.annotation.NonNull; | ||
import android.text.TextUtils; | ||
import android.util.Pair; | ||
|
||
import com.kickstarter.libs.ActivityViewModel; | ||
import com.kickstarter.libs.Environment; | ||
import com.kickstarter.services.KSUri; | ||
import com.kickstarter.ui.activities.DeepLinkActivity; | ||
|
||
import java.util.List; | ||
|
||
import rx.Observable; | ||
import rx.subjects.BehaviorSubject; | ||
import rx.subjects.PublishSubject; | ||
|
||
import static com.kickstarter.libs.rx.transformers.Transformers.ignoreValues; | ||
|
||
public interface DeepLinkViewModel { | ||
|
||
interface Inputs { | ||
/** | ||
* Call when user clicks link that can't be deep linked. | ||
*/ | ||
void packageManager(PackageManager packageManager); | ||
} | ||
|
||
interface Outputs { | ||
/** | ||
* Emits when we need to get {@link PackageManager} to query for activities that can open a link. | ||
*/ | ||
Observable<Void> requestPackageManager(); | ||
|
||
/** | ||
* Emits when we should start an external browser because we don't want to deep link. | ||
*/ | ||
Observable<List<Intent>> startBrowser(); | ||
|
||
/** | ||
* Emits when we should start the {@link com.kickstarter.ui.activities.DiscoveryActivity}. | ||
*/ | ||
Observable<Void> startDiscoveryActivity(); | ||
|
||
/** | ||
* Emits when we should start the {@link com.kickstarter.ui.activities.ProjectActivity}. | ||
*/ | ||
Observable<Uri> startProjectActivity(); | ||
} | ||
|
||
final class ViewModel extends ActivityViewModel<DeepLinkActivity> implements Outputs, Inputs { | ||
public ViewModel(final @NonNull Environment environment) { | ||
super(environment); | ||
|
||
final Observable<Uri> uriFromIntent = intent() | ||
.map(Intent::getData) | ||
.ofType(Uri.class); | ||
|
||
uriFromIntent | ||
.filter(uri -> uri.getLastPathSegment().equals("projects")) | ||
.compose(ignoreValues()) | ||
.compose(bindToLifecycle()) | ||
.subscribe(this.startDiscoveryActivity::onNext); | ||
|
||
this.startDiscoveryActivity | ||
.subscribe(__ -> koala.trackContinueUserActivityAndOpenedDeepLink()); | ||
|
||
uriFromIntent | ||
.filter(uri -> KSUri.isProjectUri(uri, uri.toString())) | ||
.compose(bindToLifecycle()) | ||
.subscribe(this.startProjectActivity::onNext); | ||
|
||
this.startProjectActivity | ||
.subscribe(__ -> koala.trackContinueUserActivityAndOpenedDeepLink()); | ||
|
||
final Observable<Pair<PackageManager, Uri>> packageManagerAndUri = | ||
Observable.combineLatest(this.packageManager, uriFromIntent, Pair::create); | ||
|
||
final Observable<List<Intent>> targetIntents = packageManagerAndUri | ||
.flatMap(pair -> { | ||
/* We use a fake Uri because in Android 6.0 and above, | ||
if a link is domain verified, only that app is returned. */ | ||
final Uri fakeUri = Uri.parse("http://www.kickstarter.com"); | ||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, fakeUri); | ||
return Observable.from(pair.first.queryIntentActivities(browserIntent, 0)) | ||
.filter(resolveInfo -> !resolveInfo.activityInfo.packageName.contains("com.kickstarter")) | ||
.map(resolveInfo -> { | ||
final Intent intent = new Intent(Intent.ACTION_VIEW, pair.second); | ||
intent.setPackage(resolveInfo.activityInfo.packageName); | ||
intent.setData(pair.second); | ||
return intent; | ||
}) | ||
.toList(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. niceee |
||
}); | ||
|
||
targetIntents | ||
.compose(bindToLifecycle()) | ||
.subscribe(this.startBrowser::onNext); | ||
|
||
uriFromIntent | ||
.filter(uri -> !uri.getLastPathSegment().equals("projects") && !KSUri.isProjectUri(uri, uri.toString())) | ||
.map(Uri::toString) | ||
.filter(url -> !TextUtils.isEmpty(url)) | ||
.compose(ignoreValues()) | ||
.compose(bindToLifecycle()) | ||
.subscribe(this.requestPackageManager::onNext); | ||
} | ||
|
||
private final PublishSubject<PackageManager> packageManager = PublishSubject.create(); | ||
|
||
private final BehaviorSubject<Void> requestPackageManager = BehaviorSubject.create(); | ||
private final BehaviorSubject<List<Intent>> startBrowser = BehaviorSubject.create(); | ||
private final BehaviorSubject<Void> startDiscoveryActivity = BehaviorSubject.create(); | ||
private final BehaviorSubject<Uri> startProjectActivity = BehaviorSubject.create(); | ||
|
||
public final Inputs inputs = this; | ||
public final Outputs outputs = this; | ||
|
||
@Override public void packageManager(final PackageManager packageManager) { | ||
this.packageManager.onNext(packageManager); | ||
} | ||
|
||
@Override public @NonNull Observable<Void> requestPackageManager() { | ||
return this.requestPackageManager; | ||
} | ||
@Override public @NonNull Observable<List<Intent>> startBrowser() { | ||
return this.startBrowser; | ||
} | ||
@Override public @NonNull Observable<Void> startDiscoveryActivity() { | ||
return this.startDiscoveryActivity; | ||
} | ||
@Override public @NonNull Observable<Uri> startProjectActivity() { | ||
return this.startProjectActivity; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
package com.kickstarter.viewmodels; | ||
|
||
import android.content.Intent; | ||
import android.net.Uri; | ||
|
||
import com.kickstarter.KSRobolectricTestCase; | ||
import com.kickstarter.libs.KoalaEvent; | ||
|
||
import org.junit.Test; | ||
|
||
import java.util.List; | ||
|
||
import rx.observers.TestSubscriber; | ||
|
||
public class DeepLinkViewModelTest extends KSRobolectricTestCase { | ||
private DeepLinkViewModel.ViewModel vm; | ||
private final TestSubscriber<Void> requestPackageManager = new TestSubscriber<>(); | ||
private final TestSubscriber<List<Intent>> startBrowser = new TestSubscriber<>(); | ||
private final TestSubscriber<Void> startDiscoveryActivity = new TestSubscriber<>(); | ||
private final TestSubscriber<Uri> startProjectActivity = new TestSubscriber<>(); | ||
|
||
protected void setUpEnvironment() { | ||
this.vm = new DeepLinkViewModel.ViewModel(environment()); | ||
this.vm.outputs.requestPackageManager().subscribe(this.requestPackageManager); | ||
this.vm.outputs.startBrowser().subscribe(this.startBrowser); | ||
this.vm.outputs.startDiscoveryActivity().subscribe(this.startDiscoveryActivity); | ||
this.vm.outputs.startProjectActivity().subscribe(this.startProjectActivity); | ||
} | ||
|
||
@Test | ||
public void testNonDeepLink_startsBrowser() { | ||
setUpEnvironment(); | ||
|
||
final String url = "https://www.kickstarter.com/projects/smithsonian/smithsonian-anthology-of-hip-hop-and-rap/comments"; | ||
this.vm.intent(intentWithData(url)); | ||
this.vm.packageManager(application().getPackageManager()); | ||
|
||
this.requestPackageManager.assertValueCount(1); | ||
this.startBrowser.assertValueCount(1); | ||
this.startDiscoveryActivity.assertNoValues(); | ||
this.startProjectActivity.assertNoValues(); | ||
this.koalaTest.assertNoValues(); | ||
} | ||
|
||
@Test | ||
public void testProjectDeepLink_startsProjectActivity() { | ||
setUpEnvironment(); | ||
|
||
final String url = "https://www.kickstarter.com/projects/smithsonian/smithsonian-anthology-of-hip-hop-and-rap"; | ||
this.vm.intent(intentWithData(url)); | ||
|
||
this.startProjectActivity.assertValue(Uri.parse(url)); | ||
this.startBrowser.assertNoValues(); | ||
this.requestPackageManager.assertNoValues(); | ||
this.startDiscoveryActivity.assertNoValues(); | ||
this.koalaTest.assertValues(KoalaEvent.CONTINUE_USER_ACTIVITY, KoalaEvent.OPENED_DEEP_LINK); | ||
} | ||
|
||
@Test | ||
public void testDiscoveryDeepLink_startsDiscoveryActivity() { | ||
setUpEnvironment(); | ||
|
||
final String url = "https://www.kickstarter.com/projects"; | ||
this.vm.intent(intentWithData(url)); | ||
|
||
this.startDiscoveryActivity.assertValueCount(1); | ||
this.startBrowser.assertNoValues(); | ||
this.requestPackageManager.assertNoValues(); | ||
this.startProjectActivity.assertNoValues(); | ||
this.koalaTest.assertValues(KoalaEvent.CONTINUE_USER_ACTIVITY, KoalaEvent.OPENED_DEEP_LINK); | ||
} | ||
|
||
private Intent intentWithData(final String url) { | ||
return new Intent() | ||
.setData(Uri.parse(url)); | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. siqqqqq tests! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to confirm we want to call this method even if the
targetIntents
are an empty list? If we don't need to call this then we could add a filter on the end of the output and have it not emit (but maybe we wantfinish()
to get called here?)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we want to finish the
DeepLinkActivity
!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
noice