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