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.entities.Conversational;
59import eu.siacs.conversations.ui.adapter.ConversationAdapter;
60import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
61import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
62import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
63import eu.siacs.conversations.ui.util.PendingActionHelper;
64import eu.siacs.conversations.ui.util.PendingItem;
65import eu.siacs.conversations.ui.util.ScrollState;
66import eu.siacs.conversations.ui.util.StyledAttributes;
67import eu.siacs.conversations.utils.ThemeHelper;
68
69import static android.support.v7.widget.helper.ItemTouchHelper.LEFT;
70import static android.support.v7.widget.helper.ItemTouchHelper.RIGHT;
71
72public class ConversationsOverviewFragment extends XmppFragment {
73
74 private static final String STATE_SCROLL_POSITION = ConversationsOverviewFragment.class.getName()+".scroll_state";
75
76 private final List<Conversation> conversations = new ArrayList<>();
77 private final PendingItem<Conversation> swipedConversation = new PendingItem<>();
78 private final PendingItem<ScrollState> pendingScrollState = new PendingItem<>();
79 private FragmentConversationsOverviewBinding binding;
80 private ConversationAdapter conversationsAdapter;
81 private XmppActivity activity;
82 private float mSwipeEscapeVelocity = 0f;
83 private PendingActionHelper pendingActionHelper = new PendingActionHelper();
84
85 private ItemTouchHelper.SimpleCallback callback = new ItemTouchHelper.SimpleCallback(0,LEFT|RIGHT) {
86 @Override
87 public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
88 //todo maybe we can manually changing the position of the conversation
89 return false;
90 }
91
92 @Override
93 public float getSwipeEscapeVelocity (float defaultValue) {
94 return mSwipeEscapeVelocity;
95 }
96
97 @Override
98 public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
99 float dX, float dY, int actionState, boolean isCurrentlyActive) {
100 super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
101 if(actionState != ItemTouchHelper.ACTION_STATE_IDLE){
102 Paint paint = new Paint();
103 paint.setColor(StyledAttributes.getColor(activity,R.attr.conversations_overview_background));
104 paint.setStyle(Paint.Style.FILL);
105 c.drawRect(viewHolder.itemView.getLeft(),viewHolder.itemView.getTop()
106 ,viewHolder.itemView.getRight(),viewHolder.itemView.getBottom(), paint);
107 }
108 }
109
110 @Override
111 public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
112 super.clearView(recyclerView, viewHolder);
113 viewHolder.itemView.setAlpha(1f);
114 }
115
116 @Override
117 public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
118 pendingActionHelper.execute();
119 int position = viewHolder.getLayoutPosition();
120 try {
121 swipedConversation.push(conversations.get(position));
122 } catch (IndexOutOfBoundsException e) {
123 return;
124 }
125 conversationsAdapter.remove(swipedConversation.peek(), position);
126 activity.xmppConnectionService.markRead(swipedConversation.peek());
127
128 if (position == 0 && conversationsAdapter.getItemCount() == 0) {
129 final Conversation c = swipedConversation.pop();
130 activity.xmppConnectionService.archiveConversation(c);
131 return;
132 }
133 final boolean formerlySelected = ConversationFragment.getConversation(getActivity()) == swipedConversation.peek();
134 if (activity instanceof OnConversationArchived) {
135 ((OnConversationArchived) activity).onConversationArchived(swipedConversation.peek());
136 }
137 final Conversation c = swipedConversation.peek();
138 final int title;
139 if (c.getMode() == Conversational.MODE_MULTI) {
140 if (c.getMucOptions().isPrivateAndNonAnonymous()) {
141 title = R.string.title_undo_swipe_out_group_chat;
142 } else {
143 title = R.string.title_undo_swipe_out_channel;
144 }
145 } else {
146 title = R.string.title_undo_swipe_out_conversation;
147 }
148
149 final Snackbar snackbar = Snackbar.make(binding.list, title, 5000)
150 .setAction(R.string.undo, v -> {
151 pendingActionHelper.undo();
152 Conversation conversation = swipedConversation.pop();
153 conversationsAdapter.insert(conversation, position);
154 if (formerlySelected) {
155 if (activity instanceof OnConversationSelected) {
156 ((OnConversationSelected) activity).onConversationSelected(c);
157 }
158 }
159 LinearLayoutManager layoutManager = (LinearLayoutManager) binding.list.getLayoutManager();
160 if (position > layoutManager.findLastVisibleItemPosition()) {
161 binding.list.smoothScrollToPosition(position);
162 }
163 })
164 .addCallback(new Snackbar.Callback() {
165 @Override
166 public void onDismissed(Snackbar transientBottomBar, int event) {
167 switch (event) {
168 case DISMISS_EVENT_SWIPE:
169 case DISMISS_EVENT_TIMEOUT:
170 pendingActionHelper.execute();
171 break;
172 }
173 }
174 });
175
176 pendingActionHelper.push(() -> {
177 if (snackbar.isShownOrQueued()) {
178 snackbar.dismiss();
179 }
180 final Conversation conversation = swipedConversation.pop();
181 if(conversation != null){
182 if (!conversation.isRead() && conversation.getMode() == Conversation.MODE_SINGLE) {
183 return;
184 }
185 activity.xmppConnectionService.archiveConversation(c);
186 }
187 });
188
189 ThemeHelper.fix(snackbar);
190 snackbar.show();
191 }
192 };
193
194 private ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
195
196 public static Conversation getSuggestion(Activity activity) {
197 final Conversation exception;
198 Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
199 if (fragment instanceof ConversationsOverviewFragment) {
200 exception = ((ConversationsOverviewFragment) fragment).swipedConversation.peek();
201 } else {
202 exception = null;
203 }
204 return getSuggestion(activity, exception);
205 }
206
207 public static Conversation getSuggestion(Activity activity, Conversation exception) {
208 Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
209 if (fragment instanceof ConversationsOverviewFragment) {
210 List<Conversation> conversations = ((ConversationsOverviewFragment) fragment).conversations;
211 if (conversations.size() > 0) {
212 Conversation suggestion = conversations.get(0);
213 if (suggestion == exception) {
214 if (conversations.size() > 1) {
215 return conversations.get(1);
216 }
217 } else {
218 return suggestion;
219 }
220 }
221 }
222 return null;
223
224 }
225
226 @Override
227 public void onActivityCreated(Bundle savedInstanceState) {
228 super.onActivityCreated(savedInstanceState);
229 if (savedInstanceState == null) {
230 return;
231 }
232 pendingScrollState.push(savedInstanceState.getParcelable(STATE_SCROLL_POSITION));
233 }
234
235 @Override
236 public void onAttach(Activity activity) {
237 super.onAttach(activity);
238 if (activity instanceof XmppActivity) {
239 this.activity = (XmppActivity) activity;
240 } else {
241 throw new IllegalStateException("Trying to attach fragment to activity that is not an XmppActivity");
242 }
243 }
244
245 @Override
246 public void onPause() {
247 Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onPause()");
248 pendingActionHelper.execute();
249 super.onPause();
250 }
251
252 @Override
253 public void onDetach() {
254 super.onDetach();
255 this.activity = null;
256 }
257
258 @Override
259 public void onCreate(Bundle savedInstanceState) {
260 super.onCreate(savedInstanceState);
261 setHasOptionsMenu(true);
262 }
263
264 @Override
265 public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
266 this.mSwipeEscapeVelocity = getResources().getDimension(R.dimen.swipe_escape_velocity);
267 this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversations_overview, container, false);
268 this.binding.fab.setOnClickListener((view) -> StartConversationActivity.launch(getActivity()));
269
270 this.conversationsAdapter = new ConversationAdapter(this.activity, this.conversations);
271 this.conversationsAdapter.setConversationClickListener((view, conversation) -> {
272 if (activity instanceof OnConversationSelected) {
273 ((OnConversationSelected) activity).onConversationSelected(conversation);
274 } else {
275 Log.w(ConversationsOverviewFragment.class.getCanonicalName(), "Activity does not implement OnConversationSelected");
276 }
277 });
278 this.binding.list.setAdapter(this.conversationsAdapter);
279 this.binding.list.setLayoutManager(new LinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false));
280 this.touchHelper.attachToRecyclerView(this.binding.list);
281 return binding.getRoot();
282 }
283
284 @Override
285 public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
286 menuInflater.inflate(R.menu.fragment_conversations_overview, menu);
287 }
288
289 @Override
290 public void onBackendConnected() {
291 refresh();
292 }
293
294 @Override
295 public void onSaveInstanceState(Bundle bundle) {
296 super.onSaveInstanceState(bundle);
297 ScrollState scrollState = getScrollState();
298 if (scrollState != null) {
299 bundle.putParcelable(STATE_SCROLL_POSITION, scrollState);
300 }
301 }
302
303 private ScrollState getScrollState() {
304 if (this.binding == null) {
305 return null;
306 }
307 LinearLayoutManager layoutManager = (LinearLayoutManager) this.binding.list.getLayoutManager();
308 int position = layoutManager.findFirstVisibleItemPosition();
309 final View view = this.binding.list.getChildAt(0);
310 if (view != null) {
311 return new ScrollState(position,view.getTop());
312 } else {
313 return new ScrollState(position, 0);
314 }
315 }
316
317 @Override
318 public void onStart() {
319 super.onStart();
320 Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onStart()");
321 if (activity.xmppConnectionService != null) {
322 refresh();
323 }
324 }
325
326 @Override
327 public void onResume() {
328 super.onResume();
329 Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onResume()");
330 }
331
332 @Override
333 public boolean onOptionsItemSelected(final MenuItem item) {
334 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
335 return false;
336 }
337 switch (item.getItemId()) {
338 case R.id.action_search:
339 startActivity(new Intent(getActivity(), SearchActivity.class));
340 return true;
341 }
342 return super.onOptionsItemSelected(item);
343 }
344
345 @Override
346 void refresh() {
347 if (this.binding == null || this.activity == null) {
348 Log.d(Config.LOGTAG,"ConversationsOverviewFragment.refresh() skipped updated because view binding or activity was null");
349 return;
350 }
351 this.activity.xmppConnectionService.populateWithOrderedConversations(this.conversations);
352 Conversation removed = this.swipedConversation.peek();
353 if (removed != null) {
354 if (removed.isRead()) {
355 this.conversations.remove(removed);
356 } else {
357 pendingActionHelper.execute();
358 }
359 }
360 this.conversationsAdapter.notifyDataSetChanged();
361 ScrollState scrollState = pendingScrollState.pop();
362 if (scrollState != null) {
363 setScrollPosition(scrollState);
364 }
365 }
366
367 private void setScrollPosition(ScrollState scrollPosition) {
368 if (scrollPosition != null) {
369 LinearLayoutManager layoutManager = (LinearLayoutManager) binding.list.getLayoutManager();
370 layoutManager.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset);
371 }
372 }
373}