Skip to content

Commit

Permalink
code cleanup and add README
Browse files Browse the repository at this point in the history
  • Loading branch information
pk-218 committed Mar 27, 2022
1 parent 693d72a commit a5f28dc
Show file tree
Hide file tree
Showing 22 changed files with 259 additions and 245 deletions.
114 changes: 114 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Knox

<p align="center">
<a href="https://github.com/pk-218/KotlinX">
<img src="./assets/logo.png" alt="Knox - Logo" width="150" height="150">
</a>
</p>

## 📌 About

Knox is a peer-to-peer mobile chat application that uses no intermediary, no server, ensuring user's privacy and security. This is made possible by sockets to exchange text and files between peers present in the same network.

_**Developed by Team KotlinX**_

- [Azeez Dandawala](https://github.com/azeez-72)
- [Pankaj Khushalani](https://github.com/pk-218)
- [Prasad Thakare](https://github.com/sans2801)
- [Shivam Pawar](https://github.com/theshivv)

## 🎯 Key Features

- Create an alias to ensure privacy

- Send text messages to peers

- Send files such as images, audio, and other documents to peers

- Block a peer from further communication

## 🛠 Project Setup

- Prerequisites

- Java Development Kit version 8 or higher. Download JDK 11 [here](https://www.oracle.com/in/java/technologies/javase/jdk11-archive-downloads.html).

- Android Studio Bumblebee or higher installed with Android Gradle Plugin 7.1.4 or higher and Kotlin plguin version 1.5 or higher. Download Android Studio Bumblebee [here](https://developer.android.com/studio).

- An Android Virtual Device with API level 21 or above to run the mobile application locally or a physical Android device with USB debugging turned on.

- Steps

1. Clone the GitHub repository using Git.

```
git clone https://github.com/pk-218/KotlinX.git
```

2. Build the project using Gradle CLI

```
./gradlew build
```

3. Run the project on your virtual or physical Android device.

```
./gradlew installDebug
```

## ⛓ Mobile Application Architecture

Knox follows the single Activity, MVVM (Model-View-ViewModel) architecture. The data stores and sources are interfaces by a repository that is accessed by ViewModels via dependency injection. The ViewModels supply the data to the UI or Views (here, Fragments).

<p align="center">
<img src="./assets/knox_architecture.png" alt="Knox - Architecture" width="450" height="450">
</p>

</br>

## ⚡ Technologies Used

- Kotlin: An open source, modern statically typed programming language used by over 60% of professional Android developers and officially supported by Google since 2019.

- XML: Extensible Markup Language used to defined the views for the Android mobile application.

- Android Studio: THe official integrated development environment for Google's Android operating system, built on JetBrains' IntelliJ IDEA software and designed specifically for Android development.

- Jetpack Libraries:
A suite of libraries maintained by Google that work consistently across Android versions and devices.

- Fragment - Allows creation of multiple screens hosted within a single Activity.

- ViewModel - Designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

- Room - Provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite.

- Navigation - Helps implement the Navigation components such as NavHost and NavController.

- Hilt - Built on top of Jetpack Dagger that provides dependency injection with ease.

- Preferences DataStore - A data storage solution that allows to store key-value pairs locally using coroutines and Flow.

</br>
<p align="center">
<img src="./assets/mad_scorecard.png" alt="MAD Scorecard" />
</p>
</br>

## 📸 Screenshots & Video

Check out the demonstration video [here](https://drive.google.com/file/d/1-oxsrs8NbKyoVPGcngqP7tlPwLVyYqZ1/view?usp=sharing).
</br>

<p align="center">
<img src="./assets/home_fragment.jpg" alt="HomeFragment" width="32%"/>
<img src="./assets/connection_details_fragment.jpg" alt="ConnectionDetailsFragment" width="32%"/>
<img src="./assets/chat_fragment.jpg" alt="ChatFragment" width="32%"/>
</p>

<p align="center">
<img src="./assets/chat_fragment_files.jpg" alt="Select files from internal storage (ChatFragment)" width="32%"/>
<img src="./assets/chat_fragment_send_text_image.jpg" alt="Send and receive messages & images (ChatFragment)" width="32%"/>
<img src="./assets/chat_fragment_final.jpg" alt="ChatFragment final" width="32%"/>
</p>
83 changes: 20 additions & 63 deletions app/src/main/java/tech/kotlinx/knox/ChatFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,27 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.Toast
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.loader.content.CursorLoader
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
import tech.kotlinx.knox.adapter.MessageAdapter
import tech.kotlinx.knox.databinding.FragmentChatBinding
import tech.kotlinx.knox.ui.viewmodels.ChatViewModel
import tech.kotlinx.knox.util.RealPath
import java.io.File


@AndroidEntryPoint
class ChatFragment : Fragment() {

private val viewModel: ChatViewModel by viewModels()
private var myPort = 5000
private var myUserName: String? = ""
private val args by navArgs<ChatFragmentArgs>()

private var _binding: FragmentChatBinding? = null
Expand All @@ -47,30 +43,30 @@ class ChatFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

//End Chat
binding.endChat.setOnClickListener(){
// end Chat
binding.endChat.setOnClickListener {
Toast.makeText(context, "Chat Ended", Toast.LENGTH_SHORT).show()
findNavController().navigate(
R.id.action_ChatFragment_to_ConnectionDetailsFragment,
)
}




// get receiver IP address and port from args
Log.d("ChatFragmentArgs", args.senderUserName + args.receiverIP + ":" + args.receiverPort.toString())
Log.d(
"ChatFragmentArgs",
args.senderUserName + args.receiverIP + ":" + args.receiverPort.toString()
)

// render messages
binding.messageView.adapter = context?.let {
MessageAdapter(it, viewModel.messages.value!!)
}

viewModel.userName.observe(viewLifecycleOwner) {username ->
viewModel.userName.observe(viewLifecycleOwner) { username ->
binding.name.text = username
}

viewModel.status.observe(viewLifecycleOwner) {status ->
viewModel.status.observe(viewLifecycleOwner) { status ->
binding.status.text = status
}
val a = "@" + args.senderUserName
Expand All @@ -81,14 +77,14 @@ class ChatFragment : Fragment() {
}
}

//start file server
// start file server
// start chat server
viewModel.startServer(myPort)

viewModel.sendMessage(a, args.receiverIP, args.receiverPort)

//start file server
viewModel.startFileServer(myPort) // 1
// start file server
viewModel.startFileServer(myPort)

binding.buttonChatboxSend.setOnClickListener {
if (binding.edittextChatbox.text.isNotBlank()) {
Expand All @@ -104,15 +100,12 @@ class ChatFragment : Fragment() {
}
}


//file attachment send

// file attachment send
binding.fileSend.setOnClickListener {
Log.d("File send", "clicked on file send")
val intent = Intent()
.setType("*/*")
.setAction(Intent.ACTION_GET_CONTENT)

startActivityForResult(Intent.createChooser(intent, "Select a file"), 111)
}
}
Expand All @@ -127,20 +120,20 @@ class ChatFragment : Fragment() {
viewModel.sendMessage("Online", args.receiverIP, args.receiverPort)
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if (requestCode == 111 && resultCode == RESULT_OK) {
val selectedFile = data?.data //The uri with the location of the file
// the uri with the location of the file
val selectedFile = data?.data
Log.d("URI of Selected File", selectedFile.toString())
//get real path from URI
// get real path from URI
val realPath = RealPath()
val filePath = realPath.getPathFromUri(context, selectedFile)
//send file to receiver
// send file to receiver
if (selectedFile != null) {
Log.d("Path of file ",filePath.toString())
Log.d("Path of file ", filePath.toString())
if (filePath != null) {
viewModel.sendFile( // 2
viewModel.sendFile(
filePath,
args.receiverIP,
args.receiverPort
Expand All @@ -150,40 +143,4 @@ class ChatFragment : Fragment() {

}
}






// Function for displaying an AlertDialogue for choosing an image
// private fun selectImage() {
// val choice = arrayOf<CharSequence>("Take Photo", "Choose from Gallery", "Cancel")
// val myAlertDialog: AlertDialog.Builder = AlertDialog.Builder(this)
// myAlertDialog.setTitle("Select Image")
// myAlertDialog.setItems(choice, DialogInterface.OnClickListener { dialog, item ->
// when {
// // Select "Choose from Gallery" to pick image from gallery
// choice[item] == "Choose from Gallery" -> {
// val pickFromGallery = Intent(Intent.ACTION_GET_CONTENT, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
// pickFromGallery.type = "/image"
// startActivityForResult(pickFromGallery, 1)
// }
// // Select "Take Photo" to take a photo
// choice[item] == "Take Photo" -> {
// val cameraPicture = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
// startActivityForResult(cameraPicture, 0)
// }
// // Select "Cancel" to cancel the task
// choice[item] == "Cancel" -> {
// myAlertDialog.dismiss()
// }
// }
// })
// myAlertDialog.show()
// }




}
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,14 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint
import tech.kotlinx.knox.databinding.FragmentFirstBinding
import tech.kotlinx.knox.ui.viewmodels.FirstViewModel
import tech.kotlinx.knox.ui.viewmodels.HomeViewModel

/**
* A simple [Fragment] subclass as the default destination in the navigation.
*/
@AndroidEntryPoint
class FirstFragment : Fragment() {
class HomeFragment : Fragment() {

private val viewModel: FirstViewModel by viewModels()
private val viewModel: HomeViewModel by viewModels()
private var _binding: FragmentFirstBinding? = null

// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/tech/kotlinx/knox/KnoxApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,4 @@ import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class KnoxApplication: Application() {
}
class KnoxApplication : Application() { }
17 changes: 7 additions & 10 deletions app/src/main/java/tech/kotlinx/knox/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ class MainActivity : AppCompatActivity() {

private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding
private val REQUEST_CODE = 200

companion object {
private const val REQUEST_CODE = 200
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -39,7 +42,7 @@ class MainActivity : AppCompatActivity() {
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)

//permission calls
// permission calls
if (!permissionAlreadyGranted()) {
requestPermission()
Toast.makeText(this, "Permission is already granted!", Toast.LENGTH_SHORT)
Expand All @@ -48,15 +51,11 @@ class MainActivity : AppCompatActivity() {
}

override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
return true
}

override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
Expand All @@ -70,7 +69,7 @@ class MainActivity : AppCompatActivity() {
}


//permission request based on shared preferences data
// permission request based on shared preferences data
private fun permissionAlreadyGranted(): Boolean {
val result = ContextCompat.checkSelfPermission(this, permission.WRITE_EXTERNAL_STORAGE)
return result == PackageManager.PERMISSION_GRANTED
Expand Down Expand Up @@ -98,9 +97,7 @@ class MainActivity : AppCompatActivity() {
Toast.makeText(this, "Permission is denied!", Toast.LENGTH_SHORT).show()
val showRationale =
shouldShowRequestPermissionRationale(permission.WRITE_EXTERNAL_STORAGE)
if (!showRationale) {
// openSettingsDialog()
}
if (showRationale) return
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import kotlinx.coroutines.flow.map

const val USER_PREFERENCES_NAME = "user_preferences"

val Context.datastore : DataStore<Preferences> by preferencesDataStore(name = USER_PREFERENCES_NAME)
val Context.datastore: DataStore<Preferences> by preferencesDataStore(name = USER_PREFERENCES_NAME)

class RepositoryImpl(private val context: Context): Repository {
class RepositoryImpl(private val context: Context) : Repository {

companion object {
val USER_NAME = stringPreferencesKey("NAME")
Expand Down
Loading

0 comments on commit a5f28dc

Please sign in to comment.