Skip to content

Commit

Permalink
Merge pull request #52 from OwlAIProject/ethan/android-api
Browse files Browse the repository at this point in the history
Model objects and API for conversation
  • Loading branch information
etown committed Mar 9, 2024
2 parents 4f89f6a + 2004be8 commit 8b4f3e3
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 24 deletions.
1 change: 1 addition & 0 deletions clients/android/.idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion clients/android/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 17 additions & 9 deletions clients/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.21'
}

android {
Expand Down Expand Up @@ -52,25 +53,32 @@ dependencies {
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:1.0.0-alpha20"
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2'
implementation 'io.socket:socket.io-client:2.0.1'
implementation 'androidx.appcompat:appcompat:1.4.1'


implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0"
implementation 'androidx.core:core-ktx:1.8.0'
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation "com.google.code.gson:gson:2.8.6"

implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
implementation 'androidx.activity:activity-compose:1.5.1'
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:<latest_version>"
// ViewModel and Lifecycle components
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.4.0"

// Compose with BOM
implementation platform('androidx.compose:compose-bom:2022.10.00')
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-graphics'
implementation 'androidx.compose.material:material'
implementation 'androidx.compose.ui:ui-tooling-preview'
implementation 'androidx.compose.material3:material3'

// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.owl.Owl

object ApiServiceSingleton {
val apiService: ConversationApiService by lazy {
ConversationApiService()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.owl.Owl

import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request

class ConversationApiService {

private val client = OkHttpClient()
private val gson = Gson()

suspend fun fetchConversations(): List<Conversation> = withContext(Dispatchers.IO) {
try {
val request = Request.Builder()
.url("${AppConstants.apiBaseURL}/conversations")
.addHeader("Authorization", "Bearer ${AppConstants.clientToken}")
.build()

client.newCall(request).execute().use { response ->
if (!response.isSuccessful) throw Exception("Server responded with code $response")

val responseBody = response.body?.string() ?: throw Exception("Null Response Body")
val conversationsResponse = gson.fromJson(responseBody, ConversationsResponse::class.java)
conversationsResponse.conversations
}
} catch (e: Exception) {
e.printStackTrace()
emptyList<Conversation>()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.owl.Owl

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun ConversationsScreen(viewModel: ConversationsViewModel = viewModel()) {
val conversations = viewModel.conversations.collectAsState(initial = emptyList())

LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(conversations.value, key = { it.id }) { conversation ->
ConversationItem(conversation = conversation)
}
}
}

@Composable
fun ConversationItem(conversation: Conversation) {
Column(modifier = Modifier.padding(vertical = 8.dp)) {
Text(
text = "Start Time: ${conversation.startTime}",
modifier = Modifier.padding(bottom = 4.dp)
)
Text(
text = "State: ${conversation.state}",
modifier = Modifier.padding(bottom = 4.dp)
)
Text(
text = "Summary: ${conversation.shortSummary ?: conversation.summary ?: "No summary"}",
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.owl.Owl

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

class ConversationsViewModel : ViewModel() {

private val apiService = ApiServiceSingleton.apiService

private val _conversations = MutableStateFlow<List<Conversation>>(emptyList())
val conversations = _conversations.asStateFlow()

init {
fetchConversations()
}

fun fetchConversations() {
viewModelScope.launch {
_conversations.value = apiService.fetchConversations()
}
}
}
72 changes: 58 additions & 14 deletions clients/android/app/src/main/java/com/owl/Owl/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,22 @@ import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.UUID

class MainActivity : AppCompatActivity() {
class MainActivity : ComponentActivity() {

private lateinit var cameraHandler: CameraHandler
private lateinit var audioStreamer: AudioStreamer
Expand All @@ -19,54 +29,88 @@ class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

setContent {
MaterialTheme {
Surface {
var isCaptureStarted by remember { mutableStateOf(false) }
Column {
Button(onClick = {
if (isCaptureStarted) {
stopCapture()
isCaptureStarted = false
} else {
if (checkAndRequestPermissions()) {
startCapture()
isCaptureStarted = true
}
}
}) {
Text(if (isCaptureStarted) "Stop Local Capture" else "Start Local Capture")
}
ConversationsScreen()
}
}
}
}
cameraHandler = CameraHandler(this, captureUUID)
requestPermissions()
audioStreamer = AudioStreamer(this, captureUUID)
}

private fun requestPermissions() {
private fun checkAndRequestPermissions(): Boolean {
val requiredPermissions = arrayOf(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
val permissionsToRequest = requiredPermissions.filter {
ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
}.toTypedArray()

if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(this, permissionsToRequest, REQUEST_PERMISSIONS)
return false
} else {
permissionsGranted()
return true
}
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_PERMISSIONS && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
permissionsGranted()
startCapture()
} else {
Log.e(TAG, "Permissions not granted by the user.")
}
}

private fun permissionsGranted() {
private fun startCapture() {
cameraHandler.startBackgroundThread()
cameraHandler.openCamera()
audioStreamer = AudioStreamer(this, captureUUID)
audioStreamer.startStreaming()
}

private fun stopCapture() {
cameraHandler.closeCamera()
cameraHandler.stopBackgroundThread()
audioStreamer.stopStreaming()
}

override fun onResume() {
super.onResume()
cameraHandler.startBackgroundThread()
if (::cameraHandler.isInitialized && ::audioStreamer.isInitialized) {
cameraHandler.startBackgroundThread()
}
}

override fun onPause() {
cameraHandler.stopBackgroundThread()
if (::cameraHandler.isInitialized && ::audioStreamer.isInitialized) {
cameraHandler.stopBackgroundThread()
}
super.onPause()
}

override fun onDestroy() {
if (::cameraHandler.isInitialized && ::audioStreamer.isInitialized) {
cameraHandler.closeCamera()
cameraHandler.stopBackgroundThread()
audioStreamer.stopStreaming()
}
super.onDestroy()
cameraHandler.closeCamera()
cameraHandler.stopBackgroundThread()
}
}
90 changes: 90 additions & 0 deletions clients/android/app/src/main/java/com/owl/Owl/Models.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.owl.Owl

import com.google.gson.annotations.SerializedName
import java.util.Date

data class ConversationsResponse(
@SerializedName("conversations") val conversations: List<Conversation>
)

enum class ConversationState {
@SerializedName("CAPTURING") CAPTURING,
@SerializedName("PROCESSING") PROCESSING,
@SerializedName("COMPLETED") COMPLETED,
@SerializedName("FAILED_PROCESSING") FAILED_PROCESSING
}

data class SuggestedLink(
@SerializedName("url") val url: String
)

data class Conversation(
@SerializedName("id") val id: Int,
@SerializedName("start_time") val startTime: String,
@SerializedName("end_time") val endTime: String? = null,
@SerializedName("conversation_uuid") val conversationUUID: String,
@SerializedName("capture_segment_file") val captureFileSegment: CaptureFileSegment,
@SerializedName("device_type") val deviceType: String,
@SerializedName("summary") val summary: String? = null,
@SerializedName("summarization_model") val summarizationModel: String? = null,
@SerializedName("short_summary") val shortSummary: String? = null,
@SerializedName("state") val state: ConversationState,
@SerializedName("transcriptions") val transcriptions: List<Transcription>,
@SerializedName("primary_location") val primaryLocation: Location? = null,
@SerializedName("suggested_links") val suggestedLinks: List<SuggestedLink>? = null
)

data class CaptureFile(
@SerializedName("id") val id: Int,
@SerializedName("filepath") val filePath: String,
@SerializedName("start_time") val startTime: String,
@SerializedName("device_type") val deviceType: String
)

data class CaptureFileSegment(
@SerializedName("id") val id: Int,
@SerializedName("filepath") val filePath: String,
@SerializedName("duration") val duration: Double? = null,
@SerializedName("source_capture") val sourceCapture: CaptureFile
)

data class Transcription(
@SerializedName("id") val id: Int,
@SerializedName("model") val model: String,
@SerializedName("realtime") val realtime: Boolean,
@SerializedName("transcription_time") val transcriptionTime: Double,
@SerializedName("utterances") val utterances: List<Utterance>
)

data class Utterance(
@SerializedName("id") val id: Int,
@SerializedName("start") val start: Double? = null,
@SerializedName("end") val end: Double? = null,
@SerializedName("text") val text: String? = null,
@SerializedName("speaker") val speaker: String? = null
)

data class Word(
@SerializedName("id") val id: Int,
@SerializedName("word") val word: String,
@SerializedName("start") val start: Double? = null,
@SerializedName("end") val end: Double? = null,
@SerializedName("score") val score: Double? = null,
@SerializedName("speaker") val speaker: String? = null,
@SerializedName("utterance_id") val utteranceId: Int? = null
)

data class Location(
@SerializedName("id") val id: Int? = null,
@SerializedName("latitude") val latitude: Double,
@SerializedName("longitude") val longitude: Double,
@SerializedName("address") val address: String? = null,
@SerializedName("capture_uuid") val captureUUID: String? = null
)

data class Capture(
@SerializedName("capture_uuid") val captureUUID: String,
@SerializedName("device_name") val deviceName: String,
@SerializedName("last_disconnect_time") var lastDisconnectTime: String? = null,
@SerializedName("last_connect_time") var lastConnectTime: String? = null
)

0 comments on commit 8b4f3e3

Please sign in to comment.