diff --git a/app/build.gradle b/app/build.gradle index 1d45f11..ce56baa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc7f881..a9d4f55 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + @@ -198,6 +199,10 @@ android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE" /> + + () 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() 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() 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() } } - 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?) { diff --git a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt index ea9828c..a900abf 100644 --- a/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt +++ b/app/src/main/java/ch/dissem/apps/abit/ComposeMessageActivity.kt @@ -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 } diff --git a/app/src/main/java/ch/dissem/apps/abit/ConversationDetailFragment.kt b/app/src/main/java/ch/dissem/apps/abit/ConversationDetailFragment.kt new file mode 100644 index 0000000..f611ff1 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ConversationDetailFragment.kt @@ -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" + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/ConversationListFragment.kt b/app/src/main/java/ch/dissem/apps/abit/ConversationListFragment.kt new file mode 100644 index 0000000..4e9439c --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/ConversationListFragment.kt @@ -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