├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── twoboysandhats │ │ └── androidnfcemulator │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── org │ │ │ └── twoboysandhats │ │ │ └── androidnfcemulator │ │ │ ├── MainActivity.kt │ │ │ └── NFCServerService.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.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 │ └── org │ └── twoboysandhats │ └── androidnfcemulator │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | xmlns:android 17 | 18 | ^$ 19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 | xmlns:.* 28 | 29 | ^$ 30 | 31 | 32 | BY_NAME 33 | 34 |
35 |
36 | 37 | 38 | 39 | .*:id 40 | 41 | http://schemas.android.com/apk/res/android 42 | 43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | .*:name 51 | 52 | http://schemas.android.com/apk/res/android 53 | 54 | 55 | 56 |
57 |
58 | 59 | 60 | 61 | name 62 | 63 | ^$ 64 | 65 | 66 | 67 |
68 |
69 | 70 | 71 | 72 | style 73 | 74 | ^$ 75 | 76 | 77 | 78 |
79 |
80 | 81 | 82 | 83 | .* 84 | 85 | ^$ 86 | 87 | 88 | BY_NAME 89 | 90 |
91 |
92 | 93 | 94 | 95 | .* 96 | 97 | http://schemas.android.com/apk/res/android 98 | 99 | 100 | ANDROID_ATTRIBUTE_ORDER 101 | 102 |
103 |
104 | 105 | 106 | 107 | .* 108 | 109 | .* 110 | 111 | 112 | BY_NAME 113 | 114 |
115 |
116 |
117 |
118 | 119 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 2BoysAndHats 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidNFCEmulator 2 | 3 | ![AndroidNFCEmulator screenshot](img.png) 4 | 5 | Testing NFC-based Android apps can sometimes be a little frustrating, especially if you don't have access to a physical device, tags, or even if you just want to use the emulator to test out a couple different device configurations. 6 | 7 | In an attempt to remedy this, AndroidNFCEmulator lets you fake the intent produced by Android when a tag is scanned via a simple HTTP request. Currently, it can only simulate NDEF-formatted tags with a single URI record, but expanding it to encompass multiple records and different formats shouldn't be difficult. 8 | 9 | ## Setup 10 | Download or clone the source, open it in Android Studio, build it, and install it on your target device. Open the app, and start the service. It's at this point that you'll need to determine how to talk to your device. 11 | 12 | - On a real device, the IP address show on-screen will (probably) accurately reflect your device's IP address, and can be used as such. 13 | - On the Android emulator, due to the configuration of the virtual router within the emulator's networking setup, you won't be able to directly talk to it without an extra step. Using ADB, you can port-forward port 8080 of the emulator (the default port used) to port 8080 of your own computer with `adb forward tcp:8080 tcp:8080`, and use `localhost` or `127.0.0.1` as the IP. 14 | 15 | ## Use 16 | Once you have the service running, simply make a HTTP GET request of the format `http:///?uri=` (e.g. `http://192.168.1.27/?uri=https://google.com`). The service will then dispatch an `ACTION_NDEF_DISCOVERED` intent with the data parameter set to the provided URI (and with `EXTRA_NDEF_MESSAGES` set accordingly), hopefully to your app! 17 | 18 | ## Shortcomings / TODO 19 | - As mentioned above, AndroidNFCEmulator can only currently emulate NDEF URI tags with a single record. 20 | - Only the data URI and `EXTRA_NDEF_MESSAGES` are passed in the intent - `EXTRA_TAG` is **not** passed to the receiving application! 21 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-android-extensions' 4 | 5 | android { 6 | compileSdkVersion 29 7 | buildToolsVersion "29.0.3" 8 | 9 | defaultConfig { 10 | applicationId "org.twoboysandhats.androidnfcemulator" 11 | minSdkVersion 19 12 | targetSdkVersion 29 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 23 | } 24 | } 25 | 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 31 | implementation 'androidx.appcompat:appcompat:1.1.0' 32 | implementation 'androidx.core:core-ktx:1.2.0' 33 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 34 | testImplementation 'junit:junit:4.12' 35 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 36 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 37 | 38 | implementation 'org.nanohttpd:nanohttpd:2.3.1' 39 | } 40 | -------------------------------------------------------------------------------- /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/org/twoboysandhats/androidnfcemulator/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package org.twoboysandhats.androidnfcemulator 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("org.twoboysandhats.androidnfcemulator", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/twoboysandhats/androidnfcemulator/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package org.twoboysandhats.androidnfcemulator 2 | 3 | import android.app.ActivityManager 4 | import android.content.Context 5 | import android.content.Intent 6 | import androidx.appcompat.app.AppCompatActivity 7 | import android.os.Bundle 8 | 9 | import android.net.wifi.WifiManager 10 | import android.text.format.Formatter 11 | import android.view.View 12 | import android.widget.Button 13 | import android.widget.TextView 14 | 15 | class MainActivity : AppCompatActivity(), View.OnClickListener { 16 | 17 | var serviceButton: Button? = null 18 | 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContentView(R.layout.activity_main) 22 | 23 | val ipText: TextView = findViewById(R.id.ip_text) 24 | ipText.text = "Server IP: " + getIPAddress() 25 | 26 | serviceButton = findViewById(R.id.service_button) 27 | serviceButton?.setOnClickListener(this) 28 | } 29 | 30 | override fun onClick(view: View?) { 31 | if (view == serviceButton) { 32 | // Toggle the service 33 | val serviceIntent: Intent = Intent(this, NFCServerService::class.java) 34 | 35 | if (getServiceState(NFCServerService::class.java)) { 36 | // Service is running, stop the service 37 | stopService(serviceIntent) 38 | serviceButton?.text = resources.getString(R.string.service_on) 39 | } else { 40 | // Service is stopped, run the service 41 | startService(serviceIntent) 42 | serviceButton?.text = resources.getString(R.string.service_off) 43 | } 44 | } 45 | } 46 | 47 | fun getServiceState(serviceClass: Class): Boolean { 48 | val manager: ActivityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager 49 | val serviceNames: List = manager.getRunningServices(Integer.MAX_VALUE).map { it.service.className } 50 | return serviceNames.contains(serviceClass.name) 51 | } 52 | 53 | 54 | fun getIPAddress (): String { 55 | val wm: WifiManager = applicationContext.getSystemService(WIFI_SERVICE) as WifiManager 56 | return Formatter.formatIpAddress(wm.connectionInfo.ipAddress) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/org/twoboysandhats/androidnfcemulator/NFCServerService.kt: -------------------------------------------------------------------------------- 1 | package org.twoboysandhats.androidnfcemulator 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.nfc.NdefMessage 7 | import android.nfc.NdefRecord 8 | import android.nfc.NfcAdapter 9 | import android.os.IBinder 10 | 11 | import fi.iki.elonen.NanoHTTPD 12 | import java.io.IOException 13 | 14 | class NFCServerService: Service() { 15 | 16 | var server: NFCServer? = null 17 | 18 | override fun onBind(intent: Intent?): IBinder? { 19 | // We don't need to report anything back to an activity that binds us 20 | return null 21 | } 22 | 23 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 24 | server = NFCServer(this, 8080) 25 | try { 26 | server?.start() 27 | } catch (e: IOException) { 28 | println("Server startup error") 29 | } 30 | 31 | return START_STICKY // We'll be started and stopped at will 32 | } 33 | 34 | override fun onDestroy() { 35 | super.onDestroy() 36 | server?.stop() 37 | } 38 | } 39 | 40 | class NFCServer(val service: Service, val port: Int): NanoHTTPD(port) { 41 | override fun serve(session: IHTTPSession?): Response { 42 | // Grab the parameters 43 | val parameters = session?.parameters 44 | val uriString = parameters?.get("uri")?.get(0) 45 | 46 | try { 47 | // create the Ndef message 48 | val ndefMessage = NdefMessage(NdefRecord.createUri(uriString)) 49 | 50 | // create our NFC intent 51 | val nfcIntent: Intent = Intent(NfcAdapter.ACTION_NDEF_DISCOVERED) 52 | nfcIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 53 | nfcIntent.setData(Uri.parse(uriString)) 54 | nfcIntent.putExtra(NfcAdapter.EXTRA_NDEF_MESSAGES, arrayOf(ndefMessage)) 55 | 56 | service.startActivity(nfcIntent) 57 | 58 | } catch(e: Exception) { 59 | // just leave it. 60 | return newFixedLengthResponse("Error: ${e.message}") 61 | } 62 | 63 | return newFixedLengthResponse("") 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 |