Skip to content

Commit e124a1d

Browse files
author
Justin Boswell
authored
Android support (#49)
* Use System.currentTimeMillis() instead of Instant * Added android app which can run samples * Updated to v0.5.4 of CRT, added android docs
1 parent 1509462 commit e124a1d

39 files changed

Lines changed: 973 additions & 6 deletions

File tree

README.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,44 @@ mvn install
3838
```
3939

4040
## Build CRT from source
41-
```
41+
```sh
4242
# NOTE: use the latest version of the CRT here
43-
git clone --branch v0.5.1 https://github.com/awslabs/aws-crt-java.git
43+
git clone --branch v0.5.4 https://github.com/awslabs/aws-crt-java.git
4444
git clone https://github.com/awslabs/aws-iot-device-sdk-java-v2.git
4545
cd aws-crt-java
4646
mvn install -Dmaven.test.skip=true
4747
cd ../aws-iot-device-sdk-java-v2
4848
mvn install
4949
```
5050

51+
### Android
52+
Supports API 26 or newer.
53+
NOTE: The shadow sample does not currently complete on android due to its dependence on stdin keyboard input.
54+
```sh
55+
git clone --branch v0.5.4 https://github.com/awslabs/aws-crt-java.git
56+
git clone https://github.com/awslabs/aws-iot-device-sdk-java-v2.git
57+
cd aws-crt-java/android
58+
./gradlew connectedCheck # optional, will run the unit tests on any connected devices/emulators
59+
./gradlew publishToMavenLocal
60+
cd ../aws-iot-device-sdk-java-v2/android
61+
./gradlew publishToMavenLocal
62+
./gradlew installDebug # optional, will install the IoTSamples app to any connected devices/emulators
63+
```
64+
65+
Add the following to your project's build.gradle:
66+
```groovy
67+
repositories {
68+
mavenCentral()
69+
maven {
70+
url System.getenv('HOME') + "/.m2/repository"
71+
}
72+
}
73+
74+
dependencies {
75+
implementation 'software.amazon.awssdk.crt:android:0.5.4'
76+
}
77+
```
78+
5179
# Samples
5280

5381
## Shadow

android/.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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

android/app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

android/app/build.gradle

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 "software.amazon.awssdk.iotsamples"
11+
minSdkVersion 26
12+
targetSdkVersion 29
13+
ndkVersion "21.0.6113669"
14+
versionCode 1
15+
versionName "1.0"
16+
17+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18+
}
19+
20+
sourceSets {
21+
main {
22+
java.srcDir '../../sdk/src/main/java'
23+
java.srcDir '../../samples/BasicPubSub/src/main/java'
24+
java.srcDir '../../samples/Jobs/src/main/java'
25+
java.srcDir '../../samples/PubSubStress/src/main/java'
26+
java.srcDir '../../samples/Shadow/src/main/java'
27+
java.srcDir 'src/main/java'
28+
}
29+
}
30+
31+
buildTypes {
32+
release {
33+
minifyEnabled false
34+
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
35+
}
36+
}
37+
compileOptions {
38+
sourceCompatibility = 1.8
39+
targetCompatibility = 1.8
40+
}
41+
42+
}
43+
44+
repositories {
45+
mavenCentral()
46+
maven {
47+
url System.getenv('HOME') + "/.m2/repository"
48+
}
49+
}
50+
51+
dependencies {
52+
implementation fileTree(dir: 'libs', include: ['*.jar'])
53+
implementation 'software.amazon.awssdk.crt:android:0.5.4'
54+
implementation 'com.google.code.gson:gson:2.8.5'
55+
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
56+
implementation 'androidx.appcompat:appcompat:1.1.0'
57+
implementation 'androidx.core:core:1.2.0'
58+
implementation 'androidx.core:core-ktx:1.2.0'
59+
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
60+
testImplementation 'junit:junit:4.13'
61+
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
62+
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
63+
}

android/app/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package software.amazon.awssdk.iotsamples
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("software.amazon.awssdk.iotsamples", appContext.packageName)
23+
}
24+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
package="software.amazon.awssdk.iotsamples">
4+
5+
<application
6+
android:allowBackup="true"
7+
android:icon="@mipmap/ic_launcher"
8+
android:label="@string/app_name"
9+
android:roundIcon="@mipmap/ic_launcher_round"
10+
android:supportsRtl="true"
11+
android:theme="@style/AppTheme">
12+
<activity android:name=".MainActivity">
13+
<intent-filter>
14+
<action android:name="android.intent.action.MAIN" />
15+
16+
<category android:name="android.intent.category.LAUNCHER" />
17+
</intent-filter>
18+
</activity>
19+
</application>
20+
21+
</manifest>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.txt
2+
*.pem
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Files required to run samples:
2+
ca-certificates.crt - Taken from any recent Linux /etc/ssl
3+
certificate.pem - IoT Thing Certificate
4+
privatekey.pem - IoT Thing Private Key
5+
endpoint.txt - IoT ATS Endpoint
6+
AmazonRootCA1.pem - Available from https://www.amazontrust.com/repository/AmazonRootCA1.pem
7+
8+
Optional:
9+
clientId.txt - specifies --clientId CLI argument
10+
topic.txt - specifies --topic CLI argument
11+
message.txt - specifies --message CLI argument
12+
port.txt - specifies --port CLI argument
13+
thingName.txt - specifies --thingName CLI argument
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package software.amazon.awssdk.iotsamples
2+
3+
import androidx.appcompat.app.AppCompatActivity
4+
import android.os.Bundle
5+
import android.view.View
6+
import android.widget.AdapterView
7+
import android.widget.ArrayAdapter
8+
import android.widget.Spinner
9+
import android.widget.TextView
10+
import java.io.FileNotFoundException
11+
import java.io.FileOutputStream
12+
import java.io.OutputStream
13+
import java.io.PrintStream
14+
import java.lang.Exception
15+
import kotlin.concurrent.thread
16+
17+
val SAMPLES = mapOf(
18+
"Publish/Subscribe Sample" to "pubsub.PubSub",
19+
"Jobs Client Sample" to "jobs.JobsSample",
20+
"Shadow Client Sample" to "shadow.ShadowSample",
21+
"Publish/Subscribe Load Test" to "pubsubstress.PubSubStress"
22+
)
23+
24+
class MainActivity : AppCompatActivity(), AdapterView.OnItemSelectedListener {
25+
26+
private class StreamTee(val source: OutputStream, val log: (message: String) -> Unit)
27+
: PrintStream(source, true) {
28+
init {
29+
if (source == System.out) {
30+
System.setOut(this)
31+
} else if (source == System.err) {
32+
System.setErr(this)
33+
}
34+
}
35+
36+
override fun write(buf: ByteArray, off: Int, len: Int) {
37+
source.write(buf, off, len)
38+
log(String(buf.slice(IntRange(off, off+len-1)).toByteArray()))
39+
}
40+
41+
override fun write(b: ByteArray) {
42+
source.write(b)
43+
log(String(b))
44+
}
45+
}
46+
47+
private val stdout : StreamTee;
48+
private val stderr : StreamTee;
49+
50+
private var console: TextView? = null;
51+
private var sampleSelect: Spinner? = null;
52+
53+
init {
54+
stdout = StreamTee(System.out) { writeToConsole(it) }
55+
stderr = StreamTee(System.err) { writeToConsole(it) }
56+
}
57+
58+
override fun onCreate(savedInstanceState: Bundle?) {
59+
super.onCreate(savedInstanceState)
60+
setContentView(R.layout.activity_main)
61+
62+
console = findViewById<TextView>(R.id.console)
63+
console?.isEnabled = false
64+
65+
sampleSelect = findViewById<Spinner>(R.id.sampleSelect);
66+
67+
val samples = SAMPLES.keys.toMutableList()
68+
samples.add(0, "Please select a sample")
69+
val samplesAdapter = ArrayAdapter<String>(this, R.layout.support_simple_spinner_dropdown_item, samples)
70+
sampleSelect?.adapter = samplesAdapter
71+
72+
sampleSelect?.onItemSelectedListener = this
73+
}
74+
75+
private fun clearConsole() {
76+
runOnUiThread() {
77+
console?.text = ""
78+
}
79+
}
80+
81+
private fun writeToConsole(message: String) {
82+
runOnUiThread() {
83+
console?.append(message)
84+
}
85+
}
86+
87+
private fun onSampleComplete() {
88+
runOnUiThread() {
89+
sampleSelect?.isEnabled = true
90+
}
91+
}
92+
93+
private fun assetContents(assetName: String) : String {
94+
resources.assets.open(assetName).use {res ->
95+
val bytes = ByteArray(res.available())
96+
res.read(bytes)
97+
return String(bytes).trim()
98+
}
99+
}
100+
101+
private fun assetContentsOr(assetName: String, defaultValue: String) : String {
102+
return try {
103+
assetContents(assetName)
104+
} catch (fnf: FileNotFoundException) {
105+
defaultValue
106+
}
107+
}
108+
109+
private fun runSample(name: String) {
110+
val classLoader = Thread.currentThread().contextClassLoader
111+
val sampleClass = classLoader.loadClass(name);
112+
if (sampleClass == null) {
113+
clearConsole()
114+
writeToConsole("Could not find sample '${name}'")
115+
}
116+
117+
thread(name="sample_runner", contextClassLoader = classLoader) {
118+
// find resources, copy them into the cache so samples can get paths to them
119+
val resourceNames = listOf("AmazonRootCA1.pem", "certificate.pem", "privatekey.pem")
120+
val resourceMap = HashMap<String, String>()
121+
for (resourceName in resourceNames) {
122+
resources.assets.open(resourceName).use { res ->
123+
val cachedName = "${externalCacheDir}/${resourceName}"
124+
FileOutputStream(cachedName).use { cachedRes ->
125+
res.copyTo(cachedRes)
126+
}
127+
128+
resourceMap[resourceName] = cachedName
129+
}
130+
}
131+
132+
val args = mutableListOf(
133+
"--endpoint", assetContents("endpoint.txt"),
134+
"--rootca", resourceMap["AmazonRootCA1.pem"],
135+
"--cert", resourceMap["certificate.pem"],
136+
"--key", resourceMap["privatekey.pem"],
137+
"--port", assetContentsOr("port.txt", "8883"),
138+
"--clientId", assetContentsOr("clientId.txt", "android-java-crt-test")
139+
)
140+
if (name == "pubsub.PubSub") {
141+
args.addAll(arrayOf(
142+
"--topic", assetContentsOr("topic.txt", "/samples/test"),
143+
"--message", assetContentsOr("message.txt", "Hello World From Android")))
144+
} else if (name in arrayOf("jobs.JobsSample", "shadow.ShadowSample")) {
145+
args.addAll(arrayOf(
146+
"--thingName", assetContentsOr("thingName.txt", "aws-iot-unit-test")
147+
))
148+
}
149+
val main = sampleClass.getMethod("main", Array<String>::class.java)
150+
try {
151+
main.invoke(null, args.toTypedArray())
152+
} catch (e: Exception) {
153+
writeToConsole(e.toString())
154+
}
155+
onSampleComplete();
156+
}
157+
}
158+
159+
override fun onNothingSelected(p0: AdapterView<*>?) {
160+
clearConsole()
161+
writeToConsole("Please select a sample above")
162+
}
163+
164+
override fun onItemSelected(parent: AdapterView<*>?, view: View?, pos: Int, id: Long) {
165+
clearConsole()
166+
val sampleName = parent?.getItemAtPosition(pos).toString()
167+
val sampleClassName = SAMPLES[sampleName]
168+
if (sampleClassName != null) {
169+
return runSample(sampleClassName)
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)