├── .gitignore ├── .idea └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── .travis.yml ├── DeepLinkHelperSample.gif ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── adammcneilly │ │ └── deeplinkhelper │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── adammcneilly │ │ │ └── deeplinkhelper │ │ │ ├── DeepLink.kt │ │ │ ├── DeepLinkAdapter.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityViewModel.kt │ │ │ └── data │ │ │ ├── DLDatabase.kt │ │ │ ├── DLRepository.kt │ │ │ └── DeepLinkDAO.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── list_item_uri.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── adammcneilly │ └── deeplinkhelper │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/*.xml 5 | /.idea/caches 6 | /.idea/libraries 7 | /.idea/modules.xml 8 | /.idea/workspace.xml 9 | /.idea/navEditor.xml 10 | /.idea/assetWizardSettings.xml 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | 15 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | xmlns:android 24 | 25 | ^$ 26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 | xmlns:.* 35 | 36 | ^$ 37 | 38 | 39 | BY_NAME 40 | 41 |
42 |
43 | 44 | 45 | 46 | .*:id 47 | 48 | http://schemas.android.com/apk/res/android 49 | 50 | 51 | 52 |
53 |
54 | 55 | 56 | 57 | .*:name 58 | 59 | http://schemas.android.com/apk/res/android 60 | 61 | 62 | 63 |
64 |
65 | 66 | 67 | 68 | name 69 | 70 | ^$ 71 | 72 | 73 | 74 |
75 |
76 | 77 | 78 | 79 | style 80 | 81 | ^$ 82 | 83 | 84 | 85 |
86 |
87 | 88 | 89 | 90 | .* 91 | 92 | ^$ 93 | 94 | 95 | BY_NAME 96 | 97 |
98 |
99 | 100 | 101 | 102 | .* 103 | 104 | http://schemas.android.com/apk/res/android 105 | 106 | 107 | ANDROID_ATTRIBUTE_ORDER 108 | 109 |
110 |
111 | 112 | 113 | 114 | .* 115 | 116 | .* 117 | 118 | 119 | BY_NAME 120 | 121 |
122 |
123 |
124 |
125 | 126 | 137 |
138 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | android: 4 | components: 5 | - tools 6 | - build-tools-28.0.3 7 | - android-28 8 | licenses: 9 | - android-sdk-license-.+ 10 | 11 | script: 12 | - chmod +x gradlew 13 | - ./gradlew clean build test ktlintCheck 14 | -------------------------------------------------------------------------------- /DeepLinkHelperSample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AdamMc331/DeepLinkHelper/843230221cdf266d8e0126ef9b1d0d6e9904f28a/DeepLinkHelperSample.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### DeepLink Helper 2 | 3 | This app intends to help Android developers who want to test how the apps they develop handle deep links. While we have some ways to do this, like Android Studio run configurations or push notifications, I found some limitations to those. Run configurations in Android Studio don't allow me to test how an app that's already open handles a deep link, and sending push notifications can be a hassle. 4 | 5 | This helper app comes with persistent storage, showing your latest URIs at the top, and clicking on a suggestion fills in the text box for you to easily update the URI if necessary. 6 | 7 | I hope you find this helpful! If you have any requests, please submit an issue. :) 8 | 9 | ### Sample 10 | 11 | I purposely put in some URIs that wouldn't work, both to show the error but also just to demonstrate how the functionality works. 12 | 13 | ![](DeepLinkHelperSample.gif) 14 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | apply plugin: 'kotlin-android' 4 | 5 | apply plugin: 'kotlin-android-extensions' 6 | 7 | apply plugin: 'kotlin-kapt' 8 | 9 | apply plugin: 'org.jlleitschuh.gradle.ktlint' 10 | 11 | android { 12 | compileSdkVersion 31 13 | 14 | defaultConfig { 15 | applicationId "com.adammcneilly.deeplinkhelper" 16 | minSdkVersion 16 17 | targetSdkVersion 31 18 | versionCode 1 19 | versionName "1.0" 20 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | } 29 | 30 | ktlint { 31 | enableExperimentalRules = true 32 | } 33 | 34 | dependencies { 35 | implementation fileTree(dir: 'libs', include: ['*.jar']) 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 37 | implementation 'androidx.appcompat:appcompat:1.4.0' 38 | implementation 'androidx.core:core-ktx:1.7.0' 39 | implementation 'androidx.constraintlayout:constraintlayout:2.1.2' 40 | implementation 'com.google.android.material:material:1.4.0' 41 | implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 42 | implementation 'androidx.room:room-runtime:2.4.0' 43 | annotationProcessor 'androidx.room:room-compiler:2.4.0' 44 | kapt 'androidx.room:room-compiler:2.4.0' 45 | implementation 'org.greenrobot:eventbus:3.1.1' 46 | testImplementation 'junit:junit:4.13.2' 47 | androidTestImplementation 'androidx.test:runner:1.4.1-alpha03' 48 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0-alpha03' 49 | } 50 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/adammcneilly/deeplinkhelper/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.adammcneilly.deeplinkhelper 2 | 3 | import androidx.test.InstrumentationRegistry 4 | import androidx.test.runner.AndroidJUnit4 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getTargetContext() 20 | assertEquals("com.adammcneilly.deeplinkhelper", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/adammcneilly/deeplinkhelper/DeepLink.kt: -------------------------------------------------------------------------------- 1 | package com.adammcneilly.deeplinkhelper 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | /** 7 | * A model class representing a deep link the user can click on. 8 | */ 9 | @Entity 10 | data class DeepLink( 11 | @PrimaryKey val uri: String = "", 12 | val lastTimeSent: Long = System.currentTimeMillis() 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/adammcneilly/deeplinkhelper/DeepLinkAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.adammcneilly.deeplinkhelper 2 | 3 | import android.view.LayoutInflater 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import android.widget.TextView 7 | import androidx.recyclerview.widget.RecyclerView 8 | 9 | class DeepLinkAdapter(private val deepLinkClicked: (DeepLink) -> Unit) : RecyclerView.Adapter() { 10 | var deepLinks: List = emptyList() 11 | set(value) { 12 | field = value 13 | notifyDataSetChanged() 14 | } 15 | 16 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeepLinkViewHolder { 17 | val context = parent.context 18 | val inflater = LayoutInflater.from(context) 19 | val view = inflater.inflate(R.layout.list_item_uri, parent, false) 20 | return DeepLinkViewHolder(view) 21 | } 22 | 23 | override fun getItemCount(): Int { 24 | return deepLinks.size 25 | } 26 | 27 | override fun onBindViewHolder(holder: DeepLinkViewHolder, position: Int) { 28 | holder.bindDeepLink(deepLinks[position], deepLinkClicked) 29 | } 30 | 31 | class DeepLinkViewHolder(view: View) : RecyclerView.ViewHolder(view) { 32 | private val uriTextView = view.findViewById(R.id.uri) 33 | 34 | fun bindDeepLink(deepLink: DeepLink, deepLinkClicked: (DeepLink) -> Unit) { 35 | uriTextView.text = deepLink.uri 36 | itemView.setOnClickListener { 37 | deepLinkClicked.invoke(deepLink) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/adammcneilly/deeplinkhelper/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.adammcneilly.deeplinkhelper 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import android.os.Bundle 8 | import android.view.inputmethod.InputMethodManager 9 | import android.widget.Button 10 | import android.widget.Toast 11 | import androidx.appcompat.app.AppCompatActivity 12 | import androidx.lifecycle.Observer 13 | import androidx.lifecycle.ViewModel 14 | import androidx.lifecycle.ViewModelProvider 15 | import androidx.lifecycle.ViewModelProviders 16 | import androidx.recyclerview.widget.DividerItemDecoration 17 | import androidx.recyclerview.widget.LinearLayoutManager 18 | import androidx.recyclerview.widget.RecyclerView 19 | import com.adammcneilly.deeplinkhelper.data.DLDatabase 20 | import com.adammcneilly.deeplinkhelper.data.DLRepository 21 | import com.google.android.material.textfield.TextInputEditText 22 | import org.greenrobot.eventbus.EventBus 23 | import org.greenrobot.eventbus.Subscribe 24 | 25 | class MainActivity : AppCompatActivity() { 26 | private val adapter = DeepLinkAdapter(this::deepLinkClicked) 27 | private lateinit var viewModel: MainActivityViewModel 28 | 29 | private var uriInput: TextInputEditText? = null 30 | 31 | private val viewModelFactory = object : ViewModelProvider.Factory { 32 | override fun create(p0: Class): T { 33 | val database = DLDatabase.getInMemoryDatabase(this@MainActivity) 34 | val repository = DLRepository(database) 35 | 36 | @Suppress("UNCHECKED_CAST") 37 | return MainActivityViewModel(repository) as T 38 | } 39 | } 40 | 41 | private fun deepLinkClicked(deepLink: DeepLink) { 42 | viewModel.deepLinkClicked(deepLink) 43 | } 44 | 45 | override fun onCreate(savedInstanceState: Bundle?) { 46 | super.onCreate(savedInstanceState) 47 | setContentView(R.layout.activity_main) 48 | 49 | setupViews() 50 | setupViewModel() 51 | setupRecyclerView() 52 | setupSendButton() 53 | } 54 | 55 | override fun onResume() { 56 | super.onResume() 57 | EventBus.getDefault().register(this) 58 | } 59 | 60 | override fun onPause() { 61 | super.onPause() 62 | EventBus.getDefault().unregister(this) 63 | } 64 | 65 | private fun setupViews() { 66 | uriInput = findViewById(R.id.uri_input) 67 | } 68 | 69 | private fun setupRecyclerView() { 70 | val recyclerView = findViewById(R.id.deep_link_list) 71 | recyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL)) 72 | recyclerView.layoutManager = LinearLayoutManager(this) 73 | recyclerView.adapter = adapter 74 | } 75 | 76 | private fun setupViewModel() { 77 | viewModel = 78 | ViewModelProviders.of(this, viewModelFactory).get(MainActivityViewModel::class.java) 79 | 80 | viewModel.deepLinks.observe( 81 | this, 82 | Observer { 83 | it?.let(adapter::deepLinks::set) 84 | } 85 | ) 86 | 87 | viewModel.inputText.observe( 88 | this, 89 | Observer { 90 | uriInput?.setText(it) 91 | } 92 | ) 93 | 94 | viewModel.inputErrorRes.observe( 95 | this, 96 | Observer { 97 | val errorString = it?.let(this::getString) 98 | uriInput?.error = errorString 99 | } 100 | ) 101 | } 102 | 103 | private fun setupSendButton() { 104 | findViewById