diff --git a/app/build.gradle b/app/build.gradle index 97321b4..0289097 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId "ch.dissem.apps.${appName.toLowerCase()}" minSdkVersion 19 targetSdkVersion 27 - versionCode 20 - versionName "1.0-beta20" + versionCode 22 + versionName "1.0-rc1" multiDexEnabled true } compileOptions { @@ -62,7 +62,8 @@ dependencies { implementation "com.android.support:appcompat-v7:$supportVersion" implementation "com.android.support:preference-v7:$supportVersion" implementation "com.android.support:cardview-v7:$supportVersion" - implementation "com.android.support:support-v4:$supportVersion" + implementation "com.android.support:support-v13:$supportVersion" + implementation "com.android.support:preference-v14:$supportVersion" implementation "com.android.support:design:$supportVersion" implementation "com.android.support:multidex:1.0.2" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ddca7b0..cc7f881 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,32 +1,32 @@ - + - - - - - - - - - + + + + + + + + + + android:supportsRtl="true" + android:theme="@style/AppTheme"> - + - + + android:value=".MainActivity" /> + android:value=".MainActivity" /> + android:theme="@style/Theme.AppCompat.Light.Dialog" /> + android:value=".MainActivity" /> - + - - - + + + - + - + - + - + - + - + - + - + - - - + + + - - + + + android:value=".MainActivity" /> - + + android:scheme="file" /> - - + + + android:exported="false" /> + android:exported="false" /> + android:syncable="true" /> - + + android:resource="@xml/authenticator" /> - + + android:resource="@xml/syncadapter" /> + android:exported="false" /> - + - + + - + + + android:exported="true" + android:permission="android.permission.BIND_JOB_SERVICE" /> + android:value=".MainActivity" /> diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt index 9a4ea03..af66c9e 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt +++ b/app/src/main/java/ch/dissem/apps/abit/AddressDetailFragment.kt @@ -204,7 +204,7 @@ class AddressDetailFragment : Fragment() { * The fragment argument representing the item ID that this fragment * represents. */ - val ARG_ITEM = "item" - val EXPORT_POSTFIX = ".keys.dat" + const val ARG_ITEM = "item" + const val EXPORT_POSTFIX = ".keys.dat" } } diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt index dcea268..ea9828c 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt @@ -43,16 +43,19 @@ class ComposeMessageActivity : AppCompatActivity() { setHomeButtonEnabled(false) } - // Display the fragment as the main content. - val fragment = ComposeMessageFragment() - fragment.arguments = intent.extras - supportFragmentManager - .beginTransaction() - .replace(R.id.content, fragment) - .commit() + if (supportFragmentManager.findFragmentById(R.id.content) == null) { + // Display the fragment as the main content. + val fragment = ComposeMessageFragment() + fragment.arguments = intent.extras + supportFragmentManager + .beginTransaction() + .replace(R.id.content, fragment) + .commit() + } } companion object { + const val EXTRA_DRAFT = "ch.dissem.abit.Message.DRAFT" const val EXTRA_IDENTITY = "ch.dissem.abit.Message.SENDER" const val EXTRA_RECIPIENT = "ch.dissem.abit.Message.RECIPIENT" const val EXTRA_SUBJECT = "ch.dissem.abit.Message.SUBJECT" @@ -62,10 +65,13 @@ class ComposeMessageActivity : AppCompatActivity() { const val EXTRA_PARENT = "ch.dissem.abit.Message.PARENT" fun launchReplyTo(fragment: Fragment, item: Plaintext) = - fragment.startActivity(getReplyIntent( - ctx = fragment.activity ?: throw IllegalStateException("Fragment not attached to an activity"), - item = item - )) + fragment.startActivity( + getReplyIntent( + ctx = fragment.activity + ?: throw IllegalStateException("Fragment not attached to an activity"), + item = item + ) + ) fun launchReplyTo(activity: Activity, item: Plaintext) = activity.startActivity(getReplyIntent(activity, item)) @@ -89,15 +95,20 @@ class ComposeMessageActivity : AppCompatActivity() { } replyIntent.putExtra(EXTRA_PARENT, item) item.subject?.let { subject -> - val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals("RE:", ignoreCase = true)) { + val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals( + "RE:", + ignoreCase = true + )) { "" } else { "RE: " } replyIntent.putExtra(EXTRA_SUBJECT, prefix + subject) } - replyIntent.putExtra(EXTRA_CONTENT, - "\n\n------------------------------------------------------\n" + item.text!!) + replyIntent.putExtra( + EXTRA_CONTENT, + "\n\n------------------------------------------------------\n" + item.text!! + ) return replyIntent } } diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt index 75755ba..dce64fa 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageFragment.kt @@ -17,6 +17,7 @@ package ch.dissem.apps.abit import android.app.Activity.RESULT_OK +import android.content.Context import android.content.Intent import android.os.Bundle import android.support.v4.app.Fragment @@ -25,6 +26,7 @@ import android.widget.AdapterView import android.widget.Toast import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_CONTENT +import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_DRAFT import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_ENCODING import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_PARENT @@ -38,6 +40,9 @@ import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST import ch.dissem.bitmessage.entity.Plaintext.Type.MSG +import ch.dissem.bitmessage.entity.valueobject.ExtendedEncoding +import ch.dissem.bitmessage.entity.valueobject.InventoryVector +import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.extended.Message import kotlinx.android.synthetic.main.fragment_compose_message.* @@ -52,72 +57,108 @@ class ComposeMessageFragment : Fragment() { private var broadcast: Boolean = false private var encoding: Plaintext.Encoding = Plaintext.Encoding.SIMPLE - private var parent: Plaintext? = null + private val parents = mutableListOf() + + private var draft: Plaintext? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - arguments?.let { arguments -> - var id = arguments.getSerializable(EXTRA_IDENTITY) as? BitmessageAddress - if (context != null && (id == null || id.privateKey == null)) { - id = Singleton.getIdentity(context!!) - } - if (id?.privateKey != null) { - identity = id + retainInstance = true + arguments?.apply { + val draft = getSerializable(EXTRA_DRAFT) as Plaintext? + if (draft != null) { + this@ComposeMessageFragment.draft = draft + identity = draft.from + recipient = draft.to + subject = draft.subject ?: "" + content = draft.text ?: "" + encoding = draft.encoding ?: Plaintext.Encoding.SIMPLE + parents.addAll(draft.parents) } else { - throw IllegalStateException("No identity set for ComposeMessageFragment") - } - broadcast = arguments.getBoolean(EXTRA_BROADCAST, false) - if (arguments.containsKey(EXTRA_RECIPIENT)) { - recipient = arguments.getSerializable(EXTRA_RECIPIENT) as BitmessageAddress - } - if (arguments.containsKey(EXTRA_SUBJECT)) { - subject = arguments.getString(EXTRA_SUBJECT) - } - if (arguments.containsKey(EXTRA_CONTENT)) { - content = arguments.getString(EXTRA_CONTENT) - } - encoding = arguments.getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE + var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress + if (context != null && (id == null || id.privateKey == null)) { + id = Singleton.getIdentity(context!!) + } + if (id?.privateKey != null) { + identity = id + } else { + throw IllegalStateException("No identity set for ComposeMessageFragment") + } + broadcast = getBoolean(EXTRA_BROADCAST, false) + if (containsKey(EXTRA_RECIPIENT)) { + recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress + } + if (containsKey(EXTRA_SUBJECT)) { + subject = getString(EXTRA_SUBJECT) + } + if (containsKey(EXTRA_CONTENT)) { + content = getString(EXTRA_CONTENT) + } + encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: + Plaintext.Encoding.SIMPLE - if (arguments.containsKey(EXTRA_PARENT)) { - parent = arguments.getSerializable(EXTRA_PARENT) as Plaintext + if (containsKey(EXTRA_PARENT)) { + val parent = getSerializable(EXTRA_PARENT) as Plaintext + parent.inventoryVector?.let { parents.add(it) } + } } - } ?: { - throw IllegalStateException("No identity set for ComposeMessageFragment") - }.invoke() + } ?: throw IllegalStateException("No identity set for ComposeMessageFragment") + setHasOptionsMenu(true) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View = - inflater.inflate(R.layout.fragment_compose_message, container, false) + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.fragment_compose_message, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (broadcast) { - recipient_input.visibility = View.GONE - } else { - val adapter = ContactAdapter(context!!) - recipient_input.setAdapter(adapter) - recipient_input.onItemClickListener = AdapterView.OnItemClickListener { _, _, pos, _ -> adapter.getItem(pos) } - recipient_input.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { - recipient = adapter.getItem(position) - } - - override fun onNothingSelected(parent: AdapterView<*>) = Unit // leave current selection + context?.let { ctx -> + val identities = Singleton.getAddressRepository(ctx).getIdentities() + sender_input.adapter = ContactAdapter(ctx, identities, true) + val index = identities.indexOf(Singleton.getIdentity(ctx)) + if (index >= 0) { + sender_input.setSelection(index) } - recipient?.let { recipient_input.setText(it.toString()) } - } - subject_input.setText(subject) - body_input.setText(content) - when { - recipient == null -> recipient_input.requestFocus() - subject.isEmpty() -> subject_input.requestFocus() - else -> { - body_input.requestFocus() - body_input.setSelection(0) + if (broadcast) { + recipient_input.visibility = View.GONE + } else { + val adapter = ContactAdapter( + ctx, + Singleton.getAddressRepository(ctx).getContacts() + ) + recipient_input.setAdapter(adapter) + recipient_input.onItemClickListener = + AdapterView.OnItemClickListener { _, _, pos, _ -> adapter.getItem(pos) } + recipient_input.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>, + view: View, + position: Int, + id: Long + ) { + recipient = adapter.getItem(position) + } + + override fun onNothingSelected(parent: AdapterView<*>) = + Unit // leave current selection + } + recipient?.let { recipient_input.setText(it.toString()) } + } + subject_input.setText(subject) + body_input.setText(content) + + when { + recipient == null -> recipient_input.requestFocus() + subject.isEmpty() -> subject_input.requestFocus() + else -> { + body_input.requestFocus() + body_input.setSelection(0) + } } } } @@ -146,18 +187,17 @@ class ComposeMessageFragment : Fragment() { } } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = if (requestCode == 0 && data != null && resultCode == RESULT_OK) { - encoding = data.getSerializableExtra(EXTRA_ENCODING) as Plaintext.Encoding - } else { - super.onActivityResult(requestCode, resultCode, data) - } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) = + if (requestCode == 0 && data != null && resultCode == RESULT_OK) { + encoding = data.getSerializableExtra(EXTRA_ENCODING) as Plaintext.Encoding + } else { + super.onActivityResult(requestCode, resultCode, data) + } - private fun send() { + private fun build(ctx: Context): Plaintext { val builder: Plaintext.Builder - val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity") - val bmc = Singleton.getBitmessageContext(ctx) if (broadcast) { - builder = Plaintext.Builder(BROADCAST).from(identity) + builder = Plaintext.Builder(BROADCAST) } else { val inputString = recipient_input.text.toString() if (recipient == null || recipient?.toString() != inputString) { @@ -175,42 +215,77 @@ class ComposeMessageFragment : Fragment() { } } - if (recipient == null) { - Toast.makeText(context, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG).show() - return - } builder = Plaintext.Builder(MSG) - .from(identity) - .to(recipient) + .to(recipient) } + val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress + sender?.let { builder.from(it) } if (!Preferences.requestAcknowledgements(ctx)) { builder.preventAck() } when (encoding) { Plaintext.Encoding.SIMPLE -> builder.message( - subject_input.text.toString(), - body_input.text.toString() + subject_input.text.toString(), + body_input.text.toString() ) Plaintext.Encoding.EXTENDED -> builder.message( - Message.Builder() - .subject(subject_input.text.toString()) - .body(body_input.text.toString()) - .addParent(parent) - .build() + ExtendedEncoding( + Message( + subject = subject_input.text.toString(), + body = body_input.text.toString(), + parents = parents, + files = emptyList() + ) + ) ) else -> { Toast.makeText( - ctx, - ctx.getString(R.string.error_unsupported_encoding, encoding), - Toast.LENGTH_LONG + ctx, + ctx.getString(R.string.error_unsupported_encoding, encoding), + Toast.LENGTH_LONG ).show() builder.message( - subject_input.text.toString(), - body_input.text.toString() + subject_input.text.toString(), + body_input.text.toString() ) } } - bmc.send(builder.build()) + draft?.id?.let { builder.id(it) } + return builder.build() + } + + override fun onPause() { + if (draft?.labels?.any { it.type == Label.Type.DRAFT } != false) { + context?.let { ctx -> + draft = build(ctx).also { msg -> + Singleton.labeler.markAsDraft(msg) + Singleton.getMessageRepository(ctx).save(msg) + } + Toast.makeText(ctx, "Message saved as draft", Toast.LENGTH_LONG).show() + } ?: throw IllegalStateException("Context is not available") + } + super.onPause() + } + + override fun onDestroyView() { + identity = sender_input.selectedItem as BitmessageAddress + // recipient is set when one is selected + subject = subject_input.text?.toString() ?: "" + content = body_input.text?.toString() ?: "" + super.onDestroyView() + } + + private fun send() { + val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity") + if (recipient == null) { + Toast.makeText(ctx, R.string.error_msg_recipient_missing, Toast.LENGTH_LONG) + .show() + return + } + build(ctx).let { message -> + draft = message + Singleton.getBitmessageContext(ctx).send(message) + } ctx.finish() } } diff --git a/app/src/main/java/ch/dissem/apps/abit/Identicon.kt b/app/src/main/java/ch/dissem/apps/abit/Identicon.kt index 172c22d..595158f 100644 --- a/app/src/main/java/ch/dissem/apps/abit/Identicon.kt +++ b/app/src/main/java/ch/dissem/apps/abit/Identicon.kt @@ -92,7 +92,7 @@ class Identicon(input: BitmessageAddress) : Drawable() { override fun getOpacity() = PixelFormat.TRANSPARENT companion object { - private val SIZE = 9 - private val CENTER_COLUMN = 5 + private const val SIZE = 9 + private const val CENTER_COLUMN = 5 } } diff --git a/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.kt b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.kt index c65b3ed..fdce2b8 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.kt +++ b/app/src/main/java/ch/dissem/apps/abit/ImportIdentitiesFragment.kt @@ -25,12 +25,12 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button - -import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator - import ch.dissem.apps.abit.adapter.AddressSelectorAdapter import ch.dissem.apps.abit.service.Singleton import ch.dissem.bitmessage.wif.WifImporter +import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator +import org.ini4j.InvalidFileFormatException +import org.jetbrains.anko.longToast /** * @author Christian Basler @@ -39,8 +39,12 @@ class ImportIdentitiesFragment : Fragment() { private lateinit var adapter: AddressSelectorAdapter private lateinit var importer: WifImporter - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View = - inflater.inflate(R.layout.fragment_import_select_identities, container, false) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = + inflater.inflate(R.layout.fragment_import_select_identities, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -48,17 +52,29 @@ class ImportIdentitiesFragment : Fragment() { val wifData = arguments.getString(WIF_DATA) val bmc = Singleton.getBitmessageContext(activity) - importer = WifImporter(bmc, wifData) + try { + importer = WifImporter(bmc, wifData) + } catch (e: InvalidFileFormatException) { + longToast(R.string.invalid_wif_file) + activity.finish() + return + } + adapter = AddressSelectorAdapter(importer.getIdentities()) - val layoutManager = LinearLayoutManager(activity, - LinearLayoutManager.VERTICAL, - false) + val layoutManager = LinearLayoutManager( + activity, + LinearLayoutManager.VERTICAL, + false + ) val recyclerView = view.findViewById(R.id.recycler_view) recyclerView.layoutManager = layoutManager recyclerView.adapter = adapter - recyclerView.addItemDecoration(SimpleListDividerDecorator( - ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true)) + recyclerView.addItemDecoration( + SimpleListDividerDecorator( + ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true + ) + ) view.findViewById