🔀 Merge branch 'feature/conversations' into develop

This commit is contained in:
Christian Basler 2018-06-12 16:58:19 +02:00
commit 76317a2488
70 changed files with 2378 additions and 491 deletions

View File

@ -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 {

View File

@ -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"

View File

@ -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())
}
}

View File

@ -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?) {

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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"

View File

@ -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)

View File

@ -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

View File

@ -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 -> {

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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
}
}
}

View File

@ -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]

View File

@ -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,

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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))

View File

@ -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
}

View File

@ -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"

View File

@ -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())
}
}
}

View File

@ -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()
}

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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"

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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())
}
}

View File

@ -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()
}

View File

@ -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)

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@ -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 &amp; 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>

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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))

View File

@ -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()
)
}

View File

@ -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'

Binary file not shown.

View File

@ -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