Skip to content

Commit

Permalink
I60 Connected Android to server (#74)
Browse files Browse the repository at this point in the history
* Removed firebase folder

* Removed firebase functions deployment from travis.yaml

* Random IntelliJ changes

* Made initial hello world server

* Added more packages

* Minor change to README

* Minor change to README

* Basic implementation of a CRUD server with mongodb

* Added Bonjour Publisher and Client

* added bonjour

* Robot comms teset

* WIP makefile

* Re-added package-lock.json

* Robot turns on when button pressed

* Android can now find server via zeroconf

* Added test mode for when disconnected from robot

* Refactored ServerConnection

* Updated server name in android

* Added turn off on

* Fixed minor things

* App can display list of items

* Re-added addItem and getItems

* Fixed app not connecting to server

* Added ability to refresh list of items

* Implemented adding order on android

* Adding order now removes the items in it from the database
  • Loading branch information
oktay-sen committed Feb 4, 2019
1 parent ee5fa77 commit bdc863e
Show file tree
Hide file tree
Showing 18 changed files with 478 additions and 64 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
5 changes: 4 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ android {
compileSdkVersion 28
defaultConfig {
applicationId "io.github.assis10t.bobandroid"
minSdkVersion 22
minSdkVersion 23
targetSdkVersion 28
versionCode 1
versionName "1.0"
Expand All @@ -29,10 +29,13 @@ dependencies {
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
implementation 'com.android.support:support-v4:28.0.0'
implementation 'com.jakewharton.timber:timber:4.7.1'
testImplementation "com.nhaarman:mockito-kotlin:1.1.0"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'org.jmdns:jmdns:3.5.1'
implementation 'com.squareup.okhttp3:okhttp:3.12.0'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'org.jetbrains.anko:anko-common:0.9'
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package io.github.assis10t.bobandroid

import android.app.Application
import timber.log.Timber

class Application: Application() {

override fun onCreate() {
super.onCreate()
ServerConnection.initialize()

//From: https://github.com/oktay-sen/Coinz
Timber.plant(object : Timber.DebugTree() {
override fun createStackElementTag(element: StackTraceElement): String? {
return super.createStackElementTag(element) + ':' + element.lineNumber
}
})
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package io.github.assis10t.bobandroid

import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import java.net.InetAddress
import android.support.v7.widget.CardView
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.github.assis10t.bobandroid.pojo.Item
import io.github.assis10t.bobandroid.pojo.Order
import kotlinx.android.synthetic.main.activity_main.*
import timber.log.Timber

class MainActivity : AppCompatActivity() {

Expand All @@ -15,5 +21,97 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

container.isRefreshing = true
container.setOnRefreshListener { refreshItems() }
ServerConnection().connect {
container.isRefreshing = false
}
item_list.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false)
item_list.adapter = ItemAdapter { selected ->
if (selected.isEmpty())
make_order.hide()
else
make_order.show()
}

//make_order.hide()
make_order.setOnClickListener {
container.isRefreshing = true
make_order.hide()
val adapter = item_list.adapter as ItemAdapter
ServerConnection().makeOrder(Order(null, adapter.selectedItems)) { success ->
if (!success)
Timber.e("Could not make order.")
else {
Timber.d("Order made.")
refreshItems()
}
}
}

refreshItems()
}

fun refreshItems() {
container.isRefreshing = true
ServerConnection().getItems { success, items ->
container.isRefreshing = false
if (!success) {
Timber.e("getItems failed.")
return@getItems
}
Timber.d("GetItems success. Items: ${items?.size}")
val adapter = item_list.adapter as ItemAdapter
adapter.updateItems(items!!)
}
}

class ItemAdapter(var onSelectionChanged: (selected: List<Item>) -> Unit): RecyclerView.Adapter<ItemAdapter.ViewHolder>() {
var itemList: List<Item> = listOf()
val selectedItems: MutableList<Item> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.fragment_shop_item, parent, false)
return ViewHolder(view)
}

override fun getItemCount(): Int = itemList.size

override fun onBindViewHolder(vh: ViewHolder, pos: Int) {
val item = itemList[pos]
val context = vh.container.context
vh.title.text = item.name
vh.container.setCardBackgroundColor(
if (selectedItems.contains(item))
vh.container.context.getColor(R.color.selectHighlight)
else
vh.container.context.getColor(R.color.white)
)
vh.container.cardElevation =
if (selectedItems.contains(item))
dp(context, 4f)
else
dp(context, 1f)
vh.container.setOnClickListener {
if (selectedItems.contains(item))
selectedItems.remove(item)
else
selectedItems.add(item)
onSelectionChanged(itemList)
notifyItemChanged(pos)
}
}

fun updateItems(items: List<Item>) {
this.itemList = items
this.selectedItems.clear()
notifyDataSetChanged()
}

class ViewHolder(view: View): RecyclerView.ViewHolder(view) {
val title: TextView = view.findViewById(R.id.title)
val quantity: TextView = view.findViewById(R.id.quantity)
val container: CardView = view.findViewById(R.id.container)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,60 +1,148 @@
package io.github.assis10t.bobandroid

import android.os.AsyncTask
import android.util.Log
import java.net.Inet4Address
import com.google.gson.Gson
import io.github.assis10t.bobandroid.pojo.GetItemsResponse
import io.github.assis10t.bobandroid.pojo.Item
import io.github.assis10t.bobandroid.pojo.Order
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import timber.log.Timber
import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.jmdns.JmDNS
import javax.jmdns.ServiceEvent
import javax.jmdns.ServiceListener

class ServerConnection {

companion object {
private val TAG = "ServerConnection"
val SERVER_NAME = "assis10t"
var serverIp: String? = null
var serverAddress: String? = null
val httpClient: OkHttpClient = OkHttpClient()

val onConnectedListeners: MutableList<(String) -> Unit> = mutableListOf()
val onConnectedListeners: MutableList<(serverAddress: String) -> Unit> = mutableListOf()

class ConnectTask: AsyncTask<Unit, JmDNS, Unit>() {
override fun doInBackground(vararg params: Unit?) {
Log.d(TAG, "Discovery started")
fun initialize() {
doAsync {
Timber.d("Discovery started")
val mJmDNS = JmDNS.create()
mJmDNS.addServiceListener("_http._tcp.local.", object : ServiceListener {

override fun serviceResolved(event: ServiceEvent?) {
val info = mJmDNS.getServiceInfo(event!!.type, event.name)
Log.d(TAG, "Service resolved: $info")
Timber.d("Service resolved: $info")
if (info.name.contains(SERVER_NAME)) {
serverIp = "${info.inet4Addresses[0]!!.hostAddress}:${info.port}"
onConnectedListeners.forEach { it(serverIp!!) }
onConnectedListeners.clear()
uiThread {
serverAddress = "http:https://${info.inet4Addresses[0]!!.hostAddress}:${info.port}"
onConnectedListeners.forEach { it(serverAddress!!) }
onConnectedListeners.clear()
}
}
}

override fun serviceRemoved(event: ServiceEvent?) {
Log.d(TAG, "Service removed")
Timber.d("Service removed")
}

override fun serviceAdded(event: ServiceEvent?) {
val info = mJmDNS.getServiceInfo(event!!.type, event.name)
Log.d(TAG, "Service added: $info")
Timber.d("Service added: $info")
}
})
}
}

fun initialize() {
ConnectTask().execute()
ServerConnection().connect { ip ->
Log.d(TAG, "Server found at $ip")
Timber.d("Server found at $ip")
}
}
}

fun connect(onConnected: (String) -> Unit) {
if (serverIp != null)
onConnected(serverIp!!)
if (serverAddress != null)
onConnected(serverAddress!!)
else
onConnectedListeners.add(onConnected)
}

val getRequestFactory = { http: OkHttpClient ->
{ url: String, onGetComplete: (success: Boolean, response: String?) -> Unit ->
doAsync {
Timber.d("Get request to $url")
try {
val request = Request.Builder().url(url).build()
val response = http.newCall(request).execute()
Timber.d("Response received.")
if (!response.isSuccessful) {
Timber.e("Get Request failed: (${response.code()}) ${response.body().toString()}")
uiThread { onGetComplete(false, null) }
} else {
uiThread { onGetComplete(true, response.body()?.string()) }
}
} catch (e: IOException) {
Timber.e(e, "Get Request failed")
uiThread { onGetComplete(false, null) }
}
}
}
}

val postRequestFactory = { http: OkHttpClient, gson: Gson ->
{ url: String, body: Any, onPostComplete: (success: Boolean, response: String?) -> Unit ->
doAsync {
Timber.d("Post request to $url")
try {
val JSON = MediaType.get("application/json; charset=utf-8")
val requestBody = RequestBody.create(JSON, gson.toJson(body))
val request = Request.Builder().url(url).post(requestBody).build()
val response = http.newCall(request).execute()
Timber.d("Response received.")
if (!response.isSuccessful) {
Timber.e("Post Request failed: (${response.code()}) ${response.body().toString()}")
uiThread { onPostComplete(false, null) }
} else {
uiThread { onPostComplete(true, response.body()?.string()) }
}
} catch (e: IOException) {
Timber.e(e, "Post Request failed")
uiThread { onPostComplete(false, null) }
}
}
}
}

val getItemsFactory = { http: OkHttpClient, gson: Gson ->
{ onGetItems: (success: Boolean, items: List<Item>?) -> Unit ->
connect { server ->
getRequestFactory(http)("$server/items") { success, str ->
Timber.d("Result: $success, response: $str")
if (!success) {
onGetItems(success, null)
} else {
val response = gson.fromJson(str!!, GetItemsResponse::class.java)
onGetItems(response.success, response.items)
}
}
}
}
}
val getItems = getItemsFactory(httpClient, Gson())

val makeOrderFactory = { http: OkHttpClient, gson: Gson ->
{ order: Order, onOrderComplete: ((success: Boolean) -> Unit)? ->
connect { server ->
postRequestFactory(http, gson)("$server/order", order) { success, str ->
Timber.d("Result: $success, response: $str")
if (!success) {
onOrderComplete?.invoke(success)
} else {
val response = gson.fromJson(str!!, GetItemsResponse::class.java)
onOrderComplete?.invoke(response.success)
}
}
}
}
}
val makeOrder = makeOrderFactory(httpClient, Gson())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package io.github.assis10t.bobandroid.pojo

class GetItemsResponse(val success: Boolean = false, val items:List<Item> = listOf())
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.assis10t.bobandroid.pojo

class Item (
val _id:String? = null,
val name:String? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.assis10t.bobandroid.pojo

class Order(
val _id: String? = null,
val items: List<Item> = listOf()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.github.assis10t.bobandroid

import android.content.Context
import android.util.TypedValue

fun dp(context: Context, dp: Float) =
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.resources.displayMetrics)
9 changes: 9 additions & 0 deletions android/app/src/main/res/drawable/ic_send_black_24dp.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http:https://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/>
</vector>
Loading

0 comments on commit bdc863e

Please sign in to comment.