10 Commits

Author SHA1 Message Date
6d7b77fd4b Update .drone.yml
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-08 14:55:02 +01:00
6b8066d473 Add .drone.yml
Some checks failed
continuous-integration/drone/push Build is failing
2021-12-08 14:41:36 +01:00
0405d9e04f 🤯 Update dependencies, switch to AndroidX 2018-10-15 20:00:52 +02:00
e67a4ea71b 🐛 Fix connectivity issue 2018-08-24 17:34:58 +02:00
a9602368fb 🎉 Separate messages by identity
Also, allow deleting all messages/conversations in a list
2018-08-24 17:32:45 +02:00
9f2508c1a5 Merge branch 'feature/fix-connectivity-issues' into develop 2018-08-21 07:39:57 +02:00
6128fd32f9 👻 Clean up services 2018-08-21 07:39:16 +02:00
ccfdff7fd8 😎 Make code more Kotliney 2018-07-11 16:59:50 +02:00
3767d976c8 🔥 Massively simplified how Abit connects to the network
* Removed features "Synchronization" and "Server POW"
* Service isn't foreground anymore (not yet sure this is a good decision)
* "Full node" renamed to "online"
2018-07-05 19:44:04 +02:00
87bc01701c ⬆️ Bump dependencies 2018-07-05 18:40:48 +02:00
113 changed files with 1448 additions and 2273 deletions

9
.drone.yml Normal file
View File

@ -0,0 +1,9 @@
kind: pipeline
name: default
steps:
- name: test
image: androidsdk/android-28
commands:
- ./gradlew assemble
- ./gradlew check

View File

@ -13,8 +13,8 @@ if (project.hasProperty("project.configs")
//noinspection GroovyMissingReturnStatement
android {
compileSdkVersion 27
buildToolsVersion "27.0.3"
compileSdkVersion 28
buildToolsVersion "28.0.3"
signingConfigs {
release
@ -22,7 +22,7 @@ android {
defaultConfig {
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
minSdkVersion 21
targetSdkVersion 27
targetSdkVersion 28
versionCode 23
versionName "1.0-rc1"
multiDexEnabled true
@ -62,13 +62,16 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.anko:anko:$anko_version"
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-v13:$supportVersion"
implementation "com.android.support:preference-v14:$supportVersion"
implementation "com.android.support:design:$supportVersion"
implementation "com.android.support:multidex:1.0.3"
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.multidex:multidex:2.0.0'
implementation 'androidx.core:core-ktx:1.0.0'
implementation 'androidx.sqlite:sqlite-ktx:2.0.0-rc01'
implementation 'androidx.fragment:fragment-ktx:1.0.0'
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
@ -80,38 +83,39 @@ dependencies {
implementation 'org.slf4j:slf4j-android:1.7.25'
implementation 'com.mikepenz:materialize:1.1.2@aar'
implementation('com.mikepenz:materialdrawer:6.0.6@aar') {
implementation 'com.mikepenz:materialize:1.2.0-rc01@aar'
implementation('com.mikepenz:materialdrawer:6.1.0-rc01.2@aar') {
transitive = true
}
implementation('com.mikepenz:aboutlibraries:6.0.6@aar') {
implementation('com.mikepenz:aboutlibraries:6.2.0-rc01@aar') {
transitive = true
}
implementation "com.mikepenz:iconics-core:3.0.3@aar"
implementation "com.mikepenz:iconics-views:3.0.3@aar"
implementation "com.mikepenz:iconics-core:3.1.0-rc01@aar"
implementation "com.mikepenz:iconics-views:3.1.0-rc01@aar"
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
implementation 'com.journeyapps:zxing-android-embedded:3.6.0@aar'
implementation 'com.google.zxing:core:3.3.2'
implementation 'com.google.zxing:core:3.3.3'
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.0'
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0@aar'
implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') {
transitive = true
}
implementation 'com.github.angads25:filepicker:1.1.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-alpha2'
implementation "io.reactivex.rxjava2:rxjava:2.2.2"
implementation "io.reactivex.rxjava2:rxkotlin:2.3.0"
implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
testImplementation 'junit:junit:4.12'
testImplementation 'org.mockito:mockito-core:2.15.0'
testImplementation 'org.mockito:mockito-core:2.19.0'
testImplementation 'org.hamcrest:hamcrest-library:1.3'
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.6.0'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'org.robolectric:robolectric:3.7.1'
testImplementation "org.robolectric:shadows-multidex:3.7.1"
androidTestImplementation "com.android.support:multidex:1.0.3"
androidTestImplementation "androidx.multidex:multidex:2.0.0"
}
idea.module {

View File

@ -11,11 +11,10 @@
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name="android.support.multidex.MultiDexApplication"
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@ -120,23 +119,13 @@
</intent-filter>
</activity>
<service
android:name=".service.BitmessageService"
android:exported="false" />
<service
android:name=".service.ProofOfWorkService"
android:exported="false" />
<!-- Synchronization -->
<provider
android:name=".synchronization.StubProvider"
android:authorities="ch.dissem.apps.abit.provider"
android:exported="false"
android:syncable="true" />
<!-- Exports -->
<provider
android:name="android.support.v4.content.FileProvider"
android:name="androidx.core.content.FileProvider"
android:authorities="ch.dissem.apps.abit.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
@ -145,30 +134,6 @@
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".synchronization.AuthenticatorService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>
<service
android:name=".synchronization.SyncService"
android:exported="true"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/syncadapter" />
</service>
<service
android:name=".service.BitmessageIntentService"
android:exported="false" />
@ -182,7 +147,12 @@
</receiver>
<service
android:name=".service.StartupNodeOnWifiService"
android:name=".service.NodeStartupService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".service.CleanupService"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />

View File

@ -18,7 +18,7 @@ package ch.dissem.apps.abit
import android.content.Context
import android.os.Bundle
import android.support.v4.app.ListFragment
import androidx.fragment.app.ListFragment
import android.view.View
import android.widget.ListView

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import androidx.fragment.app.Fragment
import android.text.Editable
import android.text.TextWatcher
import android.view.*

View File

@ -28,7 +28,6 @@ import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.BitmessageAddress
import com.google.zxing.integration.android.IntentIntegrator
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
@ -44,7 +43,7 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
super.onCreate(savedInstanceState)
adapter = object : ArrayAdapter<BitmessageAddress>(
activity,
activity!!,
R.layout.subscription_row,
R.id.name,
LinkedList()
@ -85,10 +84,10 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
super.onResume()
initFab(activity as MainActivity)
updateList()
reloadList()
}
fun updateList() {
override fun reloadList() {
adapter.clear()
context?.let { context ->
val addressRepo = Singleton.getAddressRepository(context)
@ -109,9 +108,10 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
activity.initFab(R.drawable.ic_action_add_contact, menu)
.addOnMenuItemClickListener { _, _, itemId ->
when (itemId) {
1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
.initiateScan()
// FIXME
// 1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
// .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
// .initiateScan()
2 -> {
val intent = Intent(getActivity(), CreateAddressActivity::class.java)
startActivity(intent)
@ -138,7 +138,7 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
}
}
override fun updateList(label: Void) = updateList()
override fun updateList(label: Void) = reloadList()
private data class ViewHolder(
val ctx: Context,

View File

@ -20,8 +20,8 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.appcompat.app.AppCompatActivity
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Encoding.EXTENDED

View File

@ -20,7 +20,7 @@ import android.app.Activity.RESULT_OK
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import androidx.fragment.app.Fragment
import android.view.*
import android.widget.AdapterView
import android.widget.Toast
@ -35,7 +35,7 @@ import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_SUBJECT
import ch.dissem.apps.abit.adapter.ContactAdapter
import ch.dissem.apps.abit.dialog.SelectEncodingDialogFragment
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.Plaintext.Type.BROADCAST
@ -76,7 +76,7 @@ class ComposeMessageFragment : Fragment() {
parents.addAll(draft.parents)
} else {
var id = getSerializable(EXTRA_IDENTITY) as? BitmessageAddress
if (context != null && (id == null || id.privateKey == null)) {
if (context != null && id?.privateKey == null) {
id = Singleton.getIdentity(context!!)
}
if (id?.privateKey != null) {
@ -89,13 +89,12 @@ class ComposeMessageFragment : Fragment() {
recipient = getSerializable(EXTRA_RECIPIENT) as BitmessageAddress
}
if (containsKey(EXTRA_SUBJECT)) {
subject = getString(EXTRA_SUBJECT)
subject = getString(EXTRA_SUBJECT) ?: throw IllegalStateException("EXTRA_SUBJECT expected")
}
if (containsKey(EXTRA_CONTENT)) {
content = getString(EXTRA_CONTENT)
content = getString(EXTRA_CONTENT) ?: throw IllegalStateException("EXTRA_CONTENT expected")
}
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?:
Plaintext.Encoding.SIMPLE
encoding = getSerializable(EXTRA_ENCODING) as? Plaintext.Encoding ?: Plaintext.Encoding.SIMPLE
if (containsKey(EXTRA_PARENT)) {
val parent = getSerializable(EXTRA_PARENT) as Plaintext
@ -221,7 +220,7 @@ class ComposeMessageFragment : Fragment() {
}
val sender = sender_input.selectedItem as? ch.dissem.bitmessage.entity.BitmessageAddress
sender?.let { builder.from(it) }
if (!Preferences.requestAcknowledgements(ctx)) {
if (!ctx.preferences.requestAcknowledgements) {
builder.preventAck()
}
when (encoding) {

View File

@ -17,8 +17,8 @@
package ch.dissem.apps.abit
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v7.widget.LinearLayoutManager
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import android.view.*
import ch.dissem.apps.abit.adapter.ConversationAdapter
import ch.dissem.apps.abit.service.Singleton
@ -74,8 +74,7 @@ class ConversationDetailFragment : Fragment() {
item?.let { item ->
subject.text = item.subject
avatar.setImageDrawable(MultiIdenticon(item.participants))
messages.adapter =
ConversationAdapter(ctx, this@ConversationDetailFragment, item, Singleton.currentLabel.value)
messages.adapter = ConversationAdapter(ctx, this@ConversationDetailFragment, item, Singleton.currentLabel)
messages.layoutManager = LinearLayoutManager(activity)
}
}

View File

@ -19,33 +19,29 @@ package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.RecyclerView.OnScrollListener
import android.view.*
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
import ch.dissem.apps.abit.adapter.EventListener
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
import ch.dissem.apps.abit.adapter.SwipeableConversationAdapter
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.utils.ConversationService
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread
import org.jetbrains.anko.*
import java.util.*
private const val PAGE_SIZE = 15
@ -67,12 +63,9 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
private var layoutManager: LinearLayoutManager? = null
private var swipeableConversationAdapter: SwipeableConversationAdapter? = null
private var wrappedAdapter: RecyclerView.Adapter<*>? = null
private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null
private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = null
private val recyclerViewOnScrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
layoutManager?.let { layoutManager ->
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
@ -90,10 +83,18 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
}
private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository
private lateinit var conversationService: ConversationService
private var activateOnItemClick: Boolean = false
private var subscription: Disposable? = null
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableConversationAdapter?.activateOnItemClick = activateOnItemClick
this.activateOnItemClick = activateOnItemClick
}
private val backStack = Stack<Label>()
fun loadMoreItems() {
@ -103,11 +104,12 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
val conversationIds = messageRepo.findConversations(
currentLabel.value,
messageAdapter.itemCount,
PAGE_SIZE
PAGE_SIZE,
context?.preferences?.separateIdentities == true
)
conversationIds.forEach { conversationId ->
val conversation = conversationService.getConversation(conversationId)
onUiThread {
uiThread {
messageAdapter.add(conversation)
}
}
@ -130,15 +132,17 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
messageRepo = Singleton.getMessageRepository(activity)
conversationService = Singleton.getConversationService(activity)
currentLabel.addObserver(this) { new -> doUpdateList(new) }
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
doUpdateList(currentLabel.value)
}
override fun onPause() {
currentLabel.removeObserver(this)
subscription?.dispose()
super.onPause()
}
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) {
val mainActivity = activity as? MainActivity
swipeableConversationAdapter?.clear(label)
@ -148,7 +152,10 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
return
}
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
mainActivity?.apply {
// I'm not yet sure if it's a good idea in conversation views, so it's off for now
deleteAllMenuItem?.isVisible = false
MainActivity.apply {
if ("archive" == label.toString()) {
updateTitle(getString(R.string.archive))
} else {
@ -171,75 +178,57 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
val context = context ?: throw IllegalStateException("No context available")
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
// animation is running)
val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply {
setInterceptVerticalScrollingWhileAnimationRunning(true)
isEnabled = true
}
// swipe manager
val swipeManager = RecyclerViewSwipeManager()
//swipeableConversationAdapter
val adapter = SwipeableConversationAdapter(context).apply {
setActivateOnItemClick(activateOnItemClick)
}
adapter.eventListener = object : SwipeableConversationAdapter.EventListener {
override fun onItemDeleted(item: Conversation) {
val listener = object : EventListener {
override fun onItemDeleted(position: Int) {
swipeableConversationAdapter?.getItem(position)?.let { item ->
item.messages.forEach {
Singleton.labeler.delete(it)
messageRepo.save(it)
}
}
override fun onItemArchived(item: Conversation) {
item.messages.forEach { Singleton.labeler.archive(it) }
swipeableConversationAdapter?.removeAt(position)
}
override fun onItemViewClicked(v: View?) {
val position = recycler_view.getChildAdapterPosition(v)
adapter.setSelectedPosition(position)
override fun onItemArchived(position: Int) {
swipeableConversationAdapter?.getItem(position)?.let { item ->
item.messages.forEach {
Singleton.labeler.archive(it)
messageRepo.save(it)
}
}
swipeableConversationAdapter?.removeAt(position)
}
override fun onItemSelected(position: Int) {
swipeableConversationAdapter?.selectedPosition = position
if (position != RecyclerView.NO_POSITION) {
MainActivity.apply { onItemSelected(adapter.getItem(position)) }
swipeableConversationAdapter?.getItem(position)?.let { item ->
MainActivity.apply { onItemSelected(item) }
}
}
}
// wrap for swiping
wrappedAdapter = swipeManager.createWrappedAdapter(adapter)
val animator = SwipeDismissItemAnimator()
// Change animations are enabled by default since support-v7-recyclerview v22.
// Disable the change animation in order to make turning back animation of swiped item
// works properly.
animator.supportsChangeAnimations = false
}
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recycler_view.layoutManager = layoutManager
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableConversationAdapter
recycler_view.itemAnimator = animator
swipeableConversationAdapter = SwipeableConversationAdapter(context).apply {
activateOnItemClick = this@ConversationListFragment.activateOnItemClick
eventListener = listener
}
recycler_view.adapter = swipeableConversationAdapter
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
recycler_view.addItemDecoration(
SimpleListDividerDecorator(
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true
)
)
val dirs = when (currentLabel.value?.type) {
Label.Type.TRASH -> ItemTouchHelper.LEFT
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
}
// NOTE:
// The initialization order is very important! This order determines the priority of
// touch event handling.
//
// priority: TouchActionGuard > Swipe > DragAndDrop
touchActionGuardManager.attachRecyclerView(recycler_view)
swipeManager.attachRecyclerView(recycler_view)
val swipeHandler = SwipeToDeleteCallback(context, dirs, listener)
recyclerViewTouchActionGuardManager = touchActionGuardManager
recyclerViewSwipeManager = swipeManager
swipeableConversationAdapter = adapter
val itemTouchHelper = ItemTouchHelper(swipeHandler)
itemTouchHelper.attachToRecyclerView(recycler_view)
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
}
@ -277,18 +266,6 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
}
override fun onDestroyView() {
recyclerViewSwipeManager?.release()
recyclerViewSwipeManager = null
recyclerViewTouchActionGuardManager?.release()
recyclerViewTouchActionGuardManager = null
recycler_view.itemAnimator = null
recycler_view.adapter = null
wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) }
wrappedAdapter = null
swipeableConversationAdapter = null
layoutManager = null
@ -298,6 +275,7 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
super.onCreateOptionsMenu(menu, inflater)
}
@ -307,12 +285,22 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true
doAsync {
for (message in messageRepo.findMessages(label)) {
messageRepo.remove(message)
deleteAllMessages(label)
}
uiThread { doUpdateList(label) }
return true
}
R.id.delete_all -> {
currentLabel.value?.let { label ->
context?.apply {
alert(
R.string.delete_all_messages_in_list,
R.string.delete_all_messages_in_list_ask
) {
positiveButton(R.string.delete) {
deleteAllMessages(label)
}
cancelButton { }
}.show()
}
}
return true
@ -321,19 +309,24 @@ class ConversationListFragment : Fragment(), ListHolder<Label> {
}
}
override fun updateList(label: Label) {
currentLabel.value = label
private fun deleteAllMessages(label: Label) {
doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
}
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableConversationAdapter?.setActivateOnItemClick(activateOnItemClick)
this.activateOnItemClick = activateOnItemClick
uiThread { doUpdateList(label) }
}
}
override fun updateList(label: Label) {
currentLabel.onNext(label)
}
override fun showPreviousList() = if (backStack.isEmpty()) {
false
} else {
currentLabel.value = backStack.pop()
currentLabel.onNext(backStack.pop())
true
}
}

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit
import android.app.Activity
import android.net.Uri
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import androidx.appcompat.app.AppCompatActivity
import android.util.Base64
import android.util.Base64.URL_SAFE
import android.widget.Button

View File

@ -2,8 +2,8 @@ package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.NavUtils
import android.support.v7.app.AppCompatActivity
import androidx.core.app.NavUtils
import androidx.appcompat.app.AppCompatActivity
import android.view.MenuItem
import com.mikepenz.materialize.MaterializeBuilder
import kotlinx.android.synthetic.main.scrolling_toolbar_layout.*

View File

@ -18,7 +18,7 @@ package ch.dissem.apps.abit
import android.graphics.*
import android.graphics.drawable.Drawable
import android.support.annotation.ColorInt
import androidx.annotation.ColorInt
import android.text.TextPaint
import ch.dissem.bitmessage.entity.BitmessageAddress
import org.jetbrains.anko.collections.forEachWithIndex
@ -68,8 +68,8 @@ class Identicon(input: BitmessageAddress) : Drawable() {
}
override fun draw(canvas: Canvas) {
val width = canvas.width.toFloat()
val height = canvas.height.toFloat()
val width = bounds.width().toFloat()
val height = bounds.height().toFloat()
draw(canvas, 0f, 0f, width, height)
}
@ -124,11 +124,11 @@ class MultiIdenticon(input: List<BitmessageAddress>, @ColorInt private val backg
color = backgroundColor
}
private val identicons = input.sortedBy { it.isChan }.map { Identicon(it) }.take(4)
private val identicons = input.asSequence().sortedBy { it.isChan }.map { Identicon(it) }.take(4).toList()
override fun draw(canvas: Canvas) {
val width = canvas.width.toFloat()
val height = canvas.height.toFloat()
val width = bounds.width().toFloat()
val height = bounds.height().toFloat()
when (identicons.size) {
0 -> canvas.drawCircle(width / 2, height / 2, width / 2, paint)

View File

@ -16,19 +16,18 @@
package ch.dissem.apps.abit
import android.app.Fragment
import android.os.Bundle
import android.support.v4.content.ContextCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
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
@ -47,34 +46,31 @@ class ImportIdentitiesFragment : Fragment() {
inflater.inflate(R.layout.fragment_import_select_identities, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val ctx = activity ?: throw IllegalStateException("No activity available")
super.onViewCreated(view, savedInstanceState)
val wifData = arguments.getString(WIF_DATA)
val bmc = Singleton.getBitmessageContext(activity)
val wifData = arguments?.getString(WIF_DATA) ?: throw IllegalStateException("No WIF data")
val bmc = Singleton.getBitmessageContext(ctx)
try {
importer = WifImporter(bmc, wifData)
} catch (e: InvalidFileFormatException) {
longToast(R.string.invalid_wif_file)
activity.finish()
ctx.longToast(R.string.invalid_wif_file)
ctx.finish()
return
}
adapter = AddressSelectorAdapter(importer.getIdentities())
val layoutManager = LinearLayoutManager(
activity,
LinearLayoutManager.VERTICAL,
RecyclerView.VERTICAL,
false
)
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = adapter
recyclerView.addItemDecoration(
SimpleListDividerDecorator(
ContextCompat.getDrawable(activity, R.drawable.list_divider_h), true
)
)
recyclerView.addItemDecoration(DividerItemDecoration(ctx, DividerItemDecoration.HORIZONTAL))
view.findViewById<Button>(R.id.finish).setOnClickListener {
importer.importAll(adapter.selected)
@ -83,7 +79,7 @@ class ImportIdentitiesFragment : Fragment() {
addIdentityEntry(selected)
}
}
activity.finish()
ctx.finish()
}
}

View File

@ -17,6 +17,7 @@
package ch.dissem.apps.abit
import android.os.Bundle
import androidx.fragment.app.transaction
/**
* @author Christian Basler
@ -29,19 +30,20 @@ class ImportIdentityActivity : DetailActivity() {
val wifData: String? = savedInstanceState?.getString(ImportIdentitiesFragment.WIF_DATA)
if (wifData == null) {
fragmentManager.beginTransaction()
.replace(R.id.content, InputWifFragment())
.commit()
supportFragmentManager.transaction {
replace(R.id.content, InputWifFragment())
}
} else {
val bundle = Bundle()
bundle.putString(ImportIdentitiesFragment.WIF_DATA, wifData)
val fragment = ImportIdentitiesFragment()
fragment.arguments = bundle
val fragment = ImportIdentitiesFragment().apply {
arguments = bundle
}
fragmentManager.beginTransaction()
.replace(R.id.content, fragment)
.commit()
supportFragmentManager.transaction {
replace(R.id.content, fragment)
}
}
}

View File

@ -16,10 +16,11 @@
package ch.dissem.apps.abit
import android.app.Fragment
import android.os.Bundle
import android.view.*
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.transaction
import com.github.angads25.filepicker.model.DialogConfigs
import com.github.angads25.filepicker.model.DialogProperties
import com.github.angads25.filepicker.view.FilePickerDialog
@ -42,7 +43,7 @@ class InputWifFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.fragment_import_input, container, false)
override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
next.setOnClickListener {
val bundle = Bundle()
@ -52,9 +53,9 @@ class InputWifFragment : Fragment() {
arguments = bundle
}
fragmentManager.beginTransaction()
.replace(R.id.content, fragment)
.commit()
fragmentManager?.transaction {
replace(R.id.content, fragment)
}
}
}

View File

@ -20,6 +20,8 @@ package ch.dissem.apps.abit
* @author Christian Basler
*/
interface ListHolder<in L> {
fun reloadList()
fun updateList(label: L)
fun setActivateOnItemClick(activateOnItemClick: Boolean)

View File

@ -20,22 +20,23 @@ import android.content.Intent
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Bundle
import android.support.annotation.DrawableRes
import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar
import android.view.View
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.transaction
import ch.dissem.apps.abit.drawer.ProfileImageListener
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.getColor
import ch.dissem.apps.abit.util.getIcon
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Conversation
@ -54,6 +55,7 @@ import com.mikepenz.materialdrawer.model.interfaces.IProfile
import com.mikepenz.materialdrawer.model.interfaces.Nameable
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
@ -95,7 +97,10 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
var hasDetailPane: Boolean = false
private set
private var subscription: Disposable? = null
private lateinit var bmc: BitmessageContext
private lateinit var messageRepo: AndroidMessageRepository
private lateinit var accountHeader: AccountHeader
private lateinit var drawer: Drawer
@ -108,6 +113,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
super.onCreate(savedInstanceState)
instance = WeakReference(this)
bmc = Singleton.getBitmessageContext(this)
messageRepo = Singleton.getMessageRepository(this)
setContentView(R.layout.activity_main)
fab.hide()
@ -145,11 +151,6 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
ComposeMessageActivity.launchReplyTo(this, item)
}
if (Preferences.useTrustedNode(this)) {
SyncAdapter.startSync(this)
} else {
SyncAdapter.stopSync(this)
}
if (drawer.isDrawerOpen) {
MaterialShowcaseView.Builder(this)
.setMaskColour(R.color.colorPrimary)
@ -179,8 +180,6 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
.setDelay(1000)
.show()
}
SyncAdapter.startSync(this)
}
private fun <F> changeList(listFragment: F) where F : Fragment, F : ListHolder<*> {
@ -259,14 +258,13 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
nodeSwitch = SwitchDrawerItem()
.withIdentifier(ID_NODE_SWITCH)
.withName(R.string.full_node)
.withName(R.string.online)
.withIcon(CommunityMaterial.Icon.cmd_cloud_outline)
.withChecked(Preferences.isFullNodeActive(this))
.withChecked(preferences.online)
.withOnCheckedChangeListener { _, _, isChecked ->
preferences.online = isChecked
if (isChecked) {
NetworkUtils.enableNode(this@MainActivity)
} else {
NetworkUtils.disableNode(this@MainActivity)
network.enableNode(true)
}
}
@ -303,16 +301,16 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
uiThread {
if (intent.hasExtra(EXTRA_SHOW_LABEL)) {
currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label
currentLabel.onNext(intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label)
} else if (currentLabel.value == null) {
currentLabel.value = labels[0]
currentLabel.onNext(labels[0])
}
for (label in labels) {
addLabelEntry(label)
}
currentLabel.value?.let {
drawer.setSelection(it.id as Long)
currentLabel.value?.let { label ->
drawer.setSelection(label.id as Long)
}
updateUnread()
}
@ -331,7 +329,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
val tag = item.tag
if (tag is Label) {
currentLabel.value = tag
currentLabel.onNext(tag)
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
if (itemList !is ConversationListFragment) {
changeList(ConversationListFragment())
@ -346,18 +344,17 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
when (item.name.textRes) {
R.string.contacts_and_subscriptions -> {
if (itemList is AddressListFragment) {
itemList.updateList()
itemList.reloadList()
} else {
changeList(AddressListFragment())
}
return false
}
R.string.settings -> {
supportFragmentManager
.beginTransaction()
.replace(R.id.item_list, SettingsFragment())
.addToBackStack(null)
.commit()
supportFragmentManager?.transaction {
replace(R.id.item_list, SettingsFragment())
addToBackStack(null)
}
return false
}
R.string.full_node -> return true
@ -369,14 +366,12 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
}
override fun onResume() {
network.enableNode(false)
updateUnread()
if (Preferences.isFullNodeActive(this) && Preferences.isConnectionAllowed(this@MainActivity)) {
NetworkUtils.enableNode(this, false)
}
Singleton.getMessageListener(this).resetNotification()
currentLabel.addObserver(this) { label ->
if (label != null && label.id is Long) {
drawer.setSelection(label.id as Long)
subscription = currentLabel.subscribe { label ->
if (label.id is Long) {
drawer.setSelection(label.id as Long, false)
}
}
active = true
@ -384,7 +379,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
}
override fun onPause() {
currentLabel.removeObserver(this)
subscription?.dispose()
super.onPause()
active = false
}
@ -446,7 +441,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
if (item.tag is Label) {
val label = item.tag as Label
if (label !== LABEL_ARCHIVE) {
val unread = bmc.messages.countUnread(label)
val unread = messageRepo.countUnread(label, preferences.separateIdentities)
if (unread > 0) {
(item as PrimaryDrawerItem).withBadge(unread.toString())
} else {
@ -548,9 +543,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
fun initFab(@DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
val fab = floatingActionButton ?: throw IllegalStateException("Fab must not be null")
fab.hide()
fab.removeAllOnMenuItemClickListeners()
fab.show()
fab.closeMenu()
val mainFab = fab.mainFab
mainFab.setImageResource(drawableRes)
fab.setMenu(menu)
@ -562,6 +556,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
mainFab.setImageResource(drawableRes)
}
}
fab.show()
fab.closeMenu()
return fab
}
@ -578,15 +574,6 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
private var instance: WeakReference<MainActivity>? = null
fun updateNodeSwitch() {
apply {
runOnUiThread {
nodeSwitch.withChecked(Preferences.isFullNodeActive(this))
drawer.updateStickyFooterItem(nodeSwitch)
}
}
}
/**
* Runs the given code in the main activity context, if it currently exists. Otherwise,
* it's ignored.

View File

@ -2,7 +2,7 @@ package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.NavUtils
import androidx.core.app.NavUtils
import android.view.MenuItem
import ch.dissem.bitmessage.entity.Plaintext

View File

@ -19,11 +19,11 @@ package ch.dissem.apps.abit
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.support.annotation.IdRes
import android.support.v4.app.Fragment
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import androidx.annotation.IdRes
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.text.util.Linkify
import android.text.util.Linkify.WEB_URLS
import android.view.*

View File

@ -19,32 +19,28 @@ package ch.dissem.apps.abit
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.RecyclerView.OnScrollListener
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import android.view.*
import android.widget.Toast
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
import ch.dissem.apps.abit.adapter.EventListener
import ch.dissem.apps.abit.adapter.SwipeToDeleteCallback
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.listener.ListSelectionListener
import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
import com.h6ah4i.android.widget.advrecyclerview.decoration.SimpleListDividerDecorator
import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager
import com.h6ah4i.android.widget.advrecyclerview.touchguard.RecyclerViewTouchActionGuardManager
import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import io.reactivex.disposables.Disposable
import kotlinx.android.synthetic.main.fragment_message_list.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.onUiThread
import org.jetbrains.anko.uiThread
import org.jetbrains.anko.*
import java.util.*
private const val PAGE_SIZE = 15
@ -64,21 +60,20 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
private var isLoading = false
private var isLastPage = false
private var subscription: Disposable? = null
private var layoutManager: LinearLayoutManager? = null
private var swipeableMessageAdapter: SwipeableMessageAdapter? = null
private var wrappedAdapter: RecyclerView.Adapter<*>? = null
private var recyclerViewSwipeManager: RecyclerViewSwipeManager? = null
private var recyclerViewTouchActionGuardManager: RecyclerViewTouchActionGuardManager? = null
private val recyclerViewOnScrollListener = object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
layoutManager?.let { layoutManager ->
val visibleItemCount = layoutManager.childCount
val totalItemCount = layoutManager.itemCount
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - PAGE_SIZE
&& firstVisibleItemPosition >= 0
) {
loadMoreItems()
@ -89,21 +84,29 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
}
private var emptyTrashMenuItem: MenuItem? = null
private var deleteAllMenuItem: MenuItem? = null
private lateinit var messageRepo: AndroidMessageRepository
private var activateOnItemClick: Boolean = false
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableMessageAdapter?.activateOnItemClick = activateOnItemClick
this.activateOnItemClick = activateOnItemClick
}
private val backStack = Stack<Label>()
fun loadMoreItems() {
isLoading = true
swipeableMessageAdapter?.let { messageAdapter ->
doAsync {
val label = currentLabel.value
val messages = messageRepo.findMessages(
currentLabel.value,
label,
messageAdapter.itemCount,
PAGE_SIZE
PAGE_SIZE,
context?.preferences?.separateIdentities == true && label?.type != Label.Type.BROADCAST
)
onUiThread {
uiThread {
messageAdapter.addAll(messages)
isLoading = false
isLastPage = messages.size < PAGE_SIZE
@ -124,16 +127,21 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
initFab(activity)
messageRepo = Singleton.getMessageRepository(activity)
currentLabel.addObserver(this) { new -> doUpdateList(new) }
subscription = currentLabel.subscribe { new -> doUpdateList(new) }
doUpdateList(currentLabel.value)
}
override fun onPause() {
currentLabel.removeObserver(this)
subscription?.dispose()
super.onPause()
}
override fun reloadList() = doUpdateList(currentLabel.value)
private fun doUpdateList(label: Label?) {
// If the menu item isn't available yet, we should wait - the method will be called again once it's
// initialized.
emptyTrashMenuItem?.let { menuItem ->
val mainActivity = activity as? MainActivity
swipeableMessageAdapter?.clear(label)
if (label == null) {
@ -141,8 +149,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
swipeableMessageAdapter?.notifyDataSetChanged()
return
}
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
mainActivity?.apply {
menuItem.isVisible = label.type == Label.Type.TRASH
MainActivity.apply {
if ("archive" == label.toString()) {
updateTitle(getString(R.string.archive))
} else {
@ -152,6 +160,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
loadMoreItems()
}
deleteAllMenuItem?.isVisible = label?.type != Label.Type.TRASH
}
override fun onCreateView(
inflater: LayoutInflater,
@ -165,81 +175,37 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
val context = context ?: throw IllegalStateException("No context available")
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
// touch guard manager (this class is required to suppress scrolling while swipe-dismiss
// animation is running)
val touchActionGuardManager = RecyclerViewTouchActionGuardManager().apply {
setInterceptVerticalScrollingWhileAnimationRunning(true)
isEnabled = true
}
// swipe manager
val swipeManager = RecyclerViewSwipeManager()
//swipeableMessageAdapter
val adapter = SwipeableMessageAdapter().apply {
setActivateOnItemClick(activateOnItemClick)
}
adapter.eventListener = object : SwipeableMessageAdapter.EventListener {
override fun onItemDeleted(item: Plaintext) {
if (MessageDetailFragment.isInTrash(item)) {
Singleton.labeler.delete(item)
messageRepo.remove(item)
} else {
Singleton.labeler.delete(item)
messageRepo.save(item)
}
}
override fun onItemArchived(item: Plaintext) {
Singleton.labeler.archive(item)
}
override fun onItemViewClicked(v: View?) {
val position = recycler_view.getChildAdapterPosition(v)
adapter.setSelectedPosition(position)
if (position != RecyclerView.NO_POSITION) {
val item = adapter.getItem(position)
MainActivity.apply { onItemSelected(item) }
}
}
}
// wrap for swiping
wrappedAdapter = swipeManager.createWrappedAdapter(adapter)
val animator = SwipeDismissItemAnimator()
// Change animations are enabled by default since support-v7-recyclerview v22.
// Disable the change animation in order to make turning back animation of swiped item
// works properly.
animator.supportsChangeAnimations = false
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
recycler_view.layoutManager = layoutManager
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableMessageAdapter
recycler_view.itemAnimator = animator
swipeableMessageAdapter = SwipeableMessageAdapter(context).apply {
activateOnItemClick = this@MessageListFragment.activateOnItemClick
}
recycler_view.adapter = swipeableMessageAdapter // requires *wrapped* swipeableMessageAdapter
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
recycler_view.addItemDecoration(
SimpleListDividerDecorator(
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true
)
)
val dirs = when (currentLabel.value?.type) {
Label.Type.TRASH -> ItemTouchHelper.LEFT
else -> ItemTouchHelper.LEFT + ItemTouchHelper.RIGHT
}
// NOTE:
// The initialization order is very important! This order determines the priority of
// touch event handling.
//
// priority: TouchActionGuard > Swipe > DragAndDrop
touchActionGuardManager.attachRecyclerView(recycler_view)
swipeManager.attachRecyclerView(recycler_view)
val swipeHandler = SwipeToDeleteCallback(context, dirs, object : EventListener {
override fun onItemDeleted(position: Int) {
context.toast("Deleted")
}
recyclerViewTouchActionGuardManager = touchActionGuardManager
recyclerViewSwipeManager = swipeManager
swipeableMessageAdapter = adapter
override fun onItemArchived(position: Int) {
context.toast("Archived")
}
Singleton.updateMessageListAdapterInListener(adapter)
override fun onItemSelected(position: Int) {
context.toast("Selected")
}
})
val itemTouchHelper = ItemTouchHelper(swipeHandler)
itemTouchHelper.attachToRecyclerView(recycler_view)
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
}
private fun initFab(context: MainActivity) {
@ -275,18 +241,6 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
}
override fun onDestroyView() {
recyclerViewSwipeManager?.release()
recyclerViewSwipeManager = null
recyclerViewTouchActionGuardManager?.release()
recyclerViewTouchActionGuardManager = null
recycler_view.itemAnimator = null
recycler_view.adapter = null
wrappedAdapter?.let { WrapperAdapterUtils.releaseAll(it) }
wrappedAdapter = null
swipeableMessageAdapter = null
layoutManager = null
@ -296,6 +250,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.message_list, menu)
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
deleteAllMenuItem = menu.findItem(R.id.delete_all)
// currentLabel.value?.let { doUpdateList(it) }
super.onCreateOptionsMenu(menu, inflater)
}
@ -305,12 +261,22 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
currentLabel.value?.let { label ->
if (label.type != Label.Type.TRASH) return true
doAsync {
for (message in messageRepo.findMessages(label)) {
messageRepo.remove(message)
deleteAllMessages(label)
}
uiThread { doUpdateList(label) }
return true
}
R.id.delete_all -> {
currentLabel.value?.let { label ->
context?.apply {
alert(
R.string.delete_all_messages_in_list,
R.string.delete_all_messages_in_list_ask
) {
positiveButton(R.string.delete) {
deleteAllMessages(label)
}
cancelButton { }
}.show()
}
}
return true
@ -319,19 +285,24 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
}
}
override fun updateList(label: Label) {
currentLabel.value = label
private fun deleteAllMessages(label: Label) {
doAsync {
for (message in messageRepo.findMessages(label, 0, 0, context?.preferences?.separateIdentities == true)) {
messageRepo.remove(message)
}
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
swipeableMessageAdapter?.setActivateOnItemClick(activateOnItemClick)
this.activateOnItemClick = activateOnItemClick
uiThread { doUpdateList(label) }
}
}
override fun updateList(label: Label) {
currentLabel.onNext(label)
}
override fun showPreviousList() = if (backStack.isEmpty()) {
false
} else {
currentLabel.value = backStack.pop()
currentLabel.onNext(backStack.pop())
true
}
}

View File

@ -17,42 +17,36 @@
package ch.dissem.apps.abit
import android.app.Activity
import android.content.*
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.support.v4.app.Fragment
import android.support.v4.content.ContextCompat
import android.support.v4.content.FileProvider.getUriForFile
import android.support.v7.preference.Preference
import android.support.v7.preference.Preference.OnPreferenceChangeListener
import android.support.v7.preference.PreferenceFragmentCompat
import android.support.v7.preference.PreferenceScreen
import android.support.v7.preference.SwitchPreferenceCompat
import android.view.View
import android.widget.Toast
import androidx.core.content.FileProvider.getUriForFile
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import ch.dissem.apps.abit.service.BatchProcessorService
import ch.dissem.apps.abit.service.SimpleJob
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SERVER_POW
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Exports
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.entity.Plaintext
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.indeterminateProgressDialog
import org.jetbrains.anko.support.v4.startActivity
import org.jetbrains.anko.indeterminateProgressDialog
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.uiThread
import java.util.*
/**
* @author Christian Basler
*/
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener,
PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
@ -103,7 +97,7 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
val bmc = Singleton.getBitmessageContext(ctx)
bmc.internals.nodeRegistry.clear()
bmc.cleanup()
Preferences.cleanupExportDirectory(ctx)
ctx.preferences.cleanupExportDirectory()
uiThread {
Toast.makeText(
@ -118,11 +112,11 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
}
private fun exportClickListener() = Preference.OnPreferenceClickListener {
val ctx = context ?: throw IllegalStateException("No context available")
val ctx = activity ?: throw IllegalStateException("No context available")
indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
ctx.indeterminateProgressDialog(R.string.export_data_summary, R.string.export_data).apply {
doAsync {
val exportDirectory = Preferences.getExportDirectory(ctx)
val exportDirectory = ctx.preferences.exportDirectory
exportDirectory.mkdirs()
val file = Exports.exportData(exportDirectory, ctx)
val contentUri = getUriForFile(ctx, "ch.dissem.apps.abit.fileprovider", file)
@ -153,20 +147,20 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
if (activity.hasDetailPane) {
activity.setDetailView(StatusFragment())
} else {
startActivity<StatusActivity>()
activity.startActivity<StatusActivity>()
}
return@OnPreferenceClickListener true
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
val ctx = context ?: throw IllegalStateException("No context available")
val ctx = activity ?: throw IllegalStateException("No context available")
when (requestCode) {
WRITE_EXPORT_REQUEST_CODE -> Preferences.cleanupExportDirectory(ctx)
WRITE_EXPORT_REQUEST_CODE -> ctx.preferences.cleanupExportDirectory()
READ_IMPORT_REQUEST_CODE -> {
if (resultCode == Activity.RESULT_OK && data?.data != null) {
indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply {
ctx.indeterminateProgressDialog(R.string.import_data_summary, R.string.import_data).apply {
doAsync {
Exports.importData(data.data, ctx)
Exports.importData(data.data!!, ctx)
uiThread {
dismiss()
}
@ -187,24 +181,6 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
PREFERENCE_SERVER_POW -> toggleSyncServerPOW(sharedPreferences)
}
}
private fun toggleSyncServerPOW(sharedPreferences: SharedPreferences) {
val node = sharedPreferences.getString(PREFERENCE_TRUSTED_NODE, null)
if (node != null) {
val ctx = context ?: throw IllegalStateException("No context available")
if (sharedPreferences.getBoolean(PREFERENCE_SERVER_POW, false)) {
SyncAdapter.startPowSync(ctx)
} else {
SyncAdapter.stopPowSync(ctx)
}
}
}
private val connection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
if (service is BatchProcessorService.BatchBinder) {
@ -243,44 +219,41 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
}
private fun emulateConversationChangeListener(conversationInit: Preference?) =
OnPreferenceChangeListener { _, newValue ->
Preference.OnPreferenceChangeListener { _, newValue ->
conversationInit?.isEnabled = newValue as Boolean
true
}
private fun connectivityChangeListener() =
OnPreferenceChangeListener { _, _ ->
context?.let { ctx ->
if (Preferences.isFullNodeActive(ctx)) {
NetworkUtils.scheduleNodeStart(ctx)
}
}
Preference.OnPreferenceChangeListener { _, _ ->
activity?.network?.scheduleNodeStart()
true
}
// The why-is-it-so-damn-hard-to-group-preferences section
override fun getCallbackFragment(): Fragment = this
override fun onPreferenceStartScreen(
preferenceFragmentCompat: PreferenceFragmentCompat,
preferenceScreen: PreferenceScreen
): Boolean {
fragmentManager?.beginTransaction()?.let { ft ->
val fragment = SettingsFragment()
fragment.arguments = Bundle().apply {
putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, preferenceScreen.key)
}
ft.add(R.id.item_list, fragment, preferenceScreen.key)
ft.addToBackStack(preferenceScreen.key)
ft.commit()
}
return true
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
context?.let { ctx -> view.setBackgroundColor(ContextCompat.getColor(ctx, R.color.contentBackground)) }
}
// FIXME: maybe this is once again necessary, maybe not. Test!
// override fun getCallbackFragment(): Fragment = this
//
// override fun onPreferenceStartScreen(
// preferenceFragmentCompat: PreferenceFragment,
// preferenceScreen: PreferenceScreen
// ): Boolean {
// fragmentManager?.beginTransaction()?.let { ft ->
// val fragment = SettingsFragment()
// fragment.arguments = Bundle().apply {
// putString(PreferenceFragment.ARG_PREFERENCE_ROOT, preferenceScreen.key)
// }
// ft.add(R.id.item_list, fragment, preferenceScreen.key)
// ft.addToBackStack(preferenceScreen.key)
// ft.commit()
// }
// return true
// }
//
// override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// super.onViewCreated(view, savedInstanceState)
// context?.let { ctx -> view.setBackgroundColor(ContextCompat.getColor(ctx, R.color.contentBackground)) }
// }
// End of the why-is-it-so-damn-hard-to-group-preferences section
// Afterthought: here it looks so simple: https://developer.android.com/guide/topics/ui/settings.html
// Remind me, why do we need to use PreferenceFragmentCompat?

View File

@ -17,7 +17,7 @@
package ch.dissem.apps.abit
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import androidx.appcompat.app.AppCompatActivity
import ch.dissem.apps.abit.service.Singleton
import com.mikepenz.materialize.MaterializeBuilder
import kotlinx.android.synthetic.main.activity_status.*

View File

@ -17,7 +17,7 @@
package ch.dissem.apps.abit
import android.os.Bundle
import android.support.v4.app.Fragment
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

View File

@ -16,7 +16,7 @@
package ch.dissem.apps.abit.adapter
import android.support.v7.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -31,7 +31,7 @@ import java.util.*
*/
class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView.Adapter<AddressSelectorAdapter.ViewHolder>() {
private val data = identities.map { Selectable(it) }.toMutableList()
private val data = identities.asSequence().map { Selectable(it) }.toMutableList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
@ -63,7 +63,7 @@ class AddressSelectorAdapter(identities: List<BitmessageAddress>) : RecyclerView
val selected: List<BitmessageAddress>
get() {
return data
return data.asSequence()
.filter { it.selected }
.mapTo(LinkedList()) { it.data }
}

View File

@ -142,5 +142,4 @@ class ContactAdapter(
}
}
fun indexOf(element: BitmessageAddress) = originalData.indexOf(element)
}

View File

@ -1,16 +1,16 @@
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.support.v4.app.Fragment
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.PopupMenu
import android.support.v7.widget.RecyclerView
import android.text.util.Linkify
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import ch.dissem.apps.abit.*
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Constants
@ -19,18 +19,19 @@ import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.ports.MessageRepository
import io.reactivex.subjects.BehaviorSubject
class ConversationAdapter internal constructor(
ctx: Context,
private val parent: Fragment,
conversation: Conversation,
private val label: Label?
label: BehaviorSubject<Label>
) : RecyclerView.Adapter<ConversationAdapter.ViewHolder>() {
private val messageRepo = Singleton.getMessageRepository(ctx)
private var filteredMessages = conversation.messages.filter { label == null || it.labels.any { it == label } }
private var filteredMessages = label.value?.let { l -> conversation.messages.filter { m -> m.labels.any { it == l } } } ?: emptyList()
override fun onCreateViewHolder(
parent: ViewGroup,
@ -98,13 +99,14 @@ class ConversationAdapter internal constructor(
val sender = itemView.findViewById<TextView>(R.id.sender)!!
val recipient = itemView.findViewById<TextView>(R.id.recipient)!!
val status = itemView.findViewById<ImageView>(R.id.status)!!
val menu = itemView.findViewById<ImageView>(R.id.menu)!!.also { view ->
view.setOnClickListener {
val popup = PopupMenu(itemView.context, view)
popup.menuInflater.inflate(R.menu.message, popup.menu)
popup.setOnMenuItemClickListener {
val menu = itemView.findViewById<ImageView>(R.id.menu)!!.apply {
setOnClickListener { view ->
PopupMenu(itemView.context, view).apply {
menuInflater.inflate(R.menu.message, menu)
setOnMenuItemClickListener { menuItem ->
item?.let { item ->
when (it.itemId) {
when (menuItem.itemId) {
R.id.reply -> {
ComposeMessageActivity.launchReplyTo(parent, item)
true
@ -142,7 +144,9 @@ class ConversationAdapter internal constructor(
}
} ?: false
}
popup.show()
show()
}
}
}
val text = itemView.findViewById<TextView>(R.id.text)!!.apply {

View File

@ -2,8 +2,8 @@ package ch.dissem.apps.abit.adapter
import android.content.Context
import android.content.res.ColorStateList
import android.support.annotation.ColorInt
import android.support.v7.widget.RecyclerView
import androidx.annotation.ColorInt
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

View File

@ -0,0 +1,312 @@
/*
* Copyright 2015 Haruki Hasegawa
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.graphics.*
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.MultiIdenticon
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.apps.abit.util.getDrawable
import ch.dissem.apps.abit.util.getString
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import java.util.*
/**
* Adapted from the basic swipeable example by Haruki Hasegawa. See
*
* @author Christian Basler
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
*/
abstract class SwipeableAdapter<T, H>(val ctx: Context) :
RecyclerView.Adapter<H>() where H : SwipeableAdapter.AbstractViewHolder {
protected val data = LinkedList<T>()
var eventListener: EventListener? = null
protected var label: Label? = null
var selectedPosition = -1
set(value) {
val oldPosition = field
field = value
notifyItemChanged(oldPosition)
notifyItemChanged(value)
}
var activateOnItemClick: Boolean = false
protected val labelUnknown: String = ctx.getString(R.string.unknown)
open class AbstractViewHolder(v: View, adapter: SwipeableAdapter<*, *>) : RecyclerView.ViewHolder(v) {
val container = v.findViewById<FrameLayout>(R.id.container)!!
init {
itemView.setOnClickListener { adapter.eventListener?.onItemSelected(adapterPosition) }
container.setOnClickListener { adapter.eventListener?.onItemSelected(adapterPosition) }
}
}
init {
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
setHasStableIds(true)
}
override fun onBindViewHolder(holder: H, position: Int) {
val item = data[position]
holder.apply {
if (activateOnItemClick) {
container.setBackgroundResource(
if (position == selectedPosition)
R.drawable.bg_item_selected_state
else
R.drawable.bg_item_normal_state
)
}
setData(holder, item)
}
}
abstract fun setData(holder: H, item: T)
fun add(item: T) {
val index = data.size
data.add(item)
notifyItemInserted(index)
}
fun addFirst(item: T) {
data.addFirst(item)
notifyItemInserted(0)
}
fun addAll(items: Collection<T>) {
val index = data.size
data.addAll(items)
notifyItemRangeInserted(index, items.size)
}
fun remove(item: T) {
val itemId = getItemId(item)
val index = data.indexOfFirst { getItemId(it) == itemId }
if (index >= 0) {
removeAt(index)
}
}
fun removeAt(index: Int) {
data.removeAt(index)
notifyItemRemoved(index)
}
override fun getItemId(position: Int): Long {
return getItemId(data[position])
}
abstract fun getItemId(item: T): Long
abstract fun update(item: T)
fun clear(newLabel: Label?) {
label = newLabel
data.clear()
notifyDataSetChanged()
}
fun getItem(position: Int) = data[position]
override fun getItemCount() = data.size
}
class SwipeableConversationAdapter(ctx: Context) : SwipeableAdapter<Conversation, SwipeableConversationAdapter.ViewHolder>(ctx) {
override fun getItemId(item: Conversation) = item.id.leastSignificantBits
override fun update(item: Conversation) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.conversation_row, parent, false)
return ViewHolder(v, this)
}
override fun setData(holder: ViewHolder, item: Conversation) {
holder.apply {
avatar.setImageDrawable(MultiIdenticon(item.participants))
sender.text = item.participants.sortedBy {
(it.alias?.let { 0 } ?: 1) + if (it.isChan) 2 else 0
}.map { it.alias ?: labelUnknown }.distinct().joinToString()
subject.text = prepareMessageExtract(item.subject)
extract.text = prepareMessageExtract(item.extract)
item.messages.count { it.labels.contains(label) }.let { size ->
if (size <= 1) {
count.text = ""
} else {
count.text = size.toString()
}
}
if (item.hasUnread()) {
sender.typeface = Typeface.DEFAULT_BOLD
subject.typeface = Typeface.DEFAULT_BOLD
} else {
sender.typeface = Typeface.DEFAULT
subject.typeface = Typeface.DEFAULT
}
}
}
class ViewHolder(v: View, adapter: SwipeableConversationAdapter) : AbstractViewHolder(v, adapter) {
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
val status = v.findViewById<ImageView>(R.id.status)!!
val sender = v.findViewById<TextView>(R.id.sender)!!
val subject = v.findViewById<TextView>(R.id.subject)!!
val extract = v.findViewById<TextView>(R.id.text)!!
val count = v.findViewById<TextView>(R.id.count)!!
}
}
class SwipeableMessageAdapter(ctx: Context) : SwipeableAdapter<Plaintext, SwipeableMessageAdapter.ViewHolder>(ctx) {
override fun getItemId(item: Plaintext) = item.id as Long
override fun update(item: Plaintext) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.message_row, parent, false)
return ViewHolder(v, this)
}
override fun setData(holder: ViewHolder, item: Plaintext) {
holder.apply {
avatar.setImageDrawable(Identicon(item.from))
status.setImageResource(item.status.getDrawable())
status.contentDescription = holder.status.context.getString(item.status.getString())
sender.text = item.from.toString()
subject.text = prepareMessageExtract(item.subject)
extract.text = prepareMessageExtract(item.text)
if (item.isUnread()) {
sender.typeface = Typeface.DEFAULT_BOLD
subject.typeface = Typeface.DEFAULT_BOLD
} else {
sender.typeface = Typeface.DEFAULT
subject.typeface = Typeface.DEFAULT
}
}
}
class ViewHolder(v: View, adapter: SwipeableMessageAdapter) : AbstractViewHolder(v, adapter) {
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
val status = v.findViewById<ImageView>(R.id.status)!!
val sender = v.findViewById<TextView>(R.id.sender)!!
val subject = v.findViewById<TextView>(R.id.subject)!!
val extract = v.findViewById<TextView>(R.id.text)!!
}
}
class SwipeToDeleteCallback(ctx: Context, swipeDirs: Int, private val eventListener: EventListener) : ItemTouchHelper.SimpleCallback(0, swipeDirs) {
private val backgroundLeft = ContextCompat.getDrawable(ctx, R.drawable.bg_swipe_item_left)!!
private val backgroundRight = ContextCompat.getDrawable(ctx, R.drawable.bg_swipe_item_right)!!
private val clearPaint = Paint().apply { xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) }
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
/**
* To disable "swipe" for specific item return 0 here.
* For example:
* if (viewHolder?.itemViewType == YourAdapter.SOME_TYPE) return 0
* if (viewHolder?.adapterPosition == 0) return 0
*/
if (viewHolder.adapterPosition == 10) return 0
return super.getMovementFlags(recyclerView, viewHolder)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) = false
override fun onChildDraw(
c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,
dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
) {
val itemView = viewHolder.itemView
val isCanceled = dX == 0f && !isCurrentlyActive
if (isCanceled) {
clearCanvas(c, itemView.right + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
return
}
if (dX < 0) {
backgroundLeft.setBounds(itemView.right + dX.toInt(), itemView.top, itemView.right, itemView.bottom)
backgroundLeft.draw(c)
} else {
backgroundRight.setBounds(itemView.left, itemView.top, itemView.left + dX.toInt(), itemView.bottom)
backgroundRight.draw(c)
}
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
}
private fun clearCanvas(c: Canvas?, left: Float, top: Float, right: Float, bottom: Float) {
c?.drawRect(left, top, right, bottom, clearPaint)
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
when (direction) {
ItemTouchHelper.LEFT -> eventListener.onItemDeleted(viewHolder.adapterPosition)
ItemTouchHelper.RIGHT -> eventListener.onItemArchived(viewHolder.adapterPosition)
}
}
}
interface EventListener {
fun onItemDeleted(position: Int)
fun onItemArchived(position: Int)
fun onItemSelected(position: Int)
}

View File

@ -1,275 +0,0 @@
/*
* Copyright 2015 Haruki Hasegawa
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Typeface
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.MultiIdenticon
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.*
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder
import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils
import java.util.*
/**
* Adapted from the basic swipeable example by Haruki Hasegawa. See
*
* @author Christian Basler
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
*/
class SwipeableConversationAdapter(ctx: Context) :
RecyclerView.Adapter<SwipeableConversationAdapter.ViewHolder>(),
SwipeableItemAdapter<SwipeableConversationAdapter.ViewHolder>, SwipeableItemConstants {
private val data = LinkedList<Conversation>()
var eventListener: EventListener? = null
private val itemViewOnClickListener: View.OnClickListener
private val swipeableViewContainerOnClickListener: View.OnClickListener
private var label: Label? = null
private var selectedPosition = -1
private var activateOnItemClick: Boolean = false
private val labelUnknown = ctx.getString(R.string.unknown)
fun setActivateOnItemClick(activateOnItemClick: Boolean) {
this.activateOnItemClick = activateOnItemClick
}
interface EventListener {
fun onItemDeleted(item: Conversation)
fun onItemArchived(item: Conversation)
fun onItemViewClicked(v: View?)
}
class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) {
val container = v.findViewById<FrameLayout>(R.id.container)!!
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
val status = v.findViewById<ImageView>(R.id.status)!!
val sender = v.findViewById<TextView>(R.id.sender)!!
val subject = v.findViewById<TextView>(R.id.subject)!!
val extract = v.findViewById<TextView>(R.id.text)!!
val count = v.findViewById<TextView>(R.id.count)!!
override fun getSwipeableContainerView() = container
}
init {
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
swipeableViewContainerOnClickListener =
View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
setHasStableIds(true)
}
fun add(item: Conversation) {
data.add(item)
notifyDataSetChanged()
}
fun addFirst(item: Conversation) {
val index = data.size
data.addFirst(item)
notifyItemInserted(index)
}
fun addAll(items: Collection<Conversation>) {
val index = data.size
data.addAll(items)
notifyItemRangeInserted(index, items.size)
}
fun remove(item: Conversation) {
val index = data.indexOf(item)
data.removeAll { it.id == item.id }
notifyItemRemoved(index)
}
fun update(item: Conversation) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
fun clear(newLabel: Label?) {
label = newLabel
data.clear()
notifyDataSetChanged()
}
private fun onItemViewClick(v: View) {
eventListener?.onItemViewClicked(v)
}
private fun onSwipeableViewContainerClick(v: View) {
eventListener?.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v)
)
}
fun getItem(position: Int) = data[position]
override fun getItemId(position: Int) = data[position].id.leastSignificantBits
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.conversation_row, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.apply {
if (activateOnItemClick) {
container.setBackgroundResource(
if (position == selectedPosition)
R.drawable.bg_item_selected_state
else
R.drawable.bg_item_normal_state
)
}
// set listeners
// (if the item is *pinned*, click event comes to the itemView)
itemView.setOnClickListener(itemViewOnClickListener)
// (if the item is *not pinned*, click event comes to the container)
container.setOnClickListener(swipeableViewContainerOnClickListener)
// set data
avatar.setImageDrawable(MultiIdenticon(item.participants))
sender.text = item.participants.sortedBy {
(it.alias?.let { 0 } ?: 1) + if (it.isChan) 2 else 0
}.map { it.alias ?: labelUnknown }.distinct().joinToString()
subject.text = prepareMessageExtract(item.subject)
extract.text = prepareMessageExtract(item.extract)
item.messages.count { it.labels.contains(label) }.let { size ->
if (size <= 1) {
count.text = ""
} else {
count.text = size.toString()
}
}
if (item.hasUnread()) {
sender.typeface = Typeface.DEFAULT_BOLD
subject.typeface = Typeface.DEFAULT_BOLD
} else {
sender.typeface = Typeface.DEFAULT
subject.typeface = Typeface.DEFAULT
}
}
}
override fun getItemCount() = data.size
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int =
if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT
} else {
REACTION_CAN_SWIPE_BOTH_H
}
@SuppressLint("SwitchIntDef")
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
holder.itemView.setBackgroundResource(
when (type) {
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
R.drawable.bg_swipe_item_neutral
} else {
R.drawable.bg_swipe_item_right
}
else -> R.drawable.bg_swipe_item_neutral
}
)
@SuppressLint("SwitchIntDef")
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
when (result) {
RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position)
RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position)
else -> null
}
override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit
fun setSelectedPosition(selectedPosition: Int) {
val oldPosition = this.selectedPosition
this.selectedPosition = selectedPosition
notifyItemChanged(oldPosition)
notifyItemChanged(selectedPosition)
}
private class SwipeLeftResultAction internal constructor(
adapter: SwipeableConversationAdapter,
position: Int
) : SwipeResultActionMoveToSwipedDirection() {
private var adapter: SwipeableConversationAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemDeleted(item)
adapter?.remove(item)
}
override fun onCleanUp() {
adapter = null
}
}
private class SwipeRightResultAction internal constructor(
adapter: SwipeableConversationAdapter,
position: Int
) : SwipeResultActionRemoveItem() {
private var adapter: SwipeableConversationAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemArchived(item)
adapter?.remove(item)
}
override fun onCleanUp() {
adapter = null
}
}
}

View File

@ -1,262 +0,0 @@
/*
* Copyright 2015 Haruki Hasegawa
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter
import android.annotation.SuppressLint
import android.graphics.Typeface
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Strings.prepareMessageExtract
import ch.dissem.apps.abit.util.getDrawable
import ch.dissem.apps.abit.util.getString
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants.*
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionMoveToSwipedDirection
import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultActionRemoveItem
import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractSwipeableItemViewHolder
import com.h6ah4i.android.widget.advrecyclerview.utils.RecyclerViewAdapterUtils
import java.util.*
/**
* Adapted from the basic swipeable example by Haruki Hasegawa. See
*
* @author Christian Basler
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview)
*/
class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(),
SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
private val data = LinkedList<Plaintext>()
var eventListener: EventListener? = null
private val itemViewOnClickListener: View.OnClickListener
private val swipeableViewContainerOnClickListener: View.OnClickListener
private var label: Label? = null
private var selectedPosition = -1
private var activateOnItemClick: Boolean = false
fun setActivateOnItemClick(activateOnItemClick: Boolean) {
this.activateOnItemClick = activateOnItemClick
}
interface EventListener {
fun onItemDeleted(item: Plaintext)
fun onItemArchived(item: Plaintext)
fun onItemViewClicked(v: View?)
}
class ViewHolder(v: View) : AbstractSwipeableItemViewHolder(v) {
val container = v.findViewById<FrameLayout>(R.id.container)!!
val avatar = v.findViewById<ImageView>(R.id.avatar)!!
val status = v.findViewById<ImageView>(R.id.status)!!
val sender = v.findViewById<TextView>(R.id.sender)!!
val subject = v.findViewById<TextView>(R.id.subject)!!
val extract = v.findViewById<TextView>(R.id.text)!!
override fun getSwipeableContainerView() = container
}
init {
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
swipeableViewContainerOnClickListener =
View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
// SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately.
setHasStableIds(true)
}
fun add(item: Plaintext) {
data.add(item)
notifyDataSetChanged()
}
fun addFirst(item: Plaintext) {
val index = data.size
data.addFirst(item)
notifyItemInserted(index)
}
fun addAll(items: Collection<Plaintext>) {
val index = data.size
data.addAll(items)
notifyItemRangeInserted(index, items.size)
}
fun remove(item: Plaintext) {
val index = data.indexOf(item)
data.removeAll { it.id == item.id }
notifyItemRemoved(index)
}
fun update(item: Plaintext) {
val index = data.indexOfFirst { it.id == item.id }
if (index >= 0) {
data[index] = item
notifyItemChanged(index)
}
}
fun clear(newLabel: Label?) {
label = newLabel
data.clear()
notifyDataSetChanged()
}
private fun onItemViewClick(v: View) {
eventListener?.onItemViewClicked(v)
}
private fun onSwipeableViewContainerClick(v: View) {
eventListener?.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v)
)
}
fun getItem(position: Int) = data[position]
override fun getItemId(position: Int) = data[position].id as Long
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.message_row, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = data[position]
holder.apply {
if (activateOnItemClick) {
container.setBackgroundResource(
if (position == selectedPosition)
R.drawable.bg_item_selected_state
else
R.drawable.bg_item_normal_state
)
}
// set listeners
// (if the item is *pinned*, click event comes to the itemView)
itemView.setOnClickListener(itemViewOnClickListener)
// (if the item is *not pinned*, click event comes to the container)
container.setOnClickListener(swipeableViewContainerOnClickListener)
// set data
avatar.setImageDrawable(Identicon(item.from))
status.setImageResource(item.status.getDrawable())
status.contentDescription = holder.status.context.getString(item.status.getString())
sender.text = item.from.toString()
subject.text = prepareMessageExtract(item.subject)
extract.text = prepareMessageExtract(item.text)
if (item.isUnread()) {
sender.typeface = Typeface.DEFAULT_BOLD
subject.typeface = Typeface.DEFAULT_BOLD
} else {
sender.typeface = Typeface.DEFAULT
subject.typeface = Typeface.DEFAULT
}
}
}
override fun getItemCount() = data.size
override fun onGetSwipeReactionType(holder: ViewHolder, position: Int, x: Int, y: Int): Int =
if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
REACTION_CAN_SWIPE_LEFT or REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT
} else {
REACTION_CAN_SWIPE_BOTH_H
}
@SuppressLint("SwitchIntDef")
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
holder.itemView.setBackgroundResource(
when (type) {
DRAWABLE_SWIPE_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) {
R.drawable.bg_swipe_item_neutral
} else {
R.drawable.bg_swipe_item_right
}
else -> R.drawable.bg_swipe_item_neutral
}
)
@SuppressLint("SwitchIntDef")
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
when (result) {
RESULT_SWIPED_RIGHT -> SwipeRightResultAction(this, position)
RESULT_SWIPED_LEFT -> SwipeLeftResultAction(this, position)
else -> null
}
override fun onSwipeItemStarted(holder: ViewHolder?, position: Int) = Unit
fun setSelectedPosition(selectedPosition: Int) {
val oldPosition = this.selectedPosition
this.selectedPosition = selectedPosition
notifyItemChanged(oldPosition)
notifyItemChanged(selectedPosition)
}
private class SwipeLeftResultAction internal constructor(
adapter: SwipeableMessageAdapter,
position: Int
) : SwipeResultActionMoveToSwipedDirection() {
private var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemDeleted(item)
}
override fun onCleanUp() {
adapter = null
}
}
private class SwipeRightResultAction internal constructor(
adapter: SwipeableMessageAdapter,
position: Int
) : SwipeResultActionRemoveItem() {
private var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position]
override fun onPerformAction() {
adapter?.eventListener?.onItemArchived(item)
}
override fun onCleanUp() {
adapter = null
}
}
}

View File

@ -1,48 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.adapter
import android.content.Context
import android.preference.PreferenceManager
import ch.dissem.bitmessage.InternalContext
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
/**
* Switches between two [ProofOfWorkEngine]s depending on the configuration.
*
* @author Christian Basler
*/
class SwitchingProofOfWorkEngine(
private val ctx: Context,
private val preference: String,
private val option: ProofOfWorkEngine,
private val fallback: ProofOfWorkEngine
) : ProofOfWorkEngine, InternalContext.ContextHolder {
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
if (preferences.getBoolean(preference, false)) {
option.calculateNonce(initialHash, target, callback)
} else {
fallback.calculateNonce(initialHash, target, callback)
}
}
override fun setContext(context: InternalContext) = listOf(option, fallback)
.filterIsInstance<InternalContext.ContextHolder>()
.forEach { it.setContext(context) }
}

View File

@ -19,12 +19,12 @@ package ch.dissem.apps.abit.dialog
import android.app.AlertDialog
import android.content.Context
import android.os.Bundle
import android.support.v7.app.AppCompatDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatDialogFragment
import ch.dissem.apps.abit.ImportIdentityActivity
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
@ -33,7 +33,7 @@ import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.payload.Pubkey
import kotlinx.android.synthetic.main.dialog_add_identity.*
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.startActivity
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.uiThread
/**
@ -77,7 +77,7 @@ class AddIdentityDialogFragment : AppCompatDialogFragment() {
}
}
}
R.id.import_identity -> startActivity<ImportIdentityActivity>()
R.id.import_identity -> ctx.startActivity<ImportIdentityActivity>()
R.id.add_chan -> addChanDialog()
R.id.add_deterministic_address -> DeterministicIdentityDialogFragment().show(fragmentManager, "dialog")
else -> return@OnClickListener

View File

@ -18,7 +18,7 @@ package ch.dissem.apps.abit.dialog
import android.content.Context
import android.os.Bundle
import android.support.v7.app.AppCompatDialogFragment
import androidx.appcompat.app.AppCompatDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup

View File

@ -19,8 +19,8 @@ package ch.dissem.apps.abit.dialog
import android.app.Activity
import android.os.Bundle
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
import kotlinx.android.synthetic.main.dialog_full_node.*
/**
@ -31,12 +31,12 @@ class FullNodeDialogActivity : Activity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.dialog_full_node)
ok.setOnClickListener {
Preferences.setWifiOnly(this@FullNodeDialogActivity, false)
NetworkUtils.enableNode(applicationContext)
preferences.wifiOnly = false
network.scheduleNodeStart()
finish()
}
dismiss.setOnClickListener {
NetworkUtils.scheduleNodeStart(applicationContext)
network.scheduleNodeStart()
finish()
}
}

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit.dialog
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AppCompatDialogFragment
import androidx.appcompat.app.AppCompatDialogFragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -49,7 +49,7 @@ class SelectEncodingDialogFragment : AppCompatDialogFragment() {
when (encoding) {
SIMPLE -> radioGroup.check(R.id.simple)
EXTENDED -> radioGroup.check(R.id.extended)
else -> LOG.warn("Unexpected encoding: " + encoding)
else -> LOG.warn("Unexpected encoding: $encoding")
}
ok.setOnClickListener(View.OnClickListener {
encoding = when (radioGroup.checkedRadioButtonId) {

View File

@ -2,23 +2,17 @@ package ch.dissem.apps.abit.drawer
import android.content.Context
import android.content.Intent
import android.support.v4.app.FragmentManager
import androidx.fragment.app.FragmentManager
import android.view.View
import android.widget.Toast
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
import ch.dissem.apps.abit.AddressDetailActivity
import ch.dissem.apps.abit.AddressDetailFragment
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import android.widget.Toast.LENGTH_LONG
import ch.dissem.apps.abit.*
import ch.dissem.apps.abit.dialog.AddIdentityDialogFragment
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.bitmessage.entity.BitmessageAddress
import android.widget.Toast.LENGTH_LONG
import com.mikepenz.materialdrawer.AccountHeader
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.IProfile
class ProfileSelectionListener(
private val ctx: Context,
@ -42,6 +36,13 @@ class ProfileSelectionListener(
val tag = profile.tag
if (tag is BitmessageAddress) {
Singleton.setIdentity(tag)
MainActivity.apply {
updateUnread()
val itemList = supportFragmentManager.findFragmentById(R.id.item_list)
if (itemList is ListHolder<*>) {
itemList.reloadList()
}
}
}
}
}

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit.listener
import android.content.Context
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.notification.NewMessageNotification
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.ports.MessageRepository
@ -50,7 +50,7 @@ class MessageListener(ctx: Context) : BitmessageContext.Listener.WithContext {
private lateinit var conversationService: ConversationService
init {
emulateConversations = Preferences.isEmulateConversations(ctx)
emulateConversations = ctx.preferences.emulateConversations
}
override fun receive(plaintext: Plaintext) {
@ -81,7 +81,7 @@ class MessageListener(ctx: Context) : BitmessageContext.Listener.WithContext {
}
}
fun updateConversation(plaintext: Plaintext) {
private fun updateConversation(plaintext: Plaintext) {
if (emulateConversations && plaintext.encoding != Plaintext.Encoding.EXTENDED) {
conversationService.getSubject(listOf(plaintext))?.let { subject ->
plaintext.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())

View File

@ -21,8 +21,8 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.support.annotation.ColorRes
import android.support.v4.content.ContextCompat
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import ch.dissem.apps.abit.R
import org.jetbrains.anko.notificationManager

View File

@ -17,7 +17,7 @@
package ch.dissem.apps.abit.notification
import android.content.Context
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.Job

View File

@ -17,8 +17,8 @@
package ch.dissem.apps.abit.notification
import android.content.Context
import android.support.annotation.StringRes
import android.support.v4.app.NotificationCompat
import androidx.annotation.StringRes
import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.R

View File

@ -21,11 +21,11 @@ import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.service.BitmessageIntentService
import ch.dissem.apps.abit.service.BitmessageService
import ch.dissem.apps.abit.service.NodeStartupService
import java.util.*
import kotlin.concurrent.fixedRateTimer
@ -51,9 +51,9 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
@SuppressLint("StringFormatMatches")
private fun update(): Boolean {
val running = BitmessageService.isRunning
val running = NodeStartupService.isRunning
builder.setOngoing(running)
val connections = BitmessageService.status.getProperty("network", "connections")
val connections = NodeStartupService.status.getProperty("network", "connections")
if (!running) {
builder.setSmallIcon(R.drawable.ic_notification_full_node_disconnected)
builder.setContentText(ctx.getString(R.string.connection_info_disconnected))
@ -112,7 +112,6 @@ class NetworkNotification(ctx: Context) : AbstractNotification(ctx) {
timer = fixedRateTimer(initialDelay = 10000, period = 10000) {
if (!update()) {
cancel()
ctx.stopService(Intent(ctx, BitmessageService::class.java))
}
super@NetworkNotification.show()
}

View File

@ -21,9 +21,9 @@ import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.support.v4.app.NotificationCompat
import android.support.v4.app.NotificationCompat.BigTextStyle
import android.support.v4.app.NotificationCompat.InboxStyle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.BigTextStyle
import androidx.core.app.NotificationCompat.InboxStyle
import android.text.Spannable
import android.text.SpannableString
import android.text.Spanned

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit.notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.support.v4.app.NotificationCompat
import androidx.core.app.NotificationCompat
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R

View File

@ -1,83 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.pow
import android.content.Context
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.synchronization.SyncAdapter
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.InternalContext
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
import ch.dissem.bitmessage.ports.ProofOfWorkEngine
import ch.dissem.bitmessage.utils.Singleton.cryptography
import org.slf4j.LoggerFactory
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* @author Christian Basler
*/
class ServerPowEngine(private val ctx: Context) : ProofOfWorkEngine, InternalContext.ContextHolder {
private lateinit var context: InternalContext
private val pool: ExecutorService
init {
pool = Executors.newCachedThreadPool { r ->
val thread = Executors.defaultThreadFactory().newThread(r)
thread.priority = Thread.MIN_PRIORITY
thread
}
}
override fun calculateNonce(initialHash: ByteArray, target: ByteArray, callback: ProofOfWorkEngine.Callback) =
pool.execute {
val identity = Singleton.getIdentity(ctx) ?: throw RuntimeException("No Identity for calculating POW")
val request = ProofOfWorkRequest(identity, initialHash,
CALCULATE, target)
SyncAdapter.startPowSync(ctx)
try {
val cryptoMsg = CryptoCustomMessage(request)
cryptoMsg.signAndEncrypt(
identity,
cryptography().createPublicKey(identity.publicDecryptionKey)
)
val node = Preferences.getTrustedNode(ctx)
if (node == null) {
LOG.error("trusted node is not defined")
} else {
context.networkHandler.send(
node,
Preferences.getTrustedNodePort(ctx),
cryptoMsg)
}
} catch (e: Exception) {
LOG.error(e.message, e)
}
}
override fun setContext(context: InternalContext) {
this.context = context
}
companion object {
private val LOG = LoggerFactory.getLogger(ServerPowEngine::class.java)
}
}

View File

@ -21,6 +21,7 @@ import android.database.Cursor
import android.database.DatabaseUtils
import android.database.sqlite.SQLiteDatabase
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.apps.abit.util.UuidUtils
import ch.dissem.apps.abit.util.UuidUtils.asUuid
import ch.dissem.bitmessage.entity.BitmessageAddress
@ -38,7 +39,14 @@ import java.util.*
/**
* [MessageRepository] implementation using the Android SQL API.
*/
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
class AndroidMessageRepository(private val sql: SqlHelper, private val prefs: Preferences) : AbstractMessageRepository() {
fun findMessages(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean) =
if (label === LABEL_ARCHIVE || label === null) {
find("id NOT IN (SELECT message_id FROM Message_Label)", offset, limit, separateIdentities)
} else {
find("id IN (SELECT message_id FROM Message_Label WHERE label_id=" + label.id + ")", offset, limit, separateIdentities)
}
override fun findMessages(label: Label?, offset: Int, limit: Int) =
if (label === LABEL_ARCHIVE) {
@ -54,30 +62,56 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
null
).toInt()
override fun countUnread(label: Label?) = when {
private fun getSelectIdentity(separateIdentities: Boolean): Pair<String, Array<String>> {
if (separateIdentities) {
val identity = prefs.currentIdentity
return if (prefs.separateIdentities && identity != null) {
"AND (type = 'BROADCAST' OR recipient=? OR sender=?)" to arrayOf(identity.address, identity.address)
} else {
"" to emptyArray()
}
} else {
return "" to emptyArray()
}
}
override fun countUnread(label: Label?) = countUnread(label, false)
fun countUnread(label: Label?, separateIdentities: Boolean) = getSelectIdentity(separateIdentities).let { (selectIdentityQuery, selectIdentityArgs) ->
when {
label === LABEL_ARCHIVE -> 0
label == null -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(Label.Type.UNREAD.name)
"id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
selectIdentityQuery,
arrayOf(Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt()
else -> DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
"id IN (SELECT message_id FROM Message_Label WHERE label_id=?) " +
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?))",
arrayOf(label.id.toString(), Label.Type.UNREAD.name)
"AND id IN (SELECT message_id FROM Message_Label WHERE label_id IN (SELECT id FROM Label WHERE type=?)) " +
selectIdentityQuery,
arrayOf(label.id.toString(), Label.Type.UNREAD.name, *selectIdentityArgs)
).toInt()
}
}
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> {
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> = findConversations(label, offset, limit, false)
fun findConversations(label: Label?, offset: Int, limit: Int, separateIdentities: Boolean): List<UUID> {
val projection = arrayOf(COLUMN_CONVERSATION)
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
val where = when {
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label)"
label == null -> null
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id})"
label === LABEL_ARCHIVE -> "id NOT IN (SELECT message_id FROM Message_Label) $selectIdentityQuery"
label == null -> if (selectIdentityQuery.isNotBlank()) {
"type = 'BROADCAST' OR recipient=? OR sender=?"
} else {
null
}
else -> "id IN (SELECT message_id FROM Message_Label WHERE label_id=${label.id}) $selectIdentityQuery"
}
val result = LinkedList<UUID>()
sql.readableDatabase.query(
@ -85,7 +119,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
TABLE_NAME,
projection,
where,
null, null, null,
selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c ->
@ -140,8 +174,11 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
db.update(PARENTS_TABLE_NAME, values, where, null)
}
override fun find(where: String, offset: Int, limit: Int): List<Plaintext> {
override fun find(where: String, offset: Int, limit: Int) = find(where, offset, limit, false)
private fun find(where: String, offset: Int, limit: Int, separateIdentities: Boolean): List<Plaintext> {
val result = LinkedList<Plaintext>()
val (selectIdentityQuery, selectIdentityArgs) = getSelectIdentity(separateIdentities)
// Define a projection that specifies which columns from the database
// you will actually use after this query.
@ -164,7 +201,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
sql.readableDatabase.query(
TABLE_NAME, projection,
where, null, null, null,
"$where $selectIdentityQuery", selectIdentityArgs, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c ->
@ -318,4 +355,5 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
private const val JT_COLUMN_MESSAGE = "message_id"
private const val JT_COLUMN_LABEL = "label_id"
}
}

View File

@ -3,9 +3,9 @@ package ch.dissem.apps.abit.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.support.annotation.DrawableRes
import android.support.annotation.StringRes
import android.support.v4.content.ContextCompat
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import ch.dissem.apps.abit.notification.BatchNotification
import ch.dissem.apps.abit.notification.BatchNotification.Companion.ONGOING_NOTIFICATION_ID
import org.jetbrains.anko.doAsync

View File

@ -18,7 +18,7 @@ package ch.dissem.apps.abit.service
import android.app.IntentService
import android.content.Intent
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.network
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext
@ -44,10 +44,10 @@ class BitmessageIntentService : IntentService("BitmessageIntentService") {
Singleton.getMessageListener(this).resetNotification()
}
if (it.hasExtra(EXTRA_STARTUP_NODE)) {
NetworkUtils.enableNode(this)
network.enableNode()
}
if (it.hasExtra(EXTRA_SHUTDOWN_NODE)) {
NetworkUtils.disableNode(this)
network.disableNode()
}
}
}

View File

@ -1,113 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.service
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.os.Handler
import ch.dissem.apps.abit.notification.NetworkNotification
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.utils.Property
import org.jetbrains.anko.doAsync
/**
* Define a Service that returns an IBinder for the
* sync adapter class, allowing the sync adapter framework to call
* onPerformSync().
*/
class BitmessageService : Service() {
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
private lateinit var notification: NetworkNotification
private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (bmc.isRunning() && !Preferences.isConnectionAllowed(this@BitmessageService)) {
bmc.shutdown()
}
}
}
private val cleanupHandler = Handler()
private val cleanupTask: Runnable = object : Runnable {
override fun run() {
bmc.cleanup()
if (isRunning) {
cleanupHandler.postDelayed(this, 24 * 60 * 60 * 1000L) // once a day
}
}
}
override fun onCreate() {
registerReceiver(
connectivityReceiver,
IntentFilter().apply {
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
addAction(Intent.ACTION_BATTERY_CHANGED)
}
)
notification = NetworkNotification(this)
running = false
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (!isRunning) {
running = true
notification.connecting()
startForeground(NETWORK_NOTIFICATION_ID, notification.notification)
if (!bmc.isRunning()) {
bmc.startup()
}
notification.show()
cleanupHandler.postDelayed(cleanupTask, 24 * 60 * 60 * 1000L)
}
return Service.START_STICKY
}
override fun onDestroy() {
if (bmc.isRunning()) {
bmc.shutdown()
}
running = false
notification.showShutdown()
cleanupHandler.removeCallbacks(cleanupTask)
doAsync {
bmc.cleanup()
}
unregisterReceiver(connectivityReceiver)
stopSelf()
}
override fun onBind(intent: Intent) = null
companion object {
@Volatile
private var running = false
val isRunning: Boolean
get() = running && Singleton.bitmessageContext?.isRunning() == true
val status: Property
get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context")
}
}

View File

@ -0,0 +1,18 @@
package ch.dissem.apps.abit.service
import android.app.job.JobParameters
import android.app.job.JobService
import org.jetbrains.anko.doAsync
class CleanupService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
doAsync {
Singleton.getBitmessageContext(this@CleanupService).cleanup()
jobFinished(params, false)
}
return true
}
override fun onStopJob(params: JobParameters?) = false
}

View File

@ -0,0 +1,103 @@
package ch.dissem.apps.abit.service
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import ch.dissem.apps.abit.notification.NetworkNotification
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.utils.Property
import org.jetbrains.anko.doAsync
/**
* Starts the full node if
* * it is active
* * it is not already running
*
* And stops it when the preconditions for the job (unmetered network) aren't met anymore.
*/
class NodeStartupService : JobService() {
private val bmc: BitmessageContext by lazy { Singleton.getBitmessageContext(this) }
private lateinit var notification: NetworkNotification
private val connectivityReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (bmc.isRunning() && !preferences.connectionAllowed) {
bmc.shutdown()
}
}
}
override fun onCreate() {
super.onCreate()
notification = NetworkNotification(this)
}
override fun onStartJob(params: JobParameters?): Boolean {
if (preferences.online) {
registerReceiver(
connectivityReceiver,
IntentFilter().apply {
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
addAction(Intent.ACTION_BATTERY_CHANGED)
}
)
startForeground(0, notification.notification)
NodeStartupService.running = false
if (!isRunning) {
running = true
notification.connecting()
if (!bmc.isRunning()) {
bmc.startup()
}
notification.show()
}
}
return true
}
override fun onDestroy() {
if (bmc.isRunning()) {
bmc.shutdown()
}
running = false
notification.showShutdown()
doAsync {
bmc.cleanup()
}
try {
unregisterReceiver(connectivityReceiver)
} catch (_: IllegalArgumentException) {
// For some reason, onStartJob wasn't called so the receiver isn't registered.
// Let's just ignore this.
}
stopSelf()
}
/**
* Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
* depending on Android version.
*/
override fun onStopJob(params: JobParameters?): Boolean {
network.scheduleNodeStart()
return false
}
companion object {
@Volatile
private var running = false
val isRunning: Boolean
get() = running && Singleton.bitmessageContext?.isRunning() == true
val status: Property
get() = Singleton.bitmessageContext?.status() ?: Property("bitmessage context")
}
}

View File

@ -19,7 +19,7 @@ package ch.dissem.apps.abit.service
import android.app.Service
import android.content.Intent
import android.os.Binder
import android.support.v4.content.ContextCompat
import androidx.core.content.ContextCompat
import ch.dissem.apps.abit.notification.ProofOfWorkNotification
import ch.dissem.apps.abit.notification.ProofOfWorkNotification.Companion.ONGOING_NOTIFICATION_ID
import ch.dissem.apps.abit.util.PowStats

View File

@ -21,23 +21,20 @@ import android.widget.Toast
import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter
import ch.dissem.apps.abit.adapter.SwitchingProofOfWorkEngine
import ch.dissem.apps.abit.listener.MessageListener
import ch.dissem.apps.abit.pow.ServerPowEngine
import ch.dissem.apps.abit.repository.*
import ch.dissem.apps.abit.util.Constants
import ch.dissem.apps.abit.util.Observable
import ch.dissem.apps.abit.util.preferences
import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.Pubkey
import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.factory.BufferPool
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler
import ch.dissem.bitmessage.ports.DefaultLabeler
import ch.dissem.bitmessage.utils.ConversationService
import ch.dissem.bitmessage.utils.TTL
import ch.dissem.bitmessage.utils.UnixTime.DAY
import io.reactivex.subjects.BehaviorSubject
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.lang.ref.WeakReference
@ -46,7 +43,7 @@ import java.lang.ref.WeakReference
* Provides singleton objects across the application.
*/
object Singleton {
var currentLabel = Observable<Label?>(null)
var currentLabel = BehaviorSubject.create<Label>()
private var swipeableMessageAdapter: WeakReference<SwipeableMessageAdapter>? = null
val labeler = DefaultLabeler().apply {
@ -102,27 +99,23 @@ object Singleton {
fun getBitmessageContext(context: Context): BitmessageContext =
init({ bitmessageContext }, { bitmessageContext = it }) {
BufferPool.setLimit(4)
BitmessageContext.build {
TTL.pubkey = 2 * DAY
val ctx = context.applicationContext
val sqlHelper = SqlHelper(ctx)
proofOfWorkEngine = SwitchingProofOfWorkEngine(
ctx, Constants.PREFERENCE_SERVER_POW,
ServerPowEngine(ctx),
ServicePowEngine(ctx)
)
proofOfWorkEngine = ServicePowEngine(ctx)
cryptography = SpongyCryptography()
nodeRegistry = AndroidNodeRegistry(sqlHelper)
inventory = AndroidInventory(sqlHelper)
addressRepo = AndroidAddressRepository(sqlHelper)
labelRepo = AndroidLabelRepository(sqlHelper, ctx)
messageRepo = AndroidMessageRepository(sqlHelper)
messageRepo = AndroidMessageRepository(sqlHelper, ctx.preferences)
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
networkHandler = NioNetworkHandler(4)
listener = getMessageListener(ctx)
labeler = Singleton.labeler
preferences.sendPubkeyOnIdentityCreation = false
preferences.port = context.preferences.listeningPort
}
}
@ -138,8 +131,6 @@ object Singleton {
fun getAddressRepository(ctx: Context) = getBitmessageContext(ctx).addresses as AndroidAddressRepository
fun getProofOfWorkRepository(ctx: Context) = powRepo ?: getBitmessageContext(ctx).internals.proofOfWorkRepository
fun getIdentity(ctx: Context): BitmessageAddress? =
init<BitmessageAddress?>(ctx, { identity }, { identity = it }) { bmc ->
val identities = bmc.addresses.getIdentities()

View File

@ -3,18 +3,17 @@ package ch.dissem.apps.abit.service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import android.content.Intent.ACTION_BOOT_COMPLETED
import ch.dissem.apps.abit.util.network
import ch.dissem.apps.abit.util.preferences
/**
* Starts the Bitmessage "full node" service if conditions allow it
*/
class StartServiceReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent?) {
if (intent?.action == "android.intent.action.BOOT_COMPLETED") {
if (Preferences.isFullNodeActive(context)) {
NetworkUtils.enableNode(context, false)
}
if (intent?.action == ACTION_BOOT_COMPLETED && context.preferences.online) {
context.network.enableNode(false)
}
}
}

View File

@ -1,33 +0,0 @@
package ch.dissem.apps.abit.service
import android.app.job.JobParameters
import android.app.job.JobService
import android.os.Build
import android.support.annotation.RequiresApi
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
/**
* Starts the full node if
* * it is active
* * it is not already running
*
* And stops it when the preconditions for the job (unmetered network) aren't met anymore.
*/
class StartupNodeOnWifiService : JobService() {
override fun onStartJob(params: JobParameters?): Boolean {
val bmc = Singleton.getBitmessageContext(this)
if (Preferences.isFullNodeActive(this) && !bmc.isRunning()) {
NetworkUtils.doStartBitmessageService(applicationContext)
}
return true
}
/**
* Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
* depending on Android version.
*/
override fun onStopJob(params: JobParameters?) = Preferences.isFullNodeActive(this)
}

View File

@ -1,62 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.synchronization
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.NetworkErrorException
import android.content.Context
import android.os.Bundle
/**
* Implement AbstractAccountAuthenticator and stub out all
* of its methods
*/
class Authenticator(context: Context) : AbstractAccountAuthenticator(context) {
override fun editProperties(r: AccountAuthenticatorResponse, s: String) =
throw UnsupportedOperationException("Editing properties is not supported")
// Don't add additional accounts
@Throws(NetworkErrorException::class)
override fun addAccount(r: AccountAuthenticatorResponse, s: String, s2: String, strings: Array<String>, bundle: Bundle) = null
// Ignore attempts to confirm credentials
@Throws(NetworkErrorException::class)
override fun confirmCredentials(r: AccountAuthenticatorResponse, account: Account, bundle: Bundle) = null
@Throws(NetworkErrorException::class)
override fun getAuthToken(r: AccountAuthenticatorResponse, account: Account, s: String, bundle: Bundle) =
throw UnsupportedOperationException("Getting an authentication token is not supported")
override fun getAuthTokenLabel(s: String) =
throw UnsupportedOperationException("Getting a label for the auth token is not supported")
@Throws(NetworkErrorException::class)
override fun updateCredentials(r: AccountAuthenticatorResponse, account: Account, s: String, bundle: Bundle) =
throw UnsupportedOperationException("Updating user credentials is not supported")
@Throws(NetworkErrorException::class)
override fun hasFeatures(r: AccountAuthenticatorResponse, account: Account, strings: Array<String>) =
throw UnsupportedOperationException("Checking features for the account is not supported")
companion object {
val ACCOUNT_SYNC = Account("Bitmessage", "ch.dissem.bitmessage")
val ACCOUNT_POW = Account("Proof of Work ", "ch.dissem.bitmessage")
}
}

View File

@ -1,42 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.synchronization
import android.app.Service
import android.content.Intent
/**
* A bound Service that instantiates the authenticator
* when started.
*/
class AuthenticatorService : Service() {
/**
* Instance field that stores the authenticator object
*/
private var authenticator: Authenticator? = null
override fun onCreate() {
// Create a new authenticator object
authenticator = Authenticator(this)
}
/*
* When the system binds to this Service to make the RPC call
* return the authenticator's IBinder.
*/
override fun onBind(intent: Intent) = authenticator?.iBinder
}

View File

@ -1,72 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.synchronization
import android.content.ContentProvider
import android.content.ContentValues
import android.net.Uri
/*
* Define an implementation of ContentProvider that stubs out
* all methods
*/
class StubProvider : ContentProvider() {
/**
* Always return true, indicating that the
* provider loaded correctly.
*/
override fun onCreate() = true
/**
* Return no type for MIME type
*/
override fun getType(uri: Uri) = null
/**
* query() always returns no results
*/
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?) = null
/**
* insert() always returns null (no URI)
*/
override fun insert(uri: Uri, values: ContentValues?) = null
/**
* delete() always returns "no rows affected" (0)
*/
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?) = 0
/**
* update() always returns "no rows affected" (0)
*/
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?) = 0
companion object {
const val AUTHORITY = "ch.dissem.apps.abit.provider"
}
}

View File

@ -1,188 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.synchronization
import android.accounts.Account
import android.accounts.AccountManager
import android.content.*
import android.os.Bundle
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_POW
import ch.dissem.apps.abit.synchronization.Authenticator.Companion.ACCOUNT_SYNC
import ch.dissem.apps.abit.synchronization.StubProvider.Companion.AUTHORITY
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.exception.DecryptionFailedException
import ch.dissem.bitmessage.extensions.CryptoCustomMessage
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.CALCULATE
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest.Request.COMPLETE
import ch.dissem.bitmessage.utils.Singleton.cryptography
import org.slf4j.LoggerFactory
import java.io.IOException
/**
* Sync Adapter to synchronize with the Bitmessage network - fetches
* new objects and then disconnects.
*/
class SyncAdapter(context: Context, autoInitialize: Boolean) : AbstractThreadedSyncAdapter(context, autoInitialize) {
private val bmc = Singleton.getBitmessageContext(context)
override fun onPerformSync(
account: Account,
extras: Bundle,
authority: String,
provider: ContentProviderClient,
syncResult: SyncResult
) {
try {
if (account == ACCOUNT_SYNC) {
if (Preferences.isConnectionAllowed(context)) {
syncData()
}
} else if (account == ACCOUNT_POW) {
syncPOW()
} else {
syncResult.stats.numAuthExceptions++
}
} catch (e: IOException) {
syncResult.stats.numIoExceptions++
} catch (e: DecryptionFailedException) {
syncResult.stats.numAuthExceptions++
}
}
private fun syncData() {
// If the Bitmessage context acts as a full node, synchronization isn't necessary
if (bmc.isRunning()) {
LOG.info("Synchronization skipped, Abit is acting as a full node")
return
}
val trustedNode = Preferences.getTrustedNode(context)
if (trustedNode == null) {
// As Abit tends to get killed by the system, let's leverage the sync mechanism to start it again:
NetworkUtils.scheduleNodeStart(context)
return
}
LOG.info("Synchronization started")
bmc.synchronize(
trustedNode,
Preferences.getTrustedNodePort(context),
Preferences.getTimeoutInSeconds(context),
true
)
LOG.info("Synchronization finished")
}
private fun syncPOW() {
val identity = Singleton.getIdentity(context)
if (identity == null) {
LOG.info("No identity available - skipping POW synchronization")
return
}
val trustedNode = Preferences.getTrustedNode(context)
if (trustedNode == null) {
LOG.info("Trusted node not available, disabling POW synchronization")
stopPowSync(context)
return
}
// If the Bitmessage context acts as a full node, synchronization isn't necessary
LOG.info("Looking for completed POW")
val privateKey =
identity.privateKey?.privateEncryptionKey ?: throw IllegalStateException("Identity without private key")
val signingKey = cryptography().createPublicKey(identity.publicDecryptionKey)
val reader = ProofOfWorkRequest.Reader(identity)
val powRepo = Singleton.getProofOfWorkRepository(context)
val items = powRepo.getItems()
for (initialHash in items) {
val (objectMessage, nonceTrialsPerByte, extraBytes) = powRepo.getItem(initialHash)
val target = cryptography().getProofOfWorkTarget(objectMessage, nonceTrialsPerByte, extraBytes)
val cryptoMsg = CryptoCustomMessage(
ProofOfWorkRequest(identity, initialHash, CALCULATE, target)
)
cryptoMsg.signAndEncrypt(identity, signingKey)
val response = bmc.send(
trustedNode,
Preferences.getTrustedNodePort(context),
cryptoMsg
)
if (response.isError) {
LOG.error("Server responded with error: ${String(response.getData())}")
} else {
val (_, _, request, data) = CryptoCustomMessage.read(response, reader).decrypt(privateKey)
if (request == COMPLETE) {
bmc.internals.proofOfWorkService.onNonceCalculated(initialHash, data)
}
}
}
if (items.isEmpty()) {
stopPowSync(context)
}
LOG.info("Synchronization finished")
}
companion object {
private val LOG = LoggerFactory.getLogger(SyncAdapter::class.java)
private const val SYNC_FREQUENCY = 15 * 60L // seconds
fun startSync(ctx: Context) {
// Create account, if it's missing. (Either first run, or user has deleted account.)
val account = addAccount(ctx, ACCOUNT_SYNC)
// Recommend a schedule for automatic synchronization. The system may modify this based
// on other scheduled syncs and network utilization.
ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle(), SYNC_FREQUENCY)
}
fun stopSync(ctx: Context) {
// Create account, if it's missing. (Either first run, or user has deleted account.)
val account = addAccount(ctx, ACCOUNT_SYNC)
ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle())
}
fun startPowSync(ctx: Context) {
// Create account, if it's missing. (Either first run, or user has deleted account.)
val account = addAccount(ctx, ACCOUNT_POW)
// Recommend a schedule for automatic synchronization. The system may modify this based
// on other scheduled syncs and network utilization.
ContentResolver.addPeriodicSync(account, AUTHORITY, Bundle(), SYNC_FREQUENCY)
}
fun stopPowSync(ctx: Context) {
// Create account, if it's missing. (Either first run, or user has deleted account.)
val account = addAccount(ctx, ACCOUNT_POW)
ContentResolver.removePeriodicSync(account, AUTHORITY, Bundle())
}
private fun addAccount(ctx: Context, account: Account): Account {
if (AccountManager.get(ctx).addAccountExplicitly(account, null, null)) {
// Inform the system that this account supports sync
ContentResolver.setIsSyncable(account, AUTHORITY, 1)
// Inform the system that this account is eligible for auto sync when the network is up
ContentResolver.setSyncAutomatically(account, AUTHORITY, true)
}
return account
}
}
}

View File

@ -1,50 +0,0 @@
/*
* Copyright 2016 Christian Basler
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ch.dissem.apps.abit.synchronization
import android.app.Service
import android.content.Intent
/**
* Define a Service that returns an IBinder for the
* sync adapter class, allowing the sync adapter framework to call
* onPerformSync().
*/
class SyncService : Service() {
/**
* Instantiate the sync adapter object.
*/
override fun onCreate() = synchronized(syncAdapterLock) {
if (syncAdapter == null) {
syncAdapter = SyncAdapter(this, true)
}
}
/**
* Return an object that allows the system to invoke
* the sync adapter.
*/
override fun onBind(intent: Intent) = syncAdapter?.syncAdapterBinder
companion object {
// Storage for an instance of the sync adapter
private var syncAdapter: SyncAdapter? = null
// Object to use as a thread-safe lock
private val syncAdapterLock = Any()
}
}

View File

@ -22,16 +22,14 @@ import java.util.regex.Pattern
* @author Christian Basler
*/
object Constants {
const val PREFERENCE_ONLINE = "online"
const val PREFERENCE_WIFI_ONLY = "wifi_only"
const val PREFERENCE_REQUIRE_CHARGING = "require_charging"
const val PREFERENCE_EMULATE_CONVERSATIONS = "emulate_conversations"
const val PREFERENCE_TRUSTED_NODE = "trusted_node"
const val PREFERENCE_SYNC_TIMEOUT = "sync_timeout"
const val PREFERENCE_SERVER_POW = "server_pow"
const val PREFERENCE_FULL_NODE = "full_node"
const val PREFERENCE_REQUEST_ACK = "request_acknowledgments"
const val PREFERENCE_POW_AVERAGE = "average_pow_time_ms"
const val PREFERENCE_POW_COUNT = "pow_count"
const val PREFERENCE_SEPARATE_IDENTITIES = "separate_identities"
const val BITMESSAGE_URL_SCHEMA = "bitmessage:"

View File

@ -1,7 +1,7 @@
package ch.dissem.apps.abit.util
import android.content.Context
import android.support.annotation.ColorInt
import androidx.annotation.ColorInt
import ch.dissem.apps.abit.R
import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.community_material_typeface_library.CommunityMaterial

View File

@ -6,20 +6,21 @@ import android.app.job.JobScheduler
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.support.v4.content.ContextCompat
import android.os.Build
import ch.dissem.apps.abit.dialog.FullNodeDialogActivity
import ch.dissem.apps.abit.service.BitmessageService
import ch.dissem.apps.abit.service.StartupNodeOnWifiService
import ch.dissem.apps.abit.service.CleanupService
import ch.dissem.apps.abit.service.NodeStartupService
import java.lang.ref.WeakReference
import java.util.concurrent.TimeUnit
val Context.network get() = NetworkUtils.getInstance(this)
object NetworkUtils {
class NetworkUtils internal constructor(private val ctx: Context) {
fun enableNode(ctx: Context, ask: Boolean = true) {
Preferences.setFullNodeActive(ctx, true)
private val jobScheduler by lazy { ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler }
if (Preferences.isConnectionAllowed(ctx) || !ask) {
scheduleNodeStart(ctx)
} else {
fun enableNode(ask: Boolean = true) {
if (ask && !ctx.preferences.connectionAllowed) {
// Ask for connection
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
if (ctx !is Activity) {
@ -27,30 +28,50 @@ object NetworkUtils {
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
}
ctx.startActivity(dialogIntent)
} else {
scheduleNodeStart()
}
}
fun doStartBitmessageService(ctx: Context) {
ContextCompat.startForegroundService(ctx, Intent(ctx, BitmessageService::class.java))
fun disableNode() {
jobScheduler.cancelAll()
}
fun disableNode(ctx: Context) {
Preferences.setFullNodeActive(ctx, false)
ctx.stopService(Intent(ctx, BitmessageService::class.java))
}
fun scheduleNodeStart(ctx: Context) {
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java)
val builder = JobInfo.Builder(0, serviceComponent)
if (Preferences.isWifiOnly(ctx)) {
fun scheduleNodeStart() {
JobInfo.Builder(0, ComponentName(ctx, NodeStartupService::class.java)).let { builder ->
when {
ctx.preferences.wifiOnly ->
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ->
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
else ->
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
}
if (Preferences.requireCharging(ctx)) {
builder.setRequiresCharging(true)
}
builder.setBackoffCriteria(0L, JobInfo.BACKOFF_POLICY_LINEAR)
builder.setRequiresCharging(ctx.preferences.requireCharging)
builder.setPersisted(true)
jobScheduler.schedule(builder.build())
}
JobInfo.Builder(1, ComponentName(ctx, CleanupService::class.java)).let { builder ->
builder.setPeriodic(TimeUnit.DAYS.toMillis(1))
builder.setRequiresDeviceIdle(true)
builder.setRequiresCharging(true)
jobScheduler.schedule(builder.build())
}
}
companion object {
private var instance: WeakReference<NetworkUtils>? = null
internal fun getInstance(ctx: Context): NetworkUtils {
var networkUtils = instance?.get()
if (networkUtils == null) {
networkUtils = NetworkUtils(ctx.applicationContext)
instance = WeakReference(networkUtils)
}
return networkUtils
}
}
}

View File

@ -1,37 +0,0 @@
package ch.dissem.apps.abit.util
import kotlin.properties.Delegates
/**
* A simple observable implementation that should be mostly
*/
class Observable<T>(value: T) {
private val observers = mutableMapOf<Any, (T) -> Unit>()
var value: T by Delegates.observable(value, { _, old, new ->
if (old != new) {
observers.values.forEach { it.invoke(new) }
}
})
/**
* The key will make sure the observer can easily be removed. Usually the key should be either
* the object that created the observer, or the observer itself, if it's easily available.
*
* Note that a map is used for observers, so if you define more than one observer with the same
* key, all previous ones will be removed. Also, the observers will be notified in no specific
* order.
*
* To prevent memory leaks, the observer must be removed if it isn't used anymore.
*/
fun addObserver(key: Any, observer: (T) -> Unit) {
observers[key] = observer
}
/**
* Remove the observer that was registered with the given key.
*/
fun removeObserver(key: Any) {
observers.remove(key)
}
}

View File

@ -17,127 +17,67 @@
package ch.dissem.apps.abit.util
import android.content.Context
import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.notification.ErrorNotification
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Constants.PREFERENCE_EMULATE_CONVERSATIONS
import ch.dissem.apps.abit.util.Constants.PREFERENCE_FULL_NODE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_ONLINE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUEST_ACK
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUIRE_CHARGING
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SYNC_TIMEOUT
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_SEPARATE_IDENTITIES
import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY
import org.jetbrains.anko.batteryManager
import org.jetbrains.anko.connectivityManager
import org.jetbrains.anko.defaultSharedPreferences
import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException
import java.net.InetAddress
import android.os.BatteryManager
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import java.lang.ref.WeakReference
val Context.preferences get() = Preferences.getInstance(this)
/**
* @author Christian Basler
*/
object Preferences {
class Preferences internal constructor(private val ctx: Context) {
private val LOG = LoggerFactory.getLogger(Preferences::class.java)
fun useTrustedNode(ctx: Context): Boolean {
val trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return false
return trustedNode.trim { it <= ' ' }.isNotEmpty()
}
val connectionAllowed get() = isAllowedForWiFi && isAllowedForCharging
/**
* Warning, this method might do a network call and therefore can't be called from
* the UI thread.
*/
@Throws(IOException::class)
fun getTrustedNode(ctx: Context): InetAddress? {
var trustedNode: String = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return null
trustedNode = trustedNode.trim { it <= ' ' }
if (trustedNode.isEmpty()) return null
private val isAllowedForWiFi get() = !wifiOnly || !ctx.connectivityManager.isActiveNetworkMetered
if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$".toRegex())) {
val index = trustedNode.lastIndexOf(':')
trustedNode = trustedNode.substring(0, index)
}
return InetAddress.getByName(trustedNode)
}
private val isAllowedForCharging get() = !requireCharging || isCharging
fun getTrustedNodePort(ctx: Context): Int {
var trustedNode: String = getPreference(ctx, PREFERENCE_TRUSTED_NODE) ?: return 8444
trustedNode = trustedNode.trim { it <= ' ' }
private val sharedPreferences = ctx.defaultSharedPreferences
if (trustedNode.matches("^(?![0-9a-fA-F]*:[0-9a-fA-F]*:).*(:[0-9]+)$".toRegex())) {
val index = trustedNode.lastIndexOf(':')
val portString = trustedNode.substring(index + 1)
try {
return Integer.parseInt(portString)
} catch (e: NumberFormatException) {
ErrorNotification(ctx)
.setError(R.string.error_invalid_sync_port, portString)
.show()
}
}
return 8444
}
fun getTimeoutInSeconds(ctx: Context): Long = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT)?.toLong() ?: 120
private fun getPreference(ctx: Context, name: String): String? = ctx.defaultSharedPreferences.getString(name, null)
fun isConnectionAllowed(ctx: Context) = isAllowedForWiFi(ctx) && isAllowedForCharging(ctx)
private fun isAllowedForWiFi(ctx: Context) = !isWifiOnly(ctx) || !ctx.connectivityManager.isActiveNetworkMetered
private fun isAllowedForCharging(ctx: Context) = !requireCharging(ctx) || isCharging(ctx)
private fun isCharging(ctx: Context) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
private val isCharging
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ctx.batteryManager.isCharging
} else {
val intent = ctx.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
}
fun isWifiOnly(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
fun setWifiOnly(ctx: Context, status: Boolean) {
ctx.defaultSharedPreferences.edit()
.putBoolean(PREFERENCE_WIFI_ONLY, status)
var wifiOnly
get() = sharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
set(value) {
sharedPreferences.edit()
.putBoolean(PREFERENCE_WIFI_ONLY, value)
.apply()
}
fun requireCharging(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
val requireCharging get() = sharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
fun setRequireCharging(ctx: Context, status: Boolean) {
ctx.defaultSharedPreferences.edit()
.putBoolean(PREFERENCE_REQUIRE_CHARGING, status)
.apply()
}
val emulateConversations get() = sharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true)
fun isEmulateConversations(ctx: Context) =
ctx.defaultSharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true)
val exportDirectory by lazy { File(ctx.filesDir, "exports") }
val requestAcknowledgements = sharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
fun isFullNodeActive(ctx: Context) =
ctx.defaultSharedPreferences.getBoolean(PREFERENCE_FULL_NODE, false)
fun setFullNodeActive(ctx: Context, status: Boolean) {
ctx.defaultSharedPreferences.edit()
.putBoolean(PREFERENCE_FULL_NODE, status)
.apply()
}
fun getExportDirectory(ctx: Context) = File(ctx.filesDir, "exports")
fun requestAcknowledgements(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
fun cleanupExportDirectory(ctx: Context) {
val exportDirectory = getExportDirectory(ctx)
fun cleanupExportDirectory() {
if (exportDirectory.exists()) {
exportDirectory.listFiles().forEach { file ->
try {
@ -150,4 +90,40 @@ object Preferences {
}
}
}
var online
get() = sharedPreferences.getBoolean(PREFERENCE_ONLINE, true)
set(value) {
sharedPreferences.edit()
.putBoolean(PREFERENCE_ONLINE, value)
.apply()
if (value) {
ctx.network.enableNode(true)
} else {
ctx.network.disableNode()
}
}
val separateIdentities
get() = sharedPreferences.getBoolean(PREFERENCE_SEPARATE_IDENTITIES, false)
val currentIdentity
get() = Singleton.getIdentity(ctx)
val listeningPort
get() = sharedPreferences.getString("listening_port", null)?.toIntOrNull()
?: 8444
companion object {
private var instance: WeakReference<Preferences>? = null
internal fun getInstance(ctx: Context): Preferences {
var prefs = instance?.get()
if (prefs == null) {
prefs = Preferences(ctx.applicationContext)
instance = WeakReference(prefs)
}
return prefs
}
}
}

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33V8h5.47L13,7v1h4V5.33C17,4.6 16.4,4 15.67,4z"
android:fillAlpha=".3"/>
<path
android:fillColor="#FF000000"
android:pathData="M13,12.5h2L11,20v-5.5H9L12.47,8H7v12.67C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V8h-4v4.5z"/>
</vector>

View File

@ -0,0 +1,8 @@
<!-- drawable/broom.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M19.36,2.72L20.78,4.14L15.06,9.85C16.13,11.39 16.28,13.24 15.38,14.44L9.06,8.12C10.26,7.22 12.11,7.37 13.65,8.44L19.36,2.72M5.93,17.57C3.92,15.56 2.69,13.16 2.35,10.92L7.23,8.83L14.67,16.27L12.58,21.15C10.34,20.81 7.94,19.58 5.93,17.57Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,8h-2.81c-0.45,-0.78 -1.07,-1.45 -1.82,-1.96L17,4.41 15.59,3l-2.17,2.17C12.96,5.06 12.49,5 12,5c-0.49,0 -0.96,0.06 -1.41,0.17L8.41,3 7,4.41l1.62,1.63C7.88,6.55 7.26,7.22 6.81,8L4,8v2h2.09c-0.05,0.33 -0.09,0.66 -0.09,1v1L4,12v2h2v1c0,0.34 0.04,0.67 0.09,1L4,16v2h2.81c1.04,1.79 2.97,3 5.19,3s4.15,-1.21 5.19,-3L20,18v-2h-2.09c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1L20,10L20,8zM14,16h-4v-2h4v2zM14,12h-4v-2h4v2z"/>
</vector>

View File

@ -0,0 +1,8 @@
<!-- drawable/check_all.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M0.41,13.41L6,19L7.41,17.58L1.83,12M22.24,5.58L11.66,16.17L7.5,12L6.07,13.41L11.66,19L23.66,7M18,7L16.59,5.58L10.24,11.93L11.66,13.34L18,7Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM8,14L6,14v-2h2v2zM8,11L6,11L6,9h2v2zM8,8L6,8L6,6h2v2zM15,14h-5v-2h5v2zM18,11h-8L10,9h8v2zM18,8h-8L10,6h8v2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,16h6v-6h4l-7,-7 -7,7h4zM5,18h14v2L5,20z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-6h2v6zM13,9h-2L11,7h2v2z"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12.01,21.49L23.64,7c-0.45,-0.34 -4.93,-4 -11.64,-4C5.28,3 0.81,6.66 0.36,7l11.63,14.49 0.01,0.01 0.01,-0.01z"
android:fillAlpha=".3"/>
<path
android:fillColor="#FF000000"
android:pathData="M3.53,10.95l8.46,10.54 0.01,0.01 0.01,-0.01 8.46,-10.54C20.04,10.62 16.81,8 12,8c-4.81,0 -8.04,2.62 -8.47,2.95z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7.77,6.76L6.23,5.48 0.82,12l5.41,6.52 1.54,-1.28L3.42,12l4.35,-5.24zM7,13h2v-2L7,11v2zM17,11h-2v2h2v-2zM11,13h2v-2h-2v2zM17.77,5.48l-1.54,1.28L20.58,12l-4.35,5.24 1.54,1.28L23.18,12l-5.41,-6.52z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M16,11c1.66,0 2.99,-1.34 2.99,-3S17.66,5 16,5c-1.66,0 -3,1.34 -3,3s1.34,3 3,3zM8,11c1.66,0 2.99,-1.34 2.99,-3S9.66,5 8,5C6.34,5 5,6.34 5,8s1.34,3 3,3zM8,13c-2.33,0 -7,1.17 -7,3.5L1,19h14v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5zM16,13c-0.29,0 -0.62,0.02 -0.97,0.05 1.16,0.84 1.97,1.97 1.97,3.45L17,19h6v-2.5c0,-2.33 -4.67,-3.5 -7,-3.5z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9,11.24L9,7.5C9,6.12 10.12,5 11.5,5S14,6.12 14,7.5v3.74c1.21,-0.81 2,-2.18 2,-3.74C16,5.01 13.99,3 11.5,3S7,5.01 7,7.5c0,1.56 0.79,2.93 2,3.74zM18.84,15.87l-4.54,-2.26c-0.17,-0.07 -0.35,-0.11 -0.54,-0.11L13,13.5v-6c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,6.67 10,7.5v10.74l-3.43,-0.72c-0.08,-0.01 -0.15,-0.03 -0.24,-0.03 -0.31,0 -0.59,0.13 -0.79,0.33l-0.79,0.8 4.94,4.94c0.27,0.27 0.65,0.44 1.06,0.44h6.79c0.75,0 1.33,-0.55 1.44,-1.28l0.75,-5.27c0.01,-0.07 0.02,-0.14 0.02,-0.2 0,-0.62 -0.38,-1.16 -0.91,-1.38z"/>
</vector>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2015 Haruki Hasegawa
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<shape
android:shape="rectangle"
xmlns:android="http://schemas.android.com/apk/res/android">
<size android:height="1px"/>
<solid android:color="@color/divider"/>
</shape>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="#ffffff">
<item
android:id="@android:id/mask"
android:drawable="@android:color/white"/>
</ripple>

View File

@ -1,11 +1,11 @@
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="64dp"
@ -19,7 +19,7 @@
tools:ignore="UnusedAttribute"
tools:layout_editor_absoluteX="0dp" />
<android.support.constraint.Guideline
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -74,4 +74,4 @@
tools:ignore="UnusedAttribute"
tools:layout_editor_absoluteX="8dp" />
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -4,7 +4,7 @@
android:layout_height="wrap_content"
android:padding="24dp">
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/address_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -17,16 +17,17 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/address"
android:importantForAutofill="no"
android:inputType="textNoSuggestions" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/address_wrapper"
android:layout_below="@+id/address_wrapper"
android:layout_alignStart="@+id/address_wrapper"
android:layout_marginTop="16dp">
<EditText
@ -34,18 +35,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label"
android:importantForAutofill="no"
android:inputType="textPersonName" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/subscribe"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignStart="@+id/address_wrapper"
android:layout_below="@+id/label_wrapper"
android:layout_marginBottom="8dp"
android:layout_alignStart="@+id/address_wrapper"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="@string/subscribe" />
<Button
@ -53,10 +55,10 @@
style="?android:attr/borderlessButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_below="@+id/subscribe"
android:layout_marginBottom="12dp"
android:layout_alignParentEnd="true"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:text="@string/do_import" />
<Button

View File

@ -5,7 +5,7 @@
android:layout_height="match_parent"
android:gravity="center">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"

View File

@ -14,7 +14,7 @@
android:textSize="10dp"
tools:ignore="SpUsage" />
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -27,9 +27,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label"
android:importantForAutofill="no"
android:inputType="textPersonName" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/subscribe"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -19,11 +19,11 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</TextView>
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
@ -36,6 +36,6 @@
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
tools:ignore="UnusedAttribute"/>
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -14,15 +14,15 @@
~ limitations under the License.
-->
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="18dp"
android:paddingEnd="24dp"
android:paddingStart="24dp"
android:paddingTop="18dp">
android:paddingTop="18dp"
android:paddingEnd="24dp"
android:paddingBottom="18dp">
<TextView
android:id="@+id/description"
@ -34,7 +34,7 @@
tools:layout_constraintLeft_creator="1"
tools:layout_constraintTop_creator="1" />
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -47,11 +47,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/label"
android:autofillHints="label"
android:inputType="text" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passphrase_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -63,11 +64,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase"
android:autofillHints="passphrase"
android:inputType="textMultiLine" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/number_of_identities_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -80,11 +82,12 @@
android:layout_height="wrap_content"
android:ems="10"
android:hint="@string/number_of_identities"
android:autofillHints="numberOfIdentities"
android:inputType="number"
android:text="1"
tools:ignore="HardcodedText" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/shorter"
@ -115,4 +118,4 @@
app:layout_constraintBottom_toBottomOf="@id/ok"
app:layout_constraintRight_toLeftOf="@id/ok" />
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -98,4 +98,4 @@
android:textColor="@color/colorAccent"
app:layout_constraintRight_toLeftOf="@+id/ok"
app:layout_constraintTop_toBottomOf="@+id/radioGroup"/>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -56,4 +56,4 @@
app:layout_constraintEnd_toEndOf="@id/description"
app:layout_constraintTop_toBottomOf="@+id/ok"
tools:layout_editor_absoluteX="8dp"/>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -14,5 +14,6 @@
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:hint="@string/passphrase"
android:autofillHints="passphrase"
android:inputType="textMultiLine"/>
</LinearLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2015 Christian Basler
~
~ Licensed under the Apache License, Version 2.0 (the "License");
@ -34,10 +33,11 @@
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignTop="@+id/avatar"
android:layout_alignParentEnd="true"
android:layout_marginEnd="16dp"
android:layout_toEndOf="@+id/avatar"
android:importantForAutofill="no"
android:inputType="textPersonName"
android:text=""
tools:ignore="LabelFor" />
@ -72,19 +72,19 @@
android:layout_height="wrap_content"
android:layout_below="@+id/stream_number"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingTop="16dp"
android:paddingRight="16dp"
android:text="@string/subscribed" />
<ImageView
android:id="@+id/pubkey_available"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_below="@+id/active"
android:paddingEnd="4dp"
android:layout_alignParentStart="true"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="4dp"
android:src="@drawable/public_key"
tools:ignore="ContentDescription" />
@ -95,8 +95,8 @@
android:layout_alignBottom="@id/pubkey_available"
android:layout_alignParentEnd="true"
android:layout_toEndOf="@id/pubkey_available"
android:paddingEnd="16dp"
android:paddingStart="0dp"
android:paddingEnd="16dp"
android:text="@string/pubkey_available"
android:textAppearance="?android:attr/textAppearanceSmall" />
@ -104,14 +104,14 @@
android:id="@+id/qr_code"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/pubkey_available"
android:layout_marginBottom="64dp"
android:layout_marginEnd="16dp"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_marginStart="16dp"
android:layout_marginTop="24dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="64dp"
android:contentDescription="@string/alt_qr_code"
android:elevation="2dp"
tools:ignore="UnusedAttribute"

View File

@ -4,7 +4,7 @@
android:layout_height="match_parent">
<ListView
android:id="@id/android:list"
android:id="@android:id/list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"

View File

@ -24,7 +24,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="4dp">
@ -33,13 +33,14 @@
android:id="@+id/recipient_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="bitmessageAddress"
android:hint="@string/to"
android:inputType="textNoSuggestions"
android:maxLines="1" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<android.support.design.widget.TextInputLayout
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -47,17 +48,19 @@
android:id="@+id/subject_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="subject"
android:hint="@string/subject"
android:inputType="textEmailSubject"
android:textAppearance="?android:attr/textAppearanceLarge" />
</android.support.design.widget.TextInputLayout>
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/body_input"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:autofillHints="body, message"
android:gravity="start|top"
android:hint="@string/compose_body_hint"
android:inputType="textMultiLine|textCapSentences"

View File

@ -41,7 +41,7 @@
android:layout_below="@id/subject"
android:background="@color/divider" />
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messages"
android:layout_width="fill_parent"
android:layout_height="wrap_content"

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@ -41,6 +41,7 @@
android:gravity="start|top"
android:hint="@string/wif_string"
android:inputType="textMultiLine|text"
android:autofillHints="wif, comment, data"
app:layout_constraintBottom_toTopOf="@+id/next"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -57,4 +58,4 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,7 +15,7 @@
~ limitations under the License.
-->
<android.support.constraint.ConstraintLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
@ -33,7 +33,7 @@
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
@ -55,4 +55,4 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"/>
</android.support.constraint.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -78,7 +78,7 @@
android:paddingRight="8dp"
tools:text="Recipient" />
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/parents"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
@ -99,7 +99,7 @@
tools:text="Message Body"
android:textIsSelectable="true" />
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/labels"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
@ -108,7 +108,7 @@
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"/>
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/responses"
android:layout_width="fill_parent"
android:layout_height="wrap_content"

View File

@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"

View File

@ -88,7 +88,7 @@
android:textIsSelectable="true"
tools:text="Message Body" />
<android.support.v7.widget.RecyclerView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/labels"
android:layout_width="fill_parent"
android:layout_height="wrap_content"

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -16,11 +16,11 @@
tools:context=".ComposeMessageActivity"
tools:layout="@layout/fragment_compose_message"/>
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
@ -30,7 +30,7 @@
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
tools:ignore="UnusedAttribute"/>
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
@ -16,11 +16,11 @@
tools:context=".ComposeMessageActivity"
tools:layout="@layout/fragment_compose_message"/>
<android.support.design.widget.AppBarLayout
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
@ -31,7 +31,7 @@
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
tools:ignore="UnusedAttribute"/>
</android.support.design.widget.AppBarLayout>
</com.google.android.material.appbar.AppBarLayout>
</android.support.design.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -4,8 +4,15 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/empty_trash"
app:showAsAction="never"
android:icon="@drawable/ic_action_delete"
android:title="@string/empty_trash"
android:visible="false"/>
android:visible="false"
app:showAsAction="never" />
<item
android:id="@+id/delete_all"
android:icon="@drawable/ic_action_delete"
android:title="@string/delete_all_messages_in_list"
android:visible="true"
app:showAsAction="never" />
</menu>

Some files were not shown because too many files have changed in this diff Show More