🔀 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 //noinspection GroovyMissingReturnStatement
android { android {
compileSdkVersion 27 compileSdkVersion 27
buildToolsVersion "26.0.2" buildToolsVersion "27.0.3"
signingConfigs {
release
}
defaultConfig { defaultConfig {
applicationId "ch.dissem.apps.${appName.toLowerCase()}" applicationId "ch.dissem.apps.${appName.toLowerCase()}"
minSdkVersion 19 minSdkVersion 19
@ -51,11 +54,11 @@ android {
//ext.jabitVersion = '2.0.4' //ext.jabitVersion = '2.0.4'
ext.jabitVersion = 'feature-refactoring-SNAPSHOT' ext.jabitVersion = 'feature-refactoring-SNAPSHOT'
ext.supportVersion = '27.0.2' ext.supportVersion = '27.1.1'
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 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.kotlin:kotlin-reflect:$kotlin_version"
implementation "org.jetbrains.anko:anko:$anko_version" implementation "org.jetbrains.anko:anko:$anko_version"
@ -65,49 +68,50 @@ dependencies {
implementation "com.android.support:support-v13:$supportVersion" implementation "com.android.support:support-v13:$supportVersion"
implementation "com.android.support:preference-v14:$supportVersion" implementation "com.android.support:preference-v14:$supportVersion"
implementation "com.android.support:design:$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-core:$jabitVersion"
implementation "ch.dissem.jabit:jabit-networking:$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-extensions:$jabitVersion"
implementation "ch.dissem.jabit:jabit-wif:$jabitVersion" implementation "ch.dissem.jabit:jabit-wif:$jabitVersion"
implementation "ch.dissem.jabit:jabit-exports:$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 'org.slf4j:slf4j-android:1.7.25'
implementation 'com.mikepenz:materialize:1.1.2@aar' 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 transitive = true
} }
implementation('com.mikepenz:aboutlibraries:6.0.2@aar') { implementation('com.mikepenz:aboutlibraries:6.0.6@aar') {
transitive = true transitive = true
} }
implementation "com.mikepenz:iconics-core:3.0.0@aar" implementation "com.mikepenz:iconics-core:3.0.3@aar"
implementation "com.mikepenz:iconics-views:3.0.0@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:google-material-typeface:3.0.1.2.original@aar'
implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar' implementation 'com.mikepenz:community-material-typeface:2.0.46.1@aar'
implementation 'com.journeyapps:zxing-android-embedded:3.5.0@aar' implementation 'com.journeyapps:zxing-android-embedded:3.6.0@aar'
implementation 'com.google.zxing:core:3.3.1' implementation 'com.google.zxing:core:3.3.2'
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.1.8' implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.0'
implementation 'com.github.amlcurran.showcaseview:library:5.4.3' implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0@aar'
implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') { implementation('com.github.h6ah4i:android-advancedrecyclerview:0.11.0@aar') {
transitive = true transitive = true
} }
implementation 'com.github.angads25:filepicker:1.1.1' 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 '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 'org.hamcrest:hamcrest-library:1.3'
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0' testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'org.robolectric:robolectric:3.6.1' testImplementation 'org.robolectric:robolectric:3.7.1'
testImplementation "org.robolectric:shadows-multidex:3.6.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 { idea.module {

View File

@ -6,6 +6,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_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.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" /> <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
@ -198,6 +199,10 @@
android:exported="true" android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" /> android:permission="android.permission.BIND_JOB_SERVICE" />
<service
android:name=".service.BatchProcessorService"
android:exported="false" />
<activity <activity
android:name=".StatusActivity" android:name=".StatusActivity"
android:label="@string/title_activity_status" android:label="@string/title_activity_status"

View File

@ -26,6 +26,7 @@ import android.view.*
import android.widget.Toast import android.widget.Toast
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.Drawables import ch.dissem.apps.abit.util.Drawables
import ch.dissem.apps.abit.util.qrCode
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.wif.WifExporter import ch.dissem.bitmessage.wif.WifExporter
import com.mikepenz.community_material_typeface_library.CommunityMaterial import com.mikepenz.community_material_typeface_library.CommunityMaterial
@ -185,7 +186,7 @@ class AddressDetailFragment : Fragment() {
} }
// QR code // 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.ImageView
import android.widget.TextView import android.widget.TextView
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.util.FabUtils
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import com.google.zxing.integration.android.IntentIntegrator import com.google.zxing.integration.android.IntentIntegrator
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
@ -48,7 +47,8 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
activity, activity,
R.layout.subscription_row, R.layout.subscription_row,
R.id.name, R.id.name,
LinkedList()) { LinkedList()
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val result: View val result: View
val v: ViewHolder val v: ViewHolder
@ -72,7 +72,8 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
v.avatar.setImageDrawable(Identicon(item)) v.avatar.setImageDrawable(Identicon(item))
v.name.text = item.toString() v.name.text = item.toString()
v.streamNumber.text = v.ctx.getString(R.string.stream_number, item.stream) 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 return result
} }
@ -105,11 +106,11 @@ class AddressListFragment : AbstractItemListFragment<Void, BitmessageAddress>()
val menu = FabSpeedDialMenu(activity) val menu = FabSpeedDialMenu(activity)
menu.add(R.string.scan_qr_code).setIcon(R.drawable.ic_action_qr_code) 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) 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 -> .addOnMenuItemClickListener { _, _, itemId ->
when (itemId) { when (itemId) {
1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment) 1 -> IntentIntegrator.forSupportFragment(this@AddressListFragment)
.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES) .setDesiredBarcodeFormats(IntentIntegrator.QR_CODE)
.initiateScan() .initiateScan()
2 -> { 2 -> {
val intent = Intent(getActivity(), CreateAddressActivity::class.java) 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) inflater.inflate(R.layout.fragment_address_list, container, false)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 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( val prefix: String = if (subject.length >= 3 && subject.substring(0, 3).equals(
"RE:", "RE:",
ignoreCase = true ignoreCase = true
)) { )
) {
"" ""
} else { } else {
"RE: " "RE: "
@ -107,7 +108,7 @@ class ComposeMessageActivity : AppCompatActivity() {
} }
replyIntent.putExtra( replyIntent.putExtra(
EXTRA_CONTENT, EXTRA_CONTENT,
"\n\n------------------------------------------------------\n" + item.text!! "\n\n------------------------------------------------------\n${item.text ?: ""}"
) )
return replyIntent 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.*
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.support.annotation.ColorInt
import android.text.TextPaint import android.text.TextPaint
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import org.jetbrains.anko.collections.forEachWithIndex
import kotlin.math.sqrt
/** /**
* @author Christian Basler * @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 color = Color.HSVToColor(
private val background = Color.HSVToColor(floatArrayOf((Math.abs(hash[1] * hash[2] + hash[0]) % 360).toFloat(), 0.8f, 1.0f)) 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 { private val textPaint = TextPaint().apply {
textAlign = Paint.Align.CENTER textAlign = Paint.Align.CENTER
color = 0xFF607D8B.toInt() color = 0xFF607D8B.toInt()
@ -54,20 +68,24 @@ class Identicon(input: BitmessageAddress) : Drawable() {
} }
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
var x: Float
var y: Float
val width = canvas.width.toFloat() val width = canvas.width.toFloat()
val height = canvas.height.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 cellWidth = width / SIZE.toFloat()
val cellHeight = height / SIZE.toFloat() val cellHeight = height / SIZE.toFloat()
paint.color = background 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 paint.color = color
for (row in 0 until SIZE) { for (row in 0 until SIZE) {
for (column in 0 until SIZE) { for (column in 0 until SIZE) {
if (fields[row][column]) { if (fields[row][column]) {
x = cellWidth * column x = offsetX + cellWidth * column
y = cellHeight * row y = offsetY + cellHeight * row
canvas.drawCircle( canvas.drawCircle(
x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2, x + cellWidth / 2, y + cellHeight / 2, cellHeight / 2,
paint paint
@ -77,7 +95,7 @@ class Identicon(input: BitmessageAddress) : Drawable() {
} }
if (isChan) { if (isChan) {
textPaint.textSize = 2 * cellHeight 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 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 package ch.dissem.apps.abit
import android.content.Intent import android.content.Intent
import android.graphics.Point import android.graphics.Canvas
import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.support.annotation.DrawableRes
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import android.support.v7.app.AppCompatActivity import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.Toolbar import android.support.v7.widget.Toolbar
import android.view.View 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.ProfileImageListener
import ch.dissem.apps.abit.drawer.ProfileSelectionListener import ch.dissem.apps.abit.drawer.ProfileSelectionListener
import ch.dissem.apps.abit.listener.ListSelectionListener 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
import ch.dissem.apps.abit.service.Singleton.currentLabel import ch.dissem.apps.abit.service.Singleton.currentLabel
import ch.dissem.apps.abit.synchronization.SyncAdapter 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.NetworkUtils
import ch.dissem.apps.abit.util.Preferences 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.BitmessageContext
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.Conversation
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.github.amlcurran.showcaseview.ShowcaseView
import com.mikepenz.community_material_typeface_library.CommunityMaterial import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.IconicsDrawable 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.IProfile
import com.mikepenz.materialdrawer.model.interfaces.Nameable import com.mikepenz.materialdrawer.model.interfaces.Nameable
import io.github.kobakei.materialfabspeeddial.FabSpeedDial import io.github.kobakei.materialfabspeeddial.FabSpeedDial
import io.github.kobakei.materialfabspeeddial.FabSpeedDialMenu
import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.activity_main.*
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread 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.io.Serializable
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
@ -110,7 +115,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
val toolbar = findViewById<Toolbar>(R.id.toolbar) val toolbar = findViewById<Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
val listFragment = MessageListFragment() val listFragment = ConversationListFragment()
supportFragmentManager supportFragmentManager
.beginTransaction() .beginTransaction()
.replace(R.id.item_list, listFragment) .replace(R.id.item_list, listFragment)
@ -146,33 +151,33 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
SyncAdapter.stopSync(this) SyncAdapter.stopSync(this)
} }
if (drawer.isDrawerOpen) { if (drawer.isDrawerOpen) {
val lps = RelativeLayout.LayoutParams( MaterialShowcaseView.Builder(this)
ViewGroup .setMaskColour(R.color.colorPrimary)
.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT .setTitleText(R.string.full_node)
).apply { .setContentText(R.string.full_node_description)
addRule(RelativeLayout.ALIGN_PARENT_BOTTOM) .setDismissOnTouch(true)
addRule(RelativeLayout.ALIGN_PARENT_LEFT) .setDismissText(R.string.got_it)
val margin = ((resources.displayMetrics.density * 12) as Number).toInt() .setShape(object : Shape {
setMargins(margin, margin, margin, margin) var w = 0
var h = 0
override fun updateTarget(target: Target) {
w = target.bounds.width()
h = target.bounds.height()
} }
ShowcaseView.Builder(this) override fun getHeight() = h
.withMaterialShowcase()
.setStyle(R.style.CustomShowcaseTheme) override fun draw(canvas: Canvas, paint: Paint, x: Int, y: Int, padding: Int) {
.setContentTitle(R.string.full_node) val r = h.toFloat() / 2
.setContentText(R.string.full_node_description) canvas.drawCircle(x + w / 2 - r * 1.8f, y.toFloat(), r, paint)
.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)
} }
.replaceEndButton(R.layout.showcase_button)
.hideOnTouchOutside() override fun getWidth() = w
.build() })
.setButtonPosition(lps) .setTarget(drawer.stickyFooter)
.setDelay(1000)
.show()
} }
} }
@ -299,6 +304,7 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label currentLabel.value = intent.getSerializableExtra(EXTRA_SHOW_LABEL) as Label
} else if (currentLabel.value == null) { } else if (currentLabel.value == null) {
currentLabel.value = labels[0] currentLabel.value = labels[0]
} }
for (label in labels) { for (label in labels) {
addLabelEntry(label) addLabelEntry(label)
@ -324,9 +330,15 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
val tag = item.tag val tag = item.tag
if (tag is Label) { if (tag is Label) {
currentLabel.value = tag currentLabel.value = tag
if (tag.type == Label.Type.INBOX || tag == LABEL_ARCHIVE) {
if (itemList !is ConversationListFragment) {
changeList(ConversationListFragment())
}
} else {
if (itemList !is MessageListFragment) { if (itemList !is MessageListFragment) {
changeList(MessageListFragment()) changeList(MessageListFragment())
} }
}
return false return false
} else if (item is Nameable<*>) { } else if (item is Nameable<*>) {
when (item.name.textRes) { when (item.name.textRes) {
@ -398,8 +410,8 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
.withIdentifier(label.id as Long) .withIdentifier(label.id as Long)
.withName(label.toString()) .withName(label.toString())
.withTag(label) .withTag(label)
.withIcon(Labels.getIcon(label)) .withIcon(label.getIcon())
.withIconColor(Labels.getColor(label)) .withIconColor(label.getColor(0xFF000000.toInt()))
drawer.addItemAtPosition(item, drawer.drawerItems.size - 3) drawer.addItemAtPosition(item, drawer.drawerItems.size - 3)
} }
@ -456,6 +468,13 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
// adding or replacing the detail fragment using a // adding or replacing the detail fragment using a
// fragment transaction. // fragment transaction.
val fragment = when (item) { val fragment = when (item) {
is Conversation -> {
ConversationDetailFragment().apply {
arguments = Bundle().apply {
putSerializable(ConversationDetailFragment.ARG_ITEM_ID, item.id)
}
}
}
is Plaintext -> { is Plaintext -> {
if (item.labels.any { it.type == Label.Type.DRAFT }) { if (item.labels.any { it.type == Label.Type.DRAFT }) {
ComposeMessageFragment().apply { ComposeMessageFragment().apply {
@ -487,6 +506,11 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
// In single-pane mode, simply start the detail activity // In single-pane mode, simply start the detail activity
// for the selected item ID. // for the selected item ID.
val detailIntent = when (item) { val detailIntent = when (item) {
is Conversation -> {
Intent(this, MessageDetailActivity::class.java).apply {
putExtra(ConversationDetailFragment.ARG_ITEM_ID, item.id)
}
}
is Plaintext -> { is Plaintext -> {
if (item.labels.any { it.type == Label.Type.DRAFT }) { if (item.labels.any { it.type == Label.Type.DRAFT }) {
Intent(this, ComposeMessageActivity::class.java).apply { Intent(this, ComposeMessageActivity::class.java).apply {
@ -520,6 +544,25 @@ class MainActivity : AppCompatActivity(), ListSelectionListener<Serializable> {
supportActionBar?.title = title 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 { companion object {
const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage" const val EXTRA_SHOW_MESSAGE = "ch.dissem.abit.ShowMessage"
const val EXTRA_SHOW_LABEL = "ch.dissem.abit.ShowLabel" 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.os.Bundle
import android.support.v4.app.NavUtils import android.support.v4.app.NavUtils
import android.view.MenuItem 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 // Create the detail fragment and add it to the activity
// using a fragment transaction. // using a fragment transaction.
val arguments = Bundle() val arguments = Bundle()
arguments.putSerializable(MessageDetailFragment.ARG_ITEM, val item = intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)
intent.getSerializableExtra(MessageDetailFragment.ARG_ITEM)) arguments.putSerializable(MessageDetailFragment.ARG_ITEM, item)
val fragment = MessageDetailFragment() 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 fragment.arguments = arguments
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.add(R.id.content, fragment) .add(R.id.content, fragment)

View File

@ -29,17 +29,17 @@ import android.text.util.Linkify.WEB_URLS
import android.view.* import android.view.*
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import ch.dissem.apps.abit.adapter.LabelAdapter
import ch.dissem.apps.abit.service.Singleton 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_ADDRESS_PATTERN
import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA import ch.dissem.apps.abit.util.Constants.BITMESSAGE_URL_SCHEMA
import ch.dissem.apps.abit.util.Drawables 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.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.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.view.IconicsImageView
import kotlinx.android.synthetic.main.fragment_message_detail.* import kotlinx.android.synthetic.main.fragment_message_detail.*
import java.util.* import java.util.*
@ -85,8 +85,8 @@ class MessageDetailFragment : Fragment() {
// Show the dummy content as text in a TextView. // Show the dummy content as text in a TextView.
item?.let { item -> item?.let { item ->
subject.text = item.subject subject.text = item.subject
status.setImageResource(Assets.getStatusDrawable(item.status)) status.setImageResource(item.status.getDrawable())
status.contentDescription = getString(Assets.getStatusString(item.status)) status.contentDescription = getString(item.status.getString())
avatar.setImageDrawable(Identicon(item.from)) avatar.setImageDrawable(Identicon(item.from))
val senderClickListener: (View) -> Unit = { val senderClickListener: (View) -> Unit = {
MainActivity.apply { MainActivity.apply {
@ -229,7 +229,7 @@ class MessageDetailFragment : Fragment() {
val message = messages[position] val message = messages[position]
viewHolder.avatar.setImageDrawable(Identicon(message.from)) 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.sender.text = message.from.toString()
viewHolder.extract.text = prepareMessageExtract(message.text) viewHolder.extract.text = prepareMessageExtract(message.text)
viewHolder.item = message 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 { companion object {
/** /**
* The fragment argument representing the item ID that this fragment * 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.repository.AndroidMessageRepository
import ch.dissem.apps.abit.service.Singleton import ch.dissem.apps.abit.service.Singleton
import ch.dissem.apps.abit.service.Singleton.currentLabel 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.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator import com.h6ah4i.android.widget.advrecyclerview.animator.SwipeDismissItemAnimator
@ -80,7 +79,8 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
if (!isLoading && !isLastPage) { if (!isLoading && !isLastPage) {
if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5 if (visibleItemCount + firstVisibleItemPosition >= totalItemCount - 5
&& firstVisibleItemPosition >= 0) { && firstVisibleItemPosition >= 0
) {
loadMoreItems() loadMoreItems()
} }
} }
@ -98,7 +98,11 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
isLoading = true isLoading = true
swipeableMessageAdapter?.let { messageAdapter -> swipeableMessageAdapter?.let { messageAdapter ->
doAsync { doAsync {
val messages = messageRepo.findMessages(currentLabel.value, messageAdapter.itemCount, PAGE_SIZE) val messages = messageRepo.findMessages(
currentLabel.value,
messageAdapter.itemCount,
PAGE_SIZE
)
onUiThread { onUiThread {
messageAdapter.addAll(messages) messageAdapter.addAll(messages)
isLoading = false isLoading = false
@ -149,7 +153,11 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
loadMoreItems() 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) inflater.inflate(R.layout.fragment_message_list, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -193,7 +201,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
adapter.setSelectedPosition(position) adapter.setSelectedPosition(position)
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {
val item = adapter.getItem(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.itemAnimator = animator
recycler_view.addOnScrollListener(recyclerViewOnScrollListener) recycler_view.addOnScrollListener(recyclerViewOnScrollListener)
recycler_view.addItemDecoration(SimpleListDividerDecorator( recycler_view.addItemDecoration(
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true)) SimpleListDividerDecorator(
ContextCompat.getDrawable(context, R.drawable.list_divider_h), true
)
)
// NOTE: // NOTE:
// The initialization order is very important! This order determines the priority of // The initialization order is very important! This order determines the priority of
@ -226,7 +237,7 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
recyclerViewTouchActionGuardManager = touchActionGuardManager recyclerViewTouchActionGuardManager = touchActionGuardManager
recyclerViewSwipeManager = swipeManager recyclerViewSwipeManager = swipeManager
this.swipeableMessageAdapter = adapter swipeableMessageAdapter = adapter
Singleton.updateMessageListAdapterInListener(adapter) Singleton.updateMessageListAdapterInListener(adapter)
} }
@ -235,12 +246,14 @@ class MessageListFragment : Fragment(), ListHolder<Label> {
val menu = FabSpeedDialMenu(context) val menu = FabSpeedDialMenu(context)
menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast) menu.add(R.string.broadcast).setIcon(R.drawable.ic_action_broadcast)
menu.add(R.string.personal_message).setIcon(R.drawable.ic_action_personal) 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 -> .addOnMenuItemClickListener { _, _, itemId ->
val identity = Singleton.getIdentity(context) val identity = Singleton.getIdentity(context)
if (identity == null) { if (identity == null) {
Toast.makeText(activity, R.string.no_identity_warning, Toast.makeText(
Toast.LENGTH_LONG).show() activity, R.string.no_identity_warning,
Toast.LENGTH_LONG
).show()
} else { } else {
when (itemId) { when (itemId) {
1 -> { 1 -> {

View File

@ -17,42 +17,65 @@
package ch.dissem.apps.abit package ch.dissem.apps.abit
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.*
import android.content.Intent import android.os.Build
import android.content.SharedPreferences import android.os.Build.VERSION_CODES.LOLLIPOP
import android.os.Bundle 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.v4.content.FileProvider.getUriForFile
import android.support.v7.preference.Preference import android.support.v7.preference.Preference
import android.support.v7.preference.Preference.OnPreferenceChangeListener
import android.support.v7.preference.PreferenceFragmentCompat 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 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.service.Singleton
import ch.dissem.apps.abit.synchronization.SyncAdapter 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_SERVER_POW
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Exports import ch.dissem.apps.abit.util.Exports
import ch.dissem.apps.abit.util.NetworkUtils
import ch.dissem.apps.abit.util.Preferences import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.entity.Plaintext
import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.LibsBuilder import com.mikepenz.aboutlibraries.LibsBuilder
import org.jetbrains.anko.doAsync import org.jetbrains.anko.doAsync
import org.jetbrains.anko.support.v4.indeterminateProgressDialog import org.jetbrains.anko.support.v4.indeterminateProgressDialog
import org.jetbrains.anko.support.v4.startActivity import org.jetbrains.anko.support.v4.startActivity
import org.jetbrains.anko.uiThread import org.jetbrains.anko.uiThread
import java.util.*
/** /**
* @author Christian Basler * @author Christian Basler
*/ */
class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener,
PreferenceFragmentCompat.OnPreferenceStartScreenCallback {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.preferences) setPreferencesFromResource(R.xml.preferences, rootKey)
findPreference("about")?.onPreferenceClickListener = aboutClickListener() findPreference("about")?.onPreferenceClickListener = aboutClickListener()
val cleanup = findPreference("cleanup") findPreference("cleanup")?.let { it.onPreferenceClickListener = cleanupClickListener(it) }
cleanup?.onPreferenceClickListener = cleanupClickListener(cleanup)
findPreference("export")?.onPreferenceClickListener = exportClickListener() findPreference("export")?.onPreferenceClickListener = exportClickListener()
findPreference("import")?.onPreferenceClickListener = importClickListener() 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 { private fun aboutClickListener() = Preference.OnPreferenceClickListener {
@ -73,7 +96,8 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
} }
private fun cleanupClickListener(cleanup: Preference) = Preference.OnPreferenceClickListener { 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 cleanup.isEnabled = false
Toast.makeText(ctx, R.string.cleanup_notification_start, Toast.LENGTH_SHORT).show() 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?) { override fun onAttach(ctx: Context?) {
super.onAttach(ctx) super.onAttach(ctx)
(ctx as? MainActivity)?.floatingActionButton?.hide() ctx?.let {
PreferenceManager.getDefaultSharedPreferences(ctx) if (it is MainActivity) {
.registerOnSharedPreferenceChangeListener(this) it.floatingActionButton?.hide()
it.updateTitle(getString(R.string.settings))
(ctx as? MainActivity)?.updateTitle(getString(R.string.settings)) }
}
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { 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 { companion object {
const val WRITE_EXPORT_REQUEST_CODE = 1 const val WRITE_EXPORT_REQUEST_CODE = 1
const val READ_IMPORT_REQUEST_CODE = 2 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.Identicon
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.repository.AndroidLabelRepository.Companion.LABEL_ARCHIVE 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.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.Plaintext
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter
@ -48,7 +49,8 @@ import java.util.*
* @author Christian Basler * @author Christian Basler
* @see [https://github.com/h6ah4i/android-advancedrecyclerview](https://github.com/h6ah4i/android-advancedrecyclerview) * @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>() private val data = LinkedList<Plaintext>()
var eventListener: EventListener? = null var eventListener: EventListener? = null
@ -84,7 +86,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
init { init {
itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) } itemViewOnClickListener = View.OnClickListener { view -> onItemViewClick(view) }
swipeableViewContainerOnClickListener = View.OnClickListener { view -> onSwipeableViewContainerClick(view) } swipeableViewContainerOnClickListener =
View.OnClickListener { view -> onSwipeableViewContainerClick(view) }
// SwipeableItemAdapter requires stable ID, and also // SwipeableItemAdapter requires stable ID, and also
// have to implement the getItemId() method appropriately. // have to implement the getItemId() method appropriately.
@ -134,7 +137,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
private fun onSwipeableViewContainerClick(v: View) { private fun onSwipeableViewContainerClick(v: View) {
eventListener?.onItemViewClicked( eventListener?.onItemViewClicked(
RecyclerViewAdapterUtils.getParentViewHolderItemView(v)) RecyclerViewAdapterUtils.getParentViewHolderItemView(v)
)
} }
fun getItem(position: Int) = data[position] fun getItem(position: Int) = data[position]
@ -168,8 +172,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
// set data // set data
avatar.setImageDrawable(Identicon(item.from)) avatar.setImageDrawable(Identicon(item.from))
status.setImageResource(Assets.getStatusDrawable(item.status)) status.setImageResource(item.status.getDrawable())
status.contentDescription = holder.status.context.getString(Assets.getStatusString(item.status)) status.contentDescription = holder.status.context.getString(item.status.getString())
sender.text = item.from.toString() sender.text = item.from.toString()
subject.text = prepareMessageExtract(item.subject) subject.text = prepareMessageExtract(item.subject)
extract.text = prepareMessageExtract(item.text) extract.text = prepareMessageExtract(item.text)
@ -194,7 +198,8 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
@SuppressLint("SwitchIntDef") @SuppressLint("SwitchIntDef")
override fun onSetSwipeBackground(holder: ViewHolder, position: Int, type: Int) = 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_NEUTRAL_BACKGROUND -> R.drawable.bg_swipe_item_neutral
DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left DRAWABLE_SWIPE_LEFT_BACKGROUND -> R.drawable.bg_swipe_item_left
DRAWABLE_SWIPE_RIGHT_BACKGROUND -> if (label === LABEL_ARCHIVE || label?.type == Label.Type.TRASH) { 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 R.drawable.bg_swipe_item_right
} }
else -> R.drawable.bg_swipe_item_neutral else -> R.drawable.bg_swipe_item_neutral
}) }
)
@SuppressLint("SwitchIntDef") @SuppressLint("SwitchIntDef")
override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) = override fun onSwipeItem(holder: ViewHolder, position: Int, result: Int) =
@ -222,7 +228,10 @@ class SwipeableMessageAdapter : RecyclerView.Adapter<SwipeableMessageAdapter.Vie
notifyItemChanged(selectedPosition) 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 var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position] 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 var adapter: SwipeableMessageAdapter? = adapter
private val item = adapter.data[position] private val item = adapter.data[position]

View File

@ -10,7 +10,7 @@ import android.view.WindowManager
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import ch.dissem.apps.abit.service.Singleton 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.AccountHeader
import com.mikepenz.materialdrawer.model.interfaces.IProfile 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) dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
val imageView = ImageView(ctx) val imageView = ImageView(ctx)
imageView.setImageBitmap(Drawables.qrCode(Singleton.getIdentity(ctx))) imageView.setImageBitmap(Singleton.getIdentity(ctx)?.qrCode())
imageView.setOnClickListener { dialog.dismiss() } imageView.setOnClickListener { dialog.dismiss() }
dialog.addContentView( dialog.addContentView(
imageView, imageView,

View File

@ -19,8 +19,11 @@ package ch.dissem.apps.abit.listener
import android.content.Context import android.content.Context
import ch.dissem.apps.abit.MainActivity import ch.dissem.apps.abit.MainActivity
import ch.dissem.apps.abit.notification.NewMessageNotification import ch.dissem.apps.abit.notification.NewMessageNotification
import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.BitmessageContext import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
import ch.dissem.bitmessage.ports.MessageRepository
import ch.dissem.bitmessage.utils.ConversationService
import java.util.* import java.util.*
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -33,14 +36,26 @@ import java.util.concurrent.Executors
* notifications should be combined. * 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 val unacknowledged = LinkedList<Plaintext>()
private var numberOfUnacknowledgedMessages = 0 private var numberOfUnacknowledgedMessages = 0
private val notification = NewMessageNotification(ctx) private val notification = NewMessageNotification(ctx)
private val pool = Executors.newSingleThreadExecutor() 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) { override fun receive(plaintext: Plaintext) {
pool.submit { pool.submit {
updateConversation(plaintext)
unacknowledged.addFirst(plaintext) unacknowledged.addFirst(plaintext)
numberOfUnacknowledgedMessages++ numberOfUnacknowledgedMessages++
if (unacknowledged.size > 5) { if (unacknowledged.size > 5) {
@ -65,4 +80,17 @@ class MessageListener(ctx: Context) : BitmessageContext.Listener {
numberOfUnacknowledgedMessages = 0 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 package ch.dissem.apps.abit.notification
import android.app.PendingIntent import android.app.PendingIntent
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
@ -27,18 +28,15 @@ import android.text.Spannable
import android.text.SpannableString import android.text.SpannableString
import android.text.Spanned import android.text.Spanned
import android.text.style.StyleSpan import android.text.style.StyleSpan
import ch.dissem.apps.abit.Identicon import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.MainActivity 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_REPLY_TO_MESSAGE
import ch.dissem.apps.abit.MainActivity.Companion.EXTRA_SHOW_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.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) { 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) bigText.setSpan(SPAN_EMPHASIS, 0, subject.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
} }
builder.setSmallIcon(R.drawable.ic_notification_new_message) builder.setSmallIcon(R.drawable.ic_notification_new_message)
.setLargeIcon(toBitmap(Identicon(plaintext.from), 192)) .setLargeIcon(Identicon(plaintext.from).toBitmap(192))
.setContentTitle(plaintext.from.toString()) .setContentTitle(plaintext.from.toString())
.setContentText(plaintext.subject) .setContentText(plaintext.subject)
.setStyle(BigTextStyle().bigText(bigText)) .setStyle(BigTextStyle().bigText(bigText))

View File

@ -20,7 +20,7 @@ import android.content.ContentValues
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.database.DatabaseUtils 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.entity.valueobject.Label
import ch.dissem.bitmessage.ports.AbstractLabelRepository import ch.dissem.bitmessage.ports.AbstractLabelRepository
import ch.dissem.bitmessage.ports.MessageRepository import ch.dissem.bitmessage.ports.MessageRepository
@ -30,7 +30,8 @@ import java.util.*
/** /**
* [MessageRepository] implementation using the Android SQL API. * [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> { override fun find(where: String): List<Label> {
val result = LinkedList<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())) db.update(TABLE_NAME, values, "id=?", arrayOf(label.id.toString()))
} else { } else {
db.transaction { 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) { if (exists) {
val values = ContentValues() 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 { companion object {
val LABEL_ARCHIVE = Label("archive", null, 0).apply { id = Long.MAX_VALUE } 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 { internal fun getLabel(c: Cursor, context: Context): Label {
val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE)) val typeName = c.getString(c.getColumnIndex(COLUMN_TYPE))
val type = if (typeName == null) null else Label.Type.valueOf(typeName) 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( val label = Label(
text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)), text ?: c.getString(c.getColumnIndex(COLUMN_LABEL)),
type, type,
c.getInt(c.getColumnIndex(COLUMN_COLOR))) c.getInt(c.getColumnIndex(COLUMN_COLOR))
)
label.id = c.getLong(c.getColumnIndex(COLUMN_ID)) label.id = c.getLong(c.getColumnIndex(COLUMN_ID))
return label return label
} }

View File

@ -40,12 +40,20 @@ import java.util.*
*/ */
class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepository() { 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) super.findMessages(null as Label?, offset, limit)
} else { } else {
super.findMessages(label, offset, limit) super.findMessages(label, offset, limit)
} }
fun count() = DatabaseUtils.queryNumEntries(
sql.readableDatabase,
TABLE_NAME,
null,
null
).toInt()
override fun countUnread(label: Label?) = when { override fun countUnread(label: Label?) = when {
label === LABEL_ARCHIVE -> 0 label === LABEL_ARCHIVE -> 0
label == null -> DatabaseUtils.queryNumEntries( label == null -> DatabaseUtils.queryNumEntries(
@ -63,7 +71,7 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
).toInt() ).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 projection = arrayOf(COLUMN_CONVERSATION)
val where = when { val where = when {
@ -74,8 +82,12 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
val result = LinkedList<UUID>() val result = LinkedList<UUID>()
sql.readableDatabase.query( sql.readableDatabase.query(
true, true,
TABLE_NAME, projection, where, TABLE_NAME,
null, null, null, null, null projection,
where,
null, null, null,
"$COLUMN_RECEIVED DESC, $COLUMN_SENT DESC",
if (limit == 0) null else "$offset, $limit"
).use { c -> ).use { c ->
while (c.moveToNext()) { while (c.moveToNext()) {
val uuidBytes = c.getBlob(c.getColumnIndex(COLUMN_CONVERSATION)) 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 // Define a projection that specifies which columns from the database
// you will actually use after this query. // 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( sql.readableDatabase.query(
TABLE_NAME, projection, TABLE_NAME, projection,
@ -174,7 +201,8 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
labels = findLabels(id!!) 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) { override fun save(message: Plaintext) {
saveContactIfNecessary(message.from) saveContactIfNecessary(message.from)
@ -233,6 +261,39 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo
sql.writableDatabase.delete(TABLE_NAME, "id = ?", arrayOf(message.id.toString())) 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 { companion object {
private const val TABLE_NAME = "Message" private const val TABLE_NAME = "Message"
private const val COLUMN_ID = "id" 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.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.os.BatteryManager
import android.os.Handler import android.os.Handler
import ch.dissem.apps.abit.notification.NetworkNotification import ch.dissem.apps.abit.notification.NetworkNotification
import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID import ch.dissem.apps.abit.notification.NetworkNotification.Companion.NETWORK_NOTIFICATION_ID
import ch.dissem.apps.abit.util.Preferences import ch.dissem.apps.abit.util.Preferences
import ch.dissem.bitmessage.BitmessageContext import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.utils.Property import ch.dissem.bitmessage.utils.Property
import org.jetbrains.anko.doAsync
/** /**
* Define a Service that returns an IBinder for the * Define a Service that returns an IBinder for the
@ -60,7 +62,10 @@ class BitmessageService : Service() {
override fun onCreate() { override fun onCreate() {
registerReceiver( registerReceiver(
connectivityReceiver, connectivityReceiver,
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) IntentFilter().apply {
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
addAction(Intent.ACTION_BATTERY_CHANGED)
}
) )
notification = NetworkNotification(this) notification = NetworkNotification(this)
running = false running = false
@ -87,7 +92,9 @@ class BitmessageService : Service() {
running = false running = false
notification.showShutdown() notification.showShutdown()
cleanupHandler.removeCallbacks(cleanupTask) cleanupHandler.removeCallbacks(cleanupTask)
doAsync {
bmc.cleanup() bmc.cleanup()
}
unregisterReceiver(connectivityReceiver) unregisterReceiver(connectivityReceiver)
stopSelf() stopSelf()
} }

View File

@ -32,6 +32,7 @@ import ch.dissem.bitmessage.BitmessageContext
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import ch.dissem.bitmessage.entity.payload.Pubkey import ch.dissem.bitmessage.entity.payload.Pubkey
import ch.dissem.bitmessage.entity.valueobject.Label import ch.dissem.bitmessage.entity.valueobject.Label
import ch.dissem.bitmessage.factory.BufferPool
import ch.dissem.bitmessage.networking.nio.NioNetworkHandler import ch.dissem.bitmessage.networking.nio.NioNetworkHandler
import ch.dissem.bitmessage.ports.DefaultLabeler import ch.dissem.bitmessage.ports.DefaultLabeler
import ch.dissem.bitmessage.utils.ConversationService import ch.dissem.bitmessage.utils.ConversationService
@ -101,6 +102,7 @@ object Singleton {
fun getBitmessageContext(context: Context): BitmessageContext = fun getBitmessageContext(context: Context): BitmessageContext =
init({ bitmessageContext }, { bitmessageContext = it }) { init({ bitmessageContext }, { bitmessageContext = it }) {
BufferPool.setLimit(4)
BitmessageContext.build { BitmessageContext.build {
TTL.pubkey = 2 * DAY TTL.pubkey = 2 * DAY
val ctx = context.applicationContext val ctx = context.applicationContext
@ -117,7 +119,7 @@ object Singleton {
labelRepo = AndroidLabelRepository(sqlHelper, ctx) labelRepo = AndroidLabelRepository(sqlHelper, ctx)
messageRepo = AndroidMessageRepository(sqlHelper) messageRepo = AndroidMessageRepository(sqlHelper)
proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it } proofOfWorkRepo = AndroidProofOfWorkRepository(sqlHelper).also { powRepo = it }
networkHandler = NioNetworkHandler() networkHandler = NioNetworkHandler(4)
listener = getMessageListener(ctx) listener = getMessageListener(ctx)
labeler = Singleton.labeler labeler = Singleton.labeler
preferences.sendPubkeyOnIdentityCreation = false preferences.sendPubkeyOnIdentityCreation = false

View File

@ -24,11 +24,9 @@ class StartupNodeOnWifiService : JobService() {
return true 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 * Don't actually stop the service, otherwise it will be stopped after 1 or 10 minutes
// depending on Android version. * depending on Android version.
Preferences.isFullNodeActive(this) */
} else { override fun onStopJob(params: JobParameters?) = Preferences.isFullNodeActive(this)
false
}
} }

View File

@ -43,11 +43,10 @@ object Assets {
} catch (e: IOException) { } catch (e: IOException) {
throw RuntimeException(e) throw RuntimeException(e)
} }
}
} }
@DrawableRes fun Plaintext.Status.getDrawable() = when (this) {
fun getStatusDrawable(status: Plaintext.Status) = when (status) {
Plaintext.Status.RECEIVED -> 0 Plaintext.Status.RECEIVED -> 0
Plaintext.Status.DRAFT -> R.drawable.draft Plaintext.Status.DRAFT -> R.drawable.draft
Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key Plaintext.Status.PUBKEY_REQUESTED -> R.drawable.public_key
@ -57,8 +56,7 @@ object Assets {
else -> 0 else -> 0
} }
@StringRes fun Plaintext.Status.getString() = when (this) {
fun getStatusString(status: Plaintext.Status) = when (status) {
Plaintext.Status.RECEIVED -> R.string.status_received Plaintext.Status.RECEIVED -> R.string.status_received
Plaintext.Status.DRAFT -> R.string.status_draft Plaintext.Status.DRAFT -> R.string.status_draft
Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key Plaintext.Status.PUBKEY_REQUESTED -> R.string.status_public_key
@ -67,4 +65,3 @@ object Assets {
Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged Plaintext.Status.SENT_ACKNOWLEDGED -> R.string.status_sent_acknowledged
else -> 0 else -> 0
} }
}

View File

@ -23,6 +23,8 @@ import java.util.regex.Pattern
*/ */
object Constants { object Constants {
const val PREFERENCE_WIFI_ONLY = "wifi_only" 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_TRUSTED_NODE = "trusted_node"
const val PREFERENCE_SYNC_TIMEOUT = "sync_timeout" const val PREFERENCE_SYNC_TIMEOUT = "sync_timeout"
const val PREFERENCE_SERVER_POW = "server_pow" const val PREFERENCE_SERVER_POW = "server_pow"

View File

@ -21,13 +21,14 @@ import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.Color.BLACK import android.graphics.Color.BLACK
import android.graphics.Color.WHITE import android.graphics.Color.WHITE
import android.graphics.drawable.Drawable
import android.util.Base64 import android.util.Base64
import android.util.Base64.NO_WRAP import android.util.Base64.NO_WRAP
import android.util.Base64.URL_SAFE import android.util.Base64.URL_SAFE
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import ch.dissem.apps.abit.Identicon
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.util.Drawables.QR_CODE_SIZE
import ch.dissem.bitmessage.entity.BitmessageAddress import ch.dissem.bitmessage.entity.BitmessageAddress
import com.google.zxing.BarcodeFormat import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter import com.google.zxing.MultiFormatWriter
@ -42,47 +43,48 @@ import java.io.ByteArrayOutputStream
* Some helper methods to work with drawables. * Some helper methods to work with drawables.
*/ */
object 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 { fun addIcon(ctx: Context, menu: Menu, menuItem: Int, icon: IIcon): MenuItem {
val item = menu.findItem(menuItem) val item = menu.findItem(menuItem)
item.icon = IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar() item.icon = IconicsDrawable(ctx, icon).colorRes(R.color.colorPrimaryDarkText).actionBar()
return item 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 bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
identicon.setBounds(0, 0, canvas.width, canvas.height) setBounds(0, 0, canvas.width, canvas.height)
identicon.draw(canvas) draw(canvas)
return bitmap return bitmap
} }
fun qrCode(address: BitmessageAddress?): Bitmap? { fun BitmessageAddress.qrCode(): Bitmap? {
if (address == null) {
return null
}
val link = StringBuilder() val link = StringBuilder()
link.append(Constants.BITMESSAGE_URL_SCHEMA) link.append(Constants.BITMESSAGE_URL_SCHEMA)
link.append(address.address) link.append(address)
if (address.alias != null) { if (alias != null) {
link.append("?label=").append(address.alias) link.append("?label=").append(alias)
} }
address.pubkey?.apply { pubkey?.apply {
link.append(if (address.alias == null) '?' else '&') link.append(if (alias == null) '?' else '&')
val pubkey = ByteArrayOutputStream() val pubkey = ByteArrayOutputStream()
writer().writeUnencrypted(pubkey) 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 val result: BitMatrix
try { try {
result = MultiFormatWriter().encode(link.toString(), result = MultiFormatWriter().encode(
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null) link.toString(),
BarcodeFormat.QR_CODE, QR_CODE_SIZE, QR_CODE_SIZE, null
)
} catch (e: WriterException) { } catch (e: WriterException) {
LOG.error(e.message, e) Drawables.LOG.error(e.message, e)
return null return null
} }
@ -99,4 +101,3 @@ object Drawables {
bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h) bitmap.setPixels(pixels, 0, QR_CODE_SIZE, 0, 0, w, h)
return bitmap 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.content.Context
import android.support.annotation.ColorInt 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.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon import com.mikepenz.iconics.typeface.IIcon
import ch.dissem.apps.abit.R /*
import ch.dissem.bitmessage.entity.valueobject.Label * Helper methods to help with translating the default labels, getting label colors and so on.
/**
* Helper class 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.INBOX -> ctx.getString(R.string.inbox)
Label.Type.DRAFT -> ctx.getString(R.string.draft) Label.Type.DRAFT -> ctx.getString(R.string.draft)
Label.Type.OUTBOX -> ctx.getString(R.string.outbox) Label.Type.OUTBOX -> ctx.getString(R.string.outbox)
@ -27,7 +25,7 @@ object Labels {
else -> alternative 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.INBOX -> GoogleMaterial.Icon.gmd_inbox
Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file Label.Type.DRAFT -> CommunityMaterial.Icon.cmd_file
Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up Label.Type.OUTBOX -> CommunityMaterial.Icon.cmd_inbox_arrow_up
@ -39,7 +37,6 @@ object Labels {
} }
@ColorInt @ColorInt
fun getColor(label: Label) = if (label.type == null) { fun Label.getColor(@ColorInt default: Int) = if (type == null) {
label.color color
} else 0xFF000000.toInt() } else default
}

View File

@ -19,28 +19,35 @@ object NetworkUtils {
fun enableNode(ctx: Context, ask: Boolean = true) { fun enableNode(ctx: Context, ask: Boolean = true) {
Preferences.setFullNodeActive(ctx, 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.isWifiOnly(ctx)) {
if (Preferences.isConnectionAllowed(ctx)) { if (Preferences.isConnectionAllowed(ctx)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { doStartBitmessageService(ctx)
scheduleNodeStart(ctx) MainActivity.updateNodeSwitch()
} else if (ask) {
askForConnection(ctx)
}
} else { } else {
doStartBitmessageService(ctx) doStartBitmessageService(ctx)
MainActivity.updateNodeSwitch() MainActivity.updateNodeSwitch()
} }
} else if (ask) { }
}
private fun askForConnection(ctx: Context) {
val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java) val dialogIntent = Intent(ctx, FullNodeDialogActivity::class.java)
if (ctx !is Activity) { if (ctx !is Activity) {
dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) dialogIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) ctx.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
} }
ctx.startActivity(dialogIntent) ctx.startActivity(dialogIntent)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
scheduleNodeStart(ctx)
}
} else {
doStartBitmessageService(ctx)
MainActivity.updateNodeSwitch()
}
} }
fun doStartBitmessageService(ctx: Context) { fun doStartBitmessageService(ctx: Context) {
@ -54,11 +61,17 @@ object NetworkUtils {
@RequiresApi(Build.VERSION_CODES.LOLLIPOP) @RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun scheduleNodeStart(ctx: Context) { fun scheduleNodeStart(ctx: Context) {
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java) val serviceComponent = ComponentName(ctx, StartupNodeOnWifiService::class.java)
val builder = JobInfo.Builder(0, serviceComponent) val builder = JobInfo.Builder(0, serviceComponent)
if (Preferences.isWifiOnly(ctx)) {
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
}
if (Preferences.requireCharging(ctx)) {
builder.setRequiresCharging(true)
}
builder.setBackoffCriteria(0L, JobInfo.BACKOFF_POLICY_LINEAR) builder.setBackoffCriteria(0L, JobInfo.BACKOFF_POLICY_LINEAR)
val jobScheduler = ctx.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler builder.setPersisted(true)
jobScheduler.schedule(builder.build()) jobScheduler.schedule(builder.build())
} }
} }

View File

@ -23,7 +23,7 @@ object PowStats {
powCount = preferences.getLong(PREFERENCE_POW_COUNT, 0L) 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) { fun addPow(ctx: Context, time: Long, target: ByteArray) {
@ -32,7 +32,7 @@ object PowStats {
synchronized(this) { synchronized(this) {
powCount++ powCount++
averagePowUnitTime = ( averagePowUnitTime = (
(BigInteger.valueOf(averagePowUnitTime) * powCountBefore + (BigInteger.valueOf(time) * TWO_POW_64 / targetBigInt)) / BigInteger.valueOf(powCount) (averagePowUnitTime * powCountBefore + (time * TWO_POW_64 / targetBigInt)) / powCount
).toLong() ).toLong()
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
@ -42,4 +42,7 @@ object PowStats {
.apply() .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 package ch.dissem.apps.abit.util
import android.content.Context import android.content.Context
import android.preference.PreferenceManager
import ch.dissem.apps.abit.R import ch.dissem.apps.abit.R
import ch.dissem.apps.abit.notification.ErrorNotification 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_FULL_NODE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_REQUEST_ACK 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_SYNC_TIMEOUT
import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE import ch.dissem.apps.abit.util.Constants.PREFERENCE_TRUSTED_NODE
import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY import ch.dissem.apps.abit.util.Constants.PREFERENCE_WIFI_ONLY
import org.jetbrains.anko.batteryManager
import org.jetbrains.anko.connectivityManager import org.jetbrains.anko.connectivityManager
import org.jetbrains.anko.defaultSharedPreferences
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.InetAddress import java.net.InetAddress
import android.os.BatteryManager
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
/** /**
* @author Christian Basler * @author Christian Basler
@ -77,50 +85,56 @@ object Preferences {
return 8444 return 8444
} }
fun getTimeoutInSeconds(ctx: Context): Long { fun getTimeoutInSeconds(ctx: Context): Long = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT)?.toLong() ?: 120
val preference = getPreference(ctx, PREFERENCE_SYNC_TIMEOUT) ?: return 120
return preference.toLong() 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? { fun isWifiOnly(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_WIFI_ONLY, true)
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 setWifiOnly(ctx: Context, status: Boolean) { fun setWifiOnly(ctx: Context, status: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) ctx.defaultSharedPreferences.edit()
preferences.edit().putBoolean(PREFERENCE_WIFI_ONLY, status).apply() .putBoolean(PREFERENCE_WIFI_ONLY, status)
.apply()
} }
fun isFullNodeActive(ctx: Context): Boolean { fun requireCharging(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUIRE_CHARGING, true)
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx)
return preferences.getBoolean(PREFERENCE_FULL_NODE, false) 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) { fun setFullNodeActive(ctx: Context, status: Boolean) {
val preferences = PreferenceManager.getDefaultSharedPreferences(ctx) ctx.defaultSharedPreferences.edit()
preferences.edit().putBoolean(PREFERENCE_FULL_NODE, status).apply() .putBoolean(PREFERENCE_FULL_NODE, status)
.apply()
} }
fun getExportDirectory(ctx: Context) = File(ctx.filesDir, "exports") fun getExportDirectory(ctx: Context) = File(ctx.filesDir, "exports")
fun requestAcknowledgements(ctx: Context): Boolean { fun requestAcknowledgements(ctx: Context) = ctx.defaultSharedPreferences.getBoolean(PREFERENCE_REQUEST_ACK, true)
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 cleanupExportDirectory(ctx: Context) { fun cleanupExportDirectory(ctx: Context) {
val exportDirectory = getExportDirectory(ctx) 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:id="@+id/label"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/label" /> android:hint="@string/label"
android:inputType="text" />
</android.support.design.widget.TextInputLayout> </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" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/bg_label"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:padding="2dp">
<com.mikepenz.iconics.view.IconicsImageView <com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/icon" android:id="@+id/icon"
android:layout_margin="1dp"
android:layout_width="16dp" android:layout_width="16dp"
android:layout_height="16dp" android:layout_height="16dp"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
app:ico_color="@android:color/black" app:iiv_color="@color/colorPrimaryDarkText"
app:ico_icon="cmd-label" /> app:iiv_icon="cmd-label" />
<TextView <TextView
android:id="@+id/label" android:id="@+id/label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="8dp"
android:paddingEnd="24dp"
tools:text="Label"
android:layout_alignParentTop="true" 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> </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_alignParentEnd="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
app:ico_color="@android:color/black" app:iiv_color="@android:color/black"
app:ico_icon="cmd-rss"/> app:iiv_icon="cmd-rss"/>
</RelativeLayout> </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="broadcasts">Broadcasts</string>
<string name="encoding_simple">einfach</string> <string name="encoding_simple">einfach</string>
<string name="encoding_extended">erweitert</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> </resources>

View File

@ -15,6 +15,7 @@
--> -->
<resources> <resources>
<dimen name="action_bar_offset">66dp</dimen>
<!-- Default screen margins, per the Android Design guidelines. --> <!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen> <dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_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="broadcasts">Broadcasts</string>
<string name="encoding_simple">simple</string> <string name="encoding_simple">simple</string>
<string name="encoding_extended">extended</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> </resources>

View File

@ -8,18 +8,6 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item> <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
</style> </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"> <style name="FixedDialog" parent="Theme.AppCompat.Light.Dialog.MinWidth">
<item name="windowNoTitle">false</item> <item name="windowNoTitle">false</item>
</style> </style>

View File

@ -1,58 +1,111 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<android.support.v7.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v7.preference.SwitchPreferenceCompat
<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:defaultValue="true"
android:key="wifi_only" android:key="wifi_only"
android:summary="@string/wifi_only_summary" android:summary="@string/wifi_only_summary"
android:title="@string/wifi_only" /> 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:defaultValue="true"
android:key="request_acknowledgements" android:key="request_acknowledgements"
android:summary="@string/request_acknowledgements_summary" android:summary="@string/request_acknowledgements_summary"
android:title="@string/request_acknowledgements" /> 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:inputType="textUri"
android:key="trusted_node" android:key="trusted_node"
android:summary="@string/trusted_node_summary" android:summary="@string/trusted_node_summary"
android:title="@string/trusted_node" /> android:title="@string/trusted_node" />
<android.support.v7.preference.EditTextPreference <EditTextPreference
android:defaultValue="120" android:defaultValue="120"
android:inputType="number" android:inputType="number"
android:key="sync_timeout" android:key="sync_timeout"
android:summary="@string/sync_timeout_summary" android:summary="@string/sync_timeout_summary"
android:title="@string/sync_timeout" /> android:title="@string/sync_timeout" />
<android.support.v7.preference.SwitchPreferenceCompat <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:dependency="trusted_node" android:dependency="trusted_node"
android:key="server_pow" android:key="server_pow"
android:summary="@string/server_pow_summary" android:summary="@string/server_pow_summary"
android:title="@string/server_pow" /> 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:key="about"
android:summary="@string/about_summary" android:summary="@string/about_summary"
android:title="@string/about" /> android:title="@string/about" />
<android.support.v7.preference.Preference <Preference
android:key="help_out" android:key="help_out"
android:summary="@string/help_out_summary" android:summary="@string/help_out_summary"
android:title="@string/help_out"> android:title="@string/help_out">
<intent <intent
android:action="android.intent.action.VIEW" android:action="android.intent.action.VIEW"
android:data="@string/help_out_link" /> android:data="@string/help_out_link" />
</android.support.v7.preference.Preference> </Preference>
<android.support.v7.preference.Preference </PreferenceScreen>
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>

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.AndroidLabelRepository
import ch.dissem.apps.abit.repository.AndroidMessageRepository import ch.dissem.apps.abit.repository.AndroidMessageRepository
import ch.dissem.apps.abit.repository.SqlHelper 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.BitmessageAddress
import ch.dissem.bitmessage.entity.ObjectMessage import ch.dissem.bitmessage.entity.ObjectMessage
import ch.dissem.bitmessage.entity.Plaintext import ch.dissem.bitmessage.entity.Plaintext
@ -69,7 +69,7 @@ class AndroidMessageRepositoryTest : TestBase() {
val labelRepo = AndroidLabelRepository(sqlHelper, RuntimeEnvironment.application) val labelRepo = AndroidLabelRepository(sqlHelper, RuntimeEnvironment.application)
repo = AndroidMessageRepository(sqlHelper) repo = AndroidMessageRepository(sqlHelper)
mockedInternalContext( mockedInternalContext(
cryptography = SpongyCryptography(), cryptography = BouncyCryptography(),
addressRepository = addressRepo, addressRepository = addressRepo,
labelRepository = labelRepo, labelRepository = labelRepo,
messageRepository = repo, messageRepository = repo,

View File

@ -59,7 +59,6 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
fun setUp() { fun setUp() {
RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME) RuntimeEnvironment.application.deleteDatabase(SqlHelper.DATABASE_NAME)
val sqlHelper = SqlHelper(RuntimeEnvironment.application) val sqlHelper = SqlHelper(RuntimeEnvironment.application)
addressRepo = AndroidAddressRepository(sqlHelper) addressRepo = AndroidAddressRepository(sqlHelper)
messageRepo = AndroidMessageRepository(sqlHelper) messageRepo = AndroidMessageRepository(sqlHelper)
repo = AndroidProofOfWorkRepository(sqlHelper) repo = AndroidProofOfWorkRepository(sqlHelper)
@ -94,12 +93,14 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
messageRepo.save(plaintext) messageRepo.save(plaintext)
plaintext.ackMessage!!.let { ackMessage -> plaintext.ackMessage!!.let { ackMessage ->
initialHash2 = cryptography().getInitialHash(ackMessage) initialHash2 = cryptography().getInitialHash(ackMessage)
repo.putObject(ProofOfWorkRepository.Item( repo.putObject(
ProofOfWorkRepository.Item(
objectMessage = ackMessage, objectMessage = ackMessage,
nonceTrialsPerByte = 1000, extraBytes = 1000, nonceTrialsPerByte = 1000, extraBytes = 1000,
expirationTime = UnixTime.now + 10 * UnixTime.MINUTE, expirationTime = UnixTime.now + 10 * UnixTime.MINUTE,
message = plaintext message = plaintext
)) )
)
} }
} }
@ -132,13 +133,15 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
.build() .build()
messageRepo.save(plaintext) messageRepo.save(plaintext)
plaintext.ackMessage!!.let { ackMessage -> plaintext.ackMessage!!.let { ackMessage ->
repo.putObject(ProofOfWorkRepository.Item( repo.putObject(
ProofOfWorkRepository.Item(
objectMessage = ackMessage, objectMessage = ackMessage,
nonceTrialsPerByte = 1000, nonceTrialsPerByte = 1000,
extraBytes = 1000, extraBytes = 1000,
expirationTime = UnixTime.now + 10 * UnixTime.MINUTE, expirationTime = UnixTime.now + 10 * UnixTime.MINUTE,
message = plaintext message = plaintext
)) )
)
} }
assertThat(repo.getItems().size, `is`(sizeBefore + 1)) assertThat(repo.getItems().size, `is`(sizeBefore + 1))
} }
@ -147,7 +150,10 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
fun `ensure item can be retrieved`() { fun `ensure item can be retrieved`() {
val item = repo.getItem(initialHash1) val item = repo.getItem(initialHash1)
assertThat(item, notNullValue()) 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.nonceTrialsPerByte, `is`(1000L))
assertThat(item.extraBytes, `is`(1000L)) assertThat(item.extraBytes, `is`(1000L))
} }
@ -156,7 +162,10 @@ class AndroidProofOfWorkRepositoryTest : TestBase() {
fun `ensure ack item can be retrieved`() { fun `ensure ack item can be retrieved`() {
val item = repo.getItem(initialHash2) val item = repo.getItem(initialHash2)
assertThat(item, notNullValue()) 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.nonceTrialsPerByte, `is`(1000L))
assertThat(item.extraBytes, `is`(1000L)) assertThat(item.extraBytes, `is`(1000L))
assertThat(item.expirationTime, not<Number>(0)) 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.BitmessageContext
import ch.dissem.bitmessage.InternalContext import ch.dissem.bitmessage.InternalContext
import ch.dissem.bitmessage.Preferences 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.BitmessageAddress
import ch.dissem.bitmessage.entity.ObjectMessage import ch.dissem.bitmessage.entity.ObjectMessage
import ch.dissem.bitmessage.entity.payload.V4Pubkey import ch.dissem.bitmessage.entity.payload.V4Pubkey
@ -41,7 +41,7 @@ open class TestBase {
@JvmStatic @JvmStatic
fun init() { fun init() {
mockedInternalContext( mockedInternalContext(
cryptography = SpongyCryptography(), cryptography = BouncyCryptography(),
proofOfWorkEngine = MultiThreadedPOWEngine() proofOfWorkEngine = MultiThreadedPOWEngine()
) )
} }

View File

@ -1,12 +1,12 @@
buildscript { buildscript {
ext.kotlin_version = '1.2.21' ext.kotlin_version = '1.2.41'
ext.anko_version = '0.10.4' ext.anko_version = '0.10.4'
repositories { repositories {
jcenter() jcenter()
google() google()
} }
dependencies { 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 "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0' 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 distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists 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