You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/com/etesync/syncadapter/ui/importlocal/ImportFragment.kt

442 lines
17 KiB

package com.etesync.syncadapter.ui.importlocal
import android.Manifest
import android.accounts.Account
import android.annotation.TargetApi
import android.app.Activity
import android.app.Dialog
import android.app.ProgressDialog
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.fragment.app.DialogFragment
import at.bitfire.ical4android.*
import at.bitfire.ical4android.TaskProvider.Companion.OPENTASK_PROVIDERS
import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.Contact
import at.bitfire.vcard4android.ContactsStorageException
import com.etesync.syncadapter.Constants.KEY_ACCOUNT
import com.etesync.syncadapter.Constants.KEY_COLLECTION_INFO
import com.etesync.syncadapter.R
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.CollectionInfo
import com.etesync.syncadapter.resource.*
import com.etesync.syncadapter.syncadapter.ContactsSyncManager
import com.etesync.syncadapter.ui.Refreshable
import com.etesync.syncadapter.ui.importlocal.ResultFragment.ImportResult
import com.etesync.syncadapter.utils.TaskProviderHandling
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
class ImportFragment : DialogFragment() {
private lateinit var account: Account
private lateinit var info: CollectionInfo
private var inputStream: InputStream? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isCancelable = false
retainInstance = true
account = arguments!!.getParcelable(KEY_ACCOUNT)!!
info = arguments!!.getSerializable(KEY_COLLECTION_INFO) as CollectionInfo
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
chooseFile()
} else {
val data = ImportResult()
data.e = Exception(getString(R.string.import_permission_required))
(activity as ResultFragment.OnImportCallback).onImportResult(data)
dismissAllowingStateLoss()
}
}
@TargetApi(Build.VERSION_CODES.M)
private fun requestPermissions() {
requestPermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 0)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
super.onCreateDialog(savedInstanceState)
val progress = ProgressDialog(activity)
progress.setTitle(R.string.import_dialog_title)
progress.setMessage(getString(R.string.import_dialog_loading_file))
progress.setCanceledOnTouchOutside(false)
progress.isIndeterminate = false
progress.setIcon(R.drawable.ic_import_export_black)
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL)
if (savedInstanceState == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions()
} else {
chooseFile()
}
} else {
setDialogAddEntries(progress, savedInstanceState.getInt(TAG_PROGRESS_MAX))
}
return progress
}
private fun setDialogAddEntries(dialog: ProgressDialog, length: Int) {
dialog.max = length
dialog.setMessage(getString(R.string.import_dialog_adding_entries))
Logger.log.info("Adding entries. Total: ${length}")
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val dialog = dialog as ProgressDialog
outState.putInt(TAG_PROGRESS_MAX, dialog.max)
}
override fun onDestroyView() {
val dialog = dialog
// handles https://code.google.com/p/android/issues/detail?id=17423
if (dialog != null && retainInstance) {
dialog.setDismissMessage(null)
}
super.onDestroyView()
}
fun chooseFile() {
val intent = Intent()
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.action = Intent.ACTION_GET_CONTENT
when (info.enumType) {
CollectionInfo.Type.CALENDAR -> intent.type = "text/calendar"
CollectionInfo.Type.TASKS -> intent.type = "text/calendar"
CollectionInfo.Type.ADDRESS_BOOK -> intent.type = "text/x-vcard"
}
val chooser = Intent.createChooser(
intent, getString(R.string.choose_file))
try {
startActivityForResult(chooser, REQUEST_CODE)
} catch (e: ActivityNotFoundException) {
val data = ImportResult()
data.e = Exception("Failed to open file chooser.\nPlease install one.")
(activity as ResultFragment.OnImportCallback).onImportResult(data)
dismissAllowingStateLoss()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK) {
if (data != null) {
// Get the URI of the selected file
val uri = data.data!!
Logger.log.info("Starting import into ${info.uid} from file ${uri}")
try {
inputStream = activity!!.contentResolver.openInputStream(uri)
Thread(ImportEntriesLoader()).start()
} catch (e: Exception) {
Logger.log.severe("File select error: ${e.message}")
val importResult = ImportResult()
importResult.e = e
(activity as ResultFragment.OnImportCallback).onImportResult(importResult)
dismissAllowingStateLoss()
}
}
} else {
dismissAllowingStateLoss()
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
fun loadFinished(data: ImportResult) {
(activity as ResultFragment.OnImportCallback).onImportResult(data)
Logger.log.info("Finished import")
dismissAllowingStateLoss()
if (activity is Refreshable) {
(activity as Refreshable).refresh()
}
}
private inner class ImportEntriesLoader : Runnable {
private fun finishParsingFile(length: Int) {
if (activity == null) {
return
}
activity!!.runOnUiThread { setDialogAddEntries(dialog as ProgressDialog, length) }
}
private fun entryProcessed() {
if (activity == null) {
return
}
activity!!.runOnUiThread {
val dialog = dialog as ProgressDialog
dialog.incrementProgressBy(1)
}
}
override fun run() {
val result = loadInBackground()
activity!!.runOnUiThread { loadFinished(result) }
}
fun loadInBackground(): ImportResult {
val result = ImportResult()
try {
val context = context!!
val importReader = InputStreamReader(inputStream)
if (info.enumType == CollectionInfo.Type.CALENDAR) {
val events = Event.eventsFromReader(importReader, null)
importReader.close()
if (events.isEmpty()) {
Logger.log.warning("Empty/invalid file.")
result.e = Exception("Empty/invalid file.")
return result
}
result.total = events.size.toLong()
finishParsingFile(events.size)
val provider = context.contentResolver.acquireContentProviderClient(CalendarContract.CONTENT_URI)
if (provider == null) {
result.e = Exception("Failed to acquire calendar content provider.")
return result
}
val localCalendar: LocalCalendar?
try {
localCalendar = LocalCalendar.findByName(account, provider, LocalCalendar.Factory, info.uid!!)
if (localCalendar == null) {
throw FileNotFoundException("Failed to load local resource.")
}
} catch (e: CalendarStorageException) {
Logger.log.info("Fail" + e.localizedMessage)
result.e = e
return result
} catch (e: FileNotFoundException) {
Logger.log.info("Fail" + e.localizedMessage)
result.e = e
return result
}
for (event in events) {
try {
var localEvent = localCalendar.findByUid(event.uid!!)
if (localEvent != null) {
localEvent.updateAsDirty(event)
result.updated++
} else {
localEvent = LocalEvent(localCalendar, event, event.uid, null)
localEvent.addAsDirty()
result.added++
}
} catch (e: CalendarStorageException) {
e.printStackTrace()
}
entryProcessed()
}
} else if (info.enumType == CollectionInfo.Type.TASKS) {
val tasks = Task.tasksFromReader(importReader)
importReader.close()
if (tasks.isEmpty()) {
Logger.log.warning("Empty/invalid file.")
result.e = Exception("Empty/invalid file.")
return result
}
result.total = tasks.size.toLong()
finishParsingFile(tasks.size)
val provider = TaskProviderHandling.getWantedTaskSyncProvider(requireContext())
.let {
if (it == null) {
result.e = Exception("Failed to acquire tasks content provider.")
null
} else {
TaskProvider.acquire(context, it)
}
}
provider?.let {
val localTaskList: LocalTaskList?
try {
localTaskList = LocalTaskList.findByName(account, it, LocalTaskList.Factory, info.uid!!)
if (localTaskList == null) {
throw FileNotFoundException("Failed to load local resource.")
}
} catch (e: FileNotFoundException) {
Logger.log.info("Fail" + e.localizedMessage)
result.e = e
return result
}
for (task in tasks) {
try {
var localTask = localTaskList.findByUid(task.uid!!)
if (localTask != null) {
localTask.updateAsDirty(task)
result.updated++
} else {
localTask = LocalTask(localTaskList, task, task.uid, null)
localTask.addAsDirty()
result.added++
}
} catch (e: CalendarStorageException) {
e.printStackTrace()
}
entryProcessed()
}
}
} else if (info.enumType == CollectionInfo.Type.ADDRESS_BOOK) {
val uidToLocalId = HashMap<String?, Long>()
val downloader = ContactsSyncManager.ResourceDownloader(context)
val contacts = Contact.fromReader(importReader, downloader)
if (contacts.isEmpty()) {
Logger.log.warning("Empty/invalid file.")
result.e = Exception("Empty/invalid file.")
return result
}
result.total = contacts.size.toLong()
finishParsingFile(contacts.size)
val provider = context.contentResolver.acquireContentProviderClient(ContactsContract.RawContacts.CONTENT_URI)
if (provider == null) {
result.e = Exception("Failed to acquire contacts content provider.")
return result
}
val localAddressBook = LocalAddressBook.findByUid(context, provider, account, info.uid!!)
if (localAddressBook == null) {
throw FileNotFoundException("Failed to load local address book.")
}
for (contact in contacts.filter { contact -> !contact.group }) {
try {
var localContact = localAddressBook.findByUid(contact.uid!!) as LocalContact?
if (localContact != null) {
localContact.updateAsDirty(contact)
result.updated++
} else {
localContact = LocalContact(localAddressBook, contact, contact.uid, null)
localContact.createAsDirty()
result.added++
}
uidToLocalId[contact.uid] = localContact.id!!
// Apply categories
val batch = BatchOperation(localAddressBook.provider!!)
for (category in contact.categories) {
localContact.addToGroup(batch, localAddressBook.findOrCreateGroup(category))
}
batch.commit()
} catch (e: ContactsStorageException) {
e.printStackTrace()
}
entryProcessed()
}
for (contact in contacts.filter { contact -> contact.group }) {
try {
val memberIds = contact.members.mapNotNull { memberUid ->
uidToLocalId[memberUid]
}
val group = contact
var localGroup: LocalGroup? = localAddressBook.findByUid(group.uid!!) as LocalGroup?
if (localGroup != null) {
localGroup.updateAsDirty(group, memberIds)
result.updated++
} else {
localGroup = LocalGroup(localAddressBook, group, group.uid, null)
localGroup.createAsDirty(memberIds)
result.added++
}
} catch (e: ContactsStorageException) {
e.printStackTrace()
}
entryProcessed()
}
provider.release()
}
return result
} catch (e: FileNotFoundException) {
result.e = e
return result
} catch (e: InvalidCalendarException) {
result.e = e
return result
} catch (e: IOException) {
result.e = e
return result
} catch (e: ContactsStorageException) {
result.e = e
return result
}
}
}
companion object {
private val REQUEST_CODE = 6384 // onActivityResult request
private val TAG_PROGRESS_MAX = "progressMax"
fun newInstance(account: Account, info: CollectionInfo): ImportFragment {
val frag = ImportFragment()
val args = Bundle(1)
args.putParcelable(KEY_ACCOUNT, account)
args.putSerializable(KEY_COLLECTION_INFO, info)
frag.arguments = args
return frag
}
}
}