1/*
2 * Copyright (c) 2018, Daniel Gultsch All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without modification,
5 * are permitted provided that the following conditions are met:
6 *
7 * 1. Redistributions of source code must retain the above copyright notice, this
8 * list of conditions and the following disclaimer.
9 *
10 * 2. Redistributions in binary form must reproduce the above copyright notice,
11 * this list of conditions and the following disclaimer in the documentation and/or
12 * other materials provided with the distribution.
13 *
14 * 3. Neither the name of the copyright holder nor the names of its contributors
15 * may be used to endorse or promote products derived from this software without
16 * specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
22 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29
30package eu.siacs.conversations.ui;
31
32import android.app.Activity;
33import android.app.Fragment;
34import android.content.Intent;
35import android.databinding.DataBindingUtil;
36import android.graphics.Canvas;
37import android.graphics.Paint;
38import android.os.Bundle;
39import android.support.design.widget.Snackbar;
40import android.support.v7.widget.LinearLayoutManager;
41import android.support.v7.widget.RecyclerView;
42import android.support.v7.widget.helper.ItemTouchHelper;
43import android.util.Log;
44import android.view.LayoutInflater;
45import android.view.Menu;
46import android.view.MenuInflater;
47import android.view.MenuItem;
48import android.view.View;
49import android.view.ViewGroup;
50
51import java.util.ArrayList;
52import java.util.List;
53
54import eu.siacs.conversations.Config;
55import eu.siacs.conversations.R;
56import eu.siacs.conversations.databinding.FragmentConversationsOverviewBinding;
57import eu.siacs.conversations.entities.Conversation;
58import eu.siacs.conversations.ui.adapter.ConversationAdapter;
59import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
60import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
61import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
62import eu.siacs.conversations.ui.util.PendingActionHelper;
63import eu.siacs.conversations.ui.util.PendingItem;
64import eu.siacs.conversations.ui.util.ScrollState;
65import eu.siacs.conversations.ui.util.StyledAttributes;
66import eu.siacs.conversations.utils.ThemeHelper;
67
68import static android.support.v7.widget.helper.ItemTouchHelper.LEFT;
69import static android.support.v7.widget.helper.ItemTouchHelper.RIGHT;
70
71public class ConversationsOverviewFragment extends XmppFragment {
72
73 private static final String STATE_SCROLL_POSITION = ConversationsOverviewFragment.class.getName()+".scroll_state";
74
75 private final List<Conversation> conversations = new ArrayList<>();
76 private final PendingItem<Conversation> swipedConversation = new PendingItem<>();
77 private final PendingItem<ScrollState> pendingScrollState = new PendingItem<>();
78 private FragmentConversationsOverviewBinding binding;
79 private ConversationAdapter conversationsAdapter;
80 private XmppActivity activity;
81 private float mSwipeEscapeVelocity = 0f;
82 private PendingActionHelper pendingActionHelper = new PendingActionHelper();
83
84 private ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0,LEFT|RIGHT) {
85 @Override
86 public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
87 //todo maybe we can manually changing the position of the conversation
88 return false;
89 }
90
91 @Override
92 public float getSwipeEscapeVelocity (float defaultValue) {
93 return mSwipeEscapeVelocity;
94 }
95
96 @Override
97 public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
98 float dX, float dY, int actionState, boolean isCurrentlyActive) {
99 super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
100 if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
101 Paint paint = new Paint();
102 paint.setColor(StyledAttributes.getColor(activity,R.attr.conversations_overview_background));
103 paint.setStyle(Paint.Style.FILL);
104 c.drawRect(viewHolder.itemView.getLeft(),viewHolder.itemView.getTop()
105 ,viewHolder.itemView.getRight(),viewHolder.itemView.getBottom(), paint);
106 }
107 }
108
109 @Override
110 public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
111 super.clearView(recyclerView, viewHolder);
112 viewHolder.itemView.setAlpha(1f);
113 }
114
115 @Override
116 public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
117 pendingActionHelper.execute();
118 int position = viewHolder.getLayoutPosition();
119 try {
120 swipedConversation.push(conversations.get(position));
121 } catch (IndexOutOfBoundsException e) {
122 return;
123 }
124 conversationsAdapter.remove(swipedConversation.peek(), position);
125 activity.xmppConnectionService.markRead(swipedConversation.peek());
126
127 if (position == 0 && conversationsAdapter.getItemCount() == 0) {
128 final Conversation c = swipedConversation.pop();
129 activity.xmppConnectionService.archiveConversation(c);
130 return;
131 }
132 final boolean formerlySelected = ConversationFragment.getConversation(getActivity()) == swipedConversation.peek();
133 if (activity instanceof OnConversationArchived) {
134 ((OnConversationArchived) activity).onConversationArchived(swipedConversation.peek());
135 }
136 boolean isMuc = swipedConversation.peek().getMode() == Conversation.MODE_MULTI;
137 int title = isMuc ? R.string.title_undo_swipe_out_muc : R.string.title_undo_swipe_out_conversation;
138
139 final Snackbar snackbar = Snackbar.make(binding.list, title, 5000)
140 .setAction(R.string.undo, v -> {
141 pendingActionHelper.undo();
142 Conversation c = swipedConversation.pop();
143 conversationsAdapter.insert(c, position);
144 if (formerlySelected) {
145 if (activity instanceof OnConversationSelected) {
146 ((OnConversationSelected) activity).onConversationSelected(c);
147 }
148 }
149 LinearLayoutManager layoutManager = (LinearLayoutManager) binding.list.getLayoutManager();
150 if (position > layoutManager.findLastVisibleItemPosition()) {
151 binding.list.smoothScrollToPosition(position);
152 }
153 })
154 .addCallback(new Snackbar.Callback() {
155 @Override
156 public void onDismissed(Snackbar transientBottomBar, int event) {
157 switch (event) {
158 case DISMISS_EVENT_SWIPE:
159 case DISMISS_EVENT_TIMEOUT:
160 pendingActionHelper.execute();
161 break;
162 }
163 }
164 });
165
166 pendingActionHelper.push(() -> {
167 if (snackbar.isShownOrQueued()) {
168 snackbar.dismiss();
169 }
170 Conversation c = swipedConversation.pop();
171 if(c != null){
172 if (!c.isRead() && c.getMode() == Conversation.MODE_SINGLE) {
173 return;
174 }
175 activity.xmppConnectionService.archiveConversation(c);
176 }
177 });
178
179 ThemeHelper.fix(snackbar);
180 snackbar.show();
181 }
182 };
183
184 private ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
185
186 public static Conversation getSuggestion(Activity activity) {
187 final Conversation exception;
188 Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
189 if (fragment != null && fragment instanceof ConversationsOverviewFragment) {
190 exception = ((ConversationsOverviewFragment) fragment).swipedConversation.peek();
191 } else {
192 exception = null;
193 }
194 return getSuggestion(activity, exception);
195 }
196
197 public static Conversation getSuggestion(Activity activity, Conversation exception) {
198 Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
199 if (fragment != null && fragment instanceof ConversationsOverviewFragment) {
200 List<Conversation> conversations = ((ConversationsOverviewFragment) fragment).conversations;
201 if (conversations.size() > 0) {
202 Conversation suggestion = conversations.get(0);
203 if (suggestion == exception) {
204 if (conversations.size() > 1) {
205 return conversations.get(1);
206 }
207 } else {
208 return suggestion;
209 }
210 }
211 }
212 return null;
213
214 }
215
216 @Override
217 public void onActivityCreated(Bundle savedInstanceState) {
218 super.onActivityCreated(savedInstanceState);
219 if (savedInstanceState == null) {
220 return;
221 }
222 pendingScrollState.push(savedInstanceState.getParcelable(STATE_SCROLL_POSITION));
223 }
224
225 @Override
226 public void onAttach(Activity activity) {
227 super.onAttach(activity);
228 if (activity instanceof XmppActivity) {
229 this.activity = (XmppActivity) activity;
230 } else {
231 throw new IllegalStateException("Trying to attach fragment to activity that is not an XmppActivity");
232 }
233 }
234
235 @Override
236 public void onPause() {
237 Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onPause()");
238 pendingActionHelper.execute();
239 super.onPause();
240 }
241
242 @Override
243 public void onDetach() {
244 super.onDetach();
245 this.activity = null;
246 }
247
248 @Override
249 public void onCreate(Bundle savedInstanceState) {
250 super.onCreate(savedInstanceState);
251 setHasOptionsMenu(true);
252 }
253
254 @Override
255 public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
256 this.mSwipeEscapeVelocity = getResources().getDimension(R.dimen.swipe_escape_velocity);
257 this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversations_overview, container, false);
258 this.binding.fab.setOnClickListener((view) -> StartConversationActivity.launch(getActivity()));
259
260 this.conversationsAdapter = new ConversationAdapter(this.activity, this.conversations);
261 this.conversationsAdapter.setConversationClickListener((view, conversation) -> {
262 if (activity instanceof OnConversationSelected) {
263 ((OnConversationSelected) activity).onConversationSelected(conversation);
264 } else {
265 Log.w(ConversationsOverviewFragment.class.getCanonicalName(), "Activity does not implement OnConversationSelected");
266 }
267 });
268 this.binding.list.setAdapter(this.conversationsAdapter);
269 this.binding.list.setLayoutManager(new LinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false));
270 this.touchHelper.attachToRecyclerView(this.binding.list);
271 return binding.getRoot();
272 }
273
274 @Override
275 public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
276 menuInflater.inflate(R.menu.fragment_conversations_overview, menu);
277 }
278
279 @Override
280 public void onBackendConnected() {
281 refresh();
282 }
283
284 @Override
285 public void onSaveInstanceState(Bundle bundle) {
286 super.onSaveInstanceState(bundle);
287 ScrollState scrollState = getScrollState();
288 if (scrollState != null) {
289 bundle.putParcelable(STATE_SCROLL_POSITION, scrollState);
290 }
291 }
292
293 private ScrollState getScrollState() {
294 if (this.binding == null) {
295 return null;
296 }
297 LinearLayoutManager layoutManager = (LinearLayoutManager) this.binding.list.getLayoutManager();
298 int position = layoutManager.findFirstVisibleItemPosition();
299 final View view = this.binding.list.getChildAt(0);
300 if (view != null) {
301 return new ScrollState(position,view.getTop());
302 } else {
303 return new ScrollState(position, 0);
304 }
305 }
306
307 @Override
308 public void onStart() {
309 super.onStart();
310 Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onStart()");
311 if (activity.xmppConnectionService != null) {
312 refresh();
313 }
314 }
315
316 @Override
317 public void onResume() {
318 super.onResume();
319 Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onResume()");
320 }
321
322 @Override
323 public boolean onOptionsItemSelected(final MenuItem item) {
324 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
325 return false;
326 }
327 switch (item.getItemId()) {
328 case R.id.action_search:
329 startActivity(new Intent(getActivity(), SearchActivity.class));
330 return true;
331 }
332 return super.onOptionsItemSelected(item);
333 }
334
335 @Override
336 void refresh() {
337 if (this.binding == null || this.activity == null) {
338 Log.d(Config.LOGTAG,"ConversationsOverviewFragment.refresh() skipped updated because view binding or activity was null");
339 return;
340 }
341 this.activity.xmppConnectionService.populateWithOrderedConversations(this.conversations);
342 Conversation removed = this.swipedConversation.peek();
343 if (removed != null) {
344 if (removed.isRead()) {
345 this.conversations.remove(removed);
346 } else {
347 pendingActionHelper.execute();
348 }
349 }
350 this.conversationsAdapter.notifyDataSetChanged();
351 ScrollState scrollState = pendingScrollState.pop();
352 if (scrollState != null) {
353 setScrollPosition(scrollState);
354 }
355 }
356
357 private void setScrollPosition(ScrollState scrollPosition) {
358 if (scrollPosition != null) {
359 LinearLayoutManager layoutManager = (LinearLayoutManager) binding.list.getLayoutManager();
360 layoutManager.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset);
361 }
362 }
363}