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.Color;
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.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(Color.get(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 pendingActionHelper.push(() -> {
140 Conversation c = swipedConversation.pop();
141 if(c != null){
142 if (!c.isRead() && c.getMode() == Conversation.MODE_SINGLE) {
143 return;
144 }
145 activity.xmppConnectionService.archiveConversation(c);
146 }
147 });
148 final Snackbar snackbar = Snackbar.make(binding.list, title, 5000)
149 .setAction(R.string.undo, v -> {
150 pendingActionHelper.undo();
151 Conversation c = swipedConversation.pop();
152 conversationsAdapter.insert(c, position);
153 if (formerlySelected) {
154 if (activity instanceof OnConversationSelected) {
155 ((OnConversationSelected) activity).onConversationSelected(c);
156 }
157 }
158 LinearLayoutManager layoutManager = (LinearLayoutManager) binding.list.getLayoutManager();
159 if (position > layoutManager.findLastVisibleItemPosition()) {
160 binding.list.smoothScrollToPosition(position);
161 }
162 })
163 .addCallback(new Snackbar.Callback() {
164 @Override
165 public void onDismissed(Snackbar transientBottomBar, int event) {
166 switch (event) {
167 case DISMISS_EVENT_SWIPE:
168 case DISMISS_EVENT_TIMEOUT:
169 pendingActionHelper.execute();
170 break;
171 }
172 }
173 });
174 ThemeHelper.fix(snackbar);
175 snackbar.show();
176 }
177 };
178
179 private ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
180
181 public static Conversation getSuggestion(Activity activity) {
182 final Conversation exception;
183 Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
184 if (fragment != null && fragment instanceof ConversationsOverviewFragment) {
185 exception = ((ConversationsOverviewFragment) fragment).swipedConversation.peek();
186 } else {
187 exception = null;
188 }
189 return getSuggestion(activity, exception);
190 }
191
192 public static Conversation getSuggestion(Activity activity, Conversation exception) {
193 Fragment fragment = activity.getFragmentManager().findFragmentById(R.id.main_fragment);
194 if (fragment != null && fragment instanceof ConversationsOverviewFragment) {
195 List<Conversation> conversations = ((ConversationsOverviewFragment) fragment).conversations;
196 if (conversations.size() > 0) {
197 Conversation suggestion = conversations.get(0);
198 if (suggestion == exception) {
199 if (conversations.size() > 1) {
200 return conversations.get(1);
201 }
202 } else {
203 return suggestion;
204 }
205 }
206 }
207 return null;
208
209 }
210
211 @Override
212 public void onActivityCreated(Bundle savedInstanceState) {
213 super.onActivityCreated(savedInstanceState);
214 if (savedInstanceState == null) {
215 return;
216 }
217 pendingScrollState.push(savedInstanceState.getParcelable(STATE_SCROLL_POSITION));
218 }
219
220 @Override
221 public void onAttach(Activity activity) {
222 super.onAttach(activity);
223 if (activity instanceof XmppActivity) {
224 this.activity = (XmppActivity) activity;
225 } else {
226 throw new IllegalStateException("Trying to attach fragment to activity that is not an XmppActivity");
227 }
228 }
229
230 @Override
231 public void onPause() {
232 Log.d(Config.LOGTAG,"ConversationsOverviewFragment.onPause()");
233 pendingActionHelper.execute();
234 super.onPause();
235 }
236
237 @Override
238 public void onDetach() {
239 super.onDetach();
240 this.activity = null;
241 }
242
243 @Override
244 public void onCreate(Bundle savedInstanceState) {
245 super.onCreate(savedInstanceState);
246 setHasOptionsMenu(true);
247 }
248
249 @Override
250 public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
251 this.mSwipeEscapeVelocity = getResources().getDimension(R.dimen.swipe_escape_velocity);
252 this.binding = DataBindingUtil.inflate(inflater, R.layout.fragment_conversations_overview, container, false);
253 this.binding.fab.setOnClickListener((view) -> StartConversationActivity.launch(getActivity()));
254
255 this.conversationsAdapter = new ConversationAdapter(this.activity, this.conversations);
256 this.conversationsAdapter.setConversationClickListener((view, conversation) -> {
257 if (activity instanceof OnConversationSelected) {
258 ((OnConversationSelected) activity).onConversationSelected(conversation);
259 } else {
260 Log.w(ConversationsOverviewFragment.class.getCanonicalName(), "Activity does not implement OnConversationSelected");
261 }
262 });
263 this.binding.list.setAdapter(this.conversationsAdapter);
264 this.binding.list.setLayoutManager(new LinearLayoutManager(getActivity(),LinearLayoutManager.VERTICAL,false));
265 this.touchHelper.attachToRecyclerView(this.binding.list);
266 return binding.getRoot();
267 }
268
269 @Override
270 public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
271 menuInflater.inflate(R.menu.fragment_conversations_overview, menu);
272 }
273
274 @Override
275 public void onBackendConnected() {
276 refresh();
277 }
278
279 @Override
280 public void onSaveInstanceState(Bundle bundle) {
281 super.onSaveInstanceState(bundle);
282 ScrollState scrollState = getScrollState();
283 if (scrollState != null) {
284 bundle.putParcelable(STATE_SCROLL_POSITION, scrollState);
285 }
286 }
287
288 private ScrollState getScrollState() {
289 if (this.binding == null) {
290 return null;
291 }
292 LinearLayoutManager layoutManager = (LinearLayoutManager) this.binding.list.getLayoutManager();
293 int position = layoutManager.findFirstVisibleItemPosition();
294 final View view = this.binding.list.getChildAt(0);
295 if (view != null) {
296 return new ScrollState(position,view.getTop());
297 } else {
298 return new ScrollState(position, 0);
299 }
300 }
301
302 @Override
303 public void onStart() {
304 super.onStart();
305 Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onStart()");
306 if (activity.xmppConnectionService != null) {
307 refresh();
308 }
309 }
310
311 @Override
312 public void onResume() {
313 super.onResume();
314 Log.d(Config.LOGTAG, "ConversationsOverviewFragment.onResume()");
315 }
316
317 @Override
318 public boolean onOptionsItemSelected(final MenuItem item) {
319 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
320 return false;
321 }
322 switch (item.getItemId()) {
323 case R.id.action_search:
324 startActivity(new Intent(getActivity(), SearchActivity.class));
325 return true;
326 }
327 return super.onOptionsItemSelected(item);
328 }
329
330 @Override
331 void refresh() {
332 if (this.binding == null || this.activity == null) {
333 Log.d(Config.LOGTAG,"ConversationsOverviewFragment.refresh() skipped updated because view binding or activity was null");
334 return;
335 }
336 this.activity.xmppConnectionService.populateWithOrderedConversations(this.conversations);
337 Conversation removed = this.swipedConversation.peek();
338 if (removed != null) {
339 if (removed.isRead()) {
340 this.conversations.remove(removed);
341 } else {
342 pendingActionHelper.execute();
343 }
344 }
345 this.conversationsAdapter.notifyDataSetChanged();
346 ScrollState scrollState = pendingScrollState.pop();
347 if (scrollState != null) {
348 setScrollPosition(scrollState);
349 }
350 }
351
352 private void setScrollPosition(ScrollState scrollPosition) {
353 if (scrollPosition != null) {
354 LinearLayoutManager layoutManager = (LinearLayoutManager) binding.list.getLayoutManager();
355 layoutManager.scrollToPositionWithOffset(scrollPosition.position, scrollPosition.offset);
356 }
357 }
358}