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