Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added option to share and import Contacts #25

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.app.ActivityCompat
import androidx.core.app.NavUtils
import androidx.core.net.toUri
import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
Expand Down Expand Up @@ -49,6 +50,7 @@ class EditContactActivity : AppCompatActivity() {

companion object {
const val CONTACT_ID = "contactId"
const val IMPORT_CONTACT_URI = "importContactUri"
}

override fun onCreate(savedInstanceState: Bundle?) {
Expand Down Expand Up @@ -82,6 +84,14 @@ class EditContactActivity : AppCompatActivity() {
binding.toolbar.setNavigationOnClickListener { NavUtils.navigateUpFromSameTask(this) }
}

override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)

intent.extras?.getString(IMPORT_CONTACT_URI)?.also {
viewModel.importContactFromZipFile(it.toUri())
}
}

private fun setup() {
// Don't observe the name from the viewModel. We are the only ones who change it, and it can
// cause loops with our text changed listener.
Expand Down Expand Up @@ -149,10 +159,26 @@ class EditContactActivity : AppCompatActivity() {
R.id.delete -> {
onDeleteContact()
}

R.id.share -> {
onShareContact()
}
}
return super.onOptionsItemSelected(item)
}

private fun onShareContact() {
lifecycleScope.launch {
val zipUri = viewModel.shareContact()

val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_STREAM, zipUri)
sendIntent.type = "application/zip"
startActivity(sendIntent)
}
}

private fun maybeShowPhoto(path: String) {
if (path.isNotEmpty()) {
Picasso.get()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.graphics.BitmapFactory
import android.media.MediaRecorder
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import androidx.core.net.toUri
Expand All @@ -19,8 +20,15 @@ import com.serwylo.babyphone.utils.debounce
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.*
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream

class EditContactViewModel(
private val context: Context,
Expand Down Expand Up @@ -231,6 +239,107 @@ class EditContactViewModel(
}
}

fun shareContact(): Uri {
val fileName = contact.name.ifEmpty { contact.id.toString() }
val zipFile = File(context.getExternalFilesDir("Contacts"), "$fileName.zip")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind if we change this to "$fileName.babyphone.zip"? The rationale is that in the future, I'd love to add an <intent-filter> to the app to listen for when people open such files in a file browser, or even downloading them from the internet. If it is just .zip with the contact name/id, then we would have to listen to all .zip files, which may provide a bad experience for people who want most .zip files to open in an archive manager.

Another way to future proof it would be to think about how one might allow multiple contacts to be put into a single zip file (again, imagine downloading a .zip file from online that contained 6 contacts). However, I don't think anything needs to change here. Rather, we could add a .json file in the zip file in the future which specifies which photos/sound files belong to each contact within the zip.

Having said that, while that is forwards-compatible, it is not backwards-compatible. One way to make this backwards-compatible would be to put the photo + sounds into a folder within the .zip file. When importing, open the first folder in the zip file.

Then, if in the future there is a zip file with multiple folders in it, old versions of the app will work just fine, and new versions will be able to import multiple contacts from the zip file. This would allow us to publish bundles of contacts online via GitHub pages in this repo or a sister repo, where people can download, I dunno, "Kittens and Puppies", or other quirky packages.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like your ideas! 👍
Especially exporting multiple contacts at once. Right now I have to share each contact individually with my partner.

I will change the file name to be compatible with a future intent-filter.


ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use {
zipFiles(it, contactDir)
}

return FileProvider.getUriForFile(
context,
"com.serwylo.babyphone.fileprovider",
zipFile
)
}

private fun zipFiles(zipOut: ZipOutputStream, sourceFile: File) {
val buffer = ByteArray(2048)

sourceFile.listFiles()?.asSequence()?.forEach { f ->
FileInputStream(f).use { fi ->
BufferedInputStream(fi).use { origin ->
val zipEntry = ZipEntry(f.name)
zipOut.putNextEntry(zipEntry)
while (true) {
val readBytes = origin.read(buffer)
if (readBytes == -1) {
break
}
zipOut.write(buffer, 0, readBytes)
}
zipOut.closeEntry()
}
}
}
}

fun importContactFromZipFile(zipUri: Uri) {
viewModelScope.launch(Dispatchers.IO) {

context.contentResolver.query(
zipUri,
null,
null,
null,
null
)?.use { cursor ->
val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
val name = cursor.getString(index).removeSuffix(".zip")
_name.postValue(name)
contact = contact.copy(name = name).also {
dao.update(it)
}
}

val zipFile = File(context.getExternalFilesDir("Contacts"), "temp.zip")

context.contentResolver.openInputStream(zipUri)?.use { inputStream ->
zipFile.outputStream().use {
inputStream.copyTo(it)
}
}

ZipFile(zipFile).use { zip ->
zip.entries()
.asSequence()
.filter { zipEntry ->
zipEntry.name == "photo.small.jpg" || zipEntry.name.endsWith(".aac")
}
.forEach { zipEntry ->
val outputFile = File(contactDir, zipEntry.name)

zip.getInputStream(zipEntry).use { input ->
outputFile.outputStream().use { outputStream ->
input.copyTo(outputStream)
if (zipEntry.name == "photo.small.jpg") {
// handle photo
withContext(Dispatchers.Main) {
_avatarPath.value = outputFile.toUri().toString()
}

contact =
contact.copy(avatarPath = outputFile.toUri().toString())
.also {
dao.update(it)
}
} else {
// handle audio
dao.insert(
Recording(
contact.id,
outputFile.toUri().toString(),
)
)
}
}
}
}
}
}
}
}

class EditContactViewModelFactory(private val context: Context, private val dao: ContactDao, private val contactId: Long) : ViewModelProvider.Factory {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import com.serwylo.babyphone.db.AppDatabase
import com.serwylo.babyphone.db.entities.Contact
import com.serwylo.babyphone.editcontact.EditContactActivity

private const val PICK_MYFILE_REQUEST = 1

class SettingsContactListActivity : AppCompatActivity() {

private lateinit var viewModel: SettingsContactListViewModel
Expand Down Expand Up @@ -78,10 +80,32 @@ class SettingsContactListActivity : AppCompatActivity() {
R.id.add -> {
startActivity(Intent(this, EditContactActivity::class.java))
}

R.id.import_contact -> {
onImportContact()
}
}
return super.onOptionsItemSelected(item)
}

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

if (requestCode == PICK_MYFILE_REQUEST && resultCode == RESULT_OK) {

startActivity(Intent(this, EditContactActivity::class.java).apply {
putExtra(EditContactActivity.IMPORT_CONTACT_URI, data?.data.toString())
})
}
}

private fun onImportContact() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.type = "application/zip"
intent.addCategory(Intent.CATEGORY_OPENABLE)
startActivityForResult(Intent.createChooser(intent, "Select File"), PICK_MYFILE_REQUEST);
}

private fun onToggleContact(contact: Contact, enabled: Boolean) {
if (!viewModel.toggleContact(contact, enabled)) {
Toast.makeText(this, R.string.settings__at_least_one_contact_requried, Toast.LENGTH_SHORT).show()
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_import.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#FFFFFF" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>
6 changes: 6 additions & 0 deletions app/src/main/res/menu/edit_contact_menu.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<item
android:id="@+id/share"
android:icon="@drawable/ic_share"
app:showAsAction="always"
android:title="@string/btn__share" />

<item
android:id="@+id/delete"
android:icon="@drawable/ic_delete"
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/res/menu/settings_contact_list_menu.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/import_contact"
android:icon="@drawable/ic_import"
app:showAsAction="always"
android:title="@string/btn__import_contact" />

<item
android:id="@+id/add"
android:icon="@drawable/ic_add"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
<string name="btn__play_sound">Ton abspielen</string>
<string name="default_contact__dad">Papa</string>
<string name="btn__add_contact">Aufzeichnen</string>
<string name="btn__import_contact">Importieren</string>
<string name="settings__category_display">Bildschirm</string>
<string name="settings__contact">Kontakt</string>
<string name="whats_new">Nachrichten</string>
<string name="whats_new__title">Nachrichten</string>
<string name="whats_new__continue">Weiter</string>
<string name="theme_light">Licht</string>
<string name="btn__share">Teilen</string>
<string name="btn__delete">Löschen</string>
<string name="theme_dark">Dunkel</string>
<string name="settings__category_phone">Telefon</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@
<string name="settings__category_phone">Téléphone</string>
<string name="settings__at_least_one_contact_requried">Au moins un contact est nécessaire.</string>
<string name="edit_contact__confirm_delete_recording_message">Supprimer cet enregistrement \?</string>
<string name="btn__import_contact">Importer</string>
<string name="btn__share">Partager</string>
</resources>
2 changes: 2 additions & 0 deletions app/src/main/res/values-nb-rNO/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@
<string name="whats_new">Nyheter</string>
<string name="whats_new__title">Nyheter</string>
<string name="contact_list__description">Legg til slektninger og venner og skru så av de forvalgte kontaktene for en mer personlig telefonopplevelse. Trykk på din tilpassede profil for å redigere den. Forvalg kan ikke redigeres, kun skrus av.</string>
<string name="btn__import_contact">Importere</string>
<string name="btn__share">Dele</string>
</resources>
2 changes: 2 additions & 0 deletions app/src/main/res/values-nl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@
<string name="btn__cancel">Annuleren</string>
<string name="settings__theme">Thema</string>
<string name="settings__category_phone">Telefoon</string>
<string name="btn__import_contact">Importeren</string>
<string name="btn__share">Deel</string>
</resources>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
<string name="default_contact__mum">Mum</string>

<string name="btn__add_contact">Record</string>
<string name="btn__import_contact">Import Contact</string>
<string name="btn__record_sound">Record</string>
<string name="btn__stop_recording_sound">Stop Recording</string>
<string name="btn__play_sound">Play sound</string>
<string name="btn__lock">Lock</string>
<string name="btn__share">Share</string>
<string name="btn__delete">Delete</string>
<string name="btn__cancel">Cancel</string>

Expand Down