├── sample
├── gradle.properties
├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-ldpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── values
│ │ │ └── strings.xml
│ │ └── layout
│ │ │ ├── activity_about.xml
│ │ │ └── activity_contact.xml
│ │ ├── java
│ │ └── pl
│ │ │ └── allegro
│ │ │ └── android
│ │ │ └── slinger
│ │ │ └── example
│ │ │ ├── AboutActivity.java
│ │ │ ├── ContactActivity.java
│ │ │ └── ExampleIntentResolver.java
│ │ └── AndroidManifest.xml
└── build.gradle
├── settings.gradle
├── slinger
├── gradle.properties
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ │ └── values
│ │ │ │ └── strings.xml
│ │ └── java
│ │ │ └── pl
│ │ │ └── allegro
│ │ │ └── android
│ │ │ └── slinger
│ │ │ ├── SlingerActivity.java
│ │ │ ├── util
│ │ │ └── Preconditions.java
│ │ │ ├── resolver
│ │ │ ├── RedirectRule.java
│ │ │ └── IntentResolver.java
│ │ │ ├── AppLinkBypasser.java
│ │ │ ├── Slinger.java
│ │ │ ├── ReferrerMangler.java
│ │ │ ├── ManifestParser.java
│ │ │ └── IntentStarter.java
│ └── test
│ │ ├── resources
│ │ └── robolectric.properties
│ │ └── java
│ │ └── pl
│ │ └── allegro
│ │ └── android
│ │ └── slinger
│ │ ├── util
│ │ └── IntentMatchers.java
│ │ ├── ReferrerManglerTest.java
│ │ ├── PackageManagerPreparator.java
│ │ ├── AppLinkBypasserTest.java
│ │ ├── SlingerActivityTest.java
│ │ ├── SlingerTest.java
│ │ ├── ManifestParserTest.java
│ │ ├── resolver
│ │ └── IntentResolverTest.java
│ │ └── IntentStarterTest.java
├── proguard
│ └── library.pro
└── build.gradle
├── gradle
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
└── maven
│ └── mvn-publish.gradle
├── assets
└── slinger_resolving_mechanism.png
├── CHANGELOG.md
├── .travis.yml
├── .gitignore
├── gradle.properties
├── gradlew.bat
├── README.md
└── gradlew
/sample/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Slinger Sample
2 | POM_ARTIFACT_ID=sample
3 | POM_PACKAGING=apk
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name='pl.allegro.android.slinger'
2 | include ':slinger', ':sample'
--------------------------------------------------------------------------------
/slinger/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=Slinger Library
2 | POM_ARTIFACT_ID=slinger
3 | POM_PACKAGING=aar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allegro/slinger/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/assets/slinger_resolving_mechanism.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allegro/slinger/HEAD/assets/slinger_resolving_mechanism.png
--------------------------------------------------------------------------------
/slinger/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allegro/slinger/HEAD/sample/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-ldpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allegro/slinger/HEAD/sample/src/main/res/mipmap-ldpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allegro/slinger/HEAD/sample/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allegro/slinger/HEAD/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/slinger/proguard/library.pro:
--------------------------------------------------------------------------------
1 | -keep class * extends pl.allegro.android.slinger.resolver.IntentResolver {
2 | (android.app.Activity);
3 | }
--------------------------------------------------------------------------------
/slinger/src/test/resources/robolectric.properties:
--------------------------------------------------------------------------------
1 | sdk=21
2 | constants=pl.allegro.android.slinger.BuildConfig
3 | packageName = pl.allegro.slinger
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allegro/slinger/HEAD/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/allegro/slinger/HEAD/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/slinger/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | No activities can handle this link
3 |
4 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Slinger Example
3 |
4 | Slinger About Activity
5 | Slinger Contact Activity
6 |
7 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu May 12 23:46:02 CEST 2016
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip
7 |
--------------------------------------------------------------------------------
/sample/src/main/java/pl/allegro/android/slinger/example/AboutActivity.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger.example;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 |
6 | public class AboutActivity extends Activity {
7 |
8 | @Override protected void onCreate(Bundle savedInstanceState) {
9 | super.onCreate(savedInstanceState);
10 | setContentView(R.layout.activity_about);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/sample/src/main/java/pl/allegro/android/slinger/example/ContactActivity.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger.example;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 |
6 | public class ContactActivity extends Activity {
7 |
8 | @Override protected void onCreate(Bundle savedInstanceState) {
9 | super.onCreate(savedInstanceState);
10 | setContentView(R.layout.activity_contact);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/SlingerActivity.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 |
6 | public class SlingerActivity extends Activity {
7 |
8 | @Override protected void onCreate(Bundle savedInstanceState) {
9 | super.onCreate(savedInstanceState);
10 |
11 | Slinger.startActivity(this, getIntent());
12 | finish();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Change Log
2 | ==========
3 |
4 | Version 0.2.0 *(2016-01-22)*
5 | ----------------------------
6 |
7 | * Instantiating IntentResolver from manifest
8 | * Proguard consumer files added
9 | *
10 |
11 | Version 0.1.1 *(2015-10-22)*
12 | ----------------------------
13 |
14 | * Fix: Handling case when App Links are enabled or there is no other application to handle link.
15 |
16 | Version 0.1.0 *(2015-08-24)*
17 | ----------------------------
18 |
19 | Initial release.
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/util/Preconditions.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger.util;
2 |
3 | import android.support.annotation.Nullable;
4 |
5 | public final class Preconditions {
6 | private Preconditions() {
7 | }
8 |
9 | public static T checkNotNull(T reference, @Nullable Object errorMessage) {
10 | if (reference == null) {
11 | throw new IllegalArgumentException(String.valueOf(errorMessage));
12 | }
13 | return reference;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 |
3 | android:
4 | components:
5 | - android-23
6 | - build-tools-23.0.1
7 | - extra-android-m2repository
8 | - tools
9 | - platform-tools
10 |
11 | jdk:
12 | - oraclejdk7
13 |
14 | notifications:
15 | email: false
16 |
17 | sudo: false
18 |
19 | cache:
20 | directories:
21 | - $HOME/.m2
22 | - $HOME/.gradle
23 |
24 | script:
25 | - ./gradlew build jacocoTestReport
26 |
27 | after_success:
28 | - bash <(curl -s https://codecov.io/bash)
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_about.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/activity_contact.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated files
2 | bin/
3 | gen/
4 |
5 | # Gradle files
6 | .gradle/
7 | build/
8 |
9 | # Local configuration file (sdk path, etc)
10 | local.properties
11 |
12 | # Log Files
13 | *.log
14 |
15 | # Idea project files
16 | *.iml
17 |
18 | ## Directory-based project format:
19 | /.idea/
20 | .idea
21 |
22 | ## File-based project format:
23 | *.ipr
24 | *.iws
25 |
26 | ## Plugin-specific files:
27 |
28 | # IntelliJ
29 | /out/
30 |
31 | # mpeltonen/sbt-idea plugin
32 | .idea_modules/
33 |
34 | # JIRA plugin
35 | atlassian-ide-plugin.xml
36 | sonar-project.properties
37 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | VERSION_CODE=1
2 | GROUP=pl.allegro.android.slinger
3 |
4 | POM_DESCRIPTION=Library to handle and define deep links in Android application.
5 | POM_URL=https://github.com/allegro/slinger
6 | POM_SCM_URL=https://github.com/allegro/slinger
7 | POM_SCM_CONNECTION=scm:git@github.com:allegro/slinger.git
8 | POM_SCM_DEV_CONNECTION=scm:git@github.com:allegro/slinger.git
9 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
10 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
11 | POM_LICENCE_DIST=repo
12 | POM_DEVELOPER_ID=kkocel
13 | POM_DEVELOPER_NAME=Christopher Kocel
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | android {
4 | compileSdkVersion rootProject.ext.compileSdkVersion
5 | buildToolsVersion rootProject.ext.buildToolsVersion
6 |
7 | defaultConfig {
8 | applicationId "pl.allegro.android.slinger.sample"
9 | minSdkVersion rootProject.ext.minSdkVersion
10 | targetSdkVersion rootProject.ext.targetSdkVersion
11 | versionCode 1
12 | versionName rootProject.version
13 | }
14 | compileOptions {
15 | sourceCompatibility JavaVersion.VERSION_1_7
16 | targetCompatibility JavaVersion.VERSION_1_7
17 | }
18 | buildTypes {
19 | release {
20 | minifyEnabled true
21 | signingConfig signingConfigs.debug
22 | }
23 | }
24 | lintOptions {
25 | textReport true
26 | textOutput 'stdout'
27 | }
28 | }
29 |
30 | dependencies {
31 | compile project(":slinger")
32 | }
33 |
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/util/IntentMatchers.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger.util;
2 |
3 | import android.content.Intent;
4 | import android.support.annotation.NonNull;
5 | import org.mockito.ArgumentMatcher;
6 |
7 | public class IntentMatchers {
8 |
9 | public static class IsIntentWithAction extends ArgumentMatcher {
10 |
11 | private String action;
12 |
13 | public IsIntentWithAction(@NonNull String action) {
14 | this.action = action;
15 | }
16 |
17 | @Override public boolean matches(@NonNull Object argument) {
18 | return action.equals(((Intent) argument).getAction());
19 | }
20 | }
21 |
22 | public static class IsIntentWithPackageName extends ArgumentMatcher {
23 |
24 | private String packageName;
25 |
26 | public IsIntentWithPackageName(@NonNull String packageName) {
27 | this.packageName = packageName;
28 | }
29 |
30 | @Override public boolean matches(@NonNull Object argument) {
31 | return packageName.equals(((Intent) argument).getPackage());
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/ReferrerManglerTest.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.content.Intent;
4 | import android.net.Uri;
5 |
6 | import org.junit.Test;
7 | import org.junit.runner.RunWith;
8 | import org.robolectric.RobolectricGradleTestRunner;
9 |
10 | import static android.net.Uri.parse;
11 | import static com.google.common.truth.Truth.assertThat;
12 | import static pl.allegro.android.slinger.ReferrerMangler.addReferrerToIntent;
13 | import static pl.allegro.android.slinger.ReferrerMangler.getReferrerUriFromIntent;
14 |
15 | @RunWith(RobolectricGradleTestRunner.class) public class ReferrerManglerTest {
16 |
17 | @Test public void shouldPutAndRetrieveReferrerFromIntent() {
18 | //given
19 | Intent intent = new Intent();
20 | Uri referrer =
21 | parse("android-app://com.google.android.googlequicksearchbox/https/www.google.com");
22 | addReferrerToIntent(intent, referrer);
23 |
24 | //when
25 | Uri referrerUriFromIntent = getReferrerUriFromIntent(intent);
26 |
27 | //then
28 | assertThat(referrerUriFromIntent).isEqualTo(referrer);
29 | }
30 |
31 | @Test public void shouldReturnNullForNotSetReferrer() {
32 | //given
33 | Intent intent = new Intent();
34 | addReferrerToIntent(intent, null);
35 |
36 | //when
37 | Uri referrerUriFromIntent = getReferrerUriFromIntent(intent);
38 |
39 | //then
40 | assertThat(referrerUriFromIntent).isNull();
41 | }
42 | }
--------------------------------------------------------------------------------
/slinger/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'jacoco-android'
3 | apply plugin: 'maven'
4 |
5 | android {
6 | compileSdkVersion rootProject.ext.compileSdkVersion
7 | buildToolsVersion rootProject.ext.buildToolsVersion
8 |
9 | defaultConfig {
10 | minSdkVersion rootProject.ext.minSdkVersion
11 | targetSdkVersion rootProject.ext.targetSdkVersion
12 | versionCode 1
13 | versionName "1.0"
14 | consumerProguardFiles fileTree('proguard')
15 | }
16 |
17 | compileOptions {
18 | sourceCompatibility JavaVersion.VERSION_1_7
19 | targetCompatibility JavaVersion.VERSION_1_7
20 | }
21 |
22 | lintOptions {
23 | textReport true
24 | textOutput 'stdout'
25 | }
26 |
27 | dexOptions {
28 | incremental true
29 | javaMaxHeapSize "4g"
30 | }
31 |
32 | packagingOptions {
33 | exclude 'META-INF/LICENSE.txt'
34 | exclude 'META-INF/NOTICE.txt'
35 | }
36 | }
37 |
38 | dependencies {
39 | compile rootProject.ext.supportAnnotations
40 | testCompile rootProject.ext.junit
41 | testCompile(rootProject.ext.robolectric) {
42 | exclude group: 'commons-logging', module: 'commons-logging'
43 | exclude group: 'org.apache.httpcomponents', module: 'httpclient'
44 | }
45 | testCompile rootProject.ext.mavenAntTasks
46 | testCompile rootProject.ext.mockitoCore
47 | testCompile rootProject.ext.truth
48 | }
49 |
50 | tasks.withType(JavaCompile) {
51 | options.encoding = 'UTF-8'
52 | }
53 |
54 | apply from: rootProject.file('gradle/maven/mvn-publish.gradle')
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/resolver/RedirectRule.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger.resolver;
2 |
3 | import android.content.Intent;
4 | import android.support.annotation.NonNull;
5 | import java.util.regex.Pattern;
6 |
7 | import static pl.allegro.android.slinger.util.Preconditions.checkNotNull;
8 |
9 | /**
10 | * Redirect Rule which holds regular expression and {@link Intent} corresponding to it.
11 | */
12 | public class RedirectRule {
13 |
14 | private final String regexpPattern;
15 | private final Intent intent;
16 |
17 | public RedirectRule(@NonNull Intent intent, @NonNull String regexpPattern) {
18 | this.intent = checkNotNull(intent, "intent == null");
19 | this.regexpPattern = checkNotNull(regexpPattern, "pattern == null");
20 | }
21 |
22 | public Intent getIntent() {
23 | return intent;
24 | }
25 |
26 | public Pattern getPattern() {
27 | return Pattern.compile(regexpPattern);
28 | }
29 |
30 | public static Builder builder() {
31 | return new Builder();
32 | }
33 |
34 | public static class Builder {
35 | private Intent intent;
36 | private String regexp;
37 |
38 | public Builder intent(@NonNull Intent intent) {
39 | this.intent = checkNotNull(intent, "intent == null");
40 | return this;
41 | }
42 |
43 | public Builder pattern(@NonNull String pattern) {
44 | this.regexp = checkNotNull(pattern, "pattern == null");
45 | return this;
46 | }
47 |
48 | public RedirectRule build() {
49 | return new RedirectRule(intent, regexp);
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/PackageManagerPreparator.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.content.ComponentName;
4 | import android.content.pm.ActivityInfo;
5 | import android.content.pm.PackageManager;
6 | import android.os.Bundle;
7 |
8 | import org.robolectric.RuntimeEnvironment;
9 | import org.robolectric.res.builder.RobolectricPackageManager;
10 |
11 | import static android.content.pm.PackageManager.GET_META_DATA;
12 | import static org.mockito.Matchers.any;
13 | import static org.mockito.Matchers.eq;
14 | import static org.mockito.Mockito.doReturn;
15 | import static org.mockito.Mockito.spy;
16 |
17 | public class PackageManagerPreparator {
18 | private final ActivityInfo activityInfo = new ActivityInfo();
19 |
20 | PackageManagerPreparator() throws PackageManager.NameNotFoundException {
21 | RobolectricPackageManager packageManager =
22 | spy((RobolectricPackageManager) RuntimeEnvironment.application.getPackageManager());
23 |
24 | RuntimeEnvironment.setRobolectricPackageManager(packageManager);
25 | doReturn(activityInfo).when(packageManager)
26 | .getActivityInfo(any(ComponentName.class), eq(GET_META_DATA));
27 | activityInfo.metaData = new Bundle();
28 | }
29 |
30 | void addModuleToManifest(Class> moduleClass) {
31 | addToManifest(moduleClass.getName());
32 | }
33 |
34 | void addToManifest(String value) {
35 | activityInfo.metaData.putString(ManifestParser.INTENT_RESOLVER_NAME, value);
36 | }
37 |
38 | public ActivityInfo getActivityInfo() {
39 | return activityInfo;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/sample/src/main/java/pl/allegro/android/slinger/example/ExampleIntentResolver.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger.example;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.support.annotation.Keep;
6 | import android.support.annotation.NonNull;
7 |
8 | import java.util.List;
9 |
10 | import pl.allegro.android.slinger.resolver.IntentResolver;
11 | import pl.allegro.android.slinger.resolver.RedirectRule;
12 |
13 | import static java.util.Arrays.asList;
14 | import static pl.allegro.android.slinger.resolver.RedirectRule.builder;
15 |
16 | @Keep
17 | public class ExampleIntentResolver extends IntentResolver {
18 | public static final String PATTERN_FOR_ABOUT_ACTIVITY = "http(s)?://example.com/abc\\.html\\?query=a.*";
19 | public static final String PATTERN_FOR_CONTACT_ACTIVITY = "http(s)?://example.com/abc\\.html\\?query=c.*";
20 |
21 | private List rules;
22 |
23 | public ExampleIntentResolver(Activity activity) {
24 | super(activity);
25 | rules = asList(getRedirectRuleForAbout(activity), getRedirectRuleForContact(activity));
26 | }
27 |
28 | RedirectRule getRedirectRuleForAbout(Activity activity) {
29 | return builder().intent(new Intent(activity, AboutActivity.class)).pattern(
30 | PATTERN_FOR_ABOUT_ACTIVITY).build();
31 | }
32 |
33 | RedirectRule getRedirectRuleForContact(Activity activity) {
34 | return builder().intent(new Intent(activity, ContactActivity.class)).pattern(
35 | PATTERN_FOR_CONTACT_ACTIVITY).build();
36 | }
37 |
38 | @NonNull @Override public Iterable getRules() {
39 | return rules;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/AppLinkBypasser.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.content.Intent;
4 | import android.content.pm.PackageManager;
5 | import android.content.pm.ResolveInfo;
6 | import android.net.Uri;
7 | import android.os.Build;
8 | import android.support.annotation.VisibleForTesting;
9 | import java.util.List;
10 |
11 | /**
12 | * Class bypassing App link mechanism introduced in Android Marshmallow.
13 | * If an application has link-handling setting enabled then {@link PackageManager} returns only one
14 | * result from {@link PackageManager#queryIntentActivities(Intent, int)}.
15 | * If we explicitly don't want to use this result then we query for {@link Intent}s only with
16 | * scheme defined.
17 | */
18 | class AppLinkBypasser {
19 | private final PackageManager packageManager;
20 |
21 | public AppLinkBypasser(PackageManager packageManager) {
22 | this.packageManager = packageManager;
23 | }
24 |
25 | boolean isBypassApplicable(List queryIntentActivities) {
26 | return hasMarshmallow() && onlyOneResult(queryIntentActivities);
27 | }
28 |
29 | private boolean onlyOneResult(List queryIntentActivities) {
30 | return queryIntentActivities.size() == 1;
31 | }
32 |
33 | private boolean hasMarshmallow() {
34 | return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
35 | }
36 |
37 | List resolveAdditionalActivitiesWithScheme(Intent intent) {
38 | return packageManager.queryIntentActivities(getIntentWithRawSchemeUri(intent), 0);
39 | }
40 |
41 | @VisibleForTesting Intent getIntentWithRawSchemeUri(Intent intent) {
42 | return new Intent(intent).setData(getRawSchemeUri(intent.getData()));
43 | }
44 |
45 | @VisibleForTesting Uri getRawSchemeUri(Uri uri) {
46 | return new Uri.Builder().scheme(uri.getScheme()).build();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/AppLinkBypasserTest.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.content.Intent;
4 | import android.net.Uri;
5 | import org.junit.Test;
6 | import org.junit.runner.RunWith;
7 | import org.robolectric.RobolectricGradleTestRunner;
8 | import org.robolectric.RuntimeEnvironment;
9 |
10 | import static android.net.Uri.parse;
11 | import static com.google.common.truth.Truth.assertThat;
12 |
13 | @RunWith(RobolectricGradleTestRunner.class) public class AppLinkBypasserTest {
14 |
15 | private AppLinkBypasser objectUnderTest =
16 | new AppLinkBypasser(RuntimeEnvironment.application.getPackageManager());
17 |
18 | @Test public void uriShouldContainOnlyScheme() {
19 | // given
20 | Uri uri = parse("http://example.com");
21 |
22 | // when
23 | Uri rawSchemeUri = objectUnderTest.getRawSchemeUri(uri);
24 |
25 | // then
26 | assertThatUriContainsOnlyScheme(rawSchemeUri, "http");
27 | }
28 |
29 | @Test public void intentShouldContainOnlyUriWithScheme() {
30 | // given
31 | Intent intent = new Intent().setData(parse("http://example.com"));
32 |
33 | // when
34 | Intent rawSchemeIntent = objectUnderTest.getIntentWithRawSchemeUri(intent);
35 |
36 | // then
37 | assertThat(rawSchemeIntent.getScheme()).isEqualTo(intent.getScheme());
38 | assertThatUriContainsOnlyScheme(rawSchemeIntent.getData(), "http");
39 | assertThat(rawSchemeIntent.getAction()).isEqualTo(intent.getAction());
40 | assertThat(rawSchemeIntent.getCategories()).isEqualTo(intent.getCategories());
41 | }
42 |
43 | private void assertThatUriContainsOnlyScheme(Uri rawSchemeUri, String scheme) {
44 | assertThat(rawSchemeUri.getScheme()).isEqualTo(scheme);
45 | assertThat(rawSchemeUri.getAuthority()).isNull();
46 | assertThat(rawSchemeUri.getHost()).isNull();
47 | assertThat(rawSchemeUri.getPort()).isEqualTo(-1);
48 | assertThat(rawSchemeUri.getPath()).isEmpty();
49 | assertThat(rawSchemeUri.getQuery()).isNull();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/SlingerActivityTest.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.support.annotation.NonNull;
6 | import com.google.common.collect.ImmutableList;
7 | import java.util.List;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.robolectric.Robolectric;
11 | import org.robolectric.RobolectricGradleTestRunner;
12 | import org.robolectric.annotation.Config;
13 | import pl.allegro.android.slinger.resolver.IntentResolver;
14 | import pl.allegro.android.slinger.resolver.RedirectRule;
15 |
16 | import static pl.allegro.android.slinger.resolver.RedirectRule.builder;
17 |
18 | @RunWith(RobolectricGradleTestRunner.class) @Config(manifest = Config.NONE)
19 | public class SlingerActivityTest {
20 |
21 | @Test(expected = RuntimeException.class) public void shouldFailWithoutSpecifyingUri() {
22 | // given
23 | Intent intent = new Intent(Intent.ACTION_VIEW);
24 |
25 | // when
26 | Robolectric.buildActivity(SlingerActivity.class).withIntent(intent).create().get();
27 | }
28 |
29 | @Test(expected = RuntimeException.class) public void shouldFailForNullIntent() {
30 | Robolectric.buildActivity(SlingerActivity.class).withIntent(null).create().get();
31 | }
32 |
33 | static class Activity1 extends Activity {
34 | }
35 |
36 | public static class TestIntentResolver extends IntentResolver {
37 |
38 | public static final String PATTERN_FOR_EXAMPLE_HOST = "http(s)?://example.com.*";
39 |
40 | private List rules;
41 |
42 | public TestIntentResolver(Activity activity) {
43 | super(activity);
44 | rules = ImmutableList.of(getRedirectRuleForExampleHost(activity));
45 | }
46 |
47 | RedirectRule getRedirectRuleForExampleHost(Activity activity) {
48 | return builder().intent(new Intent(activity, Activity1.class).setAction(Intent.ACTION_VIEW))
49 | .pattern(PATTERN_FOR_EXAMPLE_HOST)
50 | .build();
51 | }
52 |
53 | @NonNull @Override public Iterable getRules() {
54 | return rules;
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/Slinger.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 |
7 | import java.util.Collections;
8 |
9 | import pl.allegro.android.slinger.resolver.IntentResolver;
10 | import pl.allegro.android.slinger.resolver.RedirectRule;
11 |
12 | public class Slinger {
13 |
14 | private static IntentResolver intentResolver;
15 |
16 | /**
17 | * Starts new {@link Intent} resolved by {@link IntentResolver}
18 | * Starts passed intent and excludes parentActivity
19 | *
20 | * @param parentActivity that is used to be excluded from launch
21 | * @param intent contains uri that is used to find a new {@link Intent}
22 | */
23 | public static void startActivity(Activity parentActivity, Intent intent) {
24 | Uri uri = getOriginatingUriFromIntent(intent);
25 |
26 | if (uri == null) {
27 | throw new RuntimeException(
28 | "You cannot run this Activity without specifying Uri inside Intent!");
29 | }
30 |
31 | IntentResolver intentResolver = getIntentResolver(parentActivity);
32 |
33 | excludeSlingerAndStartTargetActivity(parentActivity,
34 | intentResolver.enrichIntent(parentActivity, intentResolver.resolveIntentToSling(uri), uri));
35 | }
36 |
37 | private static Uri getOriginatingUriFromIntent(Intent intent) {
38 | return intent != null ? intent.getData() : null;
39 | }
40 |
41 | private static void excludeSlingerAndStartTargetActivity(Activity parentActivity, Intent intent) {
42 | new IntentStarter(parentActivity.getPackageManager(), intent,
43 | Collections.>singletonList(SlingerActivity.class)).startActivity(
44 | parentActivity);
45 | }
46 |
47 | /**
48 | * @return {@link IntentResolver} which provides implementation with collection of {@link
49 | * RedirectRule}s
50 | * and default {@link Intent} when no {@link RedirectRule} is matched
51 | */
52 | public static IntentResolver getIntentResolver(Activity parentActivity) {
53 | if (intentResolver == null) {
54 | intentResolver = new ManifestParser(parentActivity).parse();
55 | }
56 | return intentResolver;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
10 |
13 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
44 |
45 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/ReferrerMangler.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.annotation.TargetApi;
4 | import android.app.Activity;
5 | import android.content.Intent;
6 | import android.net.Uri;
7 | import android.support.annotation.NonNull;
8 | import android.support.annotation.Nullable;
9 |
10 | import static android.os.Build.VERSION.SDK_INT;
11 | import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
12 |
13 | /**
14 | * Utility class for retrieving and inserting referrers.
15 | */
16 | public final class ReferrerMangler {
17 |
18 | public static final String EXTRA_REFERRER = "android.intent.extra.REFERRER";
19 | public static final String EXTRA_REFERRER_NAME = "android.intent.extra.REFERRER_NAME";
20 |
21 | private ReferrerMangler() {
22 | }
23 |
24 | /**
25 | * Gets referrer from {@link Activity} or {@link Intent}. Referrer in Android world is an {@link
26 | * Uri} that indicates from which place {@link Activity} was started.
27 | *
28 | * @param activity from which referrer would be extracted (if exists)
29 | * @return {@link Uri} with referrer if present or null
30 | */
31 | @Nullable public static Uri getReferrerUriFromActivity(@NonNull Activity activity) {
32 | if (SDK_INT >= LOLLIPOP_MR1) {
33 | return getLollipopReferrer(activity);
34 | }
35 |
36 | return getReferrerUriFromIntent(activity.getIntent());
37 | }
38 |
39 | public static Uri getReferrerUriFromIntent(Intent intent) {
40 | Uri referrerUri = intent.getParcelableExtra(EXTRA_REFERRER);
41 | if (referrerUri != null) {
42 | return referrerUri;
43 | }
44 |
45 | String referrer = intent.getStringExtra(EXTRA_REFERRER_NAME);
46 | return referrer != null ? Uri.parse(referrer) : null;
47 | }
48 |
49 | @TargetApi(LOLLIPOP_MR1) @Nullable
50 | private static Uri getLollipopReferrer(@NonNull Activity activity) {
51 | return activity.getReferrer();
52 | }
53 |
54 | /**
55 | * Adds referrer to intent
56 | *
57 | * @param intent in which referrerUri will be inserted
58 | * @param referrerUri to insert in {@link Intent}
59 | * @return {@link Intent} with referrer inside it
60 | */
61 | public static Intent addReferrerToIntent(Intent intent, Uri referrerUri) {
62 | if (referrerUri != null) {
63 | intent.putExtra(EXTRA_REFERRER, referrerUri);
64 | }
65 |
66 | return intent;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/SlingerTest.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.content.pm.PackageManager;
6 | import android.net.Uri;
7 | import android.support.annotation.NonNull;
8 |
9 | import com.google.common.collect.ImmutableList;
10 |
11 | import org.junit.Test;
12 | import org.junit.runner.RunWith;
13 | import org.robolectric.Robolectric;
14 | import org.robolectric.RobolectricGradleTestRunner;
15 |
16 | import java.util.List;
17 |
18 | import pl.allegro.android.slinger.resolver.IntentResolver;
19 | import pl.allegro.android.slinger.resolver.RedirectRule;
20 |
21 | import static org.mockito.Matchers.any;
22 | import static org.mockito.Mockito.spy;
23 | import static org.mockito.Mockito.verify;
24 | import static pl.allegro.android.slinger.IntentStarterTest.Utils.preparePackageManager;
25 | import static pl.allegro.android.slinger.resolver.RedirectRule.builder;
26 |
27 | @RunWith(RobolectricGradleTestRunner.class) public class SlingerTest {
28 |
29 | @Test public void shouldStartActivityWithUri() throws PackageManager.NameNotFoundException {
30 | // given
31 | Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://example.com"));
32 | Activity activity = spy(Robolectric.setupActivity(Activity.class));
33 | activity.setIntent(intent);
34 |
35 | preparePackageManager(new Intent(intent).setClass(activity, Activity1.class),
36 | ImmutableList.>of(Activity1.class));
37 | preparePackageManager(new Intent(intent).setClass(activity, SlingerActivity.class),
38 | ImmutableList.>of(SlingerActivity.class));
39 | new PackageManagerPreparator().getActivityInfo().metaData.putString(
40 | ManifestParser.INTENT_RESOLVER_NAME, TestModule1.class.getName());
41 |
42 | // when
43 | Slinger.startActivity(activity, intent);
44 |
45 | // then
46 | verify(activity).startActivity(any(Intent.class));
47 | }
48 |
49 | static class Activity1 extends Activity {
50 | }
51 |
52 | public static class TestModule1 extends IntentResolver {
53 |
54 | public static final String PATTERN_FOR_EXAMPLE_HOST = "http(s)?://example.com.*";
55 |
56 | private List rules;
57 |
58 | public TestModule1(Activity activity) {
59 | super(activity);
60 | rules = ImmutableList.of(getRedirectRuleForExampleHost(activity));
61 | }
62 |
63 | RedirectRule getRedirectRuleForExampleHost(Activity activity) {
64 | return builder().intent(new Intent(activity, Activity1.class).setAction(Intent.ACTION_VIEW))
65 | .pattern(PATTERN_FOR_EXAMPLE_HOST)
66 | .build();
67 | }
68 |
69 | @NonNull @Override public Iterable getRules() {
70 | return rules;
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/ManifestParser.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.app.Activity;
4 | import android.content.ComponentName;
5 | import android.content.pm.ActivityInfo;
6 | import android.content.pm.PackageManager;
7 |
8 | import java.lang.reflect.Constructor;
9 | import java.lang.reflect.InvocationTargetException;
10 |
11 | import pl.allegro.android.slinger.resolver.IntentResolver;
12 |
13 | /**
14 | * Parses {@link IntentResolver} references out of the AndroidManifest file.
15 | */
16 | public final class ManifestParser {
17 | static final String INTENT_RESOLVER_NAME = "IntentResolver";
18 |
19 | private final Activity activity;
20 |
21 | public ManifestParser(Activity activity) {
22 | this.activity = activity;
23 | }
24 |
25 | public IntentResolver parse() {
26 | try {
27 | ActivityInfo activityInfo = activity.getPackageManager()
28 | .getActivityInfo(new ComponentName(activity.getPackageName(),
29 | SlingerActivity.class.getCanonicalName()), PackageManager.GET_META_DATA);
30 |
31 | if (activityInfo.metaData != null) {
32 | for (String key : activityInfo.metaData.keySet()) {
33 | if (INTENT_RESOLVER_NAME.equals(key)) {
34 | return parseResolver(activityInfo.metaData.getString(key));
35 | }
36 | }
37 | }
38 | } catch (PackageManager.NameNotFoundException e) {
39 | throw new RuntimeException("Unable to find metadata to parse IntentResolver", e);
40 | }
41 |
42 | throw new RuntimeException("Unable to find metadata to parse IntentResolver");
43 | }
44 |
45 | private IntentResolver parseResolver(String className) {
46 | Class> clazz;
47 | try {
48 | clazz = Class.forName(className);
49 | } catch (ClassNotFoundException e) {
50 | throw new IllegalArgumentException("Unable to find IntentResolver implementation", e);
51 | }
52 |
53 | Object module;
54 | try {
55 | Constructor> cons = clazz.getConstructor(Activity.class);
56 | module = cons.newInstance(activity);
57 | } catch (InstantiationException e) {
58 | throw new RuntimeException("Unable to instantiate IntentResolver implementation for " + clazz,
59 | e);
60 | } catch (IllegalAccessException e) {
61 | throw new RuntimeException("Unable to instantiate IntentResolver implementation for " + clazz,
62 | e);
63 | } catch (NoSuchMethodException e) {
64 | throw new RuntimeException("No constructor for " + clazz + "that has Activity as parameter",
65 | e);
66 | } catch (InvocationTargetException e) {
67 | throw new RuntimeException("Unable to instantiate IntentResolver implementation for " + clazz,
68 | e);
69 | }
70 |
71 | if (!(module instanceof IntentResolver)) {
72 | throw new RuntimeException("Expected instanceof IntentResolver, but found: " + module);
73 | }
74 | return (IntentResolver) module;
75 | }
76 | }
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/resolver/IntentResolver.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger.resolver;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.net.Uri;
6 | import android.support.annotation.NonNull;
7 |
8 | import static android.content.Intent.ACTION_VIEW;
9 | import static pl.allegro.android.slinger.ReferrerMangler.addReferrerToIntent;
10 | import static pl.allegro.android.slinger.ReferrerMangler.getReferrerUriFromActivity;
11 |
12 | /**
13 | * Class that resolves target {@link Intent} by matching {@link Uri} that started {@link Activity}
14 | * with pattern provided by {@link RedirectRule}
15 | */
16 | public abstract class IntentResolver {
17 |
18 | @SuppressWarnings("unused")
19 | public IntentResolver(Activity activity) {
20 | }
21 |
22 | /**
23 | * Resolves {@link Intent} that will be slinged
24 | *
25 | * @param originatingUri {@link Uri} retrieved from {@link Intent#getData()}
26 | * @return {@link Intent} from matching {@link RedirectRule}
27 | */
28 | @NonNull
29 | public Intent resolveIntentToSling(@NonNull Uri originatingUri) {
30 | Intent matchingIntent = getMatchingIntentForRedirectRules(originatingUri);
31 | return matchingIntent != null ? matchingIntent : getDefaultRedirectIntent(originatingUri);
32 | }
33 |
34 | /**
35 | * Checks if {@link Uri} can be handled by provided redirect rules.
36 | * @param originatingUri {@link Uri} to check
37 | */
38 | public boolean canUriBeHandledByRedirectRules(@NonNull Uri originatingUri) {
39 | return getMatchingIntentForRedirectRules(originatingUri) != null;
40 | }
41 |
42 | private Intent getMatchingIntentForRedirectRules(@NonNull Uri originatingUri) {
43 | for (RedirectRule rule : getRules()) {
44 | if (isUriMatchingPattern(originatingUri, rule)) {
45 | return rule.getIntent();
46 | }
47 | }
48 |
49 | return null;
50 | }
51 |
52 | private boolean isUriMatchingPattern(Uri originatingUri, RedirectRule redirectable) {
53 | return redirectable.getPattern().matcher(originatingUri.toString()).matches();
54 | }
55 |
56 | /**
57 | * @return {@link Iterable} with {@link RedirectRule}s
58 | */
59 | @NonNull
60 | public abstract Iterable getRules();
61 |
62 | /**
63 | * @param originatingUri that started {@link Activity}
64 | * @return default {@link Intent} when there is no {@link RedirectRule} matching {@link Uri} that
65 | * started {@link Activity}
66 | */
67 | @NonNull
68 | protected Intent getDefaultRedirectIntent(Uri originatingUri) {
69 | return new Intent(ACTION_VIEW, originatingUri);
70 | }
71 |
72 | @NonNull
73 | public Intent enrichIntent(Activity parentActivity, Intent resolvedIntent, Uri originatingUri) {
74 | // we need to inform our target Activity about originating Uri
75 | resolvedIntent.setData(originatingUri);
76 |
77 | return addReferrerToIntent(resolvedIntent, getReferrerUriFromActivity(parentActivity));
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/ManifestParserTest.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.app.Activity;
4 | import android.content.pm.PackageManager;
5 | import android.support.annotation.NonNull;
6 |
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.robolectric.Robolectric;
11 | import org.robolectric.RobolectricTestRunner;
12 | import org.robolectric.annotation.Config;
13 |
14 | import java.util.ArrayList;
15 |
16 | import pl.allegro.android.slinger.resolver.IntentResolver;
17 | import pl.allegro.android.slinger.resolver.RedirectRule;
18 |
19 | import static com.google.common.truth.Truth.assertThat;
20 | import static org.mockito.Mockito.spy;
21 | import static org.mockito.Mockito.when;
22 |
23 | @RunWith(RobolectricTestRunner.class) @Config(manifest = Config.NONE)
24 | public class ManifestParserTest {
25 | private PackageManagerPreparator preparator;
26 | private ManifestParser parser;
27 | private Activity activity = spy(Robolectric.setupActivity(Activity.class));
28 |
29 | @Before public void setUp() throws PackageManager.NameNotFoundException {
30 | preparator = new PackageManagerPreparator();
31 | parser = new ManifestParser(activity);
32 | }
33 |
34 | @Test(expected = RuntimeException.class) public void shouldFailIfThereAreNoModules() {
35 | assertThat(parser.parse());
36 | }
37 |
38 | @Test public void shouldParseSingleModule() {
39 | preparator.addModuleToManifest(TestModule1.class);
40 |
41 | IntentResolver modules = parser.parse();
42 | assertThat(modules).isInstanceOf(TestModule1.class);
43 | }
44 |
45 | @Test(expected = RuntimeException.class) public void shouldFailIfAddedWithWrongKey() {
46 | preparator.getActivityInfo().metaData.putString(
47 | ManifestParser.INTENT_RESOLVER_NAME + "test", TestModule1.class.getName());
48 | parser.parse();
49 | }
50 |
51 | @Test(expected = RuntimeException.class) public void shouldFailIfFakeClassNameWasAdded() {
52 | preparator.addToManifest("fakeClassName");
53 |
54 | parser.parse();
55 | }
56 |
57 | @Test(expected = RuntimeException.class) public void shouldFailIfInvalidClassNameWasAdded() {
58 | preparator.addModuleToManifest(InvalidClass.class);
59 |
60 | parser.parse();
61 | }
62 |
63 | @Test(expected = RuntimeException.class) public void shouldFailIfPackageNotFound() {
64 | when(activity.getPackageName()).thenReturn("fakePackageName");
65 |
66 | parser.parse();
67 | }
68 |
69 | public static class InvalidClass {
70 | }
71 |
72 | public static class TestModule1 extends IntentResolver {
73 |
74 | public TestModule1(Activity activity) {
75 | super(activity);
76 | }
77 |
78 | @NonNull @Override public Iterable getRules() {
79 | return new ArrayList<>();
80 | }
81 | }
82 |
83 | public static class TestModule2 extends IntentResolver {
84 |
85 | public TestModule2(Activity activity) {
86 | super(activity);
87 | }
88 |
89 | @NonNull @Override public Iterable getRules() {
90 | return new ArrayList<>();
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Slinger - deep linking library for Android
2 | ====================
3 |
4 | [](https://travis-ci.org/allegro/slinger)
5 |
6 | Slinger is a small Android library for handling custom Uri which uses regular expression to
7 | catch and route URLs which won’t be handled by normal [intent-filter](http://developer.android.com/guide/topics/manifest/data-element.html#path) mechanism.
8 |
9 | With slinger it’s possible to provide deep links for quite complicated URLs.
10 |
11 | 
12 |
13 | ## How do I use it?
14 |
15 | Declare Activity in your manifest with your own `IntentResolver` that will handle links within particular domain.
16 |
17 | ```xml
18 |
23 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
36 | ```
37 |
38 | `IntentResolver` is a class that redirects URLs to concrete Activities based on regular expressions.
39 |
40 | ```java
41 |
42 | @Keep
43 | public class ExampleIntentResolver extends IntentResolver {
44 |
45 | private List rules;
46 |
47 | public ExampleIntentResolver(Activity activity) {
48 | super(activity);
49 | rules = asList(getRedirectRuleForAboutActivity(activity));
50 | }
51 |
52 | private RedirectRule getRedirectRuleForAboutActivity(Activity activity) {
53 | return RedirectRule.builder()
54 | .intent(new Intent(activity, MyConcreteActivityA.class))
55 | .pattern("http://example.com/abc\\\\.html\\\\?query=a.*")
56 | .build();
57 | }
58 |
59 | @NonNull @Override public Iterable getRules() {
60 | return rules;
61 | }
62 | }
63 | ```
64 |
65 | In case when no redirect rule is matched `IntentResolver` will fallback to default Intent - `Uri` with `ACTION_VIEW`.
66 |
67 | ## Customizing
68 |
69 | ### Matching Activities
70 |
71 | In order to provide other mechanism than regular expression matching you can override `resolveIntentToSling` in `IntentResolver`
72 |
73 | ### Enriching Slinged Intents with Referrer and input URL
74 |
75 | Slinger enriches Intents with URL and [referrer](http://developer.android.com/reference/android/app/Activity.html#getReferrer()) by default.
76 | This can be changed by overriding `enrichIntent` in `IntentResolver`
77 |
78 | ```java
79 | @Keep
80 | public class ExampleIntentResolver extends IntentResolver {
81 |
82 | @NonNull
83 | @Override
84 | public Intent resolveIntentToSling(@NonNull Uri originatingUri) {
85 | // implement own intent resolving strategy here
86 | return super.resolveIntentToSling(originatingUri);
87 | }
88 |
89 | @Override
90 | public Intent enrichIntent(Activity parentActivity, Intent resolvedIntent, Uri originatingUri) {
91 | // enrich resolved intent with custom data
92 | return super.enrichIntent(parentActivity, resolvedIntent, originatingUri).putExtra("foo","bar");
93 | }
94 | }
95 | ```
96 |
97 | ## Security considerations
98 |
99 | Slinger does not sanitize input in any way. So providing security for application is your responsibility.
100 |
101 | ## License
102 |
103 | **slinger** is published under [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0).
--------------------------------------------------------------------------------
/gradle/maven/mvn-publish.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2013 Chris Banes
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | apply plugin: 'maven'
18 | apply plugin: 'signing'
19 |
20 | def isReleaseBuild() {
21 | return scmVersion.version.contains("SNAPSHOT") == false
22 | }
23 |
24 | def getReleaseRepositoryUrl() {
25 | return hasProperty('RELEASE_REPOSITORY_URL') ? RELEASE_REPOSITORY_URL
26 | : "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
27 | }
28 |
29 | def getSnapshotRepositoryUrl() {
30 | return hasProperty('SNAPSHOT_REPOSITORY_URL') ? SNAPSHOT_REPOSITORY_URL
31 | : "https://oss.sonatype.org/content/repositories/snapshots/"
32 | }
33 |
34 | def getRepositoryUsername() {
35 | return hasProperty('nexusUsername') ? nexusUsername : ""
36 | }
37 |
38 | def getRepositoryPassword() {
39 | return hasProperty('nexusPassword') ? nexusPassword : ""
40 | }
41 |
42 | afterEvaluate { project ->
43 | uploadArchives {
44 | repositories {
45 | mavenDeployer {
46 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
47 |
48 | pom.groupId = GROUP
49 | pom.artifactId = POM_ARTIFACT_ID
50 | pom.version = scmVersion.version
51 |
52 | repository(url: getReleaseRepositoryUrl()) {
53 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
54 | }
55 | snapshotRepository(url: getSnapshotRepositoryUrl()) {
56 | authentication(userName: getRepositoryUsername(), password: getRepositoryPassword())
57 | }
58 |
59 | pom.project {
60 | name POM_NAME
61 | packaging POM_PACKAGING
62 | description POM_DESCRIPTION
63 | url POM_URL
64 |
65 | scm {
66 | url POM_SCM_URL
67 | connection POM_SCM_CONNECTION
68 | developerConnection POM_SCM_DEV_CONNECTION
69 | }
70 |
71 | licenses {
72 | license {
73 | name POM_LICENCE_NAME
74 | url POM_LICENCE_URL
75 | distribution POM_LICENCE_DIST
76 | }
77 | }
78 |
79 | developers {
80 | developer {
81 | id POM_DEVELOPER_ID
82 | name POM_DEVELOPER_NAME
83 | }
84 | }
85 | }
86 | }
87 | }
88 | }
89 |
90 | signing {
91 | required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
92 | sign configurations.archives
93 | }
94 |
95 | task androidJavadocs(type: Javadoc) {
96 | source = android.sourceSets.main.java.srcDirs
97 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator))
98 | }
99 |
100 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
101 | classifier = 'javadoc'
102 | from androidJavadocs.destinationDir
103 | }
104 |
105 | task androidSourcesJar(type: Jar) {
106 | classifier = 'sources'
107 | from android.sourceSets.main.java.sourceFiles
108 | }
109 |
110 | artifacts {
111 | archives androidSourcesJar
112 | archives androidJavadocsJar
113 | }
114 | }
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/resolver/IntentResolverTest.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger.resolver;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.net.Uri;
7 | import android.support.annotation.NonNull;
8 |
9 | import org.junit.Test;
10 | import org.junit.runner.RunWith;
11 | import org.robolectric.Robolectric;
12 | import org.robolectric.RobolectricGradleTestRunner;
13 |
14 | import pl.allegro.android.slinger.ReferrerMangler;
15 |
16 | import static android.content.Intent.ACTION_VIEW;
17 | import static android.net.Uri.parse;
18 | import static com.google.common.truth.Truth.assertThat;
19 | import static java.util.Arrays.asList;
20 | import static org.robolectric.RuntimeEnvironment.application;
21 | import static pl.allegro.android.slinger.ReferrerMangler.EXTRA_REFERRER_NAME;
22 | import static pl.allegro.android.slinger.ReferrerMangler.getReferrerUriFromIntent;
23 | import static pl.allegro.android.slinger.resolver.RedirectRule.builder;
24 |
25 | @RunWith(RobolectricGradleTestRunner.class)
26 | public class IntentResolverTest {
27 |
28 | public static final String PATTERN_FOR_A = "http://example.com/abc\\.html\\?query=a.*";
29 | public static final String PATTERN_FOR_B = "http://example.com/abc\\.html\\?query=b.*";
30 |
31 | public static final RedirectRule RULE_A =
32 | builder().intent(new Intent(getApplicationContext(), ActivityA.class))
33 | .pattern(PATTERN_FOR_A)
34 | .build();
35 |
36 | public static final RedirectRule RULE_B =
37 | builder().intent(new Intent(getApplicationContext(), ActivityB.class))
38 | .pattern(PATTERN_FOR_B)
39 | .build();
40 |
41 | private IntentResolver objectUnderTest = new IntentResolver(getActivity()) {
42 | @NonNull
43 | @Override
44 | public Iterable getRules() {
45 | return asList(RULE_A, RULE_B);
46 | }
47 | };
48 |
49 | private ActivityA getActivity() {
50 | return Robolectric.buildActivity(ActivityA.class).create().get();
51 | }
52 |
53 | @Test
54 | public void shouldReturnDefaultIntent() {
55 |
56 | //when
57 | Intent result = objectUnderTest.resolveIntentToSling(Uri.EMPTY);
58 |
59 | //then
60 | assertThat(result.getAction()).isEqualTo(ACTION_VIEW);
61 | }
62 |
63 | @Test
64 | public void shouldReturnIntentForA() {
65 |
66 | //when
67 | Intent result =
68 | objectUnderTest.resolveIntentToSling(parse("http://example.com/abc.html?query=abb"));
69 |
70 | //then
71 | assertThat(result.getComponent().getClassName()).isEqualTo(ActivityA.class.getName());
72 | }
73 |
74 | @Test
75 | public void shouldReturnIntentForB() {
76 |
77 | //when
78 | Intent result =
79 | objectUnderTest.resolveIntentToSling(parse("http://example.com/abc.html?query=baa"));
80 |
81 | //then
82 | assertThat(result.getComponent().getClassName()).isEqualTo(ActivityB.class.getName());
83 | }
84 |
85 | @Test
86 | public void shouldIndicateThatIntentCanBeResolved() {
87 |
88 | //when
89 | boolean result =
90 | objectUnderTest.canUriBeHandledByRedirectRules(parse("http://example.com/abc.html?query=abb"));
91 |
92 | //then
93 | assertThat(result).isTrue();
94 | }
95 |
96 | @Test
97 | public void shouldIndicateThatIntentCannotBeResolved() {
98 |
99 | //when
100 | boolean result =
101 | objectUnderTest.canUriBeHandledByRedirectRules(parse("http://google.com"));
102 |
103 | //then
104 | assertThat(result).isFalse();
105 | }
106 |
107 | @Test
108 | public void shouldEnrichIntentWithReferrerAndOriginatingUri() {
109 | //given
110 | Activity activity = new Activity();
111 | Uri referrerUri = parse("android-app://some.referrer/foo/bar");
112 | activity.setIntent(new Intent().putExtra(ReferrerMangler.EXTRA_REFERRER, referrerUri));
113 | Uri uriThatStartedActivity = parse("http://www.example.com");
114 |
115 | Intent intentToEnrich = new Intent();
116 |
117 | //when
118 | objectUnderTest.enrichIntent(activity, intentToEnrich, uriThatStartedActivity);
119 |
120 | //then
121 | assertThat(intentToEnrich.getData()).isEqualTo(uriThatStartedActivity);
122 | assertThat(getReferrerUriFromIntent(intentToEnrich)).isEqualTo(referrerUri);
123 | }
124 |
125 | @Test
126 | public void shouldEnrichIntentWithReferrerStringAndOriginatingUri() {
127 | //given
128 | Activity activity = new Activity();
129 | String referrerString = "android-app://some.referrer/foo/bar";
130 | activity.setIntent(new Intent().putExtra(EXTRA_REFERRER_NAME, referrerString));
131 | Uri uriThatStartedActivity = parse("http://www.example.com");
132 |
133 | Intent intentToEnrich = new Intent();
134 |
135 | //when
136 | objectUnderTest.enrichIntent(activity, intentToEnrich, uriThatStartedActivity);
137 |
138 | //then
139 | assertThat(intentToEnrich.getData()).isEqualTo(uriThatStartedActivity);
140 | assertThat(getReferrerUriFromIntent(intentToEnrich)).isEqualTo(parse(referrerString));
141 | }
142 |
143 | private static Context getApplicationContext() {
144 | return application.getApplicationContext();
145 | }
146 |
147 | static class ActivityA extends Activity {
148 | }
149 |
150 | static class ActivityB extends Activity {
151 | }
152 | }
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/slinger/src/main/java/pl/allegro/android/slinger/IntentStarter.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.app.Activity;
4 | import android.content.ActivityNotFoundException;
5 | import android.content.ComponentName;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.pm.ActivityInfo;
9 | import android.content.pm.PackageItemInfo;
10 | import android.content.pm.PackageManager;
11 | import android.content.pm.ResolveInfo;
12 | import android.os.Parcelable;
13 | import android.support.annotation.NonNull;
14 | import android.support.annotation.Nullable;
15 |
16 | import java.util.ArrayList;
17 | import java.util.Collections;
18 | import java.util.List;
19 |
20 | import static android.content.Intent.EXTRA_INITIAL_INTENTS;
21 | import static android.content.Intent.createChooser;
22 | import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
23 | import static android.widget.Toast.LENGTH_LONG;
24 | import static android.widget.Toast.makeText;
25 |
26 | /**
27 | * Starts Intent but without {@link Activity} that should be ignored.
28 | */
29 | public class IntentStarter {
30 | public static final String RESOLVER_ACTIVITY = "com.android.internal.app.ResolverActivity";
31 | private final List activitiesToIgnore;
32 | private final PackageManager packageManager;
33 | private final Intent intent;
34 | private final List targetIntents = new ArrayList<>();
35 | private final String resolverTitle;
36 | private final AppLinkBypasser appLinkBypasser;
37 | private boolean wasResolved;
38 |
39 | public IntentStarter(@NonNull PackageManager packageManager, @NonNull Intent intent) {
40 | this(packageManager, intent, Collections.>emptyList(), "", null);
41 | }
42 |
43 | public IntentStarter(@NonNull PackageManager packageManager, @NonNull Intent intent,
44 | @Nullable Class extends Activity> activityToIgnore, @Nullable String title,
45 | @Nullable AppLinkBypasser appLinkBypasser) {
46 | this.packageManager = packageManager;
47 | this.intent = intent;
48 |
49 | this.activitiesToIgnore = activityToIgnore != null ? getActivitiesCanonicalNames(
50 | Collections.>singletonList(activityToIgnore))
51 | : Collections.emptyList();
52 | this.resolverTitle = title;
53 | this.appLinkBypasser = createOrGetAppLinkBypasser(packageManager, appLinkBypasser);
54 | }
55 |
56 | public IntentStarter(@NonNull PackageManager packageManager, @NonNull Intent intent,
57 | @Nullable List> activitiesToIgnore, @Nullable String title,
58 | @Nullable AppLinkBypasser appLinkBypasser) {
59 | this.packageManager = packageManager;
60 | this.intent = intent;
61 | this.activitiesToIgnore = getIgnoredActivitiesList(activitiesToIgnore);
62 | this.resolverTitle = title;
63 | this.appLinkBypasser = createOrGetAppLinkBypasser(packageManager, appLinkBypasser);
64 | }
65 |
66 | @NonNull
67 | private AppLinkBypasser createOrGetAppLinkBypasser(@NonNull PackageManager packageManager,
68 | AppLinkBypasser appLinkBypasser) {
69 | return appLinkBypasser != null ? appLinkBypasser : new AppLinkBypasser(packageManager);
70 | }
71 |
72 | public IntentStarter(@NonNull PackageManager packageManager, @NonNull Intent intent,
73 | @Nullable List> activitiesToIgnore) {
74 | this(packageManager, intent, activitiesToIgnore, "", null);
75 | }
76 |
77 | private List getIgnoredActivitiesList(
78 | @Nullable List> activitiesToIgnore) {
79 | if (activitiesToIgnore != null) {
80 | return getActivitiesCanonicalNames(activitiesToIgnore);
81 | }
82 | return Collections.emptyList();
83 | }
84 |
85 | private List getActivitiesCanonicalNames(
86 | List> activitiesToIgnore) {
87 | List activityNames = new ArrayList<>();
88 | for (Class classObject : activitiesToIgnore) {
89 | activityNames.add(classObject.getCanonicalName());
90 | }
91 | return activityNames;
92 | }
93 |
94 | void resolveActivities() {
95 | if (wasResolved) {
96 | targetIntents.clear();
97 | }
98 | wasResolved = true;
99 | List queryIntentActivities = packageManager.queryIntentActivities(intent, 0);
100 |
101 | if (appLinkBypasser.isBypassApplicable(queryIntentActivities)) {
102 | queryIntentActivities.addAll(appLinkBypasser.resolveAdditionalActivitiesWithScheme(intent));
103 | }
104 |
105 | for (ResolveInfo resolveInfo : queryIntentActivities) {
106 | PackageItemInfo resolvedActivityInfo = resolveInfo.activityInfo;
107 | if (!isActivityToBeIgnored(resolvedActivityInfo)) {
108 |
109 | if (queryIntentActivities.size() == 2 || resolveInfo.isDefault) {
110 | clearExistingAndAddDefaultIntent(resolvedActivityInfo);
111 | break;
112 | }
113 |
114 | addIntentWithExplicitPackageName(resolvedActivityInfo);
115 | }
116 | }
117 | }
118 |
119 | private void addIntentWithExplicitPackageName(PackageItemInfo resolvedActivityInfo) {
120 | targetIntents.add((new Intent(intent)).setPackage(resolvedActivityInfo.packageName));
121 | }
122 |
123 | private void clearExistingAndAddDefaultIntent(PackageItemInfo resolvedActivityInfo) {
124 | targetIntents.clear();
125 | targetIntents.add(0, (new Intent(intent)).setPackage(resolvedActivityInfo.packageName));
126 | }
127 |
128 | private boolean isActivityToBeIgnored(PackageItemInfo resolvedActivityInfo) {
129 | for (String activityToIgnore : activitiesToIgnore) {
130 | if (activityToIgnore.equals(resolvedActivityInfo.name)) {
131 | return true;
132 | }
133 | }
134 | return false;
135 | }
136 |
137 | List getTargetIntents() {
138 | return Collections.unmodifiableList(targetIntents);
139 | }
140 |
141 | boolean hasDefaultHandler() {
142 | ResolveInfo resolvedActivity = packageManager.resolveActivity(intent, MATCH_DEFAULT_ONLY);
143 | if (resolvedActivity == null) {
144 | ComponentName component = intent.getComponent();
145 | if (component != null) {
146 | throw new ActivityNotFoundException("Unable to find explicit activity class "
147 | + component.toShortString()
148 | + "; have you declared this activity in your AndroidManifest.xml?");
149 | }
150 | throw new ActivityNotFoundException("No Activity found to handle " + intent);
151 | }
152 |
153 | ActivityInfo resolvedActivityInfo = resolvedActivity.activityInfo;
154 | return !RESOLVER_ACTIVITY.equals(resolvedActivityInfo.name) && !isActivityToBeIgnored(
155 | resolvedActivityInfo);
156 | }
157 |
158 | public void startActivity(Activity parentActivity) {
159 | if (parentActivity == null) {
160 | return;
161 | }
162 |
163 | if (!wasResolved) {
164 | resolveActivities();
165 | }
166 | if (hasDefaultHandler()) {
167 | runDefaultActivity(parentActivity);
168 | } else if (targetIntents.size() == 1) {
169 | runFirstAndOnlyOneActivity(parentActivity, targetIntents.get(0));
170 | } else if (!targetIntents.isEmpty()) {
171 | showChooser(parentActivity);
172 | } else {
173 | makeText(parentActivity, R.string.no_activities_to_handle_this_link, LENGTH_LONG).show();
174 | }
175 | }
176 |
177 | private void runDefaultActivity(Context context) {
178 | context.startActivity(intent);
179 | }
180 |
181 | private void runFirstAndOnlyOneActivity(Context context, Intent intent) {
182 | context.startActivity(intent);
183 | }
184 |
185 | private void showChooser(Activity activity) {
186 | List intentsList = getIntentList();
187 |
188 | Intent chooserIntent =
189 | createChooser(targetIntents.get(0), resolverTitle).putExtra(EXTRA_INITIAL_INTENTS,
190 | intentsList.toArray(new Parcelable[intentsList.size()]));
191 | activity.startActivity(chooserIntent);
192 | }
193 |
194 | private List getIntentList() {
195 | return targetIntents.subList(1, targetIntents.size());
196 | }
197 | }
--------------------------------------------------------------------------------
/slinger/src/test/java/pl/allegro/android/slinger/IntentStarterTest.java:
--------------------------------------------------------------------------------
1 | package pl.allegro.android.slinger;
2 |
3 | import android.app.Activity;
4 | import android.content.ActivityNotFoundException;
5 | import android.content.ComponentName;
6 | import android.content.Intent;
7 | import android.content.pm.ActivityInfo;
8 | import android.content.pm.PackageManager;
9 | import android.content.pm.ResolveInfo;
10 | import android.support.annotation.NonNull;
11 |
12 | import com.google.common.collect.ImmutableList;
13 |
14 | import org.junit.Test;
15 | import org.junit.runner.RunWith;
16 | import org.robolectric.RobolectricGradleTestRunner;
17 | import org.robolectric.RuntimeEnvironment;
18 | import org.robolectric.res.builder.RobolectricPackageManager;
19 |
20 | import java.util.ArrayList;
21 | import java.util.List;
22 |
23 | import pl.allegro.android.slinger.util.IntentMatchers.IsIntentWithAction;
24 | import pl.allegro.android.slinger.util.IntentMatchers.IsIntentWithPackageName;
25 |
26 | import static com.google.common.truth.Truth.assertThat;
27 | import static org.mockito.Matchers.anyInt;
28 | import static org.mockito.Matchers.argThat;
29 | import static org.mockito.Matchers.eq;
30 | import static org.mockito.Mockito.mock;
31 | import static org.mockito.Mockito.spy;
32 | import static org.mockito.Mockito.verify;
33 | import static org.mockito.Mockito.when;
34 | import static pl.allegro.android.slinger.IntentStarterTest.Utils.preparePackageManager;
35 |
36 | @RunWith(RobolectricGradleTestRunner.class) public class IntentStarterTest {
37 |
38 | @Test public void activityIsIgnoredGivingEmptyListOfTargetIntents() {
39 | // given
40 | Intent intent = new Intent();
41 | PackageManager packageManager =
42 | preparePackageManager(intent, ImmutableList.>of(Activity1.class));
43 | IntentStarter objectUnderTest =
44 | new IntentStarter(packageManager, intent, Activity1.class, "", null);
45 |
46 | // when
47 | objectUnderTest.resolveActivities();
48 |
49 | // then
50 | assertThat(objectUnderTest.getTargetIntents()).isEmpty();
51 | }
52 |
53 | @Test public void manyActivitiesAreIgnoredGivingEmptyListOfTargetIntents() {
54 | // given
55 | Intent intent = new Intent();
56 | ImmutableList> activitiesToResolveAndIgnore =
57 | ImmutableList.of(Activity1.class, Activity2.class);
58 | PackageManager packageManager = preparePackageManager(intent, activitiesToResolveAndIgnore);
59 | IntentStarter objectUnderTest =
60 | new IntentStarter(packageManager, intent, activitiesToResolveAndIgnore, "", null);
61 |
62 | // when
63 | objectUnderTest.resolveActivities();
64 |
65 | // then
66 | assertThat(objectUnderTest.getTargetIntents()).isEmpty();
67 | }
68 |
69 | @Test public void shouldResolveOneActivity() {
70 | // given
71 | Intent intent = new Intent();
72 | PackageManager packageManager =
73 | preparePackageManager(intent, ImmutableList.>of(Activity1.class));
74 | IntentStarter objectUnderTest = new IntentStarter(packageManager, intent);
75 |
76 | // when
77 | objectUnderTest.resolveActivities();
78 |
79 | // then
80 | List targetIntents = objectUnderTest.getTargetIntents();
81 | assertThat(targetIntents).hasSize(1);
82 | assertThat(targetIntents.get(0).getPackage()).isEqualTo(Activity1.class.getPackage().getName());
83 | }
84 |
85 | @SuppressWarnings("WrongConstant") @Test
86 | public void whenThereIsDefaultHandlerThenActivityWillBeStartedWithOriginalIntent() {
87 | // given
88 | Intent intent = new Intent();
89 |
90 | PackageManager packageManager = mock(PackageManager.class);
91 | when(packageManager.resolveActivity(eq(intent), anyInt())).thenReturn(
92 | Utils.createResolveInfo(Activity3.class, true));
93 |
94 | IntentStarter objectUnderTest =
95 | new IntentStarter(packageManager, intent, Activity1.class, "", null);
96 | Activity parentActivity = getParentActivitySpy();
97 |
98 | // when
99 | objectUnderTest.startActivity(parentActivity);
100 |
101 | // then
102 | verify(parentActivity).startActivity(intent);
103 | }
104 |
105 | @NonNull private Activity getParentActivitySpy() {
106 | return spy(Activity1.class);
107 | }
108 |
109 | @SuppressWarnings("WrongConstant") @Test
110 | public void whenIgnoredActivityIsDefaultHandlerAndThereIsOnlyOneMoreActivityAbleToHandleTheIntentThenTheOtherOneIsStarted() {
111 | // given
112 | Intent intent = new Intent();
113 |
114 | PackageManager packageManager = mock(PackageManager.class);
115 | when(packageManager.queryIntentActivities(eq(intent), anyInt())).thenReturn(
116 | Utils.makeResolveInfoList(Activity1.class, Activity2.class));
117 | when(packageManager.resolveActivity(eq(intent), anyInt())).thenReturn(
118 | Utils.createResolveInfo(Activity1.class, true));
119 |
120 | IntentStarter objectUnderTest =
121 | new IntentStarter(packageManager, intent, Activity1.class, "", null);
122 | Activity parentActivity = getParentActivitySpy();
123 |
124 | // when
125 | objectUnderTest.startActivity(parentActivity);
126 |
127 | // then
128 | verify(parentActivity).startActivity(
129 | argThat(new IsIntentWithPackageName("pl.allegro.android.slinger")));
130 | }
131 |
132 | @SuppressWarnings("WrongConstant") @Test
133 | public void whenIgnoredActivityIsDefaultHandlerAndThereAreMoreActivitiesAbleToHandleTheIntentThenChooserIsPresented() {
134 | // given
135 | Intent intent = new Intent();
136 |
137 | PackageManager packageManager = mock(PackageManager.class);
138 | when(packageManager.queryIntentActivities(eq(intent), anyInt())).thenReturn(
139 | Utils.makeResolveInfoList(Activity1.class, Activity2.class, Activity3.class));
140 | when(packageManager.resolveActivity(eq(intent), anyInt())).thenReturn(
141 | Utils.createResolveInfo(Activity1.class, true));
142 |
143 | IntentStarter objectUnderTest =
144 | new IntentStarter(packageManager, intent, Activity1.class, "", null);
145 | Activity parentActivity = getParentActivitySpy();
146 |
147 | // when
148 | objectUnderTest.startActivity(parentActivity);
149 |
150 | // then
151 | verify(parentActivity).startActivity(
152 | argThat(new IsIntentWithAction("android.intent.action.CHOOSER")));
153 | }
154 |
155 | @Test public void whenThereIsOnlyOneAdditionalActivityAbleToHandleIntentThenItWillBeStarted() {
156 | // given
157 | Intent intent = new Intent();
158 | PackageManager packageManager =
159 | preparePackageManager(intent, ImmutableList.of(Activity1.class, Activity2.class));
160 | IntentStarter objectUnderTest =
161 | new IntentStarter(packageManager, intent, Activity1.class, "", null);
162 | Activity parentActivity = getParentActivitySpy();
163 |
164 | // when
165 | objectUnderTest.startActivity(parentActivity);
166 |
167 | // then
168 | verify(parentActivity).startActivity(
169 | argThat(new IsIntentWithPackageName("pl.allegro.android.slinger")));
170 | }
171 |
172 | @Test(expected = ActivityNotFoundException.class)
173 | public void whenThereIsNoActivityAbleToHandleIntent() {
174 | // given
175 |
176 | Intent intentToBeResolved = new Intent().setComponent(
177 | new ComponentName(RuntimeEnvironment.application.getPackageName(),
178 | Activity1.class.getName()));
179 |
180 | Intent intentToStart = new Intent().setComponent(
181 | new ComponentName(RuntimeEnvironment.application.getPackageName(),
182 | Activity2.class.getName()));
183 |
184 | PackageManager packageManager = preparePackageManager(intentToBeResolved,
185 | ImmutableList.>of(Activity1.class));
186 | IntentStarter objectUnderTest =
187 | new IntentStarter(packageManager, intentToStart, Activity1.class, "", null);
188 | Activity parentActivity = getParentActivitySpy();
189 |
190 | // when
191 | objectUnderTest.startActivity(parentActivity);
192 | }
193 |
194 | @Test(expected = ActivityNotFoundException.class)
195 | public void whenThereIsNoActivityAbleToHandleIntentAndNoComponentInIntent() {
196 | // given
197 |
198 | Intent intentToBeResolved = new Intent().setComponent(
199 | new ComponentName(RuntimeEnvironment.application.getPackageName(),
200 | Activity1.class.getName()));
201 |
202 | Intent intentToStart = new Intent().setPackage(RuntimeEnvironment.application.getPackageName());
203 |
204 | PackageManager packageManager = preparePackageManager(intentToBeResolved,
205 | ImmutableList.>of(Activity1.class));
206 | IntentStarter objectUnderTest =
207 | new IntentStarter(packageManager, intentToStart, Activity1.class, "", null);
208 | Activity parentActivity = getParentActivitySpy();
209 |
210 | // when
211 | objectUnderTest.startActivity(parentActivity);
212 | }
213 |
214 | @Test public void whenThereAreMultipleActivitiesAbleToHandleIntentThenChooserWillBePresented() {
215 | // given
216 | Intent intent = new Intent();
217 | PackageManager packageManager = preparePackageManager(intent,
218 | ImmutableList.of(Activity1.class, Activity2.class, Activity3.class));
219 | IntentStarter objectUnderTest =
220 | new IntentStarter(packageManager, intent, Activity1.class, "", null);
221 | Activity parentActivity = getParentActivitySpy();
222 |
223 | // when
224 | objectUnderTest.startActivity(parentActivity);
225 |
226 | // then
227 | verify(parentActivity).startActivity(
228 | argThat(new IsIntentWithAction("android.intent.action.CHOOSER")));
229 | }
230 |
231 | static class Activity1 extends Activity {
232 |
233 | }
234 |
235 | static class Activity2 extends Activity {
236 |
237 | }
238 |
239 | static class Activity3 extends Activity {
240 |
241 | }
242 |
243 | static class Utils {
244 |
245 | private static ActivityInfo createActivityInfo(Class extends Activity> clazz) {
246 | ActivityInfo activityInfo = new ActivityInfo();
247 | activityInfo.name = clazz.getCanonicalName();
248 | activityInfo.packageName = clazz.getPackage().getName();
249 | return activityInfo;
250 | }
251 |
252 | private static ResolveInfo createResolveInfo(Class extends Activity> clazz) {
253 | return createResolveInfo(clazz, false);
254 | }
255 |
256 | static ResolveInfo createResolveInfo(Class extends Activity> clazz, boolean isDefault) {
257 | ResolveInfo resolveInfo = new ResolveInfo();
258 | resolveInfo.activityInfo = createActivityInfo(clazz);
259 | resolveInfo.isDefault = isDefault;
260 | return resolveInfo;
261 | }
262 |
263 | static PackageManager preparePackageManager(Intent intent,
264 | List> classes) {
265 | RobolectricPackageManager packageManager =
266 | (RobolectricPackageManager) RuntimeEnvironment.application.getPackageManager();
267 | for (Class extends Activity> clazz : classes) {
268 | packageManager.addResolveInfoForIntent(intent, createResolveInfo(clazz));
269 | }
270 | return (PackageManager) packageManager;
271 | }
272 |
273 | @SafeVarargs
274 | static List makeResolveInfoList(Class extends Activity>... classes) {
275 | List list = new ArrayList<>(classes.length);
276 | for (Class extends Activity> clazz : classes) {
277 | list.add(createResolveInfo(clazz));
278 | }
279 | return list;
280 | }
281 | }
282 | }
--------------------------------------------------------------------------------