🔀 Merge branch 'feature/conversations' into develop
@ -14,8 +14,11 @@ if (project.hasProperty("project.configs")
|
||||
//noinspection GroovyMissingReturnStatement
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
buildToolsVersion "26.0.2"
|
||||
buildToolsVersion "27.0.3"
|
||||
|
||||
signingConfigs {
|
||||
release
|
||||
}
|
||||
defaultConfig {
|
||||
applicationId "ch.dissem.apps.${appName.toLowerCase()}"
|
||||
minSdkVersion 19
|
||||
@ -51,11 +54,11 @@ android {
|
||||
|
||||
//ext.jabitVersion = '2.0.4'
|
||||
ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
|
||||
ext.supportVersion = '27.0.2'
|
||||
ext.supportVersion = '27.1.1'
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
implementation "org.jetbrains.anko:anko:$anko_version"
|
||||
|
||||
@ -65,49 +68,50 @@ dependencies {
|
||||
implementation "com.android.support:support-v13:$supportVersion"
|
||||
implementation "com.android.support:preference-v14:$supportVersion"
|
||||
implementation "com.android.support:design:$supportVersion"
|
||||
implementation "com.android.support:multidex:1.0.2"
|
||||
implementation "com.android.support:multidex:1.0.3"
|
||||
|
||||
implementation "ch.dissem.jabit:jabit-core:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-networking:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-extensions:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-exports:$jabitVersion"
|
||||
implementation "ch.dissem.jabit:jabit-cryptography-spongy:$jabitVersion"
|
||||
testImplementation "ch.dissem.jabit:jabit-cryptography-bouncy:$jabitVersion"
|
||||
|
||||
implementation 'org.slf4j:slf4j-android:1.7.25'
|
||||
|
||||
implementation 'com.mikepenz:materialize:1.1.2@aar'
|
||||
implementation('com.mikepenz:materialdrawer:6.0.2@aar') {
|
||||
implementation('com.mikepenz:materialdrawer:6.0.6@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation('com.mikepenz:aboutlibraries:6.0.2@aar') {
|
||||
implementation('com.mikepenz:aboutlibraries:6.0.6@aar') {
|
||||
transitive = true
|
||||
}
|
||||
implementation "com.mikepenz:iconics-core:3.0.0@aar"
|
||||
implementation "com.mikepenz:iconics-views:3.0.0@aar"
|
||||
implementation "com.mikepenz:iconics-core:3.0.3@aar"
|
||||
implementation "com.mikepenz:iconics-views:3.0.3@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.5.0@aar'
|
||||
implementation 'com.google.zxing:core:3.3.1'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:3.6.0@aar'
|
||||
implementation 'com.google.zxing:core:3.3.2'
|
||||
|
||||
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.1.8'
|
||||
implementation 'com.github.amlcurran.showcaseview:library:5.4.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.0.2'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.1.0'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.mockito:mockito-core:2.13.0'
|
||||
testImplementation 'org.mockito:mockito-core:2.15.0'
|
||||
testImplementation 'org.hamcrest:hamcrest-library:1.3'
|
||||
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
testImplementation 'org.robolectric:robolectric:3.6.1'
|
||||
testImplementation "org.robolectric:shadows-multidex:3.6.1"
|
||||
testImplementation 'org.robolectric:robolectric:3.7.1'
|
||||
testImplementation "org.robolectric:shadows-multidex:3.7.1"
|
||||
|
||||
androidTestImplementation "com.android.support:multidex:1.0.2"
|
||||
androidTestImplementation "com.android.support:multidex:1.0.3"
|
||||
}
|
||||
|
||||
idea.module {
|
||||
|
@ -6,6 +6,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||
@ -198,6 +199,10 @@
|
||||
android:exported="true"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
|
||||
<service
|
||||
android:name=".service.BatchProcessorService"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".StatusActivity"
|
||||
android:label="@string/title_activity_status"
|
||||
|
@ -26,6 +26,7 @@ import android.view.*
|
||||
import android.widget.Toast
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.apps.abit.util.qrCode
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.wif.WifExporter
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
@ -185,7 +186,7 @@ class AddressDetailFragment : Fragment() {
|
||||
}
|
||||
|
||||
// QR code
|
||||
qr_code.setImageBitmap(Drawables.qrCode(item))
|
||||
qr_code.setImageBitmap(item.qrCode())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,6 @@ import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.FabUtils
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
@ -48,7 +47,8 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
|
||||
activity,
|
||||
R.layout.subscription_row,
|
||||
R.id.name,
|
||||
LinkedList()) {
|
||||
LinkedList()
|
||||
) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val result: View
|
||||
val v: ViewHolder
|
||||
@ -72,7 +72,8 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
|
||||
v.avatar.setImageDrawable(Identicon(item))
|
||||
v.name.text = item.toString()
|
||||
v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream)
|
||||
v.subscribed.visibility = if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
|
||||
v.subscribed.visibility =
|
||||
if (item.isSubscribed) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -105,11 +106,11 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
|
||||
val menu = FabSpeedDialMenu(activity)
|
||||
menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code)
|
||||
menu.add(R.string.create_contact).setIcon(R.drawable.ic_action_create_contact)
|
||||
FabUtils.initFab(activity, R.drawable.ic_action_add_contact, menu)
|
||||
activity.initFab(R.drawable.ic_action_add_contact, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
when (itemId) {
|
||||
1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
|
||||
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES)
|
||||
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
|
||||
.initiateScan()
|
||||
2 -> {
|
||||
val intent = Intent(getActivity(), CreateAddressActivity::class.java)
|
||||
@ -121,7 +122,11 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_address_list, container, false)
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
|
@ -98,7 +98,8 @@ class ComposeMessageActivity : AppCompatActivity() {
|
||||
val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals(
|
||||
"RE:",
|
||||
ignoreCase = true
|
||||
)) {
|
||||
)
|
||||
) {
|
||||
""
|
||||
} else {
|
||||
"RE: "
|
||||
@ -107,7 +108,7 @@ class ComposeMessageActivity : AppCompatActivity() {
|
||||
}
|
||||
replyIntent.putExtra(
|
||||
EXTRA_CONTENT,
|
||||
"\n\n------------------------------------------------------\n" + item.text!!
|
||||
"\n\n------------------------------------------------------\n${item.text ?: ""}"
|
||||
)
|
||||
return replyIntent
|
||||
}
|
||||
|
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.view.*
|
||||
import ch.dissem.apps.abit.adapter.ConversationAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import kotlinx.android.synthetic.main.fragment_conversation_detail.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A fragment representing a single Message detail screen.
|
||||
* This fragment is either contained in a [MainActivity]
|
||||
* in two-pane mode (on tablets) or a [MessageDetailActivity]
|
||||
* on handsets.
|
||||
*/
|
||||
class ConversationDetailFragment : Fragment() {
|
||||
|
||||
/**
|
||||
* The content this fragment is presenting.
|
||||
*/
|
||||
private var itemId: UUID? = null
|
||||
private var item: Conversation? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
arguments?.let { arguments ->
|
||||
if (arguments.containsKey(ARG_ITEM_ID)) {
|
||||
// Load the dummy content specified by the fragment
|
||||
// arguments. In a real-world scenario, use a Loader
|
||||
// to load content from a content provider.
|
||||
itemId = arguments.getSerializable(ARG_ITEM_ID) as UUID
|
||||
}
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_conversation_detail, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val ctx = activity ?: throw IllegalStateException("Fragment is not attached to an activity")
|
||||
|
||||
item = itemId?.let { Singleton.getConversationService(ctx).getConversation(it) }
|
||||
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
subject.text = item.subject
|
||||
avatar.setImageDrawable(MultiIdenticon(item.participants))
|
||||
messages.adapter =
|
||||
ConversationAdapter(ctx, this@ConversationDetailFragment, item, Singleton.currentLabel.value)
|
||||
messages.layoutManager = LinearLayoutManager(activity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.conversation, menu)
|
||||
activity?.let { activity ->
|
||||
Drawables.addIcon(activity, menu, R.id.delete, GoogleMaterial.Icon.gmd_delete)
|
||||
Drawables.addIcon(activity, menu, R.id.archive, GoogleMaterial.Icon.gmd_archive)
|
||||
}
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
|
||||
val messageRepo = Singleton.getMessageRepository(
|
||||
context ?: throw IllegalStateException("No context available")
|
||||
)
|
||||
item?.let { item ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.delete -> {
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.delete(it)
|
||||
messageRepo.remove(it)
|
||||
}
|
||||
MainActivity.apply { updateUnread() }
|
||||
activity?.onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.archive -> {
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.archive(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
MainActivity.apply { updateUnread() }
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
* represents.
|
||||
*/
|
||||
const val ARG_ITEM_ID = "item_id"
|
||||
}
|
||||
}
|
@ -0,0 +1,339 @@
|
||||
/*
|
||||
* 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
|
||||
|
||||
|
||||
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 ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_BROADCAST
|
||||
import ch.dissem.apps.abit.ComposeMessageActivity.Companion.EXTRA_IDENTITY
|
||||
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.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 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 java.util.*
|
||||
|
||||
private const val PAGE_SIZE = 15
|
||||
|
||||
/**
|
||||
* A list fragment representing a list of Messages. This fragment
|
||||
* also supports tablet devices by allowing list items to be given an
|
||||
* 'activated' state upon selection. This helps indicate which item is
|
||||
* currently being viewed in a [MessageDetailFragment].
|
||||
*
|
||||
*
|
||||
* Activities containing this fragment MUST implement the [ListSelectionListener]
|
||||
* interface.
|
||||
*/
|
||||
class ConversationListFragment : Fragment(), ListHolder<Label> {
|
||||
|
||||
private var isLoading = false
|
||||
private var isLastPage = false
|
||||
|
||||
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) {
|
||||
layoutManager?.let { layoutManager ->
|
||||
val visibleItemCount = layoutManager.childCount
|
||||
val totalItemCount = layoutManager.itemCount
|
||||
val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (!isLoading && !isLastPage) {
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
|
||||
&& firstVisibleItemPosition >= 0
|
||||
) {
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyTrashMenuItem: MenuItem? = null
|
||||
private lateinit var messageRepo: AndroidMessageRepository
|
||||
private lateinit var conversationService: ConversationService
|
||||
private var activateOnItemClick: Boolean = false
|
||||
|
||||
private val backStack = Stack<Label>()
|
||||
|
||||
fun loadMoreItems() {
|
||||
isLoading = true
|
||||
swipeableConversationAdapter?.let { messageAdapter ->
|
||||
doAsync {
|
||||
val conversationIds = messageRepo.findConversations(
|
||||
currentLabel.value,
|
||||
messageAdapter.itemCount,
|
||||
PAGE_SIZE
|
||||
)
|
||||
conversationIds.forEach { conversationId ->
|
||||
val conversation = conversationService.getConversation(conversationId)
|
||||
onUiThread {
|
||||
messageAdapter.add(conversation)
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
isLastPage = conversationIds.size < PAGE_SIZE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
val activity = activity as MainActivity
|
||||
initFab(activity)
|
||||
messageRepo = Singleton.getMessageRepository(activity)
|
||||
conversationService = Singleton.getConversationService(activity)
|
||||
|
||||
currentLabel.addObserver(this) { new -> doUpdateList(new) }
|
||||
doUpdateList(currentLabel.value)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
currentLabel.removeObserver(this)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private fun doUpdateList(label: Label?) {
|
||||
val mainActivity = activity as? MainActivity
|
||||
swipeableConversationAdapter?.clear(label)
|
||||
if (label == null) {
|
||||
mainActivity?.updateTitle(getString(R.string.app_name))
|
||||
swipeableConversationAdapter?.notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
emptyTrashMenuItem?.isVisible = label.type == Label.Type.TRASH
|
||||
mainActivity?.apply {
|
||||
if ("archive" == label.toString()) {
|
||||
updateTitle(getString(R.string.archive))
|
||||
} else {
|
||||
updateTitle(label.toString())
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreItems()
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
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) {
|
||||
item.messages.forEach {
|
||||
Singleton.labeler.delete(it)
|
||||
messageRepo.save(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemArchived(item: Conversation) {
|
||||
item.messages.forEach { Singleton.labeler.archive(it) }
|
||||
}
|
||||
|
||||
override fun onItemViewClicked(v: View?) {
|
||||
val position = recycler_view.getChildAdapterPosition(v)
|
||||
adapter.setSelectedPosition(position)
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
MainActivity.apply { onItemSelected(adapter.getItem(position)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
recycler_view.layoutManager = layoutManager
|
||||
recycler_view.adapter = wrappedAdapter // requires *wrapped* swipeableConversationAdapter
|
||||
recycler_view.itemAnimator = animator
|
||||
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||
|
||||
recycler_view.addItemDecoration(
|
||||
SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true
|
||||
)
|
||||
)
|
||||
|
||||
// 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)
|
||||
|
||||
recyclerViewTouchActionGuardManager = touchActionGuardManager
|
||||
recyclerViewSwipeManager = swipeManager
|
||||
swipeableConversationAdapter = adapter
|
||||
|
||||
// FIXME Singleton.updateMessageListAdapterInListener(adapter)
|
||||
}
|
||||
|
||||
private fun initFab(context: MainActivity) {
|
||||
val menu = FabSpeedDialMenu(context)
|
||||
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||
context.initFab(R.drawable.ic_action_compose_message, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
val identity = Singleton.getIdentity(context)
|
||||
if (identity == null) {
|
||||
Toast.makeText(
|
||||
activity, R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
when (itemId) {
|
||||
1 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
intent.putExtra(EXTRA_BROADCAST, true)
|
||||
startActivity(intent)
|
||||
}
|
||||
2 -> {
|
||||
val intent = Intent(activity, ComposeMessageActivity::class.java)
|
||||
intent.putExtra(EXTRA_IDENTITY, identity)
|
||||
startActivity(intent)
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.message_list, menu)
|
||||
emptyTrashMenuItem = menu.findItem(R.id.empty_trash)
|
||||
super.onCreateOptionsMenu(menu, inflater)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.empty_trash -> {
|
||||
currentLabel.value?.let { label ->
|
||||
if (label.type != Label.Type.TRASH) return true
|
||||
|
||||
doAsync {
|
||||
for (message in messageRepo.findMessages(label)) {
|
||||
messageRepo.remove(message)
|
||||
}
|
||||
|
||||
uiThread { doUpdateList(label) }
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateList(label: Label) {
|
||||
currentLabel.value = label
|
||||
}
|
||||
|
||||
override fun setActivateOnItemClick(activateOnItemClick: Boolean) {
|
||||
swipeableConversationAdapter?.setActivateOnItemClick(activateOnItemClick)
|
||||
this.activateOnItemClick = activateOnItemClick
|
||||
}
|
||||
|
||||
override fun showPreviousList() = if (backStack.isEmpty()) {
|
||||
false
|
||||
} else {
|
||||
currentLabel.value = backStack.pop()
|
||||
true
|
||||
}
|
||||
}
|
@ -18,9 +18,11 @@ package ch.dissem.apps.abit
|
||||
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.support.annotation.ColorInt
|
||||
import android.text.TextPaint
|
||||
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import org.jetbrains.anko.collections.forEachWithIndex
|
||||
import kotlin.math.sqrt
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
@ -45,8 +47,20 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
}
|
||||
}
|
||||
}
|
||||
private val color = Color.HSVToColor(floatArrayOf((Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(), 0.8f, 1.0f))
|
||||
private val background = Color.HSVToColor(floatArrayOf((Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(), 0.8f, 1.0f))
|
||||
private val color = Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
(Math.abs(hash[0] * hash[1] + hash[2]) % 360).toFloat(),
|
||||
0.8f,
|
||||
1.0f
|
||||
)
|
||||
)
|
||||
private val background = Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
(Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(),
|
||||
0.8f,
|
||||
1.0f
|
||||
)
|
||||
)
|
||||
private val textPaint = TextPaint().apply {
|
||||
textAlign = Paint.Align.CENTER
|
||||
color = 0xFF607D8B.toInt()
|
||||
@ -54,20 +68,24 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
var x: Float
|
||||
var y: Float
|
||||
val width = canvas.width.toFloat()
|
||||
val height = canvas.height.toFloat()
|
||||
draw(canvas, 0f, 0f, width, height)
|
||||
}
|
||||
|
||||
internal fun draw(canvas: Canvas, offsetX: Float, offsetY: Float, width: Float, height: Float) {
|
||||
var x: Float
|
||||
var y: Float
|
||||
val cellWidth = width / SIZE.toFloat()
|
||||
val cellHeight = height / SIZE.toFloat()
|
||||
paint.color = background
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
canvas.drawCircle(offsetX + width / 2, offsetY + height / 2, width / 2, paint)
|
||||
paint.color = color
|
||||
for (row in 0 until SIZE) {
|
||||
for (column in 0 until SIZE) {
|
||||
if (fields[row][column]) {
|
||||
x = cellWidth * column
|
||||
y = cellHeight * row
|
||||
x = offsetX + cellWidth * column
|
||||
y = offsetY + cellHeight * row
|
||||
canvas.drawCircle(
|
||||
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
|
||||
paint
|
||||
@ -77,7 +95,7 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
}
|
||||
if (isChan) {
|
||||
textPaint.textSize = 2 * cellHeight
|
||||
canvas.drawText("[isChan]", width / 2, 6.7f * cellHeight, textPaint)
|
||||
canvas.drawText("[ chan ]", offsetX + width / 2, offsetY + 6.7f * cellHeight, textPaint)
|
||||
}
|
||||
}
|
||||
|
||||
@ -96,3 +114,68 @@ class Identicon(input: BitmessageAddress) : Drawable() {
|
||||
private const val CENTER_COLUMN = 5
|
||||
}
|
||||
}
|
||||
|
||||
class MultiIdenticon(input: List<BitmessageAddress>, @ColorInt private val backgroundColor: Int = 0xFFAEC2CC.toInt()) :
|
||||
Drawable() {
|
||||
|
||||
private val paint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
color = backgroundColor
|
||||
}
|
||||
|
||||
private val identicons = input.sortedBy { it.isChan }.map { Identicon(it) }.take(4)
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val width = canvas.width.toFloat()
|
||||
val height = canvas.height.toFloat()
|
||||
|
||||
when (identicons.size) {
|
||||
0 -> canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
1 -> identicons.first().draw(canvas, 0f, 0f, width, height)
|
||||
2 -> {
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
val w = width / 2
|
||||
val h = height / 2
|
||||
var x = 0f
|
||||
val y = height / 4
|
||||
identicons.forEach {
|
||||
it.draw(canvas, x, y, w, h)
|
||||
x += w
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
val scale = 2f / (1f + 2f * sqrt(3f))
|
||||
val w = width * scale
|
||||
val h = height * scale
|
||||
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
identicons[0].draw(canvas, (width - w) / 2, 0f, w, h)
|
||||
identicons[1].draw(canvas, (width - 2 * w) / 2, h * sqrt(3f) / 2, w, h)
|
||||
identicons[2].draw(canvas, width / 2, h * sqrt(3f) / 2, w, h)
|
||||
}
|
||||
4 -> {
|
||||
canvas.drawCircle(width / 2, height / 2, width / 2, paint)
|
||||
val scale = 1f / (1f + sqrt(2f))
|
||||
val borderScale = 0.5f - scale
|
||||
val w = width * scale
|
||||
val h = height * scale
|
||||
val x = width * borderScale
|
||||
val y = height * borderScale
|
||||
identicons.forEachWithIndex { i, identicon ->
|
||||
identicon.draw(canvas, x + (i % 2) * w, y + (i / 2) * h, w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) {
|
||||
identicons.forEach { it.alpha = alpha }
|
||||
}
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) {
|
||||
identicons.forEach { it.colorFilter = colorFilter }
|
||||
}
|
||||
|
||||
override fun getOpacity() = PixelFormat.TRANSPARENT
|
||||
}
|
||||
|
@ -17,14 +17,14 @@
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Point
|
||||
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 android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import ch.dissem.apps.abit.drawer.ProfileImageListener
|
||||
import ch.dissem.apps.abit.drawer.ProfileSelectionListener
|
||||
import ch.dissem.apps.abit.listener.ListSelectionListener
|
||||
@ -32,14 +32,15 @@ import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARC
|
||||
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.Labels
|
||||
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.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.github.amlcurran.showcaseview.ShowcaseView
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
@ -52,9 +53,13 @@ import com.mikepenz.materialdrawer.model.interfaces.IDrawerItem
|
||||
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 kotlinx.android.synthetic.main.activity_main.*
|
||||
import org.jetbrains.anko.doAsync
|
||||
import org.jetbrains.anko.uiThread
|
||||
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView
|
||||
import uk.co.deanwild.materialshowcaseview.shape.Shape
|
||||
import uk.co.deanwild.materialshowcaseview.target.Target
|
||||
import java.io.Serializable
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
@ -110,7 +115,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
val toolbar = findViewById<Toolbar>(R.id.toolbar)
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
val listFragment = MessageListFragment()
|
||||
val listFragment = ConversationListFragment()
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.item_list, listFragment)
|
||||
@ -146,33 +151,33 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
SyncAdapter.stopSync(this)
|
||||
}
|
||||
if (drawer.isDrawerOpen) {
|
||||
val lps = RelativeLayout.LayoutParams(
|
||||
ViewGroup
|
||||
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
|
||||
addRule(RelativeLayout.ALIGN_PARENT_LEFT)
|
||||
val margin = ((resources.displayMetrics.density * 12) as Number).toInt()
|
||||
setMargins(margin, margin, margin, margin)
|
||||
MaterialShowcaseView.Builder(this)
|
||||
.setMaskColour(R.color.colorPrimary)
|
||||
.setTitleText(R.string.full_node)
|
||||
.setContentText(R.string.full_node_description)
|
||||
.setDismissOnTouch(true)
|
||||
.setDismissText(R.string.got_it)
|
||||
.setShape(object : Shape {
|
||||
var w = 0
|
||||
var h = 0
|
||||
|
||||
override fun updateTarget(target: Target) {
|
||||
w = target.bounds.width()
|
||||
h = target.bounds.height()
|
||||
}
|
||||
|
||||
ShowcaseView.Builder(this)
|
||||
.withMaterialShowcase()
|
||||
.setStyle(R.style.CustomShowcaseTheme)
|
||||
.setContentTitle(R.string.full_node)
|
||||
.setContentText(R.string.full_node_description)
|
||||
.setTarget {
|
||||
val view = drawer.stickyFooter
|
||||
val location = IntArray(2)
|
||||
view.getLocationInWindow(location)
|
||||
val x = location[0] + 7 * view.width / 8
|
||||
val y = location[1] + view.height / 2
|
||||
Point(x, y)
|
||||
override fun getHeight() = h
|
||||
|
||||
override fun draw(canvas: Canvas, paint: Paint, x: Int, y: Int, padding: Int) {
|
||||
val r = h.toFloat() / 2
|
||||
canvas.drawCircle(x + w / 2 - r * 1.8f, y.toFloat(), r, paint)
|
||||
}
|
||||
.replaceEndButton(R.layout.showcase_button)
|
||||
.hideOnTouchOutside()
|
||||
.build()
|
||||
.setButtonPosition(lps)
|
||||
|
||||
override fun getWidth() = w
|
||||
})
|
||||
.setTarget(drawer.stickyFooter)
|
||||
.setDelay(1000)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,6 +304,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label
|
||||
} else if (currentLabel.value == null) {
|
||||
currentLabel.value = labels[0]
|
||||
|
||||
}
|
||||
for (label in labels) {
|
||||
addLabelEntry(label)
|
||||
@ -324,9 +330,15 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
val tag = item.tag
|
||||
if (tag is Label) {
|
||||
currentLabel.value = tag
|
||||
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
|
||||
if (itemList !is ConversationListFragment) {
|
||||
changeList(ConversationListFragment())
|
||||
}
|
||||
} else {
|
||||
if (itemList !is MessageListFragment) {
|
||||
changeList(MessageListFragment())
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if (item is Nameable<*>) {
|
||||
when (item.name.textRes) {
|
||||
@ -398,8 +410,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
.withIdentifier(label.id as Long)
|
||||
.withName(label.toString())
|
||||
.withTag(label)
|
||||
.withIcon(Labels.getIcon(label))
|
||||
.withIconColor(Labels.getColor(label))
|
||||
.withIcon(label.getIcon())
|
||||
.withIconColor(label.getColor(0xFF000000.toInt()))
|
||||
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
|
||||
}
|
||||
|
||||
@ -456,6 +468,13 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
// adding or replacing the detail fragment using a
|
||||
// fragment transaction.
|
||||
val fragment = when (item) {
|
||||
is Conversation -> {
|
||||
ConversationDetailFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putSerializable(ConversationDetailFragment.ARG_ITEM_ID, item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
ComposeMessageFragment().apply {
|
||||
@ -487,6 +506,11 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
// In single-pane mode, simply start the detail activity
|
||||
// for the selected item ID.
|
||||
val detailIntent = when (item) {
|
||||
is Conversation -> {
|
||||
Intent(this, MessageDetailActivity::class.java).apply {
|
||||
putExtra(ConversationDetailFragment.ARG_ITEM_ID, item.id)
|
||||
}
|
||||
}
|
||||
is Plaintext -> {
|
||||
if (item.labels.any { it.type == Label.Type.DRAFT }) {
|
||||
Intent(this, ComposeMessageActivity::class.java).apply {
|
||||
@ -520,6 +544,25 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
|
||||
supportActionBar?.title = title
|
||||
}
|
||||
|
||||
fun initFab(@DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
|
||||
val fab = floatingActionButton ?: throw IllegalStateException("Fab must not be null")
|
||||
fab.removeAllOnMenuItemClickListeners()
|
||||
fab.show()
|
||||
fab.closeMenu()
|
||||
val mainFab = fab.mainFab
|
||||
mainFab.setImageResource(drawableRes)
|
||||
fab.setMenu(menu)
|
||||
fab.addOnStateChangeListener { isOpened: Boolean ->
|
||||
if (isOpened) {
|
||||
// It will be turned 45 degrees, which makes an x out of the +
|
||||
mainFab.setImageResource(R.drawable.ic_action_add)
|
||||
} else {
|
||||
mainFab.setImageResource(drawableRes)
|
||||
}
|
||||
}
|
||||
return fab
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
|
||||
const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel"
|
||||
|
@ -4,6 +4,8 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.NavUtils
|
||||
import android.view.MenuItem
|
||||
import ch.dissem.bitmessage.entity.Conversation
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
|
||||
/**
|
||||
@ -33,9 +35,15 @@ class MessageDetailActivity : DetailActivity() {
|
||||
// Create the detail fragment and add it to the activity
|
||||
// using a fragment transaction.
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM,
|
||||
intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM))
|
||||
val fragment = MessageDetailFragment()
|
||||
val item = intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)
|
||||
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item)
|
||||
val itemId = intent.getSerializableExtra(ConversationDetailFragment.ARG_ITEM_ID)
|
||||
arguments.putSerializable(ConversationDetailFragment.ARG_ITEM_ID, itemId)
|
||||
val fragment = if (item is Plaintext) {
|
||||
MessageDetailFragment()
|
||||
} else {
|
||||
ConversationDetailFragment()
|
||||
}
|
||||
fragment.arguments = arguments
|
||||
supportFragmentManager.beginTransaction()
|
||||
.add(R.id.content, fragment)
|
||||
|
@ -29,17 +29,17 @@ import android.text.util.Linkify.WEB_URLS
|
||||
import android.view.*
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.adapter.LabelAdapter
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Assets
|
||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_ADDRESS_PATTERN
|
||||
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.apps.abit.util.Labels
|
||||
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.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.view.IconicsImageView
|
||||
import kotlinx.android.synthetic.main.fragment_message_detail.*
|
||||
import java.util.*
|
||||
|
||||
@ -85,8 +85,8 @@ class MessageDetailFragment : Fragment() {
|
||||
// Show the dummy content as text in a TextView.
|
||||
item?.let { item ->
|
||||
subject.text = item.subject
|
||||
status.setImageResource(Assets.getStatusDrawable(item.status))
|
||||
status.contentDescription = getString(Assets.getStatusString(item.status))
|
||||
status.setImageResource(item.status.getDrawable())
|
||||
status.contentDescription = getString(item.status.getString())
|
||||
avatar.setImageDrawable(Identicon(item.from))
|
||||
val senderClickListener: (View) -> Unit = {
|
||||
MainActivity.apply {
|
||||
@ -229,7 +229,7 @@ class MessageDetailFragment : Fragment() {
|
||||
val message = messages[position]
|
||||
|
||||
viewHolder.avatar.setImageDrawable(Identicon(message.from))
|
||||
viewHolder.status.setImageResource(Assets.getStatusDrawable(message.status))
|
||||
viewHolder.status.setImageResource(message.status.getDrawable())
|
||||
viewHolder.sender.text = message.from.toString()
|
||||
viewHolder.extract.text = prepareMessageExtract(message.text)
|
||||
viewHolder.item = message
|
||||
@ -259,40 +259,6 @@ class MessageDetailFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private class LabelAdapter internal constructor(private val ctx: Context, labels: Set<Label>) :
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
||||
|
||||
private val labels = labels.toMutableList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val label = labels[position]
|
||||
|
||||
viewHolder.icon.icon?.color(Labels.getColor(label))
|
||||
viewHolder.icon.icon?.icon(Labels.getIcon(label))
|
||||
viewHolder.label.text = Labels.getText(label, ctx)
|
||||
}
|
||||
|
||||
override fun getItemCount() = labels.size
|
||||
|
||||
internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
||||
var label = itemView.findViewById<TextView>(R.id.label)!!
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The fragment argument representing the item ID that this fragment
|
||||
|
@ -33,7 +33,6 @@ 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.apps.abit.util.FabUtils
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
|
||||
@ -80,7 +79,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
|
||||
if (!isLoading && !isLastPage) {
|
||||
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
|
||||
&& firstVisibleItemPosition >= 0) {
|
||||
&& firstVisibleItemPosition >= 0
|
||||
) {
|
||||
loadMoreItems()
|
||||
}
|
||||
}
|
||||
@ -98,7 +98,11 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
isLoading = true
|
||||
swipeableMessageAdapter?.let { messageAdapter ->
|
||||
doAsync {
|
||||
val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE)
|
||||
val messages = messageRepo.findMessages(
|
||||
currentLabel.value,
|
||||
messageAdapter.itemCount,
|
||||
PAGE_SIZE
|
||||
)
|
||||
onUiThread {
|
||||
messageAdapter.addAll(messages)
|
||||
isLoading = false
|
||||
@ -149,7 +153,11 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
loadMoreItems()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View =
|
||||
inflater.inflate(R.layout.fragment_message_list, container, false)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
@ -193,7 +201,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
adapter.setSelectedPosition(position)
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
val item = adapter.getItem(position)
|
||||
(activity as MainActivity).onItemSelected(item)
|
||||
MainActivity.apply { onItemSelected(item) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -213,8 +221,11 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
recycler_view.itemAnimator = animator
|
||||
recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
|
||||
|
||||
recycler_view.addItemDecoration(SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true))
|
||||
recycler_view.addItemDecoration(
|
||||
SimpleListDividerDecorator(
|
||||
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true
|
||||
)
|
||||
)
|
||||
|
||||
// NOTE:
|
||||
// The initialization order is very important! This order determines the priority of
|
||||
@ -226,7 +237,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
|
||||
recyclerViewTouchActionGuardManager = touchActionGuardManager
|
||||
recyclerViewSwipeManager = swipeManager
|
||||
this.swipeableMessageAdapter = adapter
|
||||
swipeableMessageAdapter = adapter
|
||||
|
||||
Singleton.updateMessageListAdapterInListener(adapter)
|
||||
}
|
||||
@ -235,12 +246,14 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
|
||||
val menu = FabSpeedDialMenu(context)
|
||||
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
|
||||
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal)
|
||||
FabUtils.initFab(context, R.drawable.ic_action_compose_message, menu)
|
||||
context.initFab(R.drawable.ic_action_compose_message, menu)
|
||||
.addOnMenuItemClickListener { _, _, itemId ->
|
||||
val identity = Singleton.getIdentity(context)
|
||||
if (identity == null) {
|
||||
Toast.makeText(activity, R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(
|
||||
activity, R.string.no_identity_warning,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
} else {
|
||||
when (itemId) {
|
||||
1 -> {
|
||||
|
@ -17,42 +17,65 @@
|
||||
package ch.dissem.apps.abit
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.*
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES.LOLLIPOP
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
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 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.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.uiThread
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
*/
|
||||
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
addPreferencesFromResource(R.xml.preferences)
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
|
||||
findPreference("about")?.onPreferenceClickListener = aboutClickListener()
|
||||
val cleanup = findPreference("cleanup")
|
||||
cleanup?.onPreferenceClickListener = cleanupClickListener(cleanup)
|
||||
findPreference("cleanup")?.let { it.onPreferenceClickListener = cleanupClickListener(it) }
|
||||
findPreference("export")?.onPreferenceClickListener = exportClickListener()
|
||||
findPreference("import")?.onPreferenceClickListener = importClickListener()
|
||||
findPreference("status").onPreferenceClickListener = statusClickListener()
|
||||
findPreference("status")?.onPreferenceClickListener = statusClickListener()
|
||||
|
||||
connectivityChangeListener().let {
|
||||
findPreference("wifi_only")?.onPreferenceChangeListener = it
|
||||
findPreference("require_charging")?.onPreferenceChangeListener = it
|
||||
}
|
||||
|
||||
val emulateConversations = findPreference("emulate_conversations") as? SwitchPreferenceCompat
|
||||
val conversationInit = findPreference("emulate_conversations_initialize")
|
||||
|
||||
emulateConversations?.onPreferenceChangeListener = emulateConversationChangeListener(conversationInit)
|
||||
conversationInit?.onPreferenceClickListener = conversationInitClickListener()
|
||||
conversationInit?.isEnabled = emulateConversations?.isChecked ?: false
|
||||
}
|
||||
|
||||
private fun aboutClickListener() = Preference.OnPreferenceClickListener {
|
||||
@ -73,7 +96,8 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
|
||||
}
|
||||
|
||||
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity?.applicationContext ?: throw IllegalStateException("Context not available")
|
||||
val ctx = activity?.applicationContext
|
||||
?: throw IllegalStateException("Context not available")
|
||||
cleanup.isEnabled = false
|
||||
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show()
|
||||
|
||||
@ -157,11 +181,12 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
|
||||
|
||||
override fun onAttach(ctx: Context?) {
|
||||
super.onAttach(ctx)
|
||||
(ctx as? MainActivity)?.floatingActionButton?.hide()
|
||||
PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
(ctx as? MainActivity)?.updateTitle(getString(R.string.settings))
|
||||
ctx?.let {
|
||||
if (it is MainActivity) {
|
||||
it.floatingActionButton?.hide()
|
||||
it.updateTitle(getString(R.string.settings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
|
||||
@ -193,6 +218,85 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
|
||||
}
|
||||
}
|
||||
|
||||
private val connection = object : ServiceConnection {
|
||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||
if (service is BatchProcessorService.BatchBinder) {
|
||||
val messageRepo = Singleton.getMessageRepository(service.service)
|
||||
val conversationService = Singleton.getConversationService(service.service)
|
||||
|
||||
service.process(
|
||||
SimpleJob<Plaintext>(
|
||||
messageRepo.count(),
|
||||
{ messageRepo.findNextLegacyMessages(it) },
|
||||
{ msg ->
|
||||
if (msg.encoding == Plaintext.Encoding.SIMPLE) {
|
||||
conversationService.getSubject(listOf(msg))?.let { subject ->
|
||||
msg.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
|
||||
messageRepo.save(msg)
|
||||
Thread.yield()
|
||||
}
|
||||
}
|
||||
},
|
||||
R.drawable.ic_notification_batch,
|
||||
R.string.emulate_conversations_batch
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun conversationInitClickListener() = Preference.OnPreferenceClickListener {
|
||||
val ctx = activity?.applicationContext
|
||||
?: throw IllegalStateException("Context not available")
|
||||
ctx.bindService(Intent(ctx, BatchProcessorService::class.java), connection, Context.BIND_AUTO_CREATE)
|
||||
true
|
||||
}
|
||||
|
||||
private fun emulateConversationChangeListener(conversationInit: Preference?) =
|
||||
OnPreferenceChangeListener { _, newValue ->
|
||||
conversationInit?.isEnabled = newValue as Boolean
|
||||
true
|
||||
}
|
||||
|
||||
private fun connectivityChangeListener() =
|
||||
OnPreferenceChangeListener { preference, newValue ->
|
||||
val ctx = context
|
||||
if (ctx != null && Build.VERSION.SDK_INT >= LOLLIPOP && Preferences.isFullNodeActive(ctx)) {
|
||||
NetworkUtils.scheduleNodeStart(ctx)
|
||||
}
|
||||
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)) }
|
||||
}
|
||||
// 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?
|
||||
|
||||
companion object {
|
||||
const val WRITE_EXPORT_REQUEST_CODE = 1
|
||||
const val READ_IMPORT_REQUEST_CODE = 2
|
||||
|
@ -0,0 +1,158 @@
|
||||
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 ch.dissem.apps.abit.*
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Constants
|
||||
import ch.dissem.apps.abit.util.getDrawable
|
||||
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
|
||||
|
||||
|
||||
class ConversationAdapter internal constructor(
|
||||
ctx: Context,
|
||||
private val parent: Fragment,
|
||||
conversation: Conversation,
|
||||
private val label: Label?
|
||||
) : RecyclerView.Adapter<ConversationAdapter.ViewHolder>() {
|
||||
|
||||
private val messageRepo = Singleton.getMessageRepository(ctx)
|
||||
|
||||
private var filteredMessages = conversation.messages.filter { label == null || it.labels.any { it == label } }
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): ConversationAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val messageView = inflater.inflate(R.layout.item_message_detail, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(messageView, this.parent, messageRepo)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: ConversationAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val message = filteredMessages[position]
|
||||
|
||||
viewHolder.apply {
|
||||
item = message
|
||||
avatar.setImageDrawable(Identicon(message.from))
|
||||
sender.text = message.from.toString()
|
||||
val senderClickListener: (View) -> Unit = {
|
||||
MainActivity.apply {
|
||||
onItemSelected(message.from)
|
||||
}
|
||||
}
|
||||
avatar.setOnClickListener(senderClickListener)
|
||||
sender.setOnClickListener(senderClickListener)
|
||||
|
||||
recipient.text = message.to.toString()
|
||||
status.setImageResource(message.status.getDrawable())
|
||||
text.text = message.text
|
||||
|
||||
Linkify.addLinks(text, Linkify.WEB_URLS)
|
||||
Linkify.addLinks(text,
|
||||
Constants.BITMESSAGE_ADDRESS_PATTERN,
|
||||
Constants.BITMESSAGE_URL_SCHEMA, null,
|
||||
Linkify.TransformFilter { match, _ -> match.group() }
|
||||
)
|
||||
|
||||
labelAdapter.labels = message.labels.toList()
|
||||
|
||||
// FIXME: I think that's not quite correct
|
||||
if (message.isUnread()) {
|
||||
Singleton.labeler.markAsRead(message)
|
||||
messageRepo.save(message)
|
||||
MainActivity.apply { updateUnread() }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = filteredMessages.size
|
||||
|
||||
inner class ViewHolder(
|
||||
itemView: View,
|
||||
parent: Fragment,
|
||||
messageRepo: MessageRepository
|
||||
) : RecyclerView.ViewHolder(itemView) {
|
||||
var item: Plaintext? = null
|
||||
val avatar = itemView.findViewById<ImageView>(R.id.avatar)!!
|
||||
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 {
|
||||
item?.let { item ->
|
||||
when (it.itemId) {
|
||||
R.id.reply -> {
|
||||
ComposeMessageActivity.launchReplyTo(parent, item)
|
||||
true
|
||||
}
|
||||
R.id.delete -> {
|
||||
if (MessageDetailFragment.isInTrash(item)) {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.remove(item)
|
||||
} else {
|
||||
Singleton.labeler.delete(item)
|
||||
messageRepo.save(item)
|
||||
}
|
||||
filteredMessages.indexOf(item).let { i ->
|
||||
filteredMessages -= item
|
||||
notifyItemRemoved(i)
|
||||
}
|
||||
MainActivity.apply {
|
||||
updateUnread()
|
||||
}
|
||||
true
|
||||
}
|
||||
R.id.mark_unread -> {
|
||||
Singleton.labeler.markAsUnread(item)
|
||||
messageRepo.save(item)
|
||||
MainActivity.apply { updateUnread() }
|
||||
true
|
||||
}
|
||||
R.id.archive -> {
|
||||
Singleton.labeler.archive(item)
|
||||
messageRepo.save(item)
|
||||
MainActivity.apply { updateUnread() }
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
} ?: false
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
}
|
||||
val text = itemView.findViewById<TextView>(R.id.text)!!.apply {
|
||||
linksClickable = true
|
||||
setTextIsSelectable(true)
|
||||
}
|
||||
val labelAdapter = LabelAdapter(itemView.context, emptySet<Label>())
|
||||
val labels = itemView.findViewById<RecyclerView>(R.id.labels)!!.apply {
|
||||
adapter = labelAdapter
|
||||
layoutManager = GridLayoutManager(itemView.context, 2)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package ch.dissem.apps.abit.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.os.Build
|
||||
import android.support.annotation.ColorInt
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.getColor
|
||||
import ch.dissem.apps.abit.util.getIcon
|
||||
import ch.dissem.apps.abit.util.getText
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.iconics.view.IconicsImageView
|
||||
import org.jetbrains.anko.backgroundColor
|
||||
|
||||
class LabelAdapter internal constructor(private val ctx: Context, labels: Collection<Label>) :
|
||||
RecyclerView.Adapter<LabelAdapter.ViewHolder>() {
|
||||
|
||||
var labels = labels.toList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LabelAdapter.ViewHolder {
|
||||
val context = parent.context
|
||||
val inflater = LayoutInflater.from(context)
|
||||
|
||||
// Inflate the custom layout
|
||||
val contactView = inflater.inflate(R.layout.item_label, parent, false)
|
||||
|
||||
// Return a new holder instance
|
||||
return ViewHolder(contactView)
|
||||
}
|
||||
|
||||
// Involves populating data into the item through holder
|
||||
override fun onBindViewHolder(viewHolder: LabelAdapter.ViewHolder, position: Int) {
|
||||
// Get the data model based on position
|
||||
val label = labels[position]
|
||||
|
||||
viewHolder.icon.icon?.icon(label.getIcon())
|
||||
viewHolder.label.text = label.getText(ctx)
|
||||
viewHolder.setBackground(label.getColor(0xFF607D8B.toInt()))
|
||||
}
|
||||
|
||||
override fun getItemCount() = labels.size
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var icon = itemView.findViewById<IconicsImageView>(R.id.icon)!!
|
||||
var label = itemView.findViewById<TextView>(R.id.label)!!
|
||||
|
||||
fun setBackground(@ColorInt color: Int) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
itemView.backgroundTintList = ColorStateList.valueOf(color)
|
||||
} else {
|
||||
itemView.backgroundColor = color
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,275 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
}
|
@ -29,8 +29,9 @@ 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.Assets
|
||||
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
|
||||
@ -48,7 +49,8 @@ import java.util.*
|
||||
* @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 {
|
||||
class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>(),
|
||||
SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, SwipeableItemConstants {
|
||||
|
||||
private val data = LinkedList<Plaintext>()
|
||||
var eventListener: EventListener? = null
|
||||
@ -84,7 +86,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
|
||||
init {
|
||||
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
|
||||
swipeableViewContainerOnClickListener = View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
|
||||
swipeableViewContainerOnClickListener =
|
||||
View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
|
||||
|
||||
// SwipeableItemAdapter requires stable ID, and also
|
||||
// have to implement the getItemId() method appropriately.
|
||||
@ -134,7 +137,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
|
||||
private fun onSwipeableViewContainerClick(v: View) {
|
||||
eventListener?.onItemViewClicked(
|
||||
RecyclerViewAdapterUtils.getParentViewHolderItemView(v))
|
||||
RecyclerViewAdapterUtils.getParentViewHolderItemView(v)
|
||||
)
|
||||
}
|
||||
|
||||
fun getItem(position: Int) = data[position]
|
||||
@ -168,8 +172,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
|
||||
// set data
|
||||
avatar.setImageDrawable(Identicon(item.from))
|
||||
status.setImageResource(Assets.getStatusDrawable(item.status))
|
||||
status.contentDescription = holder.status.context.getString(Assets.getStatusString(item.status))
|
||||
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)
|
||||
@ -194,7 +198,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) =
|
||||
holder.itemView.setBackgroundResource(when (type) {
|
||||
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) {
|
||||
@ -203,7 +208,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
R.drawable.bg_swipe_item_right
|
||||
}
|
||||
else -> R.drawable.bg_swipe_item_neutral
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
|
||||
@ -222,7 +228,10 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
notifyItemChanged(selectedPosition)
|
||||
}
|
||||
|
||||
private class SwipeLeftResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionMoveToSwipedDirection() {
|
||||
private class SwipeLeftResultAction internal constructor(
|
||||
adapter: SwipeableMessageAdapter,
|
||||
position: Int
|
||||
) : SwipeResultActionMoveToSwipedDirection() {
|
||||
private var adapter: SwipeableMessageAdapter? = adapter
|
||||
private val item = adapter.data[position]
|
||||
|
||||
@ -235,7 +244,10 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
|
||||
}
|
||||
}
|
||||
|
||||
private class SwipeRightResultAction internal constructor(adapter: SwipeableMessageAdapter, position: Int) : SwipeResultActionRemoveItem() {
|
||||
private class SwipeRightResultAction internal constructor(
|
||||
adapter: SwipeableMessageAdapter,
|
||||
position: Int
|
||||
) : SwipeResultActionRemoveItem() {
|
||||
private var adapter: SwipeableMessageAdapter? = adapter
|
||||
private val item = adapter.data[position]
|
||||
|
||||
|
@ -10,7 +10,7 @@ import android.view.WindowManager
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import ch.dissem.apps.abit.service.Singleton
|
||||
import ch.dissem.apps.abit.util.Drawables
|
||||
import ch.dissem.apps.abit.util.qrCode
|
||||
import com.mikepenz.materialdrawer.AccountHeader
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
|
||||
@ -23,7 +23,7 @@ class ProfileImageListener(private val ctx: Context) : AccountHeader.OnAccountHe
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||
|
||||
val imageView = ImageView(ctx)
|
||||
imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(ctx)))
|
||||
imageView.setImageBitmap(Singleton.getIdentity(ctx)?.qrCode())
|
||||
imageView.setOnClickListener { dialog.dismiss() }
|
||||
dialog.addContentView(
|
||||
imageView,
|
||||
|
@ -19,8 +19,11 @@ 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.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
import ch.dissem.bitmessage.utils.ConversationService
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@ -33,14 +36,26 @@ import java.util.concurrent.Executors
|
||||
* notifications should be combined.
|
||||
*
|
||||
*/
|
||||
class MessageListener(ctx: Context) : BitmessageContext.Listener {
|
||||
class MessageListener(ctx: Context) : BitmessageContext.Listener.WithContext {
|
||||
override fun setContext(ctx: BitmessageContext) {
|
||||
messageRepo = ctx.messages
|
||||
conversationService = ConversationService(messageRepo)
|
||||
}
|
||||
|
||||
private val unacknowledged = LinkedList<Plaintext>()
|
||||
private var numberOfUnacknowledgedMessages = 0
|
||||
private val notification = NewMessageNotification(ctx)
|
||||
private val pool = Executors.newSingleThreadExecutor()
|
||||
private lateinit var messageRepo: MessageRepository
|
||||
private lateinit var conversationService: ConversationService
|
||||
|
||||
init {
|
||||
emulateConversations = Preferences.isEmulateConversations(ctx)
|
||||
}
|
||||
|
||||
override fun receive(plaintext: Plaintext) {
|
||||
pool.submit {
|
||||
updateConversation(plaintext)
|
||||
unacknowledged.addFirst(plaintext)
|
||||
numberOfUnacknowledgedMessages++
|
||||
if (unacknowledged.size > 5) {
|
||||
@ -65,4 +80,17 @@ class MessageListener(ctx: Context) : BitmessageContext.Listener {
|
||||
numberOfUnacknowledgedMessages = 0
|
||||
}
|
||||
}
|
||||
|
||||
fun updateConversation(plaintext: Plaintext) {
|
||||
if (emulateConversations && plaintext.encoding != Plaintext.Encoding.EXTENDED) {
|
||||
conversationService.getSubject(listOf(plaintext))?.let { subject ->
|
||||
plaintext.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray())
|
||||
messageRepo.save(plaintext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var emulateConversations = false
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.notification
|
||||
|
||||
import android.content.Context
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.Job
|
||||
|
||||
/**
|
||||
* Ongoing notification while proof of work is in progress.
|
||||
*/
|
||||
class BatchNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setUsesChronometer(true)
|
||||
|
||||
init {
|
||||
initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent)
|
||||
notification = builder.build()
|
||||
}
|
||||
|
||||
override val notificationId = ONGOING_NOTIFICATION_ID
|
||||
|
||||
fun update(job: Job): BatchNotification {
|
||||
|
||||
builder.setContentTitle(ctx.getString(job.description))
|
||||
.setSmallIcon(job.icon)
|
||||
.setProgress(job.numberOfItems, job.numberOfProcessedItems, job.numberOfItems <= 0)
|
||||
|
||||
notification = builder.build()
|
||||
show()
|
||||
return this
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ONGOING_NOTIFICATION_ID = 4
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
package ch.dissem.apps.abit.notification
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Typeface
|
||||
@ -27,18 +28,15 @@ import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.StyleSpan
|
||||
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_REPLY_TO_MESSAGE
|
||||
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_MESSAGE
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService
|
||||
import ch.dissem.apps.abit.service.BitmessageIntentService.Companion.EXTRA_DELETE_MESSAGE
|
||||
import ch.dissem.apps.abit.util.Drawables.toBitmap
|
||||
import ch.dissem.apps.abit.util.toBitmap
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
|
||||
class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
|
||||
@ -53,7 +51,7 @@ class NewMessageNotification(ctx: Context) : AbstractNotification(ctx) {
|
||||
bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
builder.setSmallIcon(R.drawable.ic_notification_new_message)
|
||||
.setLargeIcon(toBitmap(Identicon(plaintext.from), 192))
|
||||
.setLargeIcon(Identicon(plaintext.from).toBitmap(192))
|
||||
.setContentTitle(plaintext.from.toString())
|
||||
.setContentText(plaintext.subject)
|
||||
.setStyle(BigTextStyle().bigText(bigText))
|
||||
|
@ -20,7 +20,7 @@ import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.database.DatabaseUtils
|
||||
import ch.dissem.apps.abit.util.Labels
|
||||
import ch.dissem.apps.abit.util.getText
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import ch.dissem.bitmessage.ports.AbstractLabelRepository
|
||||
import ch.dissem.bitmessage.ports.MessageRepository
|
||||
@ -30,7 +30,8 @@ import java.util.*
|
||||
/**
|
||||
* [MessageRepository] implementation using the Android SQL API.
|
||||
*/
|
||||
class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) : AbstractLabelRepository() {
|
||||
class AndroidLabelRepository(private val sql: SqlHelper, private val context: Context) :
|
||||
AbstractLabelRepository() {
|
||||
|
||||
override fun find(where: String): List<Label> {
|
||||
val result = LinkedList<Label>()
|
||||
@ -62,7 +63,12 @@ class AndroidLabelRepository(private val sql: SqlHelper, private val context: Co
|
||||
db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
|
||||
} else {
|
||||
db.transaction {
|
||||
val exists = DatabaseUtils.queryNumEntries(db, TABLE_NAME, "label=?", arrayOf(label.toString())) > 0
|
||||
val exists = DatabaseUtils.queryNumEntries(
|
||||
db,
|
||||
TABLE_NAME,
|
||||
"label=?",
|
||||
arrayOf(label.toString())
|
||||
) > 0
|
||||
|
||||
if (exists) {
|
||||
val values = ContentValues()
|
||||
@ -82,7 +88,8 @@ class AndroidLabelRepository(private val sql: SqlHelper, private val context: Co
|
||||
}
|
||||
}
|
||||
|
||||
internal fun findLabels(msgId: Any) = find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
|
||||
internal fun findLabels(msgId: Any) =
|
||||
find("id IN (SELECT label_id FROM Message_Label WHERE message_id=$msgId)")
|
||||
|
||||
companion object {
|
||||
val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE }
|
||||
@ -97,11 +104,12 @@ class AndroidLabelRepository(private val sql: SqlHelper, private val context: Co
|
||||
internal fun getLabel(c: Cursor, context: Context): Label {
|
||||
val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE))
|
||||
val type = if (typeName == null) null else Label.Type.valueOf(typeName)
|
||||
val text: String? = Labels.getText(type, null, context)
|
||||
val text: String? = type?.getText(null, context)
|
||||
val label = Label(
|
||||
text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)),
|
||||
type,
|
||||
c.getInt(c.getColumnIndex(COLUMN_COLOR)))
|
||||
c.getInt(c.getColumnIndex(COLUMN_COLOR))
|
||||
)
|
||||
label.id = c.getLong(c.getColumnIndex(COLUMN_ID))
|
||||
return label
|
||||
}
|
||||
|
@ -40,12 +40,20 @@ import java.util.*
|
||||
*/
|
||||
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() {
|
||||
|
||||
override fun findMessages(label: Label?, offset: Int, limit: Int) = if (label === LABEL_ARCHIVE) {
|
||||
override fun findMessages(label: Label?, offset: Int, limit: Int) =
|
||||
if (label === LABEL_ARCHIVE) {
|
||||
super.findMessages(null as Label?, offset, limit)
|
||||
} else {
|
||||
super.findMessages(label, offset, limit)
|
||||
}
|
||||
|
||||
fun count() = DatabaseUtils.queryNumEntries(
|
||||
sql.readableDatabase,
|
||||
TABLE_NAME,
|
||||
null,
|
||||
null
|
||||
).toInt()
|
||||
|
||||
override fun countUnread(label: Label?) = when {
|
||||
label === LABEL_ARCHIVE -> 0
|
||||
label == null -> DatabaseUtils.queryNumEntries(
|
||||
@ -63,7 +71,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
||||
).toInt()
|
||||
}
|
||||
|
||||
override fun findConversations(label: Label?): List<UUID> {
|
||||
override fun findConversations(label: Label?, offset: Int, limit: Int): List<UUID> {
|
||||
val projection = arrayOf(COLUMN_CONVERSATION)
|
||||
|
||||
val where = when {
|
||||
@ -74,8 +82,12 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
||||
val result = LinkedList<UUID>()
|
||||
sql.readableDatabase.query(
|
||||
true,
|
||||
TABLE_NAME, projection, where,
|
||||
null, null, null, null, null
|
||||
TABLE_NAME,
|
||||
projection,
|
||||
where,
|
||||
null, null, null,
|
||||
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
|
||||
if (limit == 0) null else "$offset, $limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION))
|
||||
@ -133,7 +145,22 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
||||
|
||||
// Define a projection that specifies which columns from the database
|
||||
// you will actually use after this query.
|
||||
val projection = arrayOf(COLUMN_ID, COLUMN_IV, COLUMN_TYPE, COLUMN_SENDER, COLUMN_RECIPIENT, COLUMN_DATA, COLUMN_ACK_DATA, COLUMN_SENT, COLUMN_RECEIVED, COLUMN_STATUS, COLUMN_TTL, COLUMN_RETRIES, COLUMN_NEXT_TRY, COLUMN_CONVERSATION)
|
||||
val projection = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_IV,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_SENDER,
|
||||
COLUMN_RECIPIENT,
|
||||
COLUMN_DATA,
|
||||
COLUMN_ACK_DATA,
|
||||
COLUMN_SENT,
|
||||
COLUMN_RECEIVED,
|
||||
COLUMN_STATUS,
|
||||
COLUMN_TTL,
|
||||
COLUMN_RETRIES,
|
||||
COLUMN_NEXT_TRY,
|
||||
COLUMN_CONVERSATION
|
||||
)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
@ -174,7 +201,8 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
||||
labels = findLabels(id!!)
|
||||
}
|
||||
|
||||
private fun findLabels(msgId: Any) = (ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
|
||||
private fun findLabels(msgId: Any) =
|
||||
(ctx.labelRepository as AndroidLabelRepository).findLabels(msgId)
|
||||
|
||||
override fun save(message: Plaintext) {
|
||||
saveContactIfNecessary(message.from)
|
||||
@ -233,6 +261,39 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
|
||||
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString()))
|
||||
}
|
||||
|
||||
fun findNextLegacyMessages(previous: Plaintext?, limit: Int = 10): List<Plaintext> {
|
||||
val result = mutableListOf<Plaintext>()
|
||||
|
||||
val projection = arrayOf(
|
||||
COLUMN_ID,
|
||||
COLUMN_IV,
|
||||
COLUMN_TYPE,
|
||||
COLUMN_SENDER,
|
||||
COLUMN_RECIPIENT,
|
||||
COLUMN_DATA,
|
||||
COLUMN_ACK_DATA,
|
||||
COLUMN_SENT,
|
||||
COLUMN_RECEIVED,
|
||||
COLUMN_STATUS,
|
||||
COLUMN_TTL,
|
||||
COLUMN_RETRIES,
|
||||
COLUMN_NEXT_TRY,
|
||||
COLUMN_CONVERSATION
|
||||
)
|
||||
|
||||
sql.readableDatabase.query(
|
||||
TABLE_NAME, projection,
|
||||
"$COLUMN_ID > ${previous?.id ?: Long.MIN_VALUE}", null, null, null,
|
||||
"$COLUMN_ID ASC",
|
||||
"$limit"
|
||||
).use { c ->
|
||||
while (c.moveToNext()) {
|
||||
result.add(getMessage(c))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TABLE_NAME = "Message"
|
||||
private const val COLUMN_ID = "id"
|
||||
|
@ -0,0 +1,117 @@
|
||||
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 ch.dissem.apps.abit.notification.BatchNotification
|
||||
import ch.dissem.apps.abit.notification.BatchNotification.Companion.ONGOING_NOTIFICATION_ID
|
||||
import org.jetbrains.anko.doAsync
|
||||
import java.util.*
|
||||
|
||||
class BatchProcessorService : Service() {
|
||||
private lateinit var notification: BatchNotification
|
||||
|
||||
override fun onCreate() {
|
||||
notification = BatchNotification(this)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent) = BatchBinder(this)
|
||||
|
||||
class BatchBinder internal constructor(val service: BatchProcessorService) : Binder() {
|
||||
private val notification = service.notification
|
||||
|
||||
fun process(job: Job) = synchronized(queue) {
|
||||
ContextCompat.startForegroundService(
|
||||
service,
|
||||
Intent(service, BatchProcessorService::class.java)
|
||||
)
|
||||
service.startForeground(
|
||||
ONGOING_NOTIFICATION_ID,
|
||||
notification.notification
|
||||
)
|
||||
if (!working) {
|
||||
working = true
|
||||
service.processQueue(job)
|
||||
} else {
|
||||
queue.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun processQueue(job: Job) {
|
||||
doAsync {
|
||||
var next: Job? = job
|
||||
while (next != null) {
|
||||
next.process(notification)
|
||||
|
||||
synchronized(queue) {
|
||||
next = queue.poll()
|
||||
if (next == null) {
|
||||
working = false
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var working = false
|
||||
private val queue = LinkedList<Job>()
|
||||
}
|
||||
}
|
||||
|
||||
interface Job {
|
||||
val icon: Int
|
||||
@DrawableRes get
|
||||
|
||||
val description: Int
|
||||
@StringRes get
|
||||
|
||||
val numberOfItems: Int
|
||||
var numberOfProcessedItems: Int
|
||||
|
||||
/**
|
||||
* Runs the job. This shouldn't happen in a separate thread, as this is handled by the service.
|
||||
*/
|
||||
fun process(notification: BatchNotification)
|
||||
}
|
||||
|
||||
data class SimpleJob<T>(
|
||||
override val numberOfItems: Int,
|
||||
/**
|
||||
* Provides the next batch of items, given the last item of the previous batch,
|
||||
* or null for the first batch.
|
||||
*/
|
||||
private val provider: (T?) -> List<T>,
|
||||
/**
|
||||
* Processes an item.
|
||||
*/
|
||||
private val processor: (T) -> Unit,
|
||||
override val icon: Int,
|
||||
override val description: Int
|
||||
) : Job {
|
||||
override var numberOfProcessedItems: Int = 0
|
||||
|
||||
override fun process(notification: BatchNotification) {
|
||||
notification.update(this)
|
||||
var batch = provider.invoke(null)
|
||||
while (batch.isNotEmpty()) {
|
||||
Thread.yield()
|
||||
batch.forEach {
|
||||
processor.invoke(it)
|
||||
Thread.yield()
|
||||
}
|
||||
numberOfProcessedItems += batch.size
|
||||
notification.update(this)
|
||||
batch = provider.invoke(batch.last())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -22,12 +22,14 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.BatteryManager
|
||||
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
|
||||
@ -60,7 +62,10 @@ class BitmessageService : Service() {
|
||||
override fun onCreate() {
|
||||
registerReceiver(
|
||||
connectivityReceiver,
|
||||
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
IntentFilter().apply {
|
||||
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
addAction(Intent.ACTION_BATTERY_CHANGED)
|
||||
}
|
||||
)
|
||||
notification = NetworkNotification(this)
|
||||
running = false
|
||||
@ -87,7 +92,9 @@ class BitmessageService : Service() {
|
||||
running = false
|
||||
notification.showShutdown()
|
||||
cleanupHandler.removeCallbacks(cleanupTask)
|
||||
doAsync {
|
||||
bmc.cleanup()
|
||||
}
|
||||
unregisterReceiver(connectivityReceiver)
|
||||
stopSelf()
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import ch.dissem.bitmessage.BitmessageContext
|
||||
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
|
||||
@ -101,6 +102,7 @@ object Singleton {
|
||||
|
||||
fun getBitmessageContext(context: Context): BitmessageContext =
|
||||
init({ bitmessageContext }, { bitmessageContext = it }) {
|
||||
BufferPool.setLimit(4)
|
||||
BitmessageContext.build {
|
||||
TTL.pubkey = 2 * DAY
|
||||
val ctx = context.applicationContext
|
||||
@ -117,7 +119,7 @@ object Singleton {
|
||||
labelRepo = AndroidLabelRepository(sqlHelper, ctx)
|
||||
messageRepo = AndroidMessageRepository(sqlHelper)
|
||||
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
|
||||
networkHandler = NioNetworkHandler()
|
||||
networkHandler = NioNetworkHandler(4)
|
||||
listener = getMessageListener(ctx)
|
||||
labeler = Singleton.labeler
|
||||
preferences.sendPubkeyOnIdentityCreation = false
|
||||
|
@ -24,11 +24,9 @@ class StartupNodeOnWifiService : JobService() {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onStopJob(params: JobParameters?) = if (Preferences.isWifiOnly(this)) {
|
||||
// Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
|
||||
// depending on Android version.
|
||||
Preferences.isFullNodeActive(this)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
@ -43,11 +43,10 @@ object Assets {
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
fun getStatusDrawable(status: Plaintext.Status) = when (status) {
|
||||
fun Plaintext.Status.getDrawable() = when (this) {
|
||||
Plaintext.Status.RECEIVED -> 0
|
||||
Plaintext.Status.DRAFT -> R.drawable.draft
|
||||
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
|
||||
@ -57,8 +56,7 @@ object Assets {
|
||||
else -> 0
|
||||
}
|
||||
|
||||
@StringRes
|
||||
fun getStatusString(status: Plaintext.Status) = when (status) {
|
||||
fun Plaintext.Status.getString() = when (this) {
|
||||
Plaintext.Status.RECEIVED -> R.string.status_received
|
||||
Plaintext.Status.DRAFT -> R.string.status_draft
|
||||
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
|
||||
@ -67,4 +65,3 @@ object Assets {
|
||||
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,8 @@ import java.util.regex.Pattern
|
||||
*/
|
||||
object Constants {
|
||||
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"
|
||||
|
@ -21,13 +21,14 @@ import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color.BLACK
|
||||
import android.graphics.Color.WHITE
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Base64
|
||||
import android.util.Base64.NO_WRAP
|
||||
import android.util.Base64.URL_SAFE
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import ch.dissem.apps.abit.Identicon
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.util.Drawables.QR_CODE_SIZE
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.MultiFormatWriter
|
||||
@ -42,47 +43,48 @@ import java.io.ByteArrayOutputStream
|
||||
* Some helper methods to work with drawables.
|
||||
*/
|
||||
object Drawables {
|
||||
private val LOG = LoggerFactory.getLogger(Drawables::class.java)
|
||||
internal val LOG = LoggerFactory.getLogger(Drawables::class.java)
|
||||
|
||||
private const val QR_CODE_SIZE = 350
|
||||
internal const val QR_CODE_SIZE = 350
|
||||
|
||||
fun addIcon(ctx: Context, menu: Menu, menuItem: Int, icon: IIcon): MenuItem {
|
||||
val item = menu.findItem(menuItem)
|
||||
item.icon = IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
fun toBitmap(identicon: Identicon, width: Int, height: Int = width): Bitmap {
|
||||
fun Drawable.toBitmap(width: Int, height: Int = width): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
identicon.setBounds(0, 0, canvas.width, canvas.height)
|
||||
identicon.draw(canvas)
|
||||
setBounds(0, 0, canvas.width, canvas.height)
|
||||
draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
fun qrCode(address: BitmessageAddress?): Bitmap? {
|
||||
if (address == null) {
|
||||
return null
|
||||
}
|
||||
fun BitmessageAddress.qrCode(): Bitmap? {
|
||||
val link = StringBuilder()
|
||||
link.append(Constants.BITMESSAGE_URL_SCHEMA)
|
||||
link.append(address.address)
|
||||
if (address.alias != null) {
|
||||
link.append("?label=").append(address.alias)
|
||||
link.append(address)
|
||||
if (alias != null) {
|
||||
link.append("?label=").append(alias)
|
||||
}
|
||||
address.pubkey?.apply {
|
||||
link.append(if (address.alias == null) '?' else '&')
|
||||
pubkey?.apply {
|
||||
link.append(if (alias == null) '?' else '&')
|
||||
val pubkey = ByteArrayOutputStream()
|
||||
writer().writeUnencrypted(pubkey)
|
||||
link.append("pubkey=").append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE or NO_WRAP))
|
||||
link.append("pubkey=")
|
||||
.append(Base64.encodeToString(pubkey.toByteArray(), URL_SAFE or NO_WRAP))
|
||||
|
||||
}
|
||||
val result: BitMatrix
|
||||
try {
|
||||
result = MultiFormatWriter().encode(link.toString(),
|
||||
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null)
|
||||
result = MultiFormatWriter().encode(
|
||||
link.toString(),
|
||||
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null
|
||||
)
|
||||
} catch (e: WriterException) {
|
||||
LOG.error(e.message, e)
|
||||
Drawables.LOG.error(e.message, e)
|
||||
return null
|
||||
}
|
||||
|
||||
@ -99,4 +101,3 @@ object Drawables {
|
||||
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h)
|
||||
return bitmap
|
||||
}
|
||||
}
|
||||
|
@ -1,31 +0,0 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.support.annotation.DrawableRes
|
||||
import ch.dissem.apps.abit.MainActivity
|
||||
import ch.dissem.apps.abit.R
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDial
|
||||
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
|
||||
|
||||
/**
|
||||
* Utilities to work with the common floating action button in the main activity
|
||||
*/
|
||||
object FabUtils {
|
||||
fun initFab(activity: MainActivity, @DrawableRes drawableRes: Int, menu: FabSpeedDialMenu): FabSpeedDial {
|
||||
val fab = activity.floatingActionButton ?: throw IllegalStateException("Fab must not be null")
|
||||
fab.removeAllOnMenuItemClickListeners()
|
||||
fab.show()
|
||||
fab.closeMenu()
|
||||
val mainFab = fab.mainFab
|
||||
mainFab.setImageResource(drawableRes)
|
||||
fab.setMenu(menu)
|
||||
fab.addOnStateChangeListener { isOpened: Boolean ->
|
||||
if (isOpened) {
|
||||
// It will be turned 45 degrees, which makes an x out of the +
|
||||
mainFab.setImageResource(R.drawable.ic_action_add)
|
||||
} else {
|
||||
mainFab.setImageResource(drawableRes)
|
||||
}
|
||||
}
|
||||
return fab
|
||||
}
|
||||
}
|
@ -2,21 +2,19 @@ package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.support.annotation.ColorInt
|
||||
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
import com.mikepenz.community_material_typeface_library.CommunityMaterial
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.typeface.IIcon
|
||||
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.bitmessage.entity.valueobject.Label
|
||||
|
||||
/**
|
||||
* Helper class to help with translating the default labels, getting label colors and so on.
|
||||
/*
|
||||
* Helper methods to help with translating the default labels, getting label colors and so on.
|
||||
*/
|
||||
object Labels {
|
||||
fun getText(label: Label, ctx: Context): String = getText(label.type, label.toString(), ctx)!!
|
||||
|
||||
fun getText(type: Label.Type?, alternative: String?, ctx: Context) = when (type) {
|
||||
fun Label.getText(ctx: Context): String = type?.getText(toString(), ctx) ?: toString()
|
||||
|
||||
fun Label.Type.getText(alternative: String?, ctx: Context) = when (this) {
|
||||
Label.Type.INBOX -> ctx.getString(R.string.inbox)
|
||||
Label.Type.DRAFT -> ctx.getString(R.string.draft)
|
||||
Label.Type.OUTBOX -> ctx.getString(R.string.outbox)
|
||||
@ -27,7 +25,7 @@ object Labels {
|
||||
else -> alternative
|
||||
}
|
||||
|
||||
fun getIcon(label: Label): IIcon = when (label.type) {
|
||||
fun Label.getIcon(): IIcon = when (type) {
|
||||
Label.Type.INBOX -> GoogleMaterial.Icon.gmd_inbox
|
||||
Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file
|
||||
Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up
|
||||
@ -39,7 +37,6 @@ object Labels {
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
fun getColor(label: Label) = if (label.type == null) {
|
||||
label.color
|
||||
} else 0xFF000000.toInt()
|
||||
}
|
||||
fun Label.getColor(@ColorInt default: Int) = if (type == null) {
|
||||
color
|
||||
} else default
|
||||
|
@ -19,28 +19,35 @@ object NetworkUtils {
|
||||
|
||||
fun enableNode(ctx: Context, ask: Boolean = true) {
|
||||
Preferences.setFullNodeActive(ctx, true)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Preferences.isConnectionAllowed(ctx) || !ask) {
|
||||
scheduleNodeStart(ctx)
|
||||
} else {
|
||||
askForConnection(ctx)
|
||||
}
|
||||
} else {
|
||||
if (Preferences.isWifiOnly(ctx)) {
|
||||
if (Preferences.isConnectionAllowed(ctx)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scheduleNodeStart(ctx)
|
||||
doStartBitmessageService(ctx)
|
||||
MainActivity.updateNodeSwitch()
|
||||
} else if (ask) {
|
||||
askForConnection(ctx)
|
||||
}
|
||||
} else {
|
||||
doStartBitmessageService(ctx)
|
||||
MainActivity.updateNodeSwitch()
|
||||
}
|
||||
} else if (ask) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun askForConnection(ctx: Context) {
|
||||
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
|
||||
if (ctx !is Activity) {
|
||||
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
||||
}
|
||||
ctx.startActivity(dialogIntent)
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
scheduleNodeStart(ctx)
|
||||
}
|
||||
} else {
|
||||
doStartBitmessageService(ctx)
|
||||
MainActivity.updateNodeSwitch()
|
||||
}
|
||||
}
|
||||
|
||||
fun doStartBitmessageService(ctx: Context) {
|
||||
@ -54,11 +61,17 @@ object NetworkUtils {
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
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)) {
|
||||
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
|
||||
}
|
||||
if (Preferences.requireCharging(ctx)) {
|
||||
builder.setRequiresCharging(true)
|
||||
}
|
||||
builder.setBackoffCriteria(0L, JobInfo.BACKOFF_POLICY_LINEAR)
|
||||
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
||||
builder.setPersisted(true)
|
||||
jobScheduler.schedule(builder.build())
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ object PowStats {
|
||||
powCount = preferences.getLong(PREFERENCE_POW_COUNT, 0L)
|
||||
}
|
||||
}
|
||||
return (BigInteger.valueOf(averagePowUnitTime) * BigInteger(target) / TWO_POW_64).toLong()
|
||||
return (averagePowUnitTime * BigInteger(target) / TWO_POW_64).toLong()
|
||||
}
|
||||
|
||||
fun addPow(ctx: Context, time: Long, target: ByteArray) {
|
||||
@ -32,7 +32,7 @@ object PowStats {
|
||||
synchronized(this) {
|
||||
powCount++
|
||||
averagePowUnitTime = (
|
||||
(BigInteger.valueOf(averagePowUnitTime) * powCountBefore + (BigInteger.valueOf(time) * TWO_POW_64 / targetBigInt)) / BigInteger.valueOf(powCount)
|
||||
(averagePowUnitTime * powCountBefore + (time * TWO_POW_64 / targetBigInt)) / powCount
|
||||
).toLong()
|
||||
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
@ -42,4 +42,7 @@ object PowStats {
|
||||
.apply()
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Long.times(other: BigInteger) = this.toBigInteger() * other
|
||||
private operator fun BigInteger.div(other: Long) = this / other.toBigInteger()
|
||||
}
|
||||
|
@ -17,19 +17,27 @@
|
||||
package ch.dissem.apps.abit.util
|
||||
|
||||
import android.content.Context
|
||||
import android.preference.PreferenceManager
|
||||
import ch.dissem.apps.abit.R
|
||||
import ch.dissem.apps.abit.notification.ErrorNotification
|
||||
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_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_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
|
||||
|
||||
|
||||
/**
|
||||
* @author Christian Basler
|
||||
@ -77,50 +85,56 @@ object Preferences {
|
||||
return 8444
|
||||
}
|
||||
|
||||
fun getTimeoutInSeconds(ctx: Context): Long {
|
||||
val preference = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT) ?: return 120
|
||||
return preference.toLong()
|
||||
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) {
|
||||
ctx.batteryManager.isCharging
|
||||
} else {
|
||||
val intent = ctx.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
|
||||
val status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
||||
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
||||
}
|
||||
|
||||
private fun getPreference(ctx: Context, name: String): String? {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
|
||||
return preferences.getString(name, null)
|
||||
}
|
||||
|
||||
fun isConnectionAllowed(ctx: Context) = !isWifiOnly(ctx) || !ctx.connectivityManager.isActiveNetworkMetered
|
||||
|
||||
fun isWifiOnly(ctx: Context): Boolean {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
return preferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
|
||||
}
|
||||
fun isWifiOnly(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
|
||||
|
||||
fun setWifiOnly(ctx: Context, status: Boolean) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
preferences.edit().putBoolean(PREFERENCE_WIFI_ONLY, status).apply()
|
||||
ctx.defaultSharedPreferences.edit()
|
||||
.putBoolean(PREFERENCE_WIFI_ONLY, status)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun isFullNodeActive(ctx: Context): Boolean {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
return preferences.getBoolean(PREFERENCE_FULL_NODE, false)
|
||||
fun requireCharging(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
|
||||
|
||||
fun setRequireCharging(ctx: Context, status: Boolean) {
|
||||
ctx.defaultSharedPreferences.edit()
|
||||
.putBoolean(PREFERENCE_REQUIRE_CHARGING, status)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun isEmulateConversations(ctx: Context) =
|
||||
ctx.defaultSharedPreferences.getBoolean(PREFERENCE_EMULATE_CONVERSATIONS, true)
|
||||
|
||||
|
||||
fun isFullNodeActive(ctx: Context) =
|
||||
ctx.defaultSharedPreferences.getBoolean(PREFERENCE_FULL_NODE, false)
|
||||
|
||||
fun setFullNodeActive(ctx: Context, status: Boolean) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
preferences.edit().putBoolean(PREFERENCE_FULL_NODE, status).apply()
|
||||
ctx.defaultSharedPreferences.edit()
|
||||
.putBoolean(PREFERENCE_FULL_NODE, status)
|
||||
.apply()
|
||||
}
|
||||
|
||||
fun getExportDirectory(ctx: Context) = File(ctx.filesDir, "exports")
|
||||
|
||||
fun requestAcknowledgements(ctx: Context): Boolean {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
return preferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
|
||||
}
|
||||
|
||||
fun setRequestAcknowledgements(ctx: Context, status: Boolean) {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
|
||||
preferences.edit().putBoolean(PREFERENCE_REQUEST_ACK, status).apply()
|
||||
}
|
||||
fun requestAcknowledgements(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
|
||||
|
||||
fun cleanupExportDirectory(ctx: Context) {
|
||||
val exportDirectory = getExportDirectory(ctx)
|
||||
|
6
app/src/main/res/drawable/bg_label.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#000000"/>
|
||||
<corners android:radius="4dp"/>
|
||||
<padding android:left="0dp" android:top="0dp" android:right="0dp" android:bottom="0dp" />
|
||||
</shape>
|
9
app/src/main/res/drawable/ic_menu.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#000"
|
||||
android:pathData="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z" />
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_notification_batch.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24"
|
||||
android:viewportWidth="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M15.9,18.45C17.25,18.45 18.35,17.35 18.35,16C18.35,14.65 17.25,13.55 15.9,13.55C14.54,13.55 13.45,14.65 13.45,16C13.45,17.35 14.54,18.45 15.9,18.45M21.1,16.68L22.58,17.84C22.71,17.95 22.75,18.13 22.66,18.29L21.26,20.71C21.17,20.86 21,20.92 20.83,20.86L19.09,20.16C18.73,20.44 18.33,20.67 17.91,20.85L17.64,22.7C17.62,22.87 17.47,23 17.3,23H14.5C14.32,23 14.18,22.87 14.15,22.7L13.89,20.85C13.46,20.67 13.07,20.44 12.71,20.16L10.96,20.86C10.81,20.92 10.62,20.86 10.54,20.71L9.14,18.29C9.05,18.13 9.09,17.95 9.22,17.84L10.7,16.68L10.65,16L10.7,15.31L9.22,14.16C9.09,14.05 9.05,13.86 9.14,13.71L10.54,11.29C10.62,11.13 10.81,11.07 10.96,11.13L12.71,11.84C13.07,11.56 13.46,11.32 13.89,11.15L14.15,9.29C14.18,9.13 14.32,9 14.5,9H17.3C17.47,9 17.62,9.13 17.64,9.29L17.91,11.15C18.33,11.32 18.73,11.56 19.09,11.84L20.83,11.13C21,11.07 21.17,11.13 21.26,11.29L22.66,13.71C22.75,13.86 22.71,14.05 22.58,14.16L21.1,15.31L21.15,16L21.1,16.68M6.69,8.07C7.56,8.07 8.26,7.37 8.26,6.5C8.26,5.63 7.56,4.92 6.69,4.92A1.58,1.58 0 0,0 5.11,6.5C5.11,7.37 5.82,8.07 6.69,8.07M10.03,6.94L11,7.68C11.07,7.75 11.09,7.87 11.03,7.97L10.13,9.53C10.08,9.63 9.96,9.67 9.86,9.63L8.74,9.18L8,9.62L7.81,10.81C7.79,10.92 7.7,11 7.59,11H5.79C5.67,11 5.58,10.92 5.56,10.81L5.4,9.62L4.64,9.18L3.5,9.63C3.41,9.67 3.3,9.63 3.24,9.53L2.34,7.97C2.28,7.87 2.31,7.75 2.39,7.68L3.34,6.94L3.31,6.5L3.34,6.06L2.39,5.32C2.31,5.25 2.28,5.13 2.34,5.03L3.24,3.47C3.3,3.37 3.41,3.33 3.5,3.37L4.63,3.82L5.4,3.38L5.56,2.19C5.58,2.08 5.67,2 5.79,2H7.59C7.7,2 7.79,2.08 7.81,2.19L8,3.38L8.74,3.82L9.86,3.37C9.96,3.33 10.08,3.37 10.13,3.47L11.03,5.03C11.09,5.13 11.07,5.25 11,5.32L10.03,6.06L10.06,6.5L10.03,6.94Z" />
|
||||
</vector>
|
125
app/src/main/res/layout/conversation_row.xml
Normal file
@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright 2015 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.
|
||||
-->
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_swipe_item_neutral">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_item_normal_state"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
tools:ignore="UselessParent">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@color/colorPrimaryDark"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sender"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignTop="@id/avatar"
|
||||
android:layout_marginTop="-5dp"
|
||||
android:layout_toEndOf="@id/avatar"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:paddingBottom="0dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="0dp"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textStyle="bold"
|
||||
tools:text="Sender" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subject"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@id/count"
|
||||
android:layout_below="@id/sender"
|
||||
android:layout_toEndOf="@id/avatar"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:text="Subject" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_below="@id/subject"
|
||||
android:layout_toEndOf="@id/avatar"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:lines="1"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:text="Text" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignBottom="@id/avatar"
|
||||
android:layout_alignEnd="@id/avatar"
|
||||
android:layout_marginBottom="-8dp"
|
||||
android:layout_marginEnd="-8dp"
|
||||
android:tint="@color/colorAccent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/ic_notification_proof_of_work" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/count"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_alignBottom="@id/subject"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="@color/md_blue_grey_500"
|
||||
tools:text="0" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
@ -46,7 +46,8 @@
|
||||
android:id="@+id/label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/label" />
|
||||
android:hint="@string/label"
|
||||
android:inputType="text" />
|
||||
|
||||
</android.support.design.widget.TextInputLayout>
|
||||
|
||||
|
53
app/src/main/res/layout/fragment_conversation_detail.xml
Normal file
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subject"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toStartOf="@+id/avatar"
|
||||
android:elegantTextHeight="false"
|
||||
android:enabled="false"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="16dp"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
tools:ignore="UnusedAttribute"
|
||||
tools:text="Subject" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_margin="10dp"
|
||||
android:src="@color/colorAccent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="2dip"
|
||||
android:layout_below="@id/subject"
|
||||
android:background="@color/divider" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/messages"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/divider"
|
||||
android:scrollbarStyle="outsideOverlay"
|
||||
android:scrollbars="vertical"
|
||||
tools:listitem="@layout/item_message_detail" />
|
||||
|
||||
</RelativeLayout>
|
@ -2,27 +2,32 @@
|
||||
<RelativeLayout 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="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/bg_label"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:padding="2dp">
|
||||
|
||||
<com.mikepenz.iconics.view.IconicsImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_margin="1dp"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
app:ico_color="@android:color/black"
|
||||
app:ico_icon="cmd-label" />
|
||||
app:iiv_color="@color/colorPrimaryDarkText"
|
||||
app:iiv_icon="cmd-label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="24dp"
|
||||
tools:text="Label"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toEndOf="@+id/icon" />
|
||||
android:layout_toEndOf="@+id/icon"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingStart="8dp"
|
||||
android:textColor="@color/colorPrimaryDarkText"
|
||||
tools:text="Label" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
105
app/src/main/res/layout/item_message_detail.xml
Normal file
@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:fitsSystemWindows="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginTop="8dp"
|
||||
android:src="@color/colorAccent"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sender"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:layout_alignTop="@+id/avatar"
|
||||
android:layout_toEndOf="@+id/avatar"
|
||||
android:layout_toStartOf="@+id/status"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingStart="8dp"
|
||||
android:textStyle="bold"
|
||||
tools:text="Sender" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:layout_alignBottom="@+id/avatar"
|
||||
android:layout_toEndOf="@+id/avatar"
|
||||
android:layout_toStartOf="@+id/status"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingEnd="0dp"
|
||||
android:paddingStart="8dp"
|
||||
tools:text="Recipient" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="40dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@+id/menu"
|
||||
android:paddingBottom="8dp"
|
||||
android:paddingTop="8dp"
|
||||
android:tint="@color/colorAccent"
|
||||
tools:ignore="ContentDescription"
|
||||
tools:src="@drawable/ic_notification_proof_of_work" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/menu"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/context_menu"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/ic_menu" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:textIsSelectable="true"
|
||||
tools:text="Message Body" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/labels"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="2dip"
|
||||
android:background="@color/divider" />
|
||||
|
||||
</LinearLayout>
|
@ -70,7 +70,7 @@
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:ico_color="@android:color/black"
|
||||
app:ico_icon="cmd-rss"/>
|
||||
app:iiv_color="@android:color/black"
|
||||
app:iiv_icon="cmd-rss"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
15
app/src/main/res/menu/conversation.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/delete"
|
||||
app:showAsAction="ifRoom"
|
||||
android:icon="@drawable/ic_action_delete"
|
||||
android:title="@string/delete"/>
|
||||
<item
|
||||
android:id="@+id/archive"
|
||||
app:showAsAction="ifRoom"
|
||||
android:icon="@drawable/ic_action_archive"
|
||||
android:title="@string/archive"/>
|
||||
</menu>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 4.2 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 7.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 9.6 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
After Width: | Height: | Size: 14 KiB |
@ -137,4 +137,6 @@ Als Alternative kann in den Einstellungen ein vertrauenswürdiger Knoten konfigu
|
||||
<string name="broadcasts">Broadcasts</string>
|
||||
<string name="encoding_simple">einfach</string>
|
||||
<string name="encoding_extended">erweitert</string>
|
||||
<string name="emulate_conversations">Konversation erraten</string>
|
||||
<string name="emulate_conversations_summary">Benutze Betreff um zu erraten welche Nachrichten zusammengehören. Die Reihenfolge stimmt häufig nicht.</string>
|
||||
</resources>
|
||||
|
@ -15,6 +15,7 @@
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<dimen name="action_bar_offset">66dp</dimen>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
4
app/src/main/res/values/ic_launcher_background.xml
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
@ -136,4 +136,21 @@ As an alternative you could configure a trusted node in the settings, but as of
|
||||
<string name="broadcasts">Broadcasts</string>
|
||||
<string name="encoding_simple">simple</string>
|
||||
<string name="encoding_extended">extended</string>
|
||||
<string name="context_menu">actions</string>
|
||||
<string name="emulate_conversations">Guess conversations</string>
|
||||
<string name="emulate_conversations_summary">Use subject to determine which messages belong together. The order will likely be wrong.</string>
|
||||
<string name="emulate_conversations_initialize">Group existing messages by subject</string>
|
||||
<string name="emulate_conversations_batch">Grouping existing messages by subject</string>
|
||||
<string name="preference_group_user_experience">Behaviour</string>
|
||||
<string name="preference_group_user_experience_summary">Change how messages are displayed</string>
|
||||
<string name="preference_group_network_and_performance">Network & Performance</string>
|
||||
<string name="preference_group_network_and_performance_summary">Tweak network usage and protocol details</string>
|
||||
<string name="preference_group_advanced">Advanced</string>
|
||||
<string name="preference_group_advanced_summary"></string>
|
||||
<string name="preference_group_experimental">Experimental</string>
|
||||
<string name="preference_group_experimental_summary">Only change if you know what you\'re doing</string>
|
||||
<string name="require_charging">Require charging</string>
|
||||
<string name="require_charging_summary">Only connect when device is plugged in</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="ok">OK</string>
|
||||
</resources>
|
||||
|
@ -8,18 +8,6 @@
|
||||
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
|
||||
</style>
|
||||
|
||||
<style name="CustomShowcaseTheme" parent="ShowcaseView">
|
||||
<item name="sv_backgroundColor">#eeffc107</item>
|
||||
<item name="sv_showcaseColor">#ffc107</item>
|
||||
<item name="sv_buttonText">Hide</item>
|
||||
<item name="sv_tintButtonColor">false</item>
|
||||
<item name="sv_titleTextAppearance">@style/CustomTitle</item>
|
||||
</style>
|
||||
|
||||
<style name="CustomTitle" parent="TextAppearance.ShowcaseView.Title">
|
||||
<item name="android:textColor">@color/colorAccent</item>
|
||||
</style>
|
||||
|
||||
<style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog.MinWidth">
|
||||
<item name="windowNoTitle">false</item>
|
||||
</style>
|
||||
|
@ -1,58 +1,111 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v7.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<android.support.v7.preference.SwitchPreferenceCompat
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="preference_ux"
|
||||
android:title="@string/preference_group_user_experience"
|
||||
android:summary="@string/preference_group_user_experience_summary"
|
||||
android:persistent="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="emulate_conversations"
|
||||
android:summary="@string/emulate_conversations_summary"
|
||||
android:title="@string/emulate_conversations" />
|
||||
<Preference
|
||||
android:defaultValue="true"
|
||||
android:key="emulate_conversations_initialize"
|
||||
android:summary="@string/emulate_conversations_summary"
|
||||
android:title="@string/emulate_conversations_initialize" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="preference_network_and_performance"
|
||||
android:title="@string/preference_group_network_and_performance"
|
||||
android:summary="@string/preference_group_network_and_performance_summary"
|
||||
android:persistent="false">
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="wifi_only"
|
||||
android:summary="@string/wifi_only_summary"
|
||||
android:title="@string/wifi_only" />
|
||||
<android.support.v7.preference.SwitchPreferenceCompat
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="require_charging"
|
||||
android:enabled="@bool/is_post_api_21"
|
||||
android:summary="@string/require_charging_summary"
|
||||
android:title="@string/require_charging" />
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
android:key="request_acknowledgements"
|
||||
android:summary="@string/request_acknowledgements_summary"
|
||||
android:title="@string/request_acknowledgements" />
|
||||
<android.support.v7.preference.EditTextPreference
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="preference_advanced"
|
||||
android:title="@string/preference_group_advanced"
|
||||
android:summary="@string/preference_group_advanced_summary"
|
||||
android:persistent="false">
|
||||
|
||||
<Preference
|
||||
android:key="cleanup"
|
||||
android:summary="@string/cleanup_summary"
|
||||
android:title="@string/cleanup" />
|
||||
<Preference
|
||||
android:key="export"
|
||||
android:summary="@string/export_data_summary"
|
||||
android:title="@string/export_data" />
|
||||
<Preference
|
||||
android:key="import"
|
||||
android:summary="@string/import_data_summary"
|
||||
android:title="@string/import_data" />
|
||||
|
||||
<PreferenceScreen
|
||||
android:key="preference_experimental"
|
||||
android:title="@string/preference_group_experimental"
|
||||
android:summary="@string/preference_group_experimental_summary"
|
||||
android:persistent="false">
|
||||
|
||||
<EditTextPreference
|
||||
android:inputType="textUri"
|
||||
android:key="trusted_node"
|
||||
android:summary="@string/trusted_node_summary"
|
||||
android:title="@string/trusted_node" />
|
||||
<android.support.v7.preference.EditTextPreference
|
||||
<EditTextPreference
|
||||
android:defaultValue="120"
|
||||
android:inputType="number"
|
||||
android:key="sync_timeout"
|
||||
android:summary="@string/sync_timeout_summary"
|
||||
android:title="@string/sync_timeout" />
|
||||
<android.support.v7.preference.SwitchPreferenceCompat
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:dependency="trusted_node"
|
||||
android:key="server_pow"
|
||||
android:summary="@string/server_pow_summary"
|
||||
android:title="@string/server_pow" />
|
||||
<android.support.v7.preference.Preference
|
||||
<Preference
|
||||
android:key="status"
|
||||
android:summary="@string/status_summary"
|
||||
android:title="@string/status" />
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
<Preference
|
||||
android:key="about"
|
||||
android:summary="@string/about_summary"
|
||||
android:title="@string/about" />
|
||||
<android.support.v7.preference.Preference
|
||||
<Preference
|
||||
android:key="help_out"
|
||||
android:summary="@string/help_out_summary"
|
||||
android:title="@string/help_out">
|
||||
<intent
|
||||
android:action="android.intent.action.VIEW"
|
||||
android:data="@string/help_out_link" />
|
||||
</android.support.v7.preference.Preference>
|
||||
<android.support.v7.preference.Preference
|
||||
android:key="cleanup"
|
||||
android:summary="@string/cleanup_summary"
|
||||
android:title="@string/cleanup" />
|
||||
<android.support.v7.preference.Preference
|
||||
android:key="export"
|
||||
android:summary="@string/export_data_summary"
|
||||
android:title="@string/export_data" />
|
||||
<android.support.v7.preference.Preference
|
||||
android:key="import"
|
||||
android:summary="@string/import_data_summary"
|
||||
android:title="@string/import_data" />
|
||||
<android.support.v7.preference.Preference
|
||||
android:key="status"
|
||||
android:summary="@string/status_summary"
|
||||
android:title="@string/status" />
|
||||
</android.support.v7.preference.PreferenceScreen>
|
||||
</Preference>
|
||||
</PreferenceScreen>
|
||||
|
@ -21,7 +21,7 @@ import ch.dissem.apps.abit.repository.AndroidAddressRepository
|
||||
import ch.dissem.apps.abit.repository.AndroidLabelRepository
|
||||
import ch.dissem.apps.abit.repository.AndroidMessageRepository
|
||||
import ch.dissem.apps.abit.repository.SqlHelper
|
||||
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
|
||||
import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.Plaintext
|
||||
@ -69,7 +69,7 @@ class AndroidMessageRepositoryTest : TestBase() {
|
||||
val labelRepo = AndroidLabelRepository(sqlHelper, RuntimeEnvironment.application)
|
||||
repo = AndroidMessageRepository(sqlHelper)
|
||||
mockedInternalContext(
|
||||
cryptography = SpongyCryptography(),
|
||||
cryptography = BouncyCryptography(),
|
||||
addressRepository = addressRepo,
|
||||
labelRepository = labelRepo,
|
||||
messageRepository = repo,
|
||||
|
@ -59,7 +59,6 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
fun setUp() {
|
||||
RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME)
|
||||
val sqlHelper = SqlHelper(RuntimeEnvironment.application)
|
||||
|
||||
addressRepo = AndroidAddressRepository(sqlHelper)
|
||||
messageRepo = AndroidMessageRepository(sqlHelper)
|
||||
repo = AndroidProofOfWorkRepository(sqlHelper)
|
||||
@ -94,12 +93,14 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
messageRepo.save(plaintext)
|
||||
plaintext.ackMessage!!.let { ackMessage ->
|
||||
initialHash2 = cryptography().getInitialHash(ackMessage)
|
||||
repo.putObject(ProofOfWorkRepository.Item(
|
||||
repo.putObject(
|
||||
ProofOfWorkRepository.Item(
|
||||
objectMessage = ackMessage,
|
||||
nonceTrialsPerByte = 1000, extraBytes = 1000,
|
||||
expirationTime = UnixTime.now + 10 * UnixTime.MINUTE,
|
||||
message = plaintext
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,13 +133,15 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
.build()
|
||||
messageRepo.save(plaintext)
|
||||
plaintext.ackMessage!!.let { ackMessage ->
|
||||
repo.putObject(ProofOfWorkRepository.Item(
|
||||
repo.putObject(
|
||||
ProofOfWorkRepository.Item(
|
||||
objectMessage = ackMessage,
|
||||
nonceTrialsPerByte = 1000,
|
||||
extraBytes = 1000,
|
||||
expirationTime = UnixTime.now + 10 * UnixTime.MINUTE,
|
||||
message = plaintext
|
||||
))
|
||||
)
|
||||
)
|
||||
}
|
||||
assertThat(repo.getItems().size, `is`(sizeBefore + 1))
|
||||
}
|
||||
@ -147,7 +150,10 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
fun `ensure item can be retrieved`() {
|
||||
val item = repo.getItem(initialHash1)
|
||||
assertThat(item, notNullValue())
|
||||
assertThat<ObjectPayload>(item.objectMessage.payload, instanceOf<ObjectPayload>(GetPubkey::class.java))
|
||||
assertThat<ObjectPayload>(
|
||||
item.objectMessage.payload,
|
||||
instanceOf<ObjectPayload>(GetPubkey::class.java)
|
||||
)
|
||||
assertThat(item.nonceTrialsPerByte, `is`(1000L))
|
||||
assertThat(item.extraBytes, `is`(1000L))
|
||||
}
|
||||
@ -156,7 +162,10 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
|
||||
fun `ensure ack item can be retrieved`() {
|
||||
val item = repo.getItem(initialHash2)
|
||||
assertThat(item, notNullValue())
|
||||
assertThat<ObjectPayload>(item.objectMessage.payload, instanceOf<ObjectPayload>(GenericPayload::class.java))
|
||||
assertThat<ObjectPayload>(
|
||||
item.objectMessage.payload,
|
||||
instanceOf<ObjectPayload>(GenericPayload::class.java)
|
||||
)
|
||||
assertThat(item.nonceTrialsPerByte, `is`(1000L))
|
||||
assertThat(item.extraBytes, `is`(1000L))
|
||||
assertThat(item.expirationTime, not<Number>(0))
|
||||
|
@ -19,7 +19,7 @@ package ch.dissem.bitmessage.repository
|
||||
import ch.dissem.bitmessage.BitmessageContext
|
||||
import ch.dissem.bitmessage.InternalContext
|
||||
import ch.dissem.bitmessage.Preferences
|
||||
import ch.dissem.bitmessage.cryptography.sc.SpongyCryptography
|
||||
import ch.dissem.bitmessage.cryptography.bc.BouncyCryptography
|
||||
import ch.dissem.bitmessage.entity.BitmessageAddress
|
||||
import ch.dissem.bitmessage.entity.ObjectMessage
|
||||
import ch.dissem.bitmessage.entity.payload.V4Pubkey
|
||||
@ -41,7 +41,7 @@ open class TestBase {
|
||||
@JvmStatic
|
||||
fun init() {
|
||||
mockedInternalContext(
|
||||
cryptography = SpongyCryptography(),
|
||||
cryptography = BouncyCryptography(),
|
||||
proofOfWorkEngine = MultiThreadedPOWEngine()
|
||||
)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.2.21'
|
||||
ext.kotlin_version = '1.2.41'
|
||||
ext.anko_version = '0.10.4'
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
classpath 'com.android.tools.build:gradle:3.1.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
|
||||
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@ -1,6 +1,6 @@
|
||||
#Mon Oct 23 08:19:50 CEST 2017
|
||||
#Sat Mar 03 14:35:52 CET 2018
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip
|
||||
|