Merge branch 'feature/swipe-actions' into develop
This commit is contained in:
commit
8d58af423c
@ -28,7 +28,7 @@ import ch.dissem.bitmessage.entity.valueobject.Label;
|
|||||||
/**
|
/**
|
||||||
* @author Christian Basler
|
* @author Christian Basler
|
||||||
*/
|
*/
|
||||||
public abstract class AbstractItemListFragment<T> extends ListFragment {
|
public abstract class AbstractItemListFragment<T> extends ListFragment implements ListHolder {
|
||||||
/**
|
/**
|
||||||
* The serialization (saved instance state) Bundle key representing the
|
* The serialization (saved instance state) Bundle key representing the
|
||||||
* activated item position. Only used on tablets.
|
* activated item position. Only used on tablets.
|
||||||
@ -55,8 +55,6 @@ public abstract class AbstractItemListFragment<T> extends ListFragment {
|
|||||||
private int activatedPosition = ListView.INVALID_POSITION;
|
private int activatedPosition = ListView.INVALID_POSITION;
|
||||||
private boolean activateOnItemClick;
|
private boolean activateOnItemClick;
|
||||||
|
|
||||||
abstract void updateList(Label label);
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
@ -166,7 +166,7 @@ public class AddressListFragment extends AbstractItemListFragment<BitmessageAddr
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void updateList(Label label) {
|
public void updateList(Label label) {
|
||||||
updateList();
|
updateList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
28
app/src/main/java/ch/dissem/apps/abit/ListHolder.java
Normal file
28
app/src/main/java/ch/dissem/apps/abit/ListHolder.java
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* 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 ch.dissem.bitmessage.entity.valueobject.Label;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Christian Basler
|
||||||
|
*/
|
||||||
|
public interface ListHolder {
|
||||||
|
void updateList(Label label);
|
||||||
|
|
||||||
|
void setActivateOnItemClick(boolean activateOnItemClick);
|
||||||
|
}
|
@ -216,7 +216,7 @@ public class MainActivity extends AppCompatActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeList(AbstractItemListFragment<?> listFragment) {
|
private <F extends Fragment & ListHolder> void changeList(F listFragment) {
|
||||||
getSupportFragmentManager()
|
getSupportFragmentManager()
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(R.id.item_list, listFragment)
|
.replace(R.id.item_list, listFragment)
|
||||||
|
@ -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()) {
|
for (Label label : item.getLabels()) {
|
||||||
if (label.getType() == Label.Type.TRASH) {
|
if (label.getType() == Label.Type.TRASH) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -17,19 +17,29 @@
|
|||||||
package ch.dissem.apps.abit;
|
package ch.dissem.apps.abit;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.design.widget.FloatingActionButton;
|
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.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.ActionBarListener;
|
||||||
import ch.dissem.apps.abit.listener.ListSelectionListener;
|
import ch.dissem.apps.abit.listener.ListSelectionListener;
|
||||||
import ch.dissem.apps.abit.service.Singleton;
|
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.entity.valueobject.Label;
|
||||||
import ch.dissem.bitmessage.ports.MessageRepository;
|
import ch.dissem.bitmessage.ports.MessageRepository;
|
||||||
|
|
||||||
|
import static ch.dissem.apps.abit.MessageDetailFragment.isInTrash;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list fragment representing a list of Messages. This fragment
|
* A list fragment representing a list of Messages. This fragment
|
||||||
* also supports tablet devices by allowing list items to be given an
|
* 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}
|
* Activities containing this fragment MUST implement the {@link ListSelectionListener}
|
||||||
* interface.
|
* interface.
|
||||||
*/
|
*/
|
||||||
public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
|
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 Label currentLabel;
|
||||||
private MenuItem emptyTrashMenuItem;
|
private MenuItem emptyTrashMenuItem;
|
||||||
|
private MessageRepository messageRepo;
|
||||||
|
private List<Plaintext> messages;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mandatory empty constructor for the fragment manager to instantiate the
|
* Mandatory empty constructor for the fragment manager to instantiate the
|
||||||
@ -68,8 +89,10 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
MainActivity activity = (MainActivity) getActivity();
|
||||||
|
messageRepo = Singleton.getMessageRepository(activity);
|
||||||
|
|
||||||
doUpdateList(((MainActivity) getActivity()).getSelectedLabel());
|
doUpdateList(activity.getSelectedLabel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -82,34 +105,7 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void doUpdateList(Label label) {
|
private void doUpdateList(Label label) {
|
||||||
setListAdapter(new ArrayAdapter<Plaintext>(
|
messages = Singleton.getMessageRepository(getContext()).findMessages(label);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (getActivity() instanceof ActionBarListener) {
|
if (getActivity() instanceof ActionBarListener) {
|
||||||
if (label != null) {
|
if (label != null) {
|
||||||
((ActionBarListener) getActivity()).updateTitle(label.toString());
|
((ActionBarListener) getActivity()).updateTitle(label.toString());
|
||||||
@ -120,26 +116,127 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
|
|||||||
if (emptyTrashMenuItem != null) {
|
if (emptyTrashMenuItem != null) {
|
||||||
emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH);
|
emptyTrashMenuItem.setVisible(label != null && label.getType() == Label.Type.TRASH);
|
||||||
}
|
}
|
||||||
|
adapter.setData(label, messages);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
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.
|
// 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() {
|
fab.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
Intent intent = new Intent(getActivity().getApplicationContext(), ComposeMessageActivity.class);
|
Intent intent = new Intent(getActivity().getApplicationContext(),
|
||||||
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity(getActivity()));
|
ComposeMessageActivity.class);
|
||||||
|
intent.putExtra(ComposeMessageActivity.EXTRA_IDENTITY, Singleton.getIdentity
|
||||||
|
(getActivity()));
|
||||||
startActivity(intent);
|
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;
|
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
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
inflater.inflate(R.menu.message_list, menu);
|
inflater.inflate(R.menu.message_list, menu);
|
||||||
@ -164,4 +261,8 @@ public class MessageListFragment extends AbstractItemListFragment<Plaintext> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setActivateOnItemClick(boolean activateOnItemClick) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* 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.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;
|
||||||
|
|
||||||
|
import static ch.dissem.apps.abit.util.Strings.normalizeWhitespaces;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapted from the basic swipeable example by Haruki Hasegawa. See
|
||||||
|
*
|
||||||
|
* @author Christian Basler
|
||||||
|
* @see <a href="https://github.com/h6ah4i/android-advancedrecyclerview">
|
||||||
|
* https://github.com/h6ah4i/android-advancedrecyclerview</a>
|
||||||
|
*/
|
||||||
|
public class SwipeableMessageAdapter
|
||||||
|
extends RecyclerView.Adapter<SwipeableMessageAdapter.ViewHolder>
|
||||||
|
implements SwipeableItemAdapter<SwipeableMessageAdapter.ViewHolder>, 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 ViewHolder extends AbstractSwipeableItemViewHolder {
|
||||||
|
public FrameLayout container;
|
||||||
|
public final ImageView avatar;
|
||||||
|
public final TextView sender;
|
||||||
|
public final TextView subject;
|
||||||
|
public final TextView extract;
|
||||||
|
|
||||||
|
public ViewHolder(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 ViewHolder 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 ViewHolder(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(ViewHolder 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(normalizeWhitespaces(item.getSubject()));
|
||||||
|
holder.extract.setText(normalizeWhitespaces(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(ViewHolder holder, int position, int x, int y) {
|
||||||
|
if (label == null || 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(ViewHolder holder, int position, int type) {
|
||||||
|
int bgRes = 0;
|
||||||
|
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 == null || 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(ViewHolder 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -28,6 +28,7 @@ import android.os.Bundle;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import ch.dissem.apps.abit.service.Singleton;
|
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.BitmessageContext;
|
||||||
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
import ch.dissem.bitmessage.entity.BitmessageAddress;
|
||||||
import ch.dissem.bitmessage.entity.CustomMessage;
|
import ch.dissem.bitmessage.entity.CustomMessage;
|
||||||
|
import ch.dissem.bitmessage.exception.DecryptionFailedException;
|
||||||
import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
|
import ch.dissem.bitmessage.extensions.CryptoCustomMessage;
|
||||||
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
|
import ch.dissem.bitmessage.extensions.pow.ProofOfWorkRequest;
|
||||||
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
|
import ch.dissem.bitmessage.ports.ProofOfWorkRepository;
|
||||||
@ -68,18 +70,24 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
|
|||||||
@Override
|
@Override
|
||||||
public void onPerformSync(Account account, Bundle extras, String authority,
|
public void onPerformSync(Account account, Bundle extras, String authority,
|
||||||
ContentProviderClient provider, SyncResult syncResult) {
|
ContentProviderClient provider, SyncResult syncResult) {
|
||||||
if (account.equals(Authenticator.ACCOUNT_SYNC)) {
|
try {
|
||||||
if (Preferences.isConnectionAllowed(getContext())) {
|
if (account.equals(Authenticator.ACCOUNT_SYNC)) {
|
||||||
syncData();
|
if (Preferences.isConnectionAllowed(getContext())) {
|
||||||
|
syncData();
|
||||||
|
}
|
||||||
|
} else if (account.equals(Authenticator.ACCOUNT_POW)) {
|
||||||
|
syncPOW();
|
||||||
|
} else {
|
||||||
|
syncResult.stats.numAuthExceptions++;
|
||||||
}
|
}
|
||||||
} else if (account.equals(Authenticator.ACCOUNT_POW)) {
|
} catch (IOException e) {
|
||||||
syncPOW();
|
syncResult.stats.numIoExceptions++;
|
||||||
} else {
|
} catch (DecryptionFailedException e) {
|
||||||
throw new RuntimeException("Unknown " + account);
|
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 the Bitmessage context acts as a full node, synchronization isn't necessary
|
||||||
if (bmc.isRunning()) {
|
if (bmc.isRunning()) {
|
||||||
LOG.info("Synchronization skipped, Abit is acting as a full node");
|
LOG.info("Synchronization skipped, Abit is acting as a full node");
|
||||||
@ -87,61 +95,53 @@ public class SyncAdapter extends AbstractThreadedSyncAdapter {
|
|||||||
}
|
}
|
||||||
LOG.info("Synchronizing Bitmessage");
|
LOG.info("Synchronizing Bitmessage");
|
||||||
|
|
||||||
try {
|
LOG.info("Synchronization started");
|
||||||
LOG.info("Synchronization started");
|
bmc.synchronize(
|
||||||
bmc.synchronize(
|
Preferences.getTrustedNode(getContext()),
|
||||||
Preferences.getTrustedNode(getContext()),
|
Preferences.getTrustedNodePort(getContext()),
|
||||||
Preferences.getTrustedNodePort(getContext()),
|
Preferences.getTimeoutInSeconds(getContext()),
|
||||||
Preferences.getTimeoutInSeconds(getContext()),
|
true);
|
||||||
true);
|
LOG.info("Synchronization finished");
|
||||||
LOG.info("Synchronization finished");
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
LOG.error(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void syncPOW() {
|
private void syncPOW() throws IOException, DecryptionFailedException {
|
||||||
// If the Bitmessage context acts as a full node, synchronization isn't necessary
|
// If the Bitmessage context acts as a full node, synchronization isn't necessary
|
||||||
LOG.info("Looking for completed POW");
|
LOG.info("Looking for completed POW");
|
||||||
|
|
||||||
try {
|
BitmessageAddress identity = Singleton.getIdentity(getContext());
|
||||||
BitmessageAddress identity = Singleton.getIdentity(getContext());
|
byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey();
|
||||||
byte[] privateKey = identity.getPrivateKey().getPrivateEncryptionKey();
|
byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey());
|
||||||
byte[] signingKey = cryptography().createPublicKey(identity.getPublicDecryptionKey());
|
ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity);
|
||||||
ProofOfWorkRequest.Reader reader = new ProofOfWorkRequest.Reader(identity);
|
ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext());
|
||||||
ProofOfWorkRepository powRepo = Singleton.getProofOfWorkRepository(getContext());
|
List<byte[]> items = powRepo.getItems();
|
||||||
List<byte[]> items = powRepo.getItems();
|
for (byte[] initialHash : items) {
|
||||||
for (byte[] initialHash : items) {
|
ProofOfWorkRepository.Item item = powRepo.getItem(initialHash);
|
||||||
ProofOfWorkRepository.Item item = powRepo.getItem(initialHash);
|
byte[] target = cryptography().getProofOfWorkTarget(item.object, item
|
||||||
byte[] target = cryptography().getProofOfWorkTarget(item.object, item
|
.nonceTrialsPerByte, item.extraBytes);
|
||||||
.nonceTrialsPerByte, item.extraBytes);
|
CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>(
|
||||||
CryptoCustomMessage<ProofOfWorkRequest> cryptoMsg = new CryptoCustomMessage<>(
|
new ProofOfWorkRequest(identity, initialHash, CALCULATE, target));
|
||||||
new ProofOfWorkRequest(identity, initialHash, CALCULATE, target));
|
cryptoMsg.signAndEncrypt(identity, signingKey);
|
||||||
cryptoMsg.signAndEncrypt(identity, signingKey);
|
CustomMessage response = bmc.send(
|
||||||
CustomMessage response = bmc.send(
|
Preferences.getTrustedNode(getContext()),
|
||||||
Preferences.getTrustedNode(getContext()),
|
Preferences.getTrustedNodePort(getContext()),
|
||||||
Preferences.getTrustedNodePort(getContext()),
|
cryptoMsg
|
||||||
cryptoMsg
|
);
|
||||||
);
|
if (response.isError()) {
|
||||||
if (response.isError()) {
|
LOG.error("Server responded with error: " + new String(response.getData(),
|
||||||
LOG.error("Server responded with error: " + new String(response.getData(),
|
"UTF-8"));
|
||||||
"UTF-8"));
|
} else {
|
||||||
} else {
|
ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read(
|
||||||
ProofOfWorkRequest decryptedResponse = CryptoCustomMessage.read(
|
response, reader).decrypt(privateKey);
|
||||||
response, reader).decrypt(privateKey);
|
if (decryptedResponse.getRequest() == COMPLETE) {
|
||||||
if (decryptedResponse.getRequest() == COMPLETE) {
|
bmc.internals().getProofOfWorkService().onNonceCalculated(
|
||||||
bmc.internals().getProofOfWorkService().onNonceCalculated(
|
initialHash, decryptedResponse.getData());
|
||||||
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) {
|
public static void startSync(Context ctx) {
|
||||||
|
@ -23,6 +23,7 @@ import android.preference.PreferenceManager;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.InetAddress;
|
import java.net.InetAddress;
|
||||||
import java.net.UnknownHostException;
|
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
|
* Warning, this method might do a network call and therefore can't be called from
|
||||||
* the UI thread.
|
* the UI thread.
|
||||||
*/
|
*/
|
||||||
public static InetAddress getTrustedNode(Context ctx) {
|
public static InetAddress getTrustedNode(Context ctx) throws IOException {
|
||||||
String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE);
|
String trustedNode = getPreference(ctx, PREFERENCE_TRUSTED_NODE);
|
||||||
if (trustedNode == null) return null;
|
if (trustedNode == null) return null;
|
||||||
trustedNode = trustedNode.trim();
|
trustedNode = trustedNode.trim();
|
||||||
@ -59,15 +60,7 @@ public class Preferences {
|
|||||||
int index = trustedNode.lastIndexOf(':');
|
int index = trustedNode.lastIndexOf(':');
|
||||||
trustedNode = trustedNode.substring(0, index);
|
trustedNode = trustedNode.substring(0, index);
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
return InetAddress.getByName(trustedNode);
|
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) {
|
public static int getTrustedNodePort(Context ctx) {
|
||||||
|
31
app/src/main/java/ch/dissem/apps/abit/util/Strings.java
Normal file
31
app/src/main/java/ch/dissem/apps/abit/util/Strings.java
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Christian Basler
|
||||||
|
*/
|
||||||
|
public class Strings {
|
||||||
|
private final static Pattern WHITESPACES = Pattern.compile("\\s+");
|
||||||
|
|
||||||
|
public static String normalizeWhitespaces(CharSequence string) {
|
||||||
|
string = string.subSequence(0, Math.min(string.length(), 200));
|
||||||
|
return WHITESPACES.matcher(string).replaceAll(" ");
|
||||||
|
}
|
||||||
|
}
|
25
app/src/main/res/drawable-anydpi-v21/bg_swipe_item_left.xml
Normal file
25
app/src/main/res/drawable-anydpi-v21/bg_swipe_item_left.xml
Normal file
@ -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_trash"/>
|
||||||
|
</item>
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/ic_item_swipe_trash"
|
||||||
|
android:gravity="right|center_vertical"
|
||||||
|
android:right="16dp"/>
|
||||||
|
</layer-list>
|
25
app/src/main/res/drawable-anydpi-v21/bg_swipe_item_right.xml
Normal file
25
app/src/main/res/drawable-anydpi-v21/bg_swipe_item_right.xml
Normal file
@ -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>
|
21
app/src/main/res/drawable/bg_item_normal_state.xml
Normal file
21
app/src/main/res/drawable/bg_item_normal_state.xml
Normal file
@ -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>
|
21
app/src/main/res/drawable/bg_item_swiping_active_state.xml
Normal file
21
app/src/main/res/drawable/bg_item_swiping_active_state.xml
Normal file
@ -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>
|
21
app/src/main/res/drawable/bg_item_swiping_state.xml
Normal file
21
app/src/main/res/drawable/bg_item_swiping_state.xml
Normal file
@ -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>
|
26
app/src/main/res/drawable/bg_swipe_item_left.xml
Normal file
26
app/src/main/res/drawable/bg_swipe_item_left.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?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:right="16dp">
|
||||||
|
<bitmap
|
||||||
|
android:gravity="right|center_vertical"
|
||||||
|
android:src="@drawable/ic_item_swipe_trash"/>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
19
app/src/main/res/drawable/bg_swipe_item_neutral.xml
Normal file
19
app/src/main/res/drawable/bg_swipe_item_neutral.xml
Normal file
@ -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"/>
|
26
app/src/main/res/drawable/bg_swipe_item_right.xml
Normal file
26
app/src/main/res/drawable/bg_swipe_item_right.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?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:left="16dp">
|
||||||
|
<bitmap
|
||||||
|
android:gravity="left|center_vertical"
|
||||||
|
android:src="@drawable/ic_item_swipe_archive"/>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
9
app/src/main/res/drawable/ic_item_swipe_archive.xml
Normal file
9
app/src/main/res/drawable/ic_item_swipe_archive.xml
Normal file
@ -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>
|
9
app/src/main/res/drawable/ic_item_swipe_trash.xml
Normal file
9
app/src/main/res/drawable/ic_item_swipe_trash.xml
Normal file
@ -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>
|
22
app/src/main/res/drawable/list_divider_h.xml
Normal file
22
app/src/main/res/drawable/list_divider_h.xml
Normal file
@ -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>
|
@ -4,26 +4,26 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<ListView
|
<android.support.v7.widget.RecyclerView
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/recycler_view"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:id="@id/android:list"
|
android:layout_height="0dp"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
|
||||||
android:paddingBottom="88dp"
|
android:paddingBottom="88dp"
|
||||||
android:clipToPadding="false"
|
android:scrollbarStyle="outsideOverlay"
|
||||||
android:scrollbarStyle="outsideOverlay"
|
android:scrollbars="vertical"/>
|
||||||
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentBottom="true"/>
|
|
||||||
|
|
||||||
<android.support.design.widget.FloatingActionButton
|
<android.support.design.widget.FloatingActionButton
|
||||||
android:id="@+id/fab_compose_message"
|
android:id="@+id/fab_compose_message"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_action_compose_message"
|
android:layout_alignParentBottom="true"
|
||||||
app:elevation="8dp"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_margin="16dp"
|
||||||
android:layout_alignParentEnd="true"
|
android:src="@drawable/ic_action_compose_message"
|
||||||
android:layout_margin="16dp"/>
|
app:elevation="8dp"/>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
@ -15,64 +15,89 @@
|
|||||||
~ limitations under the License.
|
~ limitations under the License.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_height="wrap_content">
|
android:layout_width="match_parent"
|
||||||
<ImageView
|
android:layout_height="wrap_content"
|
||||||
android:id="@+id/avatar"
|
android:background="@drawable/bg_swipe_item_neutral">
|
||||||
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"/>
|
|
||||||
|
|
||||||
<TextView
|
<FrameLayout
|
||||||
android:id="@+id/sender"
|
android:id="@+id/container"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
tools:text="Sender"
|
android:background="@drawable/bg_item_normal_state"
|
||||||
android:lines="1"
|
android:clickable="true"
|
||||||
android:ellipsize="end"
|
android:foreground="?attr/selectableItemBackground"
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
tools:ignore="UselessParent">
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/subject"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
tools:text="Subject"
|
android:background="?attr/selectableItemBackground">
|
||||||
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"/>
|
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/text"
|
android:id="@+id/avatar"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="40dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="40dp"
|
||||||
tools:text="Text"
|
android:layout_alignParentStart="true"
|
||||||
android:lines="1"
|
android:layout_alignParentTop="true"
|
||||||
android:ellipsize="end"
|
android:layout_margin="16dp"
|
||||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
android:src="@color/colorAccent"
|
||||||
android:gravity="center_vertical"
|
tools:ignore="ContentDescription"/>
|
||||||
android:paddingLeft="8dp"
|
|
||||||
android:paddingRight="8dp"
|
|
||||||
android:paddingBottom="8dp"
|
|
||||||
android:layout_below="@+id/subject"
|
|
||||||
android:layout_toEndOf="@+id/avatar"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
<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: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="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
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="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:paddingLeft="8dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
tools:text="Text"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
@ -45,4 +45,12 @@
|
|||||||
<color name="material_drawer_dark_selected_text">@color/material_drawer_primary</color>
|
<color name="material_drawer_dark_selected_text">@color/material_drawer_primary</color>
|
||||||
<color name="material_drawer_dark_header_selection_text">#FFF</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>
|
</resources>
|
||||||
|
Loading…
Reference in New Issue
Block a user