├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── justeat │ │ └── app │ │ └── deeplinks │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── justeat │ │ │ └── app │ │ │ └── deeplinks │ │ │ ├── activities │ │ │ ├── AActivity.java │ │ │ ├── BActivity.java │ │ │ ├── CActivity.java │ │ │ └── LinkDispatcherActivity.java │ │ │ ├── intents │ │ │ └── IntentHelper.java │ │ │ └── links │ │ │ └── UriToIntentMapper.java │ └── res │ │ ├── layout │ │ ├── activity_a.xml │ │ ├── activity_b.xml │ │ └── activity_c.xml │ │ ├── menu │ │ └── menu_a.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── values-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── justeat │ └── app │ └── deeplinks │ ├── config │ └── RobolectricRunner.java │ └── links │ ├── DeepLinkResolutionTest.java │ ├── DeepLinks.java │ └── IntentFilterTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | **/.apt_generated/ 3 | **/bin/ 4 | **/gen/ 5 | **/build/ 6 | **/src-gen/ 7 | **/java-gen/ 8 | **/.idea/ 9 | **/.gradle/ 10 | **/local.properties 11 | **/*.iml 12 | **/*.iws 13 | **/*.ipr 14 | **/proguard.map 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 JUST EAT plc 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android.Samples.Deeplinks 2 | Sample app presenting a code structure for effective deep linking in Android. 3 | 4 | The project also showcases unit tests for the deep linking functionality. 5 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | repositories { 4 | maven { 5 | url "https://oss.sonatype.org/content/repositories/snapshots" 6 | } 7 | mavenCentral() 8 | } 9 | 10 | android { 11 | compileSdkVersion 22 12 | buildToolsVersion "22.0.1" 13 | 14 | defaultConfig { 15 | applicationId "com.justeat.app.deeplinks" 16 | minSdkVersion 10 17 | targetSdkVersion 22 18 | versionCode 1 19 | versionName "1.0" 20 | } 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | } 28 | 29 | dependencies { 30 | compile 'com.jakewharton:butterknife:6.1.0' 31 | 32 | // Unit Tests 33 | testCompile 'junit:junit:4.12' 34 | 35 | testCompile 'org.hamcrest:hamcrest-core:1.1' 36 | testCompile ('org.hamcrest:hamcrest-integration:1.1') { 37 | exclude module: 'hamcrest-core' 38 | } 39 | testCompile ('org.hamcrest:hamcrest-library:1.1') { 40 | exclude module: 'hamcrest-core' 41 | } 42 | testCompile 'org.mockito:mockito-core:1.9.5' 43 | 44 | testCompile('org.robolectric:robolectric:3.0-SNAPSHOT') { 45 | exclude group: 'commons-logging', module: 'commons-logging' 46 | exclude group: 'org.apache.httpcomponents', module: 'httpclient' 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/23.0.2/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/justeat/app/deeplinks/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.justeat.app.deeplinks; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 26 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/justeat/app/deeplinks/activities/AActivity.java: -------------------------------------------------------------------------------- 1 | package com.justeat.app.deeplinks.activities; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.Menu; 6 | import android.view.MenuItem; 7 | import android.widget.EditText; 8 | 9 | import com.justeat.app.deeplinks.R; 10 | import com.justeat.app.deeplinks.intents.IntentHelper; 11 | 12 | import butterknife.ButterKnife; 13 | import butterknife.InjectView; 14 | import butterknife.OnClick; 15 | 16 | public class AActivity extends Activity { 17 | 18 | @InjectView(R.id.query) EditText mQueryEditText; 19 | private IntentHelper mIntents = new IntentHelper(); 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_a); 25 | ButterKnife.inject(this); 26 | } 27 | 28 | @OnClick(R.id.button_go) 29 | public void onClick() { 30 | startActivity(mIntents.newBActivityIntent(this, mQueryEditText.getText().toString())); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/justeat/app/deeplinks/activities/BActivity.java: -------------------------------------------------------------------------------- 1 | package com.justeat.app.deeplinks.activities; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.view.View; 8 | import android.widget.TextView; 9 | 10 | import com.justeat.app.deeplinks.R; 11 | import com.justeat.app.deeplinks.intents.IntentHelper; 12 | 13 | import butterknife.ButterKnife; 14 | import butterknife.InjectView; 15 | import butterknife.OnClick; 16 | 17 | public class BActivity extends Activity { 18 | 19 | @InjectView(R.id.query_label) TextView mQueryText; 20 | private IntentHelper mIntents = new IntentHelper(); 21 | 22 | private String mQuery; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | super.onCreate(savedInstanceState); 27 | setContentView(R.layout.activity_b); 28 | 29 | ButterKnife.inject(this); 30 | 31 | // Parse intent arguments 32 | parseIntent(); 33 | 34 | // Do something with the arguments received 35 | mQueryText.setText(mQuery); 36 | } 37 | 38 | private void parseIntent() { 39 | final Intent intent = getIntent(); 40 | final Uri uri = intent.getData(); 41 | if (uri != null) { 42 | if ("example-scheme".equals(uri.getScheme()) && "b".equals(uri.getHost())) { 43 | // Cool, we have a URI addressed to this activity! 44 | mQuery = uri.getQueryParameter("query"); 45 | } 46 | } 47 | 48 | if (mQuery == null) { 49 | mQuery = intent.getStringExtra(IntentHelper.EXTRA_B_QUERY); 50 | } 51 | 52 | } 53 | 54 | @OnClick({R.id.button_choice1, R.id.button_choice2}) 55 | public void onClick(View view) { 56 | int choice; 57 | if (view.getId() == R.id.button_choice1) { 58 | choice = 1; 59 | } else { 60 | choice = 2; 61 | } 62 | 63 | startActivity(mIntents.newCActivityIntent(this, mQuery, choice)); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/com/justeat/app/deeplinks/activities/CActivity.java: -------------------------------------------------------------------------------- 1 | package com.justeat.app.deeplinks.activities; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.view.Menu; 7 | import android.view.MenuItem; 8 | import android.widget.TextView; 9 | 10 | import com.justeat.app.deeplinks.R; 11 | import com.justeat.app.deeplinks.intents.IntentHelper; 12 | 13 | import butterknife.ButterKnife; 14 | import butterknife.InjectView; 15 | 16 | public class CActivity extends Activity { 17 | 18 | @InjectView(R.id.query_label) TextView mQueryTextView; 19 | @InjectView(R.id.choice_label) TextView mChoiceTextView; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_c); 25 | ButterKnife.inject(this); 26 | 27 | parseIntent(); 28 | } 29 | 30 | private void parseIntent() { 31 | Intent intent = getIntent(); 32 | mQueryTextView.setText(intent.getStringExtra(IntentHelper.EXTRA_C_QUERY)); 33 | mChoiceTextView.setText(Integer.toString(intent.getIntExtra(IntentHelper.EXTRA_C_CHOICE, 0))); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/justeat/app/deeplinks/activities/LinkDispatcherActivity.java: -------------------------------------------------------------------------------- 1 | package com.justeat.app.deeplinks.activities; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | 7 | import com.justeat.app.deeplinks.BuildConfig; 8 | import com.justeat.app.deeplinks.intents.IntentHelper; 9 | import com.justeat.app.deeplinks.links.UriToIntentMapper; 10 | 11 | public class LinkDispatcherActivity extends Activity { 12 | private final UriToIntentMapper mMapper = new UriToIntentMapper(this, new IntentHelper()); 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | 18 | try { 19 | mMapper.dispatchIntent(getIntent()); 20 | 21 | } catch (IllegalArgumentException iae) { 22 | // Malformed URL 23 | if (BuildConfig.DEBUG) { 24 | Log.e("Deep links", "Invalid URI", iae); 25 | } 26 | } finally { 27 | // Always finish the Activity so that it doesn't stay in our history 28 | finish(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/justeat/app/deeplinks/intents/IntentHelper.java: -------------------------------------------------------------------------------- 1 | package com.justeat.app.deeplinks.intents; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | 6 | import com.justeat.app.deeplinks.activities.AActivity; 7 | import com.justeat.app.deeplinks.activities.BActivity; 8 | import com.justeat.app.deeplinks.activities.CActivity; 9 | 10 | public class IntentHelper { 11 | 12 | public static String EXTRA_B_QUERY = "com.justeat.app.deeplinks.intents.Intents.EXTRA_B_QUERY"; 13 | public static String EXTRA_C_QUERY = "com.justeat.app.deeplinks.intents.Intents.EXTRA_C_QUERY"; 14 | public static String EXTRA_C_CHOICE = "com.justeat.app.deeplinks.intents.Intents.EXTRA_C_CHOICE"; 15 | 16 | public Intent newAActivityIntent(Context context) { 17 | Intent i = new Intent(context, AActivity.class); 18 | 19 | i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 20 | 21 | return i; 22 | } 23 | 24 | public Intent newBActivityIntent(Context context, String query) { 25 | Intent i = new Intent(context, BActivity.class); 26 | 27 | i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 28 | 29 | i.putExtra(EXTRA_B_QUERY, query); 30 | return i; 31 | } 32 | 33 | public Intent newCActivityIntent(Context context, String query, int choice) { 34 | Intent i = new Intent(context, CActivity.class); 35 | 36 | i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 37 | 38 | i.putExtra(EXTRA_C_QUERY, query); 39 | i.putExtra(EXTRA_C_CHOICE, choice); 40 | return i; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/justeat/app/deeplinks/links/UriToIntentMapper.java: -------------------------------------------------------------------------------- 1 | package com.justeat.app.deeplinks.links; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | 7 | import com.justeat.app.deeplinks.intents.IntentHelper; 8 | 9 | public class UriToIntentMapper { 10 | private Context mContext; 11 | private IntentHelper mIntents; 12 | 13 | public UriToIntentMapper(Context context, IntentHelper intentHelper) { 14 | mContext = context; 15 | mIntents = intentHelper; 16 | } 17 | 18 | public void dispatchIntent(Intent intent) { 19 | final Uri uri = intent.getData(); 20 | Intent dispatchIntent = null; 21 | 22 | if (uri == null) throw new IllegalArgumentException("Uri cannot be null"); 23 | 24 | final String scheme = uri.getScheme().toLowerCase(); 25 | final String host = uri.getHost().toLowerCase(); 26 | 27 | if ("example-scheme".equals(scheme)) { 28 | dispatchIntent = mapAppLink(uri); 29 | } else if (("http".equals(scheme) || "https".equals(scheme)) && 30 | ("www.example.co.uk".equals(host) || "example.co.uk".equals(host))) { 31 | dispatchIntent = mapWebLink(uri); 32 | } 33 | 34 | if (dispatchIntent != null) { 35 | mContext.startActivity(dispatchIntent); 36 | } 37 | } 38 | 39 | private Intent mapAppLink(Uri uri) { 40 | final String host = uri.getHost().toLowerCase(); 41 | 42 | switch (host) { 43 | case "activitya": 44 | return mIntents.newAActivityIntent(mContext); 45 | case "activityb": 46 | String bQuery = uri.getQueryParameter("query"); 47 | return mIntents.newBActivityIntent(mContext, bQuery); 48 | case "activityc": 49 | String cQuery = uri.getQueryParameter("query"); 50 | int choice = Integer.parseInt(uri.getQueryParameter("choice")); 51 | return mIntents.newCActivityIntent(mContext, cQuery, choice); 52 | } 53 | return null; 54 | } 55 | 56 | private Intent mapWebLink(Uri uri) { 57 | final String path = uri.getPath(); 58 | 59 | switch (path) { 60 | case "/a": 61 | return mIntents.newAActivityIntent(mContext); 62 | case "/c": 63 | String cQuery = uri.getQueryParameter("query"); 64 | int choice = Integer.parseInt(uri.getQueryParameter("choice")); 65 | return mIntents.newCActivityIntent(mContext, cQuery, choice); 66 | } 67 | return null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_a.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 16 | 17 |