From 1c226a6a5b89f2341d9d443162b041d3c7231493 Mon Sep 17 00:00:00 2001 From: Christian Basler Date: Wed, 21 Sep 2016 23:46:57 +0200 Subject: [PATCH] Added swipe actions for messages. - there is a minor layout problem on pre-Lollipop devices --- .../apps/abit/AbstractItemListFragment.java | 4 +- .../dissem/apps/abit/AddressListFragment.java | 2 +- .../java/ch/dissem/apps/abit/ListHolder.java | 28 ++ .../ch/dissem/apps/abit/MainActivity.java | 2 +- .../apps/abit/MessageDetailFragment.java | 2 +- .../dissem/apps/abit/MessageListFragment.java | 177 ++++++++--- .../abit/adapter/SwipeableMessageAdapter.java | 282 ++++++++++++++++++ .../abit/synchronization/SyncAdapter.java | 110 +++---- .../ch/dissem/apps/abit/util/Preferences.java | 11 +- .../res/drawable/bg_item_normal_state.xml | 21 ++ .../drawable/bg_item_swiping_active_state.xml | 21 ++ .../res/drawable/bg_item_swiping_state.xml | 21 ++ .../main/res/drawable/bg_swipe_item_left.xml | 27 ++ .../res/drawable/bg_swipe_item_neutral.xml | 19 ++ .../main/res/drawable/bg_swipe_item_right.xml | 25 ++ .../res/drawable/ic_item_swipe_archive.xml | 9 + .../main/res/drawable/ic_item_swipe_trash.xml | 9 + app/src/main/res/drawable/list_divider_h.xml | 22 ++ .../main/res/layout/fragment_message_list.xml | 40 +-- app/src/main/res/layout/message_row.xml | 135 +++++---- app/src/main/res/values/colors.xml | 8 + 21 files changed, 790 insertions(+), 185 deletions(-) create mode 100644 app/src/main/java/ch/dissem/apps/abit/ListHolder.java create mode 100644 app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java create mode 100644 app/src/main/res/drawable/bg_item_normal_state.xml create mode 100644 app/src/main/res/drawable/bg_item_swiping_active_state.xml create mode 100644 app/src/main/res/drawable/bg_item_swiping_state.xml create mode 100644 app/src/main/res/drawable/bg_swipe_item_left.xml create mode 100644 app/src/main/res/drawable/bg_swipe_item_neutral.xml create mode 100644 app/src/main/res/drawable/bg_swipe_item_right.xml create mode 100644 app/src/main/res/drawable/ic_item_swipe_archive.xml create mode 100644 app/src/main/res/drawable/ic_item_swipe_trash.xml create mode 100644 app/src/main/res/drawable/list_divider_h.xml diff --git a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java index 5895277..3cb1e07 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AbstractItemListFragment.java @@ -28,7 +28,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label; /** * @author Christian Basler */ -public abstract class AbstractItemListFragment extends ListFragment { +public abstract class AbstractItemListFragment extends ListFragment implements ListHolder { /** * The serialization (saved instance state) Bundle key representing the * activated item position. Only used on tablets. @@ -55,8 +55,6 @@ public abstract class AbstractItemListFragment extends ListFragment { private int activatedPosition = ListView.INVALID_POSITION; private boolean activateOnItemClick; - abstract void updateList(Label label); - @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); diff --git a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java index 9f0dc48..66a52aa 100644 --- a/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/AddressListFragment.java @@ -166,7 +166,7 @@ public class AddressListFragment extends AbstractItemListFragment listFragment) { + private void changeList(F listFragment) { getSupportFragmentManager() .beginTransaction() .replace(R.id.item_list, listFragment) diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java index 6afb2d0..bce2795 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageDetailFragment.java @@ -204,7 +204,7 @@ public class MessageDetailFragment extends Fragment { } } - private boolean isInTrash(Plaintext item) { + public static boolean isInTrash(Plaintext item) { for (Label label : item.getLabels()) { if (label.getType() == Label.Type.TRASH) { return true; diff --git a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java index 71c8b20..bf79b0d 100644 --- a/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java +++ b/app/src/main/java/ch/dissem/apps/abit/MessageListFragment.java @@ -17,19 +17,29 @@ package ch.dissem.apps.abit; import android.content.Intent; -import android.graphics.Typeface; import android.os.Bundle; import android.support.design.widget.FloatingActionButton; +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.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; +import com.h6ah4i.android.widget.advrecyclerview.animator.GeneralItemAnimator; +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 java.util.List; + +import ch.dissem.apps.abit.adapter.SwipeableMessageAdapter; import ch.dissem.apps.abit.listener.ActionBarListener; import ch.dissem.apps.abit.listener.ListSelectionListener; import ch.dissem.apps.abit.service.Singleton; @@ -37,6 +47,8 @@ import ch.dissem.bitmessage.entity.Plaintext; import ch.dissem.bitmessage.entity.valueobject.Label; import ch.dissem.bitmessage.ports.MessageRepository; +import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash; + /** * A list fragment representing a list of Messages. This fragment * also supports tablet devices by allowing list items to be given an @@ -46,10 +58,19 @@ import ch.dissem.bitmessage.ports.MessageRepository; * Activities containing this fragment MUST implement the {@link ListSelectionListener} * interface. */ -public class MessageListFragment extends AbstractItemListFragment { +public class MessageListFragment extends Fragment implements ListHolder { + + private RecyclerView recyclerView; + private RecyclerView.LayoutManager layoutManager; + private SwipeableMessageAdapter adapter; + private RecyclerView.Adapter wrappedAdapter; + private RecyclerViewSwipeManager recyclerViewSwipeManager; + private RecyclerViewTouchActionGuardManager recyclerViewTouchActionGuardManager; private Label currentLabel; private MenuItem emptyTrashMenuItem; + private MessageRepository messageRepo; + private List<Plaintext> messages; /** * Mandatory empty constructor for the fragment manager to instantiate the @@ -68,8 +89,10 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { @Override public void onResume() { super.onResume(); + MainActivity activity = (MainActivity) getActivity(); + messageRepo = Singleton.getMessageRepository(activity); - doUpdateList(((MainActivity) getActivity()).getSelectedLabel()); + doUpdateList(activity.getSelectedLabel()); } @Override @@ -82,34 +105,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { } private void doUpdateList(Label label) { - setListAdapter(new ArrayAdapter<Plaintext>( - getActivity(), - android.R.layout.simple_list_item_activated_1, - android.R.id.text1, - Singleton.getMessageRepository(getContext()).findMessages(label)) { - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - LayoutInflater inflater = LayoutInflater.from(getContext()); - convertView = inflater.inflate(R.layout.message_row, null, false); - } - Plaintext item = getItem(position); - ((ImageView) convertView.findViewById(R.id.avatar)).setImageDrawable(new Identicon(item.getFrom())); - TextView sender = (TextView) convertView.findViewById(R.id.sender); - sender.setText(item.getFrom().toString()); - TextView subject = (TextView) convertView.findViewById(R.id.subject); - subject.setText(item.getSubject()); - ((TextView) convertView.findViewById(R.id.text)).setText(item.getText()); - if (item.isUnread()) { - sender.setTypeface(Typeface.DEFAULT_BOLD); - subject.setTypeface(Typeface.DEFAULT_BOLD); - } else { - sender.setTypeface(Typeface.DEFAULT); - subject.setTypeface(Typeface.DEFAULT); - } - return convertView; - } - }); + messages = Singleton.getMessageRepository(getContext()).findMessages(label); if (getActivity() instanceof ActionBarListener) { if (label != null) { ((ActionBarListener) getActivity()).updateTitle(label.toString()); @@ -120,26 +116,127 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { if (emptyTrashMenuItem != null) { emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH); } + adapter.setData(label, messages); + adapter.notifyDataSetChanged(); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle + savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_message_list, container, false); + recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view); + layoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false); // Show the dummy content as text in a TextView. - FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id.fab_compose_message); + FloatingActionButton fab = (FloatingActionButton) rootView.findViewById(R.id + .fab_compose_message); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { - Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class); - intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(getActivity())); + Intent intent = new Intent(getActivity().getApplicationContext(), + ComposeMessageActivity.class); + intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity + (getActivity())); startActivity(intent); } }); + // touch guard manager (this class is required to suppress scrolling while swipe-dismiss + // animation is running) + recyclerViewTouchActionGuardManager = new RecyclerViewTouchActionGuardManager(); + recyclerViewTouchActionGuardManager.setInterceptVerticalScrollingWhileAnimationRunning + (true); + recyclerViewTouchActionGuardManager.setEnabled(true); + + // swipe manager + recyclerViewSwipeManager = new RecyclerViewSwipeManager(); + + //adapter + adapter = new SwipeableMessageAdapter(); + adapter.setEventListener(new SwipeableMessageAdapter.EventListener() { + @Override + public void onItemDeleted(Plaintext item) { + if (isInTrash(item)) { + messageRepo.remove(item); + } else { + item.getLabels().clear(); + item.addLabels(messageRepo.getLabels(Label.Type.TRASH)); + messageRepo.save(item); + } + } + + @Override + public void onItemArchived(Plaintext item) { + item.getLabels().clear(); + messageRepo.save(item); + } + + @Override + public void onItemViewClicked(View v, boolean pinned) { + int position = recyclerView.getChildAdapterPosition(v); + if (position != RecyclerView.NO_POSITION) { + Plaintext item = adapter.getItem(position); + ((MainActivity) getActivity()).onItemSelected(item); + } + } + }); + + // wrap for swiping + wrappedAdapter = recyclerViewSwipeManager.createWrappedAdapter(adapter); + + final GeneralItemAnimator animator = new 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.setSupportsChangeAnimations(false); + + recyclerView.setLayoutManager(layoutManager); + recyclerView.setAdapter(wrappedAdapter); // requires *wrapped* adapter + recyclerView.setItemAnimator(animator); + + recyclerView.addItemDecoration(new SimpleListDividerDecorator( + ContextCompat.getDrawable(getContext(), 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 + recyclerViewTouchActionGuardManager.attachRecyclerView(recyclerView); + recyclerViewSwipeManager.attachRecyclerView(recyclerView); + return rootView; } + @Override + public void onDestroyView() { + if (recyclerViewSwipeManager != null) { + recyclerViewSwipeManager.release(); + recyclerViewSwipeManager = null; + } + + if (recyclerViewTouchActionGuardManager != null) { + recyclerViewTouchActionGuardManager.release(); + recyclerViewTouchActionGuardManager = null; + } + + if (recyclerView != null) { + recyclerView.setItemAnimator(null); + recyclerView.setAdapter(null); + recyclerView = null; + } + + if (wrappedAdapter != null) { + WrapperAdapterUtils.releaseAll(wrappedAdapter); + wrappedAdapter = null; + } + adapter = null; + layoutManager = null; + + super.onDestroyView(); + } + @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.message_list, menu); @@ -164,4 +261,8 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> { } } + @Override + public void setActivateOnItemClick(boolean activateOnItemClick) { + // TODO + } } diff --git a/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java new file mode 100644 index 0000000..0958fc0 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/adapter/SwipeableMessageAdapter.java @@ -0,0 +1,282 @@ +package ch.dissem.apps.abit.adapter; + +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 com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemConstants; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.action.SwipeResultAction; +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.Collections; +import java.util.List; + +import ch.dissem.apps.abit.Identicon; +import ch.dissem.apps.abit.R; +import ch.dissem.bitmessage.entity.Plaintext; +import ch.dissem.bitmessage.entity.valueobject.Label; + +/** + * @author Christian Basler + */ +public class SwipeableMessageAdapter + extends RecyclerView.Adapter<SwipeableMessageAdapter.MyViewHolder> + implements SwipeableItemAdapter<SwipeableMessageAdapter.MyViewHolder>, SwipeableItemConstants { + + private List<Plaintext> data = Collections.emptyList(); + private EventListener eventListener; + private View.OnClickListener itemViewOnClickListener; + private View.OnClickListener swipeableViewContainerOnClickListener; + + private Label label; + + public interface EventListener { + void onItemDeleted(Plaintext item); + + void onItemArchived(Plaintext item); + + void onItemViewClicked(View v, boolean pinned); + } + + public static class MyViewHolder extends AbstractSwipeableItemViewHolder { + public FrameLayout container; + public final ImageView avatar; + public final TextView sender; + public final TextView subject; + public final TextView extract; + + public MyViewHolder(View v) { + super(v); + container = (FrameLayout) v.findViewById(R.id.container); + avatar = (ImageView) v.findViewById(R.id.avatar); + sender = (TextView) v.findViewById(R.id.sender); + subject = (TextView) v.findViewById(R.id.subject); + extract = (TextView) v.findViewById(R.id.text); + } + + @Override + public View getSwipeableContainerView() { + return container; + } + } + + public SwipeableMessageAdapter() { + itemViewOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + onItemViewClick(v); + } + }; + swipeableViewContainerOnClickListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + onSwipeableViewContainerClick(v); + } + }; + + // SwipeableItemAdapter requires stable ID, and also + // have to implement the getItemId() method appropriately. + setHasStableIds(true); + } + + public void setData(Label label, List<Plaintext> data) { + this.label = label; + this.data = data; + } + + private void onItemViewClick(View v) { + if (eventListener != null) { + eventListener.onItemViewClicked(v, true); // pinned + } + } + + private void onSwipeableViewContainerClick(View v) { + if (eventListener != null) { + eventListener.onItemViewClicked( + RecyclerViewAdapterUtils.getParentViewHolderItemView(v), false); // not pinned + } + } + + public Plaintext getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return (long) data.get(position).getId(); + } + + @Override + public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + final LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + final View v = inflater.inflate(R.layout.message_row, parent, false); + return new MyViewHolder(v); + } + + @Override + public void onBindViewHolder(MyViewHolder holder, int position) { + final Plaintext item = data.get(position); + + // set listeners + // (if the item is *pinned*, click event comes to the itemView) + holder.itemView.setOnClickListener(itemViewOnClickListener); + // (if the item is *not pinned*, click event comes to the container) + holder.container.setOnClickListener(swipeableViewContainerOnClickListener); + + // set data + holder.avatar.setImageDrawable(new Identicon(item.getFrom())); + holder.sender.setText(item.getFrom().toString()); + holder.subject.setText(item.getSubject()); + holder.extract.setText(item.getText()); + if (item.isUnread()) { + holder.sender.setTypeface(Typeface.DEFAULT_BOLD); + holder.subject.setTypeface(Typeface.DEFAULT_BOLD); + } else { + holder.sender.setTypeface(Typeface.DEFAULT); + holder.subject.setTypeface(Typeface.DEFAULT); + } + } + + @Override + public int getItemCount() { + return data.size(); + } + + @Override + public int onGetSwipeReactionType(MyViewHolder holder, int position, int x, int y) { + if (label == null) { + return REACTION_CAN_NOT_SWIPE_BOTH_H_WITH_RUBBER_BAND_EFFECT; + } + if (label.getType() == Label.Type.TRASH) { + return REACTION_CAN_SWIPE_LEFT | REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT; + } + return REACTION_CAN_SWIPE_BOTH_H; + } + + @Override + public void onSetSwipeBackground(MyViewHolder holder, int position, int type) { + int bgRes = 0; + if (label == null) { + bgRes = R.drawable.bg_swipe_item_neutral; + } else { + switch (type) { + case DRAWABLE_SWIPE_NEUTRAL_BACKGROUND: + bgRes = R.drawable.bg_swipe_item_neutral; + break; + case DRAWABLE_SWIPE_LEFT_BACKGROUND: + bgRes = R.drawable.bg_swipe_item_left; + break; + case DRAWABLE_SWIPE_RIGHT_BACKGROUND: + if (label.getType() == Label.Type.TRASH) { + bgRes = R.drawable.bg_swipe_item_neutral; + } else { + bgRes = R.drawable.bg_swipe_item_right; + } + break; + } + } + holder.itemView.setBackgroundResource(bgRes); + } + + @Override + public SwipeResultAction onSwipeItem(MyViewHolder holder, final int position, int result) { + switch (result) { + // swipe right + case RESULT_SWIPED_RIGHT: + return new SwipeRightResultAction(this, position); + case RESULT_SWIPED_LEFT: + return new SwipeLeftResultAction(this, position); + // other --- do nothing + case RESULT_CANCELED: + default: + return null; + } + } + + public void setEventListener(EventListener eventListener) { + this.eventListener = eventListener; + } + + private static class SwipeLeftResultAction extends SwipeResultActionMoveToSwipedDirection { + private SwipeableMessageAdapter adapter; + private final int position; + private final Plaintext item; + + SwipeLeftResultAction(SwipeableMessageAdapter adapter, int position) { + this.adapter = adapter; + this.position = position; + this.item = adapter.data.get(position); + } + + @Override + protected void onPerformAction() { + super.onPerformAction(); + + adapter.data.remove(position); + adapter.notifyItemRemoved(position); + } + + @Override + protected void onSlideAnimationEnd() { + super.onSlideAnimationEnd(); + + if (adapter.eventListener != null) { + adapter.eventListener.onItemDeleted(item); + } + } + + @Override + protected void onCleanUp() { + super.onCleanUp(); + // clear the references + adapter = null; + } + } + + private static class SwipeRightResultAction extends SwipeResultActionRemoveItem { + private SwipeableMessageAdapter adapter; + private final int position; + private final Plaintext item; + + SwipeRightResultAction(SwipeableMessageAdapter adapter, int position) { + this.adapter = adapter; + this.position = position; + this.item = adapter.data.get(position); + } + + @Override + protected void onPerformAction() { + super.onPerformAction(); + + adapter.data.remove(position); + adapter.data.remove(position); + adapter.notifyItemRemoved(position); + } + + @Override + protected void onSlideAnimationEnd() { + super.onSlideAnimationEnd(); + + if (adapter.eventListener != null) { + adapter.eventListener.onItemArchived(item); + } + } + + @Override + protected void onCleanUp() { + super.onCleanUp(); + // clear the references + adapter = null; + } + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java index a1982c8..dd8f984 100644 --- a/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java +++ b/app/src/main/java/ch/dissem/apps/abit/synchronization/SyncAdapter.java @@ -28,6 +28,7 @@ import android.os.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.util.List; import ch.dissem.apps.abit.service.Singleton; @@ -35,6 +36,7 @@ import ch.dissem.apps.abit.util.Preferences; import ch.dissem.bitmessage.BitmessageContext; import ch.dissem.bitmessage.entity.BitmessageAddress; import ch.dissem.bitmessage.entity.CustomMessage; +import ch.dissem.bitmessage.exception.DecryptionFailedException; import ch.dissem.bitmessage.extensions.CryptoCustomMessage; import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest; import ch.dissem.bitmessage.ports.ProofOfWorkRepository; @@ -68,18 +70,24 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { - if (account.equals(Authenticator.ACCOUNT_SYNC)) { - if (Preferences.isConnectionAllowed(getContext())) { - syncData(); + try { + if (account.equals(Authenticator.ACCOUNT_SYNC)) { + if (Preferences.isConnectionAllowed(getContext())) { + syncData(); + } + } else if (account.equals(Authenticator.ACCOUNT_POW)) { + syncPOW(); + } else { + syncResult.stats.numAuthExceptions++; } - } else if (account.equals(Authenticator.ACCOUNT_POW)) { - syncPOW(); - } else { - throw new RuntimeException("Unknown " + account); + } catch (IOException e) { + syncResult.stats.numIoExceptions++; + } catch (DecryptionFailedException e) { + syncResult.stats.numAuthExceptions++; } } - private void syncData() { + private void syncData() throws IOException { // If the Bitmessage context acts as a full node, synchronization isn't necessary if (bmc.isRunning()) { LOG.info("Synchronization skipped, Abit is acting as a full node"); @@ -87,61 +95,53 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter { } LOG.info("Synchronizing Bitmessage"); - try { - LOG.info("Synchronization started"); - bmc.synchronize( - Preferences.getTrustedNode(getContext()), - Preferences.getTrustedNodePort(getContext()), - Preferences.getTimeoutInSeconds(getContext()), - true); - LOG.info("Synchronization finished"); - } catch (RuntimeException e) { - LOG.error(e.getMessage(), e); - } + LOG.info("Synchronization started"); + bmc.synchronize( + Preferences.getTrustedNode(getContext()), + Preferences.getTrustedNodePort(getContext()), + Preferences.getTimeoutInSeconds(getContext()), + true); + LOG.info("Synchronization finished"); } - private void syncPOW() { + private void syncPOW() throws IOException, DecryptionFailedException { // If the Bitmessage context acts as a full node, synchronization isn't necessary LOG.info("Looking for completed POW"); - try { - BitmessageAddress identity = Singleton.getIdentity(getContext()); - byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey(); - byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey()); - ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity); - ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext()); - List<byte[]> items = powRepo.getItems(); - for (byte[] initialHash : items) { - ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); - byte[] target = cryptography().getProofOfWorkTarget(item.object, item - .nonceTrialsPerByte, item.extraBytes); - CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>( - new ProofOfWorkRequest(identity, initialHash, CALCULATE, target)); - cryptoMsg.signAndEncrypt(identity, signingKey); - CustomMessage response = bmc.send( - Preferences.getTrustedNode(getContext()), - Preferences.getTrustedNodePort(getContext()), - cryptoMsg - ); - if (response.isError()) { - LOG.error("Server responded with error: " + new String(response.getData(), - "UTF-8")); - } else { - ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read( - response, reader).decrypt(privateKey); - if (decryptedResponse.getRequest() == COMPLETE) { - bmc.internals().getProofOfWorkService().onNonceCalculated( - initialHash, decryptedResponse.getData()); - } + BitmessageAddress identity = Singleton.getIdentity(getContext()); + byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey(); + byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey()); + ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity); + ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext()); + List<byte[]> items = powRepo.getItems(); + for (byte[] initialHash : items) { + ProofOfWorkRepository.Item item = powRepo.getItem(initialHash); + byte[] target = cryptography().getProofOfWorkTarget(item.object, item + .nonceTrialsPerByte, item.extraBytes); + CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>( + new ProofOfWorkRequest(identity, initialHash, CALCULATE, target)); + cryptoMsg.signAndEncrypt(identity, signingKey); + CustomMessage response = bmc.send( + Preferences.getTrustedNode(getContext()), + Preferences.getTrustedNodePort(getContext()), + cryptoMsg + ); + if (response.isError()) { + LOG.error("Server responded with error: " + new String(response.getData(), + "UTF-8")); + } else { + ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read( + response, reader).decrypt(privateKey); + if (decryptedResponse.getRequest() == COMPLETE) { + bmc.internals().getProofOfWorkService().onNonceCalculated( + initialHash, decryptedResponse.getData()); } } - if (items.size() == 0) { - stopPowSync(getContext()); - } - LOG.info("Synchronization finished"); - } catch (Exception e) { - LOG.error(e.getMessage(), e); } + if (items.size() == 0) { + stopPowSync(getContext()); + } + LOG.info("Synchronization finished"); } public static void startSync(Context ctx) { diff --git a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java index b7f3d97..66923cd 100644 --- a/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java +++ b/app/src/main/java/ch/dissem/apps/abit/util/Preferences.java @@ -23,6 +23,7 @@ import android.preference.PreferenceManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; @@ -49,7 +50,7 @@ public class Preferences { * Warning, this method might do a network call and therefore can't be called from * the UI thread. */ - public static InetAddress getTrustedNode(Context ctx) { + public static InetAddress getTrustedNode(Context ctx) throws IOException { String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE); if (trustedNode == null) return null; trustedNode = trustedNode.trim(); @@ -59,15 +60,7 @@ public class Preferences { int index = trustedNode.lastIndexOf(':'); trustedNode = trustedNode.substring(0, index); } - try { return InetAddress.getByName(trustedNode); - } catch (UnknownHostException e) { - new ErrorNotification(ctx) - .setError(R.string.error_invalid_sync_host) - .show(); - LOG.error(e.getMessage(), e); - return null; - } } public static int getTrustedNodePort(Context ctx) { diff --git a/app/src/main/res/drawable/bg_item_normal_state.xml b/app/src/main/res/drawable/bg_item_normal_state.xml new file mode 100644 index 0000000..be3568f --- /dev/null +++ b/app/src/main/res/drawable/bg_item_normal_state.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + 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. +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_item_normal_state"/> + </item> +</selector> diff --git a/app/src/main/res/drawable/bg_item_swiping_active_state.xml b/app/src/main/res/drawable/bg_item_swiping_active_state.xml new file mode 100644 index 0000000..b90dbeb --- /dev/null +++ b/app/src/main/res/drawable/bg_item_swiping_active_state.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + 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. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_item_swiping_active_state"/> + </item> +</layer-list> diff --git a/app/src/main/res/drawable/bg_item_swiping_state.xml b/app/src/main/res/drawable/bg_item_swiping_state.xml new file mode 100644 index 0000000..00ca171 --- /dev/null +++ b/app/src/main/res/drawable/bg_item_swiping_state.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + 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. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_item_swiping_state"/> + </item> +</layer-list> diff --git a/app/src/main/res/drawable/bg_swipe_item_left.xml b/app/src/main/res/drawable/bg_swipe_item_left.xml new file mode 100644 index 0000000..ef32c48 --- /dev/null +++ b/app/src/main/res/drawable/bg_swipe_item_left.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + 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. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_swipe_item_trash"/> + </item> + <item + android:drawable="@drawable/ic_item_swipe_trash" + android:gravity="right|center_vertical" + android:width="24dp" + android:height="24dp" + android:right="16dp"/> +</layer-list> diff --git a/app/src/main/res/drawable/bg_swipe_item_neutral.xml b/app/src/main/res/drawable/bg_swipe_item_neutral.xml new file mode 100644 index 0000000..890788c --- /dev/null +++ b/app/src/main/res/drawable/bg_swipe_item_neutral.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + 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. +--> +<color + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/bg_swipe_item_neutral"/> diff --git a/app/src/main/res/drawable/bg_swipe_item_right.xml b/app/src/main/res/drawable/bg_swipe_item_right.xml new file mode 100644 index 0000000..72bd0b0 --- /dev/null +++ b/app/src/main/res/drawable/bg_swipe_item_right.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + 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. +--> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item> + <color android:color="@color/bg_swipe_item_archive"/> + </item> + <item + android:drawable="@drawable/ic_item_swipe_archive" + android:gravity="left|center_vertical" + android:left="16dp"/> +</layer-list> diff --git a/app/src/main/res/drawable/ic_item_swipe_archive.xml b/app/src/main/res/drawable/ic_item_swipe_archive.xml new file mode 100644 index 0000000..ce5bf8a --- /dev/null +++ b/app/src/main/res/drawable/ic_item_swipe_archive.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M19,3L4.99,3c-1.11,0 -1.98,0.89 -1.98,2L3,19c0,1.1 0.88,2 1.99,2L19,21c1.1,0 2,-0.9 2,-2L21,5c0,-1.11 -0.9,-2 -2,-2zM19,15h-4c0,1.66 -1.35,3 -3,3s-3,-1.34 -3,-3L4.99,15L4.99,5L19,5v10z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_item_swipe_trash.xml b/app/src/main/res/drawable/ic_item_swipe_trash.xml new file mode 100644 index 0000000..f9213d2 --- /dev/null +++ b/app/src/main/res/drawable/ic_item_swipe_trash.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/> +</vector> diff --git a/app/src/main/res/drawable/list_divider_h.xml b/app/src/main/res/drawable/list_divider_h.xml new file mode 100644 index 0000000..fd1a2c0 --- /dev/null +++ b/app/src/main/res/drawable/list_divider_h.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2015 Haruki Hasegawa + + 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. +--> +<shape + android:shape="rectangle" + xmlns:android="http://schemas.android.com/apk/res/android"> + <size android:height="1px"/> + <solid android:color="@color/divider"/> +</shape> diff --git a/app/src/main/res/layout/fragment_message_list.xml b/app/src/main/res/layout/fragment_message_list.xml index 5aa714b..286612c 100644 --- a/app/src/main/res/layout/fragment_message_list.xml +++ b/app/src/main/res/layout/fragment_message_list.xml @@ -4,26 +4,26 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - <ListView - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:id="@id/android:list" + <android.support.v7.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_alignParentBottom="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:clipToPadding="false" - android:paddingBottom="88dp" - android:clipToPadding="false" - android:scrollbarStyle="outsideOverlay" - - android:layout_alignParentTop="true" - android:layout_alignParentStart="true" - android:layout_alignParentBottom="true"/> + android:paddingBottom="88dp" + android:scrollbarStyle="outsideOverlay" + android:scrollbars="vertical"/> <android.support.design.widget.FloatingActionButton - android:id="@+id/fab_compose_message" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:src="@drawable/ic_action_compose_message" - app:elevation="8dp" - android:layout_alignParentBottom="true" - android:layout_alignParentEnd="true" - android:layout_margin="16dp"/> -</RelativeLayout> \ No newline at end of file + android:id="@+id/fab_compose_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentEnd="true" + android:layout_margin="16dp" + android:src="@drawable/ic_action_compose_message" + app:elevation="8dp"/> +</RelativeLayout> diff --git a/app/src/main/res/layout/message_row.xml b/app/src/main/res/layout/message_row.xml index b80d6f1..5e9d6a7 100644 --- a/app/src/main/res/layout/message_row.xml +++ b/app/src/main/res/layout/message_row.xml @@ -15,64 +15,85 @@ ~ limitations under the License. --> -<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"> - <ImageView - android:id="@+id/avatar" - android:layout_width="40dp" - android:layout_height="40dp" - android:layout_alignParentTop="true" - android:layout_alignParentStart="true" - android:src="@color/colorAccent" - android:layout_margin="16dp" - tools:ignore="ContentDescription"/> +<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"> - <TextView - android:id="@+id/sender" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Sender" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceMedium" - android:layout_alignTop="@+id/avatar" - android:layout_toEndOf="@+id/avatar" - android:layout_marginTop="-5dp" - android:paddingTop="0dp" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingBottom="0dp" - android:textStyle="bold" - /> + <FrameLayout + android:id="@+id/container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clickable="true" + android:foreground="?attr/selectableItemBackground" + android:background="@drawable/bg_item_normal_state" + tools:ignore="UselessParent"> - <TextView - android:id="@+id/subject" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Subject" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:layout_below="@+id/sender" - android:layout_toEndOf="@+id/avatar"/> + <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"> - <TextView - android:id="@+id/text" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - tools:text="Text" - android:lines="1" - android:ellipsize="end" - android:textAppearance="?android:attr/textAppearanceSmall" - android:gravity="center_vertical" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingBottom="8dp" - android:layout_below="@+id/subject" - android:layout_toEndOf="@+id/avatar"/> + <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/colorAccent" + tools:ignore="ContentDescription"/> -</RelativeLayout> \ No newline at end of file + <TextView + android:id="@+id/sender" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@+id/avatar" + android:layout_marginTop="-5dp" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingBottom="0dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="0dp" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textStyle="bold" + tools:text="Sender" + /> + + <TextView + android:id="@+id/subject" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/sender" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:lines="1" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Subject"/> + + <TextView + android:id="@+id/text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/subject" + android:layout_toEndOf="@+id/avatar" + android:ellipsize="end" + android:gravity="center_vertical" + android:lines="1" + android:paddingBottom="8dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:textAppearance="?android:attr/textAppearanceSmall" + tools:text="Text"/> + + </RelativeLayout> + + </FrameLayout> + +</FrameLayout> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6e7e100..e36d1eb 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -45,4 +45,12 @@ <color name="material_drawer_dark_selected_text">@color/material_drawer_primary</color> <color name="material_drawer_dark_header_selection_text">#FFF</color> + <!-- swipeable list --> + <color name="bg_item_normal_state">#ffffffff</color> + <color name="bg_item_swiping_state">@color/colorPrimaryLight</color> + <color name="bg_item_swiping_active_state">@color/colorPrimary</color> + <color name="bg_swipe_item_neutral">@android:color/transparent</color> + <color name="bg_swipe_item_trash">#fff45f30</color> + <color name="bg_swipe_item_archive">#fff9930d</color> + </resources>