package com.zulip.android.networking; import android.util.Log; import com.j256.ormlite.dao.RuntimeExceptionDao; import com.j256.ormlite.stmt.Where; import com.zulip.android.ZulipApp; import com.zulip.android.filters.NarrowFilter; import com.zulip.android.models.Message; import com.zulip.android.models.MessageRange; import com.zulip.android.networking.response.GetMessagesResponse; import com.zulip.android.util.MessageListener; import com.zulip.android.util.MessageListener.LoadPosition; import com.zulip.android.util.ZLog; import org.apache.commons.lang.time.StopWatch; import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.List; import retrofit2.Response; public class AsyncGetOldMessages extends ZulipAsyncPushTask { public List<Message> receivedMessages; protected MessageRange rng; protected NarrowFilter filter; private MessageListener listener; private MessageListener.LoadPosition position; private int mainAnchor; private int before; private int afterAnchor; private int after; private boolean recursedAbove = false; private boolean recursedBelow = false; private boolean noFurtherMessages = false; private int rangeHigh = -2; public AsyncGetOldMessages(MessageListener listener) { super(ZulipApp.get()); this.listener = listener; rng = null; } /** * Get messages surrounding a specified anchor message ID, inclusive of both * endpoints and the anchor. * * @param anchor Message ID of the message to fetch around * @param before Number of messages after the anchor to return * @param after Number of messages before the anchor to return */ public final void execute(int anchor, LoadPosition pos, int before, int after, NarrowFilter filter) { this.mainAnchor = anchor; this.before = before; this.afterAnchor = mainAnchor + 1; this.after = after; this.filter = filter; position = pos; this.receivedMessages = new ArrayList<>(); Log.i("AGOM", "executing " + anchor + " " + before + " " + after); execute("GET", "v1/messages"); } @Override protected String doInBackground(String... params) { // Lets see whether we have this cached already final RuntimeExceptionDao<MessageRange, Integer> messageRangeDao = app.getDao(MessageRange.class); RuntimeExceptionDao<Message, Object> messageDao = app.getDao(Message.class); messageDao.setObjectCache(true); try { if (rng == null) { // We haven't been passed a range, see if we have a range cached MessageRange protoRng = MessageRange.getRangeContaining(mainAnchor, messageRangeDao); Log.i("AGOM", "rng retreived"); if (protoRng != null) { StopWatch watch = new StopWatch(); watch.start(); // We found a range, lets get relevant messages from the // cache rng = protoRng; rangeHigh = rng.high; // we order by descending so that our limit query DTRT Where<Message, Object> filteredWhere = messageDao .queryBuilder().orderBy(Message.ID_FIELD, false) .limit((long) before + 1).where(); if (filter != null) { filter.modWhere(filteredWhere); filteredWhere.and(); } List<Message> lowerCachedMessages = filteredWhere .le(Message.ID_FIELD, mainAnchor).and() .ge(Message.ID_FIELD, rng.low).query(); // however because of this we need to flip the ordering Collections.reverse(lowerCachedMessages); filteredWhere = messageDao.queryBuilder() .orderBy(Message.ID_FIELD, true) .limit((long) after).where(); if (filter != null) { filter.modWhere(filteredWhere); filteredWhere.and(); } List<Message> upperCachedMessages = filteredWhere .gt("id", mainAnchor).and().le("id", rng.high) .query(); before -= lowerCachedMessages.size(); // One more than size to account for missing anchor after -= upperCachedMessages.size() + 1; if (!lowerCachedMessages.isEmpty()) { mainAnchor = lowerCachedMessages.get(0).getID() - 1; } if (!upperCachedMessages.isEmpty()) { afterAnchor = upperCachedMessages.get( upperCachedMessages.size() - 1).getID() + 1; } receivedMessages.addAll(lowerCachedMessages); receivedMessages.addAll(upperCachedMessages); watch.stop(); Log.i("perf", "Retrieving cached messages: " + watch.toString()); if (!lowerCachedMessages.isEmpty() || !upperCachedMessages.isEmpty()) { if (before > 0) { this.recurse(LoadPosition.ABOVE, before, rng, mainAnchor); } // Don't fetch past the max message ID because we won't // find anything there. However, leave the final // determination up to the UI thread, because it has a // consistent view around events. With this, at // worst it has to do another fetch to get the new // messages. if (after > 0 && rng.high != app.getMaxMessageId()) { this.recurse(LoadPosition.BELOW, after, rng, afterAnchor); } return null; } } else { // We don't have anything cached Log.w("db_gom", "No messages found in specified range!"); } } if (fetchMessages(mainAnchor, before, after, params) && filter == null) { int lowest = receivedMessages.get(0).getID(); int highest = receivedMessages.get( receivedMessages.size() - 1).getID(); // We know there are no messages between the anchor and what // we received or we would have fetched them. if (lowest > mainAnchor) lowest = mainAnchor; if (highest < mainAnchor) highest = mainAnchor; MessageRange.markRange(app, lowest, highest); } } catch (SQLException e) { // Still welp. throw new RuntimeException(e); } return null; // since onPostExecute doesn't use the String result } protected void recurse(LoadPosition position, int amount, MessageRange rng, int anchor) { AsyncGetOldMessages task = new AsyncGetOldMessages(listener); task.rng = rng; switch (position) { case ABOVE: recursedAbove = true; task.execute(anchor, position, amount, 0, filter); break; case BELOW: task.execute(anchor, position, 0, amount, filter); recursedBelow = true; break; default: Log.wtf("AGOM", "recurse passed unexpected load position!"); break; } } protected boolean fetchMessages(int anchor, int numBefore, int numAfter, String[] params) { Response<GetMessagesResponse> result; try { result = app.getZulipServices() .getMessages( Integer.toString(anchor), Integer.toString(numBefore), Integer.toString(numAfter), filter) .execute(); if (!result.isSuccessful()) { return false; } } catch (IOException e) { ZLog.logException(e); return false; } List<Message> fetchedMessages = result.body().getMessages(); Message.createMessages(app, fetchedMessages); if (numAfter == 0) { receivedMessages.addAll(0, fetchedMessages); } else { receivedMessages.addAll(fetchedMessages); } if ((position == LoadPosition.ABOVE || position == LoadPosition.BELOW) && receivedMessages.size() < numBefore + numAfter) { noFurtherMessages = true; } return !receivedMessages.isEmpty(); } @Override protected void onPostExecute(String result) { if (receivedMessages != null) { Log.v("poll", "Processing messages received."); if ((position == LoadPosition.BELOW || position == LoadPosition.INITIAL) && app.getMaxMessageId() == rangeHigh) { noFurtherMessages = true; } listener.onMessages(receivedMessages.toArray(new Message[receivedMessages.size()]), position, recursedAbove, recursedBelow, noFurtherMessages); callback.onTaskComplete(receivedMessages.size() + "", null); } else { listener.onMessageError(position); Log.v("poll", "No messages returned."); callback.onTaskFailure(""); } } protected void onCancelled(String result) { listener.onMessageError(position); callback.onTaskFailure(result); } }