Compare commits

...

36 Commits
master ... mvvm

Author SHA1 Message Date
Oleg Kuznetcov e0e994695d changed "toCompletable()" to "ignoreElement()" 2018-09-21 18:56:16 +03:00
Denis Karmyshakov 75adeafbdd Removed ObservableCollection hierarchy and related classes 2018-03-20 16:46:04 +03:00
Denis Karmyshakov d83fe2d510
Merge pull request #53 from TouchInstinct/gradle_update
Gradle update
2017-12-01 16:11:49 +03:00
Denis Karmyshakov 3aa53424a7 Gradle update 2017-11-30 18:02:23 +03:00
Arseniy Borisov 8a586a9638 Update by payload fixed (#50) 2017-11-13 11:58:10 +03:00
Oleg 8af90dcdd5 Merge pull request #49 from TouchInstinct/update/gradle
Versions in constants
2017-10-05 15:50:20 +03:00
Oleg 2a3c647a30 Versions in constants 2017-10-05 15:48:40 +03:00
Denis Karmyshakov 4b78713f50 Merge pull request #47 from TouchInstinct/versions_in_constants
Versions in constants
2017-10-04 12:29:00 +03:00
Denis Karmyshakov 6e7cd65ab2 Versions in constants 2017-10-04 12:25:04 +03:00
Oleg e3e9b96fee buildToolsVersion 26.0.2 (#46) 2017-09-29 14:50:36 +03:00
Ilia Kurtov c496eba64e Merge pull request #44 from TouchInstinct/update/rxjava
update rxjava
2017-09-22 16:39:51 +03:00
Ilia Kurtov b4e3c018aa update rxjava 2017-09-22 16:36:57 +03:00
Ilia Kurtov 7142bed098 rxjava update (#40) 2017-08-15 21:02:46 +03:00
Ilia Kurtov b0610e1f51 libs update (#39) 2017-08-14 17:41:20 +03:00
Ilia Kurtov 0e4b89bb4d Merge branch 'kotlin_migration' into master-kotlin-rxjava2 2017-08-11 13:50:25 +03:00
Ilia Kurtov fb4634a6ed Rxjava2/merge kotlin (#38)
* remove retrolambda

* revert retrolambda

* Build tools update

* Support lib update
2017-08-09 20:27:02 +03:00
Arseniy Borisov e14f5b00cb Merge pull request #37 from TouchInstinct/support_lib_update
Support lib update
2017-08-03 18:22:07 +03:00
Denis Karmyshakov ac6b3e85ac Support lib update 2017-08-03 18:15:24 +03:00
Arseniy Borisov 5e4475d2b2 Merge pull request #36 from TouchInstinct/build_tools_update
Build tools update
2017-07-25 19:58:37 +03:00
Denis Karmyshakov 4166445bc6 Build tools update 2017-07-25 19:54:06 +03:00
Denis Karmyshakov e5d021c885 Merge remote-tracking branch 'origin/master-rx-java-2' into kotlin_migration 2017-07-24 12:48:56 +03:00
Denis Karmyshakov 5e9bfc701c Merge pull request #35 from TouchInstinct/idea_formatting
idea formatting
2017-07-24 12:45:36 +03:00
Arseniy Borisov 3bf6f76159 idea formatting 2017-07-24 12:31:50 +03:00
Anton Domnikov 8c1f3d35fc Merge branch 'master-rx-java-2' into kotlin_migration
# Conflicts:
#	build.gradle
2017-07-11 14:30:00 +03:00
Denis Karmyshakov e4d652b3ab Merge remote-tracking branch 'origin/master' into master-rx-java-2
# Conflicts:
#	build.gradle
#	src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableCollection.java
#	src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableFilteredList.java
#	src/main/java/ru/touchin/roboswag/core/observables/collections/loadable/LoadingMoreList.java
2017-06-30 19:15:24 +03:00
Anton Domnikov ef4632304e Merge branch 'master-rx-java-2' into kotlin_migration 2017-06-23 13:32:21 +03:00
Alexander Bubnov 6ed1cb4e92 fix missing import 2017-06-21 17:21:45 +03:00
Alexander Bubnov 50371ee530 update rxJava2 to 2.1.1 2017-06-21 17:06:07 +03:00
Anton Domnikov 20951bed03 revert retrolambda 2017-06-19 18:24:02 +03:00
Anton Domnikov 0cb3099d14 remove retrolambda 2017-06-14 17:55:17 +03:00
Gavriil Sitnikov 29dc6ce1c0 Merge branch 'master' into master-rx-java-2
# Conflicts:
#	src/main/java/ru/touchin/roboswag/core/observables/collections/ObservableFilteredList.java
2017-06-13 14:55:16 +03:00
gorodeckii 7e6513218e Merge branch 'master' into master-rx-java-2 2017-06-09 15:53:12 +03:00
Arhipov 768c83addc Merge branch 'master' into master-rx-java-2 2017-05-04 14:06:01 +03:00
gorodeckii 52c5b7fa98 rxjava version 2017-05-03 09:26:28 +03:00
Gavriil Sitnikov 2602d56cbd static fixes 2017-04-21 00:11:32 +03:00
Gavriil Sitnikov 553f4f6bfa RxJava2 migration 2017-04-17 03:11:09 +03:00
34 changed files with 375 additions and 3171 deletions

View File

@ -1,9 +1,7 @@
apply plugin: 'com.android.library'
apply plugin: 'me.tatarka.retrolambda'
android {
compileSdkVersion 25
buildToolsVersion '25.0.3'
compileSdkVersion compileSdk
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -16,7 +14,7 @@ android {
}
dependencies {
provided 'com.android.support:support-annotations:25.4.0'
provided 'io.reactivex:rxandroid:1.2.1'
provided 'io.reactivex:rxjava:1.3.0'
compileOnly "com.android.support:support-annotations:$supportLibraryVersion"
compileOnly "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
compileOnly "io.reactivex.rxjava2:rxjava:$rxJavaVersion"
}

View File

@ -1,126 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ru.touchin.roboswag.core.android.support.v7.util;
/**
* Wraps a {@link ListUpdateCallback} callback and batches operations that can be merged.
*
* <p>For instance, when 2 add operations comes that adds 2 consecutive elements,
* BatchingListUpdateCallback merges them and calls the wrapped callback only once.
*
* <p>This is a general purpose class and is also used by
* {@link DiffUtil.DiffResult DiffResult}
*
* <p>If you use this class to batch updates, you must call {@link #dispatchLastEvent()} when the
* stream of update events drain.
*/
@SuppressWarnings({"PMD", "checkstyle:all"})
public class BatchingListUpdateCallback implements ListUpdateCallback {
private static final int TYPE_NONE = 0;
private static final int TYPE_ADD = 1;
private static final int TYPE_REMOVE = 2;
private static final int TYPE_CHANGE = 3;
private final ListUpdateCallback mWrapped;
private int mLastEventType = TYPE_NONE;
private int mLastEventPosition = -1;
private int mLastEventCount = -1;
private Object mLastEventPayload = null;
public BatchingListUpdateCallback(ListUpdateCallback callback) {
mWrapped = callback;
}
/**
* BatchingListUpdateCallback holds onto the last event to see if it can be merged with the
* next one. When stream of events finish, you should call this method to dispatch the last
* event.
*/
public void dispatchLastEvent() {
if (mLastEventType == TYPE_NONE) {
return;
}
switch (mLastEventType) {
case TYPE_ADD:
mWrapped.onInserted(mLastEventPosition, mLastEventCount);
break;
case TYPE_REMOVE:
mWrapped.onRemoved(mLastEventPosition, mLastEventCount);
break;
case TYPE_CHANGE:
mWrapped.onChanged(mLastEventPosition, mLastEventCount, mLastEventPayload);
break;
}
mLastEventPayload = null;
mLastEventType = TYPE_NONE;
}
@Override
public void onInserted(int position, int count) {
if (mLastEventType == TYPE_ADD && position >= mLastEventPosition
&& position <= mLastEventPosition + mLastEventCount) {
mLastEventCount += count;
mLastEventPosition = Math.min(position, mLastEventPosition);
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventType = TYPE_ADD;
}
@Override
public void onRemoved(int position, int count) {
if (mLastEventType == TYPE_REMOVE && mLastEventPosition >= position &&
mLastEventPosition <= position + count) {
mLastEventCount += count;
mLastEventPosition = position;
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventType = TYPE_REMOVE;
}
@Override
public void onMoved(int fromPosition, int toPosition) {
dispatchLastEvent(); // moves are not merged
mWrapped.onMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, Object payload) {
if (mLastEventType == TYPE_CHANGE &&
!(position > mLastEventPosition + mLastEventCount
|| position + count < mLastEventPosition || mLastEventPayload != payload)) {
// take potential overlap into account
int previousEnd = mLastEventPosition + mLastEventCount;
mLastEventPosition = Math.min(position, mLastEventPosition);
mLastEventCount = Math.max(previousEnd, position + count) - mLastEventPosition;
return;
}
dispatchLastEvent();
mLastEventPosition = position;
mLastEventCount = count;
mLastEventPayload = payload;
mLastEventType = TYPE_CHANGE;
}
}

View File

@ -1,787 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ru.touchin.roboswag.core.android.support.v7.util;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* DiffUtil is a utility class that can calculate the difference between two lists and output a
* list of update operations that converts the first list into the second one.
*
* <p>It can be used to calculate updates for a RecyclerView Adapter.
*
* <p>DiffUtil uses Eugene W. Myers's difference algorithm to calculate the minimal number of updates
* to convert one list into another. Myers's algorithm does not handle items that are moved so
* DiffUtil runs a second pass on the result to detect items that were moved.
*
* <p>If the lists are large, this operation may take significant time so you are advised to run this
* on a background thread, get the {@link DiffResult} then apply it on the RecyclerView on the main
* thread.
*
* <p>This algorithm is optimized for space and uses O(N) space to find the minimal
* number of addition and removal operations between the two lists. It has O(N + D^2) expected time
* performance where D is the length of the edit script.
*
* <p>If move detection is enabled, it takes an additional O(N^2) time where N is the total number of
* added and removed items. If your lists are already sorted by the same constraint (e.g. a created
* timestamp for a list of posts), you can disable move detection to improve performance.
*
* <p>The actual runtime of the algorithm significantly depends on the number of changes in the list
* and the cost of your comparison methods. Below are some average run times for reference:
* (The areSame list is composed of random UUID Strings and the tests are run on Nexus 5X with M)
* <ul>
* <li>100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
* <li>100 items and 100 modifications: 3.82 ms, median: 3.75 ms
* <li>100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
* <li>1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
* <li>1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
* <li>1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
* <li>1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms
* </ul>
*
* <p>Due to implementation constraints, the max size of the list can be 2^26.
*/
@SuppressWarnings({"PMD", "checkstyle:all"})
public class DiffUtil {
private DiffUtil() {
// utility class, no instance.
}
private static final Comparator<Snake> SNAKE_COMPARATOR = new Comparator<Snake>() {
@Override
public int compare(Snake o1, Snake o2) {
int cmpX = o1.x - o2.x;
return cmpX == 0 ? o1.y - o2.y : cmpX;
}
};
// Myers' algorithm uses two lists as axis labels. In DiffUtil's implementation, `x` axis is
// used for old list and `y` axis is used for new list.
/**
* Calculates the list of update operations that can covert one list into the other one.
*
* @param cb The callback that acts as a gateway to the backing list data
*
* @return A DiffResult that contains the information about the edit sequence to convert the
* old list into the new list.
*/
public static DiffResult calculateDiff(Callback cb) {
return calculateDiff(cb, true);
}
/**
* Calculates the list of update operations that can covert one list into the other one.
* <p>
* If your old and new lists are sorted by the same constraint and items never move (swap
* positions), you can disable move detection which takes <code>O(N^2)</code> time where
* N is the number of added, moved, removed items.
*
* @param cb The callback that acts as a gateway to the backing list data
* @param detectMoves True if DiffUtil should try to detect moved items, false otherwise.
*
* @return A DiffResult that contains the information about the edit sequence to convert the
* old list into the new list.
*/
public static DiffResult calculateDiff(Callback cb, boolean detectMoves) {
final int oldSize = cb.getOldListSize();
final int newSize = cb.getNewListSize();
final List<Snake> snakes = new ArrayList<>();
// instead of a recursive implementation, we keep our own stack to avoid potential stack
// overflow exceptions
final List<Range> stack = new ArrayList<>();
stack.add(new Range(0, oldSize, 0, newSize));
final int max = oldSize + newSize + Math.abs(oldSize - newSize);
// allocate forward and backward k-lines. K lines are diagonal lines in the matrix. (see the
// paper for details)
// These arrays lines keep the max reachable position for each k-line.
final int[] forward = new int[max * 2];
final int[] backward = new int[max * 2];
// We pool the ranges to avoid allocations for each recursive call.
final List<Range> rangePool = new ArrayList<>();
while (!stack.isEmpty()) {
final Range range = stack.remove(stack.size() - 1);
final Snake snake = diffPartial(cb, range.oldListStart, range.oldListEnd,
range.newListStart, range.newListEnd, forward, backward, max);
if (snake != null) {
if (snake.size > 0) {
snakes.add(snake);
}
// offset the snake to convert its coordinates from the Range's area to global
snake.x += range.oldListStart;
snake.y += range.newListStart;
// add new ranges for left and right
final Range left = rangePool.isEmpty() ? new Range() : rangePool.remove(
rangePool.size() - 1);
left.oldListStart = range.oldListStart;
left.newListStart = range.newListStart;
if (snake.reverse) {
left.oldListEnd = snake.x;
left.newListEnd = snake.y;
} else {
if (snake.removal) {
left.oldListEnd = snake.x - 1;
left.newListEnd = snake.y;
} else {
left.oldListEnd = snake.x;
left.newListEnd = snake.y - 1;
}
}
stack.add(left);
// re-use range for right
//noinspection UnnecessaryLocalVariable
final Range right = range;
if (snake.reverse) {
if (snake.removal) {
right.oldListStart = snake.x + snake.size + 1;
right.newListStart = snake.y + snake.size;
} else {
right.oldListStart = snake.x + snake.size;
right.newListStart = snake.y + snake.size + 1;
}
} else {
right.oldListStart = snake.x + snake.size;
right.newListStart = snake.y + snake.size;
}
stack.add(right);
} else {
rangePool.add(range);
}
}
// sort snakes
Collections.sort(snakes, SNAKE_COMPARATOR);
return new DiffResult(cb, snakes, forward, backward, detectMoves);
}
private static Snake diffPartial(Callback cb, int startOld, int endOld,
int startNew, int endNew, int[] forward, int[] backward, int kOffset) {
final int oldSize = endOld - startOld;
final int newSize = endNew - startNew;
if (endOld - startOld < 1 || endNew - startNew < 1) {
return null;
}
final int delta = oldSize - newSize;
final int dLimit = (oldSize + newSize + 1) / 2;
Arrays.fill(forward, kOffset - dLimit - 1, kOffset + dLimit + 1, 0);
Arrays.fill(backward, kOffset - dLimit - 1 + delta, kOffset + dLimit + 1 + delta, oldSize);
final boolean checkInFwd = delta % 2 != 0;
for (int d = 0; d <= dLimit; d++) {
for (int k = -d; k <= d; k += 2) {
// find forward path
// we can reach k from k - 1 or k + 1. Check which one is further in the graph
int x;
final boolean removal;
if (k == -d || k != d && forward[kOffset + k - 1] < forward[kOffset + k + 1]) {
x = forward[kOffset + k + 1];
removal = false;
} else {
x = forward[kOffset + k - 1] + 1;
removal = true;
}
// set y based on x
int y = x - k;
// move diagonal as long as items match
while (x < oldSize && y < newSize
&& cb.areItemsTheSame(startOld + x, startNew + y)) {
x++;
y++;
}
forward[kOffset + k] = x;
if (checkInFwd && k >= delta - d + 1 && k <= delta + d - 1) {
if (forward[kOffset + k] >= backward[kOffset + k]) {
Snake outSnake = new Snake();
outSnake.x = backward[kOffset + k];
outSnake.y = outSnake.x - k;
outSnake.size = forward[kOffset + k] - backward[kOffset + k];
outSnake.removal = removal;
outSnake.reverse = false;
return outSnake;
}
}
}
for (int k = -d; k <= d; k += 2) {
// find reverse path at k + delta, in reverse
final int backwardK = k + delta;
int x;
final boolean removal;
if (backwardK == d + delta || backwardK != -d + delta
&& backward[kOffset + backwardK - 1] < backward[kOffset + backwardK + 1]) {
x = backward[kOffset + backwardK - 1];
removal = false;
} else {
x = backward[kOffset + backwardK + 1] - 1;
removal = true;
}
// set y based on x
int y = x - backwardK;
// move diagonal as long as items match
while (x > 0 && y > 0
&& cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
x--;
y--;
}
backward[kOffset + backwardK] = x;
if (!checkInFwd && k + delta >= -d && k + delta <= d) {
if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
Snake outSnake = new Snake();
outSnake.x = backward[kOffset + backwardK];
outSnake.y = outSnake.x - backwardK;
outSnake.size =
forward[kOffset + backwardK] - backward[kOffset + backwardK];
outSnake.removal = removal;
outSnake.reverse = true;
return outSnake;
}
}
}
}
throw new IllegalStateException("DiffUtil hit an unexpected case while trying to calculate"
+ " the optimal path. Please make sure your data is not changing during the"
+ " diff calculation.");
}
/**
* A Callback class used by DiffUtil while calculating the diff between two lists.
*/
public abstract static class Callback {
/**
* Returns the size of the old list.
*
* @return The size of the old list.
*/
public abstract int getOldListSize();
/**
* Returns the size of the new list.
*
* @return The size of the new list.
*/
public abstract int getNewListSize();
/**
* Called by the DiffUtil to decide whether two object represent the same Item.
* <p>
* For example, if your items have unique ids, this method should check their id equality.
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list
* @return True if the two items represent the same object or false if they are different.
*/
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
/**
* Called by the DiffUtil when it wants to check whether two items have the same data.
* DiffUtil uses this information to detect if the contents of an item has changed.
* <p>
* DiffUtil uses this method to check equality instead of {@link Object#equals(Object)}
* so that you can change its behavior depending on your UI.
* <p>
* This method is called only if {@link #areItemsTheSame(int, int)} returns
* {@code true} for these items.
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list which replaces the
* oldItem
* @return True if the contents of the items are the same or false if they are different.
*/
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
/**
* When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
* {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil
* calls this method to get a payload about the change.
* <p>
* Default implementation returns {@code null}.
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list
*
* @return A payload object that represents the change between the two items.
*/
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return null;
}
}
/**
* Snakes represent a match between two lists. It is optionally prefixed or postfixed with an
* add or remove operation. See the Myers' paper for details.
*/
static class Snake {
/**
* Position in the old list
*/
int x;
/**
* Position in the new list
*/
int y;
/**
* Number of matches. Might be 0.
*/
int size;
/**
* If true, this is a removal from the original list followed by {@code size} matches.
* If false, this is an addition from the new list followed by {@code size} matches.
*/
boolean removal;
/**
* If true, the addition or removal is at the end of the snake.
* If false, the addition or removal is at the beginning of the snake.
*/
boolean reverse;
}
/**
* Represents a range in two lists that needs to be solved.
* <p>
* This internal class is used when running Myers' algorithm without recursion.
*/
static class Range {
int oldListStart, oldListEnd;
int newListStart, newListEnd;
public Range() {
}
public Range(int oldListStart, int oldListEnd, int newListStart, int newListEnd) {
this.oldListStart = oldListStart;
this.oldListEnd = oldListEnd;
this.newListStart = newListStart;
this.newListEnd = newListEnd;
}
}
/**
* This class holds the information about the result of a
* {@link DiffUtil#calculateDiff(Callback, boolean)} call.
* <p>
* You can consume the updates in a DiffResult via
* {@link #dispatchUpdatesTo(ListUpdateCallback)}
*/
public static class DiffResult {
/**
* While reading the flags below, keep in mind that when multiple items move in a list,
* Myers's may pick any of them as the anchor item and consider that one NOT_CHANGED while
* picking others as additions and removals. This is completely fine as we later detect
* all moves.
* <p>
* Below, when an item is mentioned to stay in the same "location", it means we won't
* dispatch a move/add/remove for it, it DOES NOT mean the item is still in the same
* position.
*/
// item stayed the same.
private static final int FLAG_NOT_CHANGED = 1;
// item stayed in the same location but changed.
private static final int FLAG_CHANGED = FLAG_NOT_CHANGED << 1;
// Item has moved and also changed.
private static final int FLAG_MOVED_CHANGED = FLAG_CHANGED << 1;
// Item has moved but did not change.
private static final int FLAG_MOVED_NOT_CHANGED = FLAG_MOVED_CHANGED << 1;
// Ignore this update.
// If this is an addition from the new list, it means the item is actually removed from an
// earlier position and its move will be dispatched when we process the matching removal
// from the old list.
// If this is a removal from the old list, it means the item is actually added back to an
// earlier index in the new list and we'll dispatch its move when we are processing that
// addition.
private static final int FLAG_IGNORE = FLAG_MOVED_NOT_CHANGED << 1;
// since we are re-using the int arrays that were created in the Myers' step, we mask
// change flags
private static final int FLAG_OFFSET = 5;
private static final int FLAG_MASK = (1 << FLAG_OFFSET) - 1;
// The Myers' snakes. At this point, we only care about their diagonal sections.
private final List<Snake> mSnakes;
// The list to keep oldItemStatuses. As we traverse old items, we assign flags to them
// which also includes whether they were a real removal or a move (and its new index).
private final int[] mOldItemStatuses;
// The list to keep newItemStatuses. As we traverse new items, we assign flags to them
// which also includes whether they were a real addition or a move(and its old index).
private final int[] mNewItemStatuses;
// The callback that was given to calcualte diff method.
private final Callback mCallback;
private final int mOldListSize;
private final int mNewListSize;
private final boolean mDetectMoves;
/**
* @param callback The callback that was used to calculate the diff
* @param snakes The list of Myers' snakes
* @param oldItemStatuses An int[] that can be re-purposed to keep metadata
* @param newItemStatuses An int[] that can be re-purposed to keep metadata
* @param detectMoves True if this DiffResult will try to detect moved items
*/
DiffResult(Callback callback, List<Snake> snakes, int[] oldItemStatuses,
int[] newItemStatuses, boolean detectMoves) {
mSnakes = snakes;
mOldItemStatuses = oldItemStatuses;
mNewItemStatuses = newItemStatuses;
Arrays.fill(mOldItemStatuses, 0);
Arrays.fill(mNewItemStatuses, 0);
mCallback = callback;
mOldListSize = callback.getOldListSize();
mNewListSize = callback.getNewListSize();
mDetectMoves = detectMoves;
addRootSnake();
findMatchingItems();
}
/**
* We always add a Snake to 0/0 so that we can run loops from end to beginning and be done
* when we run out of snakes.
*/
private void addRootSnake() {
Snake firstSnake = mSnakes.isEmpty() ? null : mSnakes.get(0);
if (firstSnake == null || firstSnake.x != 0 || firstSnake.y != 0) {
Snake root = new Snake();
root.x = 0;
root.y = 0;
root.removal = false;
root.size = 0;
root.reverse = false;
mSnakes.add(0, root);
}
}
/**
* This method traverses each addition / removal and tries to match it to a previous
* removal / addition. This is how we detect move operations.
* <p>
* This class also flags whether an item has been changed or not.
* <p>
* DiffUtil does this pre-processing so that if it is running on a big list, it can be moved
* to background thread where most of the expensive stuff will be calculated and kept in
* the statuses maps. DiffResult uses this pre-calculated information while dispatching
* the updates (which is probably being called on the main thread).
*/
private void findMatchingItems() {
int posOld = mOldListSize;
int posNew = mNewListSize;
// traverse the matrix from right bottom to 0,0.
for (int i = mSnakes.size() - 1; i >= 0; i--) {
final Snake snake = mSnakes.get(i);
final int endX = snake.x + snake.size;
final int endY = snake.y + snake.size;
if (mDetectMoves) {
while (posOld > endX) {
// this is a removal. Check remaining snakes to see if this was added before
findAddition(posOld, posNew, i);
posOld--;
}
while (posNew > endY) {
// this is an addition. Check remaining snakes to see if this was removed
// before
findRemoval(posOld, posNew, i);
posNew--;
}
}
for (int j = 0; j < snake.size; j++) {
// matching items. Check if it is changed or not
final int oldItemPos = snake.x + j;
final int newItemPos = snake.y + j;
final boolean theSame = mCallback
.areContentsTheSame(oldItemPos, newItemPos);
final int changeFlag = theSame ? FLAG_NOT_CHANGED : FLAG_CHANGED;
mOldItemStatuses[oldItemPos] = (newItemPos << FLAG_OFFSET) | changeFlag;
mNewItemStatuses[newItemPos] = (oldItemPos << FLAG_OFFSET) | changeFlag;
}
posOld = snake.x;
posNew = snake.y;
}
}
private void findAddition(int x, int y, int snakeIndex) {
if (mOldItemStatuses[x - 1] != 0) {
return; // already set by a latter item
}
findMatchingItem(x, y, snakeIndex, false);
}
private void findRemoval(int x, int y, int snakeIndex) {
if (mNewItemStatuses[y - 1] != 0) {
return; // already set by a latter item
}
findMatchingItem(x, y, snakeIndex, true);
}
/**
* Finds a matching item that is before the given coordinates in the matrix
* (before : left and above).
*
* @param x The x position in the matrix (position in the old list)
* @param y The y position in the matrix (position in the new list)
* @param snakeIndex The current snake index
* @param removal True if we are looking for a removal, false otherwise
*
* @return True if such item is found.
*/
private boolean findMatchingItem(final int x, final int y, final int snakeIndex,
final boolean removal) {
final int myItemPos;
int curX;
int curY;
if (removal) {
myItemPos = y - 1;
curX = x;
curY = y - 1;
} else {
myItemPos = x - 1;
curX = x - 1;
curY = y;
}
for (int i = snakeIndex; i >= 0; i--) {
final Snake snake = mSnakes.get(i);
final int endX = snake.x + snake.size;
final int endY = snake.y + snake.size;
if (removal) {
// check removals for a match
for (int pos = curX - 1; pos >= endX; pos--) {
if (mCallback.areItemsTheSame(pos, myItemPos)) {
// found!
final boolean theSame = mCallback.areContentsTheSame(pos, myItemPos);
final int changeFlag = theSame ? FLAG_MOVED_NOT_CHANGED
: FLAG_MOVED_CHANGED;
mNewItemStatuses[myItemPos] = (pos << FLAG_OFFSET) | FLAG_IGNORE;
mOldItemStatuses[pos] = (myItemPos << FLAG_OFFSET) | changeFlag;
return true;
}
}
} else {
// check for additions for a match
for (int pos = curY - 1; pos >= endY; pos--) {
if (mCallback.areItemsTheSame(myItemPos, pos)) {
// found
final boolean theSame = mCallback.areContentsTheSame(myItemPos, pos);
final int changeFlag = theSame ? FLAG_MOVED_NOT_CHANGED
: FLAG_MOVED_CHANGED;
mOldItemStatuses[x - 1] = (pos << FLAG_OFFSET) | FLAG_IGNORE;
mNewItemStatuses[pos] = ((x - 1) << FLAG_OFFSET) | changeFlag;
return true;
}
}
}
curX = snake.x;
curY = snake.y;
}
return false;
}
/**
* Dispatches update operations to the given Callback.
* <p>
* These updates are atomic such that the first update call effects every update call that
* comes after it (the same as RecyclerView).
*
* @param updateCallback The callback to receive the update operations.
*/
public void dispatchUpdatesTo(ListUpdateCallback updateCallback) {
final BatchingListUpdateCallback batchingCallback;
if (updateCallback instanceof BatchingListUpdateCallback) {
batchingCallback = (BatchingListUpdateCallback) updateCallback;
} else {
batchingCallback = new BatchingListUpdateCallback(updateCallback);
// replace updateCallback with a batching callback and override references to
// updateCallback so that we don't call it directly by mistake
//noinspection UnusedAssignment
updateCallback = batchingCallback;
}
// These are add/remove ops that are converted to moves. We track their positions until
// their respective update operations are processed.
final List<PostponedUpdate> postponedUpdates = new ArrayList<>();
int posOld = mOldListSize;
int posNew = mNewListSize;
for (int snakeIndex = mSnakes.size() - 1; snakeIndex >= 0; snakeIndex--) {
final Snake snake = mSnakes.get(snakeIndex);
final int snakeSize = snake.size;
final int endX = snake.x + snakeSize;
final int endY = snake.y + snakeSize;
if (endX < posOld) {
dispatchRemovals(postponedUpdates, batchingCallback, endX, posOld - endX, endX);
}
if (endY < posNew) {
dispatchAdditions(postponedUpdates, batchingCallback, endX, posNew - endY,
endY);
}
for (int i = snakeSize - 1; i >= 0; i--) {
if ((mOldItemStatuses[snake.x + i] & FLAG_MASK) == FLAG_CHANGED) {
batchingCallback.onChanged(snake.x + i, 1,
mCallback.getChangePayload(snake.x + i, snake.y + i));
}
}
posOld = snake.x;
posNew = snake.y;
}
batchingCallback.dispatchLastEvent();
}
private static PostponedUpdate removePostponedUpdate(List<PostponedUpdate> updates,
int pos, boolean removal) {
for (int i = updates.size() - 1; i >= 0; i--) {
final PostponedUpdate update = updates.get(i);
if (update.posInOwnerList == pos && update.removal == removal) {
updates.remove(i);
for (int j = i; j < updates.size(); j++) {
// offset other ops since they swapped positions
updates.get(j).currentPos += removal ? 1 : -1;
}
return update;
}
}
return null;
}
private void dispatchAdditions(List<PostponedUpdate> postponedUpdates,
ListUpdateCallback updateCallback, int start, int count, int globalIndex) {
if (!mDetectMoves) {
updateCallback.onInserted(start, count);
return;
}
for (int i = count - 1; i >= 0; i--) {
int status = mNewItemStatuses[globalIndex + i] & FLAG_MASK;
switch (status) {
case 0: // real addition
updateCallback.onInserted(start, 1);
for (PostponedUpdate update : postponedUpdates) {
update.currentPos += 1;
}
break;
case FLAG_MOVED_CHANGED:
case FLAG_MOVED_NOT_CHANGED:
final int pos = mNewItemStatuses[globalIndex + i] >> FLAG_OFFSET;
final PostponedUpdate update = removePostponedUpdate(postponedUpdates, pos,
true);
// the item was moved from that position
//noinspection ConstantConditions
updateCallback.onMoved(update.currentPos, start);
if (status == FLAG_MOVED_CHANGED) {
// also dispatch a change
updateCallback.onChanged(start, 1,
mCallback.getChangePayload(pos, globalIndex + i));
}
break;
case FLAG_IGNORE: // ignoring this
postponedUpdates.add(new PostponedUpdate(globalIndex + i, start, false));
break;
default:
throw new IllegalStateException(
"unknown flag for pos " + (globalIndex + i) + " " + Long
.toBinaryString(status));
}
}
}
private void dispatchRemovals(List<PostponedUpdate> postponedUpdates,
ListUpdateCallback updateCallback, int start, int count, int globalIndex) {
if (!mDetectMoves) {
updateCallback.onRemoved(start, count);
return;
}
for (int i = count - 1; i >= 0; i--) {
final int status = mOldItemStatuses[globalIndex + i] & FLAG_MASK;
switch (status) {
case 0: // real removal
updateCallback.onRemoved(start + i, 1);
for (PostponedUpdate update : postponedUpdates) {
update.currentPos -= 1;
}
break;
case FLAG_MOVED_CHANGED:
case FLAG_MOVED_NOT_CHANGED:
final int pos = mOldItemStatuses[globalIndex + i] >> FLAG_OFFSET;
final PostponedUpdate update = removePostponedUpdate(postponedUpdates, pos,
false);
// the item was moved to that position. we do -1 because this is a move not
// add and removing current item offsets the target move by 1
//noinspection ConstantConditions
updateCallback.onMoved(start + i, update.currentPos - 1);
if (status == FLAG_MOVED_CHANGED) {
// also dispatch a change
updateCallback.onChanged(update.currentPos - 1, 1,
mCallback.getChangePayload(globalIndex + i, pos));
}
break;
case FLAG_IGNORE: // ignoring this
postponedUpdates.add(new PostponedUpdate(globalIndex + i, start + i, true));
break;
default:
throw new IllegalStateException(
"unknown flag for pos " + (globalIndex + i) + " " + Long
.toBinaryString(status));
}
}
}
}
/**
* Represents an update that we skipped because it was a move.
* <p>
* When an update is skipped, it is tracked as other updates are dispatched until the matching
* add/remove operation is found at which point the tracked position is used to dispatch the
* update.
*/
private static class PostponedUpdate {
int posInOwnerList;
int currentPos;
boolean removal;
public PostponedUpdate(int posInOwnerList, int currentPos, boolean removal) {
this.posInOwnerList = posInOwnerList;
this.currentPos = currentPos;
this.removal = removal;
}
}
}

View File

@ -1,59 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ru.touchin.roboswag.core.android.support.v7.util;
/**
* An interface that can receive Update operations that are applied to a list.
*
* <p>This class can be used together with DiffUtil to detect changes between two lists.
*/
@SuppressWarnings({"PMD", "checkstyle:all"})
public interface ListUpdateCallback {
/**
* Called when {@code count} number of items are inserted at the given position.
*
* @param position The position of the new item.
* @param count The number of items that have been added.
*/
void onInserted(int position, int count);
/**
* Called when {@code count} number of items are removed from the given position.
*
* @param position The position of the item which has been removed.
* @param count The number of items which have been removed.
*/
void onRemoved(int position, int count);
/**
* Called when an item changes its position in the list.
*
* @param fromPosition The previous position of the item before the move.
* @param toPosition The new position of the item.
*/
void onMoved(int fromPosition, int toPosition);
/**
* Called when {@code count} number of items are updated at the given position.
*
* @param position The position of the item which has been updated.
* @param count The number of items which has changed.
*/
void onChanged(int position, int count, Object payload);
}

View File

@ -29,8 +29,8 @@ import java.io.Serializable;
import ru.touchin.roboswag.core.utils.ObjectUtils;
import ru.touchin.roboswag.core.utils.Optional;
import rx.Observable;
import rx.subjects.BehaviorSubject;
import io.reactivex.Observable;
import io.reactivex.subjects.BehaviorSubject;
/**
* Created by Gavriil Sitnikov on 24/03/2016.
@ -47,7 +47,7 @@ public abstract class BaseChangeable<TValue, TReturnValue> implements Serializab
private transient BehaviorSubject<Optional<TValue>> valueSubject;
public BaseChangeable(@Nullable final TValue defaultValue) {
valueSubject = BehaviorSubject.create(new Optional<>(defaultValue));
valueSubject = BehaviorSubject.createDefault(new Optional<>(defaultValue));
}
@NonNull
@ -88,7 +88,7 @@ public abstract class BaseChangeable<TValue, TReturnValue> implements Serializab
@SuppressWarnings("unchecked")
private void readObject(@NonNull final ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
valueSubject = BehaviorSubject.create((Optional<TValue>) inputStream.readObject());
valueSubject = BehaviorSubject.createDefault((Optional<TValue>) inputStream.readObject());
}
@Override

View File

@ -22,16 +22,15 @@ package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import io.reactivex.Observable;
import ru.touchin.roboswag.core.utils.Optional;
import rx.Observable;
/**
* Created by Gavriil Sitnikov on 24/03/2016.
* Variant of {@link BaseChangeable} which is allows to set nullable values.
* Needed to separate non-null Changeable from nullable Changeable.
*/
//COMPATIBILITY NOTE: in RxJava2 it should extends BaseChangeable<T, Optional<T>>
public class Changeable<T> extends BaseChangeable<T, T> {
public class Changeable<T> extends BaseChangeable<T, Optional<T>> {
public Changeable(@Nullable final T defaultValue) {
super(defaultValue);
@ -44,9 +43,8 @@ public class Changeable<T> extends BaseChangeable<T, T> {
*/
@NonNull
@Override
//COMPATIBILITY NOTE: in RxJava2 it should be Observable<Optional<T>>
public Observable<T> observe() {
return observeOptionalValue().map(Optional::get);
public Observable<Optional<T>> observe() {
return observeOptionalValue();
}
}

View File

@ -23,7 +23,7 @@ import android.support.annotation.NonNull;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Observable;
import io.reactivex.Observable;
/**
* Created by Gavriil Sitnikov on 24/03/2016.

View File

@ -0,0 +1,299 @@
/**
* Copyright (c) 2016-present, RxJava Contributors.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
* the License for the specific language governing permissions and limitations under the License.
*/
package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.Observer;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.disposables.Disposables;
import io.reactivex.functions.Consumer;
import io.reactivex.internal.disposables.DisposableHelper;
import io.reactivex.internal.fuseable.HasUpstreamObservableSource;
import io.reactivex.observables.ConnectableObservable;
import io.reactivex.schedulers.Schedulers;
/**
* Returns an observable sequence that stays connected to the source as long as
* there is at least one subscription to the observable sequence.
*
* @param <T> the value type
*/
@SuppressWarnings({"PMD.CompareObjectsWithEquals", "PMD.AvoidUsingVolatile"})
//AvoidUsingVolatile: it's RxJava code
public final class ObservableRefCountWithCacheTime<T> extends Observable<T> implements HasUpstreamObservableSource<T> {
@NonNull
private final ConnectableObservable<? extends T> connectableSource;
@NonNull
private final ObservableSource<T> actualSource;
@NonNull
private volatile CompositeDisposable baseDisposable = new CompositeDisposable();
@NonNull
private final AtomicInteger subscriptionCount = new AtomicInteger();
/**
* Use this lock for every subscription and disconnect action.
*/
@NonNull
private final ReentrantLock lock = new ReentrantLock();
@NonNull
private final Scheduler scheduler = Schedulers.computation();
private final long cacheTime;
@NonNull
private final TimeUnit cacheTimeUnit;
@Nullable
private Scheduler.Worker worker;
/**
* Constructor.
*
* @param source observable to apply ref count to
*/
public ObservableRefCountWithCacheTime(@NonNull final ConnectableObservable<T> source,
final long cacheTime, @NonNull final TimeUnit cacheTimeUnit) {
super();
this.connectableSource = source;
this.actualSource = source;
this.cacheTime = cacheTime;
this.cacheTimeUnit = cacheTimeUnit;
}
@NonNull
public ObservableSource<T> source() {
return actualSource;
}
private void cleanupWorker() {
if (worker != null) {
worker.dispose();
worker = null;
}
}
@Override
public void subscribeActual(@NonNull final Observer<? super T> subscriber) {
lock.lock();
if (subscriptionCount.incrementAndGet() == 1) {
cleanupWorker();
final AtomicBoolean writeLocked = new AtomicBoolean(true);
try {
// need to use this overload of connect to ensure that
// baseDisposable is set in the case that source is a
// synchronous Observable
connectableSource.connect(onSubscribe(subscriber, writeLocked));
} finally {
// need to cover the case where the source is subscribed to
// outside of this class thus preventing the Action1 passed
// to source.connect above being called
if (writeLocked.get()) {
// Action1 passed to source.connect was not called
lock.unlock();
}
}
} else {
try {
// ready to subscribe to source so do it
doSubscribe(subscriber, baseDisposable);
} finally {
// release the read lock
lock.unlock();
}
}
}
@NonNull
private Consumer<Disposable> onSubscribe(@NonNull final Observer<? super T> observer, @NonNull final AtomicBoolean writeLocked) {
return new DisposeConsumer(observer, writeLocked);
}
private void doSubscribe(@NonNull final Observer<? super T> observer, @NonNull final CompositeDisposable currentBase) {
// handle disposing from the base CompositeDisposable
final Disposable disposable = disconnect(currentBase);
final ConnectionObserver connectionObserver = new ConnectionObserver(observer, currentBase, disposable);
observer.onSubscribe(connectionObserver);
connectableSource.subscribe(connectionObserver);
}
@NonNull
private Disposable disconnect(@NonNull final CompositeDisposable current) {
return Disposables.fromRunnable(new DisposeTask(current));
}
private final class ConnectionObserver extends AtomicReference<Disposable> implements Observer<T>, Disposable {
private static final long serialVersionUID = 3813126992133394324L;
@NonNull
private final Observer<? super T> subscriber;
@NonNull
private final CompositeDisposable currentBase;
@NonNull
private final Disposable resource;
public ConnectionObserver(@NonNull final Observer<? super T> subscriber, @NonNull final CompositeDisposable currentBase,
@NonNull final Disposable resource) {
super();
this.subscriber = subscriber;
this.currentBase = currentBase;
this.resource = resource;
}
@Override
public void onSubscribe(@NonNull final Disposable disposable) {
DisposableHelper.setOnce(this, disposable);
}
@Override
public void onError(@NonNull final Throwable throwable) {
cleanup();
subscriber.onError(throwable);
}
@Override
public void onNext(@NonNull final T item) {
subscriber.onNext(item);
}
@Override
public void onComplete() {
cleanup();
subscriber.onComplete();
}
@Override
public void dispose() {
DisposableHelper.dispose(this);
resource.dispose();
}
@Override
public boolean isDisposed() {
return DisposableHelper.isDisposed(get());
}
private void cleanup() {
// on error or completion we need to dispose the base CompositeDisposable
// and set the subscriptionCount to 0
lock.lock();
try {
if (baseDisposable == currentBase) {
cleanupWorker();
if (connectableSource instanceof Disposable) {
((Disposable) connectableSource).dispose();
}
baseDisposable.dispose();
baseDisposable = new CompositeDisposable();
subscriptionCount.set(0);
}
} finally {
lock.unlock();
}
}
}
private final class DisposeConsumer implements Consumer<Disposable> {
@NonNull
private final Observer<? super T> observer;
@NonNull
private final AtomicBoolean writeLocked;
public DisposeConsumer(@NonNull final Observer<? super T> observer, @NonNull final AtomicBoolean writeLocked) {
this.observer = observer;
this.writeLocked = writeLocked;
}
@Override
public void accept(@NonNull final Disposable subscription) {
try {
baseDisposable.add(subscription);
// ready to subscribe to source so do it
doSubscribe(observer, baseDisposable);
} finally {
// release the write lock
lock.unlock();
writeLocked.set(false);
}
}
}
private final class DisposeTask implements Runnable {
@NonNull
private final CompositeDisposable current;
public DisposeTask(@NonNull final CompositeDisposable current) {
this.current = current;
}
@Override
public void run() {
lock.lock();
try {
if (baseDisposable == current && subscriptionCount.decrementAndGet() == 0) {
if (worker != null) {
worker.dispose();
} else {
worker = scheduler.createWorker();
}
worker.schedule(() -> {
lock.lock();
try {
if (subscriptionCount.get() == 0) {
cleanupWorker();
if (connectableSource instanceof Disposable) {
((Disposable) connectableSource).dispose();
}
baseDisposable.dispose();
// need a new baseDisposable because once
// disposed stays that way
baseDisposable = new CompositeDisposable();
}
} finally {
lock.unlock();
}
}, cacheTime, cacheTimeUnit);
}
} finally {
lock.unlock();
}
}
}
}

View File

@ -1,213 +0,0 @@
/**
* Copyright 2014 Netflix, Inc.
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package ru.touchin.roboswag.core.observables;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import rx.Observable.OnSubscribe;
import rx.Scheduler;
import rx.Subscriber;
import rx.Subscription;
import rx.functions.Action1;
import rx.observables.ConnectableObservable;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
import rx.subscriptions.Subscriptions;
/**
* Returns an observable sequence that stays connected to the source as long as
* there is at least one subscription to the observable sequence and also it stays connected
* for cache time after everyone unsubscribe.
*
* @param <T> the value type
*/
@SuppressWarnings({"PMD.AvoidUsingVolatile", "PMD.CompareObjectsWithEquals"})
//AvoidUsingVolatile,CompareObjectsWithEquals: from OnSubscribeRefCount code
public final class OnSubscribeRefCountWithCacheTime<T> implements OnSubscribe<T> {
@NonNull
private final ConnectableObservable<? extends T> source;
@NonNull
private volatile CompositeSubscription baseSubscription = new CompositeSubscription();
@NonNull
private final AtomicInteger subscriptionCount = new AtomicInteger(0);
@NonNull
private final Scheduler scheduler = Schedulers.computation();
private final long cacheTime;
@NonNull
private final TimeUnit cacheTimeUnit;
@Nullable
private Scheduler.Worker worker;
/**
* Use this lock for every subscription and disconnect action.
*/
@NonNull
private final ReentrantLock lock = new ReentrantLock();
public OnSubscribeRefCountWithCacheTime(@NonNull final ConnectableObservable<? extends T> source,
final long cacheTime, @NonNull final TimeUnit cacheTimeUnit) {
this.source = source;
this.cacheTime = cacheTime;
this.cacheTimeUnit = cacheTimeUnit;
}
@Override
public void call(@NonNull final Subscriber<? super T> subscriber) {
lock.lock();
if (subscriptionCount.incrementAndGet() == 1) {
if (worker != null) {
worker.unsubscribe();
worker = null;
}
final AtomicBoolean writeLocked = new AtomicBoolean(true);
try {
// need to use this overload of connect to ensure that
// baseSubscription is set in the case that source is a
// synchronous Observable
source.connect(onSubscribe(subscriber, writeLocked));
} finally {
// need to cover the case where the source is subscribed to
// outside of this class thus preventing the Action1 passed
// to source.connect above being called
if (writeLocked.get()) {
// Action1 passed to source.connect was not called
lock.unlock();
}
}
} else {
try {
// ready to subscribe to source so do it
doSubscribe(subscriber, baseSubscription);
} finally {
// release the read lock
lock.unlock();
}
}
}
@NonNull
private Action1<Subscription> onSubscribe(@NonNull final Subscriber<? super T> subscriber,
@NonNull final AtomicBoolean writeLocked) {
return subscription -> {
try {
baseSubscription.add(subscription);
// ready to subscribe to source so do it
doSubscribe(subscriber, baseSubscription);
} finally {
// release the write lock
lock.unlock();
writeLocked.set(false);
}
};
}
private void doSubscribe(@NonNull final Subscriber<? super T> subscriber, @NonNull final CompositeSubscription currentBase) {
subscriber.add(disconnect(currentBase));
source.unsafeSubscribe(new Subscriber<T>(subscriber) {
@Override
public void onError(@NonNull final Throwable throwable) {
cleanup();
subscriber.onError(throwable);
}
@Override
public void onNext(@Nullable final T item) {
subscriber.onNext(item);
}
@Override
public void onCompleted() {
cleanup();
subscriber.onCompleted();
}
private void cleanup() {
// on error or completion we need to unsubscribe the base subscription and set the subscriptionCount to 0
lock.lock();
try {
if (baseSubscription == currentBase) {
cleanupWorker();
// backdoor into the ConnectableObservable to cleanup and reset its state
if (source instanceof Subscription) {
((Subscription) source).unsubscribe();
}
baseSubscription.unsubscribe();
baseSubscription = new CompositeSubscription();
subscriptionCount.set(0);
}
} finally {
lock.unlock();
}
}
});
}
@NonNull
private Subscription disconnect(@NonNull final CompositeSubscription current) {
return Subscriptions.create(() -> {
lock.lock();
try {
if (baseSubscription == current && subscriptionCount.decrementAndGet() == 0) {
if (worker != null) {
worker.unsubscribe();
} else {
worker = scheduler.createWorker();
}
worker.schedule(() -> {
lock.lock();
try {
if (subscriptionCount.get() == 0) {
cleanupWorker();
// backdoor into the ConnectableObservable to cleanup and reset its state
if (source instanceof Subscription) {
((Subscription) source).unsubscribe();
}
baseSubscription.unsubscribe();
// need a new baseSubscription because once
// unsubscribed stays that way
baseSubscription = new CompositeSubscription();
}
} finally {
lock.unlock();
}
}, cacheTime, cacheTimeUnit);
}
} finally {
lock.unlock();
}
});
}
private void cleanupWorker() {
if (worker != null) {
worker.unsubscribe();
worker = null;
}
}
}

View File

@ -33,12 +33,12 @@ import android.support.annotation.Nullable;
import java.util.concurrent.CountDownLatch;
import io.reactivex.Emitter;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.android.schedulers.AndroidSchedulers;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.utils.ServiceBinder;
import rx.Emitter;
import rx.Observable;
import rx.Scheduler;
import rx.android.schedulers.AndroidSchedulers;
/**
* Created by Gavriil Sitnikov on 10/01/2016.
@ -62,8 +62,8 @@ public final class RxAndroidUtils {
.<T>create(emitter -> {
onSubscribeServiceConnection.emitter = emitter;
context.bindService(new Intent(context, serviceClass), onSubscribeServiceConnection, Context.BIND_AUTO_CREATE);
}, Emitter.BackpressureMode.LATEST)
.doOnUnsubscribe(() -> {
})
.doOnDispose(() -> {
context.unbindService(onSubscribeServiceConnection);
onSubscribeServiceConnection.emitter = null;
}))
@ -87,8 +87,8 @@ public final class RxAndroidUtils {
.<Intent>create(emitter -> {
onOnSubscribeBroadcastReceiver.emitter = emitter;
context.registerReceiver(onOnSubscribeBroadcastReceiver, intentFilter);
}, Emitter.BackpressureMode.LATEST)
.doOnUnsubscribe(() -> {
})
.doOnDispose(() -> {
context.unregisterReceiver(onOnSubscribeBroadcastReceiver);
onOnSubscribeBroadcastReceiver.emitter = null;
}))

View File

@ -1,182 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import ru.touchin.roboswag.core.observables.collections.changes.Change;
import ru.touchin.roboswag.core.observables.collections.changes.CollectionChanges;
import rx.Emitter;
import rx.Observable;
/**
* Created by Gavriil Sitnikov on 23/05/16.
* Class to represent collection which is providing it's inner changes in Rx observable way.
* Use {@link #observeChanges()} and {@link #observeItems()} to observe collection changes.
* Methods {@link #size()} and {@link #get(int)} will return only already loaded items info.
*
* @param <TItem> Type of collection's items.
*/
public abstract class ObservableCollection<TItem> {
private int changesCount;
@NonNull
private transient Observable<CollectionChanges<TItem>> changesObservable;
@NonNull
private transient Observable<Collection<TItem>> itemsObservable;
@Nullable
private transient Emitter<? super CollectionChanges<TItem>> changesEmitter;
public ObservableCollection() {
this.changesObservable = createChangesObservable();
this.itemsObservable = createItemsObservable();
}
@NonNull
private Observable<CollectionChanges<TItem>> createChangesObservable() {
return Observable
.<CollectionChanges<TItem>>create(emitter -> this.changesEmitter = emitter, Emitter.BackpressureMode.BUFFER)
.doOnUnsubscribe(() -> this.changesEmitter = null)
.share();
}
@NonNull
private Observable<Collection<TItem>> createItemsObservable() {
return Observable
//switchOnNext to calculate getItems() on subscription but not on that method calling moment
.switchOnNext(Observable.fromCallable(() -> observeChanges().map(changes -> getItems()).startWith(getItems())))
.replay(1)
.refCount();
}
/**
* Return changes count number since collection creation.
*
* @return Changes count.
*/
public int getChangesCount() {
return changesCount;
}
/**
* Method to notify that collection have changed.
*
* @param change Change of collection.
*/
protected void notifyAboutChange(@NonNull final List<TItem> insertedItems,
@NonNull final List<TItem> removedItems,
@NonNull final Change change) {
notifyAboutChanges(insertedItems, removedItems, Collections.singleton(change));
}
/**
* Method to notify that collection have changed.
*
* @param insertedItems Collection of inserted items;
* @param removedItems Collection of removed items;
* @param changes Changes of collection.
*/
protected void notifyAboutChanges(@NonNull final List<TItem> insertedItems,
@NonNull final List<TItem> removedItems,
@NonNull final Collection<Change> changes) {
if (changes.isEmpty()) {
return;
}
changesCount++;
if (changesEmitter != null) {
changesEmitter.onNext(new CollectionChanges<>(changesCount, insertedItems, removedItems, changes));
}
}
/**
* Observes changes so it can be used to update UI based on changes etc.
*
* @return List of changes applied to collection.
*/
@NonNull
public Observable<CollectionChanges<TItem>> observeChanges() {
return changesObservable;
}
/**
* Returns already loaded item by position.
* Use it carefully for collections which are loading asynchronously.
*
* @param position Position of item to get;
* @return Item in collection by position.
*/
@NonNull
public abstract TItem get(int position);
/**
* Returns already loaded items.
* Use it carefully for collections which are loading asynchronously.
*
* @return Collection of items.
*/
@NonNull
public abstract Collection<TItem> getItems();
/**
* Returns {@link Observable} to observe items collection.
* Collection returned in onNext is not inner collection but it's copy, actually so you can't modify it.
*
* @return Collection's {@link Observable}.
*/
@NonNull
public Observable<Collection<TItem>> observeItems() {
return itemsObservable;
}
/**
* Returns size of already loaded items.
*
* @return Size.
*/
public abstract int size();
/**
* Returns if already loaded items are empty or not.
*
* @return True if items are empty.
*/
public boolean isEmpty() {
return size() == 0;
}
private void writeObject(@NonNull final ObjectOutputStream outputStream) throws IOException {
outputStream.writeInt(changesCount);
}
private void readObject(@NonNull final ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
changesCount = inputStream.readInt();
this.changesObservable = createChangesObservable();
this.itemsObservable = createItemsObservable();
}
}

View File

@ -1,154 +0,0 @@
package ru.touchin.roboswag.core.observables.collections;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executors;
import ru.touchin.roboswag.core.observables.collections.changes.DefaultCollectionsChangesCalculator;
import rx.Scheduler;
import rx.Subscription;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
/**
* Created by Gavriil Sitnikov on 02/06/2016.
* {@link ObservableCollection} based on simple collection with filter inside.
* Changing filter or collection will provide changes from {@link #observeChanges()}.
*
* @param <TItem> Type of collection's items.
*/
public class ObservableFilteredList<TItem> extends ObservableCollection<TItem> {
// we need to filter on 1 thread to prevent parallel filtering
private static final Scheduler FILTER_SCHEDULER = Schedulers.from(Executors.newSingleThreadExecutor());
@NonNull
private static <TItem> List<TItem> filterCollection(@NonNull final Collection<TItem> sourceCollection,
@Nullable final Func1<TItem, Boolean> filter) {
if (filter == null) {
return new ArrayList<>(sourceCollection);
}
final List<TItem> result = new ArrayList<>(sourceCollection.size());
for (final TItem item : sourceCollection) {
if (filter.call(item)) {
result.add(item);
}
}
return result;
}
@NonNull
private List<TItem> filteredList;
@NonNull
private ObservableCollection<TItem> sourceCollection;
@Nullable
private Func1<TItem, Boolean> filter;
@Nullable
private Subscription sourceCollectionSubscription;
public ObservableFilteredList() {
this(new ArrayList<>(), null);
}
public ObservableFilteredList(@NonNull final Func1<TItem, Boolean> filter) {
this(new ArrayList<>(), filter);
}
public ObservableFilteredList(@NonNull final Collection<TItem> sourceCollection, @Nullable final Func1<TItem, Boolean> filter) {
this(new ObservableList<>(sourceCollection), filter);
}
public ObservableFilteredList(@NonNull final ObservableCollection<TItem> sourceCollection, @Nullable final Func1<TItem, Boolean> filter) {
super();
this.filter = filter;
this.sourceCollection = sourceCollection;
this.filteredList = filterCollection(this.sourceCollection.getItems(), this.filter);
updateInternal();
}
/**
* Sets collection of items to filter.
*
* @param sourceCollection Collection with items.
*/
public void setSourceCollection(@Nullable final ObservableCollection<TItem> sourceCollection) {
this.sourceCollection = sourceCollection != null ? sourceCollection : new ObservableList<>();
updateInternal();
}
/**
* Sets collection of items to filter.
*
* @param sourceCollection Collection with items.
*/
public void setSourceCollection(@Nullable final Collection<TItem> sourceCollection) {
this.sourceCollection = sourceCollection != null ? new ObservableList<>(sourceCollection) : new ObservableList<>();
updateInternal();
}
/**
* Sets filter that should return false as result of call to filter item.
*
* @param filter Function to filter item. True - item will stay, false - item will be filtered.
*/
public void setFilter(@Nullable final Func1<TItem, Boolean> filter) {
this.filter = filter;
updateInternal();
}
private void updateInternal() {
if (sourceCollectionSubscription != null) {
sourceCollectionSubscription.unsubscribe();
sourceCollectionSubscription = null;
}
sourceCollectionSubscription = sourceCollection.observeItems()
.observeOn(FILTER_SCHEDULER)
.subscribe(items -> {
final List<TItem> oldFilteredList = filteredList;
filteredList = filterCollection(items, filter);
final DefaultCollectionsChangesCalculator<TItem> calculator
= new DefaultCollectionsChangesCalculator<>(oldFilteredList, filteredList, false);
notifyAboutChanges(calculator.calculateInsertedItems(), calculator.calculateRemovedItems(), calculator.calculateChanges());
});
}
/**
* Updates collection by current filter. Use it if some item's parameter which is important for filtering have changing.
*/
public void update() {
updateInternal();
}
@Override
public int size() {
return filteredList.size();
}
@NonNull
@Override
public TItem get(final int position) {
return filteredList.get(position);
}
@NonNull
@Override
public Collection<TItem> getItems() {
return Collections.unmodifiableCollection(filteredList);
}
/**
* Returns source non-filtered observable collection of items.
*
* @return Non-filtered collection of items.
*/
@NonNull
public ObservableCollection<TItem> getSourceCollection() {
return sourceCollection;
}
}

View File

@ -1,298 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.observables.collections.changes.Change;
import ru.touchin.roboswag.core.observables.collections.changes.ChangePayloadProducer;
import ru.touchin.roboswag.core.observables.collections.changes.CollectionsChangesCalculator;
import ru.touchin.roboswag.core.observables.collections.changes.DefaultCollectionsChangesCalculator;
import ru.touchin.roboswag.core.observables.collections.changes.DiffCollectionsChangesCalculator;
import ru.touchin.roboswag.core.observables.collections.changes.SameItemsPredicate;
/**
* Created by Gavriil Sitnikov on 23/05/16.
* {@link ObservableCollection} that is based on list.
* So it is providing similar List's methods like adding/removing/clearing etc.
* But! You can observe it's changes.
*
* @param <TItem> Type of collection's items.
*/
public class ObservableList<TItem> extends ObservableCollection<TItem> implements Serializable {
private static final long serialVersionUID = 1L;
@NonNull
private List<TItem> items;
private boolean detectMoves;
@Nullable
private SameItemsPredicate<TItem> sameItemsPredicate;
@Nullable
private ChangePayloadProducer<TItem> changePayloadProducer;
public ObservableList() {
super();
items = new ArrayList<>();
}
public ObservableList(@NonNull final Collection<TItem> initialItems) {
super();
items = new ArrayList<>(initialItems);
}
/**
* Adding item at the end of list.
*
* @param item Item to add.
*/
public void add(@NonNull final TItem item) {
add(items.size(), item);
}
/**
* Adding item at specific list position.
*
* @param position Position to add item to;
* @param item Item to add.
*/
public void add(final int position, @NonNull final TItem item) {
synchronized (this) {
items.add(position, item);
notifyAboutChange(Collections.singletonList(item), Collections.emptyList(), new Change.Inserted(position, 1));
}
}
/**
* Adding items at the end of list.
*
* @param itemsToAdd Items to add.
*/
public void addAll(@NonNull final Collection<TItem> itemsToAdd) {
addAll(items.size(), itemsToAdd);
}
/**
* Adding items at specific list position.
*
* @param position Position to add items to;
* @param itemsToAdd Items to add.
*/
public void addAll(final int position, @NonNull final Collection<TItem> itemsToAdd) {
synchronized (this) {
if (!itemsToAdd.isEmpty()) {
items.addAll(position, itemsToAdd);
notifyAboutChange(new ArrayList<>(itemsToAdd), Collections.emptyList(), new Change.Inserted(position, itemsToAdd.size()));
}
}
}
/**
* Removing item.
*
* @param item Item to remove.
*/
public void remove(@NonNull final TItem item) {
synchronized (this) {
final int position = indexOf(item);
if (position < 0) {
Lc.assertion("Illegal removing of item " + item);
return;
}
remove(position);
}
}
/**
* Removing item by position.
*
* @param position Position to remove item from.
*/
public void remove(final int position) {
remove(position, 1);
}
/**
* Removing items by position.
*
* @param position Position to remove items from;
* @param count Count of items to remove.
*/
public void remove(final int position, final int count) {
if (count == 0) {
return;
}
synchronized (this) {
final List<TItem> removedItems = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
removedItems.add(items.remove(position));
}
notifyAboutChange(Collections.emptyList(), removedItems, new Change.Removed(position, count));
}
}
/**
* Removing all items from list.
*/
public void clear() {
synchronized (this) {
if (!items.isEmpty()) {
final List<TItem> removedItems = new ArrayList<>(items);
items.clear();
notifyAboutChange(Collections.emptyList(), removedItems, new Change.Removed(0, removedItems.size()));
}
}
}
@NonNull
@Override
public TItem get(final int position) {
synchronized (this) {
return items.get(position);
}
}
@NonNull
@Override
public Collection<TItem> getItems() {
synchronized (this) {
return Collections.unmodifiableCollection(new ArrayList<>(items));
}
}
/**
* Replace item at specific position.
*
* @param position Position to replace item;
* @param item Item to place.
*/
public void update(final int position, @NonNull final TItem item) {
update(position, Collections.singleton(item));
}
/**
* Replace items at specific position.
*
* @param position Position to replace items;
* @param updatedItems Items to place.
*/
public void update(final int position, @NonNull final Collection<TItem> updatedItems) {
if (updatedItems.isEmpty()) {
return;
}
int index = position;
synchronized (this) {
for (final TItem item : updatedItems) {
items.set(index, item);
index++;
}
notifyAboutChange(Collections.emptyList(), Collections.emptyList(), new Change.Changed(position, updatedItems.size(), null));
}
}
/**
* Resetting all items in list to new ones.
*
* @param newItems New items to set.
*/
public void set(@NonNull final Collection<TItem> newItems) {
synchronized (this) {
final List<TItem> oldList = new ArrayList<>(items);
final List<TItem> newList = new ArrayList<>(newItems);
final CollectionsChangesCalculator<TItem> calculator = sameItemsPredicate != null
? new DiffCollectionsChangesCalculator<>(oldList, newList, detectMoves, sameItemsPredicate, changePayloadProducer)
: new DefaultCollectionsChangesCalculator<>(oldList, newList, false);
items.clear();
items.addAll(newItems);
notifyAboutChanges(calculator.calculateInsertedItems(), calculator.calculateRemovedItems(), calculator.calculateChanges());
}
}
@Override
public int size() {
synchronized (this) {
return items.size();
}
}
/**
* Enable diff utils algorithm in collection changes.
*
* @param detectMoves The flag that determines whether the {@link Change.Moved} changes will be generated or not;
* @param sameItemsPredicate Predicate for the determination of the same elements;
* @param changePayloadProducer Function that calculate change payload when items the same but contents are different.
*/
public void enableDiffUtils(final boolean detectMoves,
@NonNull final SameItemsPredicate<TItem> sameItemsPredicate,
@Nullable final ChangePayloadProducer<TItem> changePayloadProducer) {
this.detectMoves = detectMoves;
this.sameItemsPredicate = sameItemsPredicate;
this.changePayloadProducer = changePayloadProducer;
}
/**
* Disable diff utils algorithm.
*/
public void disableDiffUtils() {
this.sameItemsPredicate = null;
}
/**
* Returns enabled flag of diff utils.
*
* @return true if diff utils is enabled.
*/
public boolean diffUtilsIsEnabled() {
return sameItemsPredicate != null;
}
/**
* Returns position of item in list.
*
* @param item Item to find index of;
* @return Position of item in list or -1 if item not found.
*/
public int indexOf(@NonNull final TItem item) {
synchronized (this) {
return items.indexOf(item);
}
}
private void writeObject(@NonNull final ObjectOutputStream outputStream) throws IOException {
outputStream.writeObject(items);
}
@SuppressWarnings("unchecked")
private void readObject(@NonNull final ObjectInputStream inputStream) throws IOException, ClassNotFoundException {
items = (List<TItem>) inputStream.readObject();
}
}

View File

@ -1,134 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.changes;
import android.support.annotation.Nullable;
/**
* Created by Gavriil Sitnikov on 23/05/16.
* Class representing simple change of collection like insertion, remove or replacing/changing items.
*/
public abstract class Change {
/**
* Represents a insert operation in collection.
*/
public static class Inserted extends Change {
private final int position;
private final int count;
public Inserted(final int position, final int count) {
super();
this.position = position;
this.count = count;
}
public int getPosition() {
return position;
}
public int getCount() {
return count;
}
}
/**
* Represents a remove operation from collection.
*/
public static class Removed extends Change {
private final int position;
private final int count;
public Removed(final int position, final int count) {
super();
this.position = position;
this.count = count;
}
public int getPosition() {
return position;
}
public int getCount() {
return count;
}
}
/**
* Represents a move operation in collection.
*/
public static class Moved extends Change {
private final int fromPosition;
private final int toPosition;
public Moved(final int fromPosition, final int toPosition) {
super();
this.fromPosition = fromPosition;
this.toPosition = toPosition;
}
public int getFromPosition() {
return fromPosition;
}
public int getToPosition() {
return toPosition;
}
}
/**
* Represents a modification operation in a collection.
*/
public static class Changed extends Change {
private final int position;
private final int count;
@Nullable
private final Object payload;
public Changed(final int position, final int count, @Nullable final Object payload) {
super();
this.position = position;
this.count = count;
this.payload = payload;
}
public int getPosition() {
return position;
}
public int getCount() {
return count;
}
@Nullable
public Object getPayload() {
return payload;
}
}
}

View File

@ -1,42 +0,0 @@
/*
* Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.changes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
* Functional interface for calculating change payload between two items same type.
* Payload calculating when items are same {@link SameItemsPredicate}, but content different.
*/
public interface ChangePayloadProducer<TItem> {
/**
* Calculate change payload between two items.
*
* @param item1 First item;
* @param item2 Second item;
* @return Object that represents minimal changes between two items.
*/
@Nullable
Object getChangePayload(@NonNull TItem item1, @NonNull TItem item2);
}

View File

@ -1,90 +0,0 @@
/*
* Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.changes;
import android.support.annotation.NonNull;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Class which is representing change of collection. There could be multiple changes applied to collection.
*/
public class CollectionChanges<TItem> {
private final int number;
@NonNull
private final List<TItem> insertedItems;
@NonNull
private final List<TItem> removedItems;
@NonNull
private final Collection<Change> changes;
public CollectionChanges(final int number,
@NonNull final List<TItem> insertedItems,
@NonNull final List<TItem> removedItems,
@NonNull final Collection<Change> changes) {
this.number = number;
this.insertedItems = Collections.unmodifiableList(insertedItems);
this.removedItems = Collections.unmodifiableList(removedItems);
this.changes = Collections.unmodifiableCollection(changes);
}
/**
* Returns number of change.
*
* @return Number of change.
*/
public int getNumber() {
return number;
}
/**
* Returns collection of changes.
*
* @return Collection of changes.
*/
@NonNull
public Collection<Change> getChanges() {
return changes;
}
/**
* Returns inserted items in change.
*
* @return Inserted items.
*/
@NonNull
public List<TItem> getInsertedItems() {
return insertedItems;
}
/**
* Returns removed items in change.
*
* @return Removed items.
*/
@NonNull
public List<TItem> getRemovedItems() {
return removedItems;
}
}

View File

@ -1,55 +0,0 @@
/*
* Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.changes;
import android.support.annotation.NonNull;
import java.util.List;
/**
* Interface that represent changes calculator between two collections.
*/
public interface CollectionsChangesCalculator<TItem> {
/**
* Calculate changes between two collection as collection of objects {@link Change}.
*
* @return List of changes.
*/
@NonNull
List<Change> calculateChanges();
/**
* Calculate changes between two collection as collection of inserted items.
*
* @return List of inserted item.
*/
@NonNull
List<TItem> calculateInsertedItems();
/**
* Calculate changes between two collection as collection of removed items.
*
* @return List of removed item.
*/
@NonNull
List<TItem> calculateRemovedItems();
}

View File

@ -1,162 +0,0 @@
/*
* Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.changes;
import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Default calculator between two collections that use equals function.
*/
public class DefaultCollectionsChangesCalculator<TItem> implements CollectionsChangesCalculator<TItem> {
@NonNull
private final Collection<TItem> initialCollection;
@NonNull
private final Collection<TItem> modifiedCollection;
private final boolean shrinkChangesToModifiedSize;
@NonNull
private final Collection<TItem> itemsToAdd = new ArrayList<>();
private int currentSize;
private int oldSize;
private int newSize;
private int couldBeAdded;
/**
* Default calculator of changes between two collections.
*
* @param initialCollection Initial collection;
* @param modifiedCollection Changed collection;
* @param shrinkChangesToModifiedSize Flag to make position of changed items be less then modified collection size.
* It is needed sometimes to not get exceptions like {@link ArrayIndexOutOfBoundsException}.
*/
public DefaultCollectionsChangesCalculator(@NonNull final Collection<TItem> initialCollection,
@NonNull final Collection<TItem> modifiedCollection,
final boolean shrinkChangesToModifiedSize) {
super();
this.initialCollection = initialCollection;
this.modifiedCollection = modifiedCollection;
this.shrinkChangesToModifiedSize = shrinkChangesToModifiedSize;
}
@NonNull
@Override
public List<Change> calculateChanges() {
int initialOffset = 0;
itemsToAdd.clear();
currentSize = 0;
oldSize = initialCollection.size();
newSize = modifiedCollection.size();
couldBeAdded = modifiedCollection.size() - initialCollection.size();
final List<Change> result = new ArrayList<>();
for (final TItem modifiedItem : modifiedCollection) {
int foundPosition = 0;
for (final Object initialObject : initialCollection) {
if (foundPosition >= initialOffset && modifiedItem.equals(initialObject)) {
if (tryAddSkipped(result) == MethodAction.RETURN
|| tryRemoveRest(result, foundPosition - initialOffset) == MethodAction.RETURN) {
return result;
}
initialOffset = foundPosition + 1;
currentSize++;
break;
}
foundPosition++;
}
// if not found
if (foundPosition >= initialCollection.size()) {
itemsToAdd.add(modifiedItem);
}
}
if (tryAddSkipped(result) == MethodAction.RETURN) {
return result;
}
tryRemoveRest(result, initialCollection.size() - initialOffset);
return result;
}
@NonNull
@Override
public List<TItem> calculateInsertedItems() {
final List<TItem> insertedItems = new ArrayList<>();
for (final TItem newItem : modifiedCollection) {
if (!initialCollection.contains(newItem)) {
insertedItems.add(newItem);
}
}
return insertedItems;
}
@NonNull
@Override
public List<TItem> calculateRemovedItems() {
final List<TItem> removedItems = new ArrayList<>();
for (final TItem oldItem : initialCollection) {
if (!modifiedCollection.contains(oldItem)) {
removedItems.add(oldItem);
}
}
return removedItems;
}
@NonNull
private MethodAction tryAddSkipped(@NonNull final Collection<Change> changes) {
if (!itemsToAdd.isEmpty()) {
if (shrinkChangesToModifiedSize && couldBeAdded < itemsToAdd.size()) {
addSimpleDifferenceChanges(changes);
return MethodAction.RETURN;
}
changes.add(new Change.Inserted(currentSize, itemsToAdd.size()));
currentSize += itemsToAdd.size();
couldBeAdded -= itemsToAdd.size();
itemsToAdd.clear();
}
return MethodAction.CONTINUE;
}
@NonNull
private MethodAction tryRemoveRest(@NonNull final Collection<Change> changes, final int itemsToRemove) {
if (itemsToRemove > 0) {
if (shrinkChangesToModifiedSize && couldBeAdded < -itemsToRemove) {
addSimpleDifferenceChanges(changes);
return MethodAction.RETURN;
}
changes.add(new Change.Removed(currentSize, itemsToRemove));
}
return MethodAction.CONTINUE;
}
private void addSimpleDifferenceChanges(@NonNull final Collection<Change> changes) {
changes.add(new Change.Changed(currentSize, newSize - currentSize, null));
if (oldSize - newSize > 0) {
changes.add(new Change.Removed(newSize, oldSize - newSize));
}
}
private enum MethodAction {
RETURN,
CONTINUE
}
}

View File

@ -1,147 +0,0 @@
/*
* Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.changes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import ru.touchin.roboswag.core.android.support.v7.util.DiffUtil;
import ru.touchin.roboswag.core.android.support.v7.util.ListUpdateCallback;
/**
* Implementation of {@link CollectionsChangesCalculator} based on DiffUtils from support library.
*/
public class DiffCollectionsChangesCalculator<TItem> extends DiffUtil.Callback implements CollectionsChangesCalculator<TItem> {
@NonNull
private final List<TItem> oldList;
@NonNull
private final List<TItem> newList;
private final boolean detectMoves;
@NonNull
private final SameItemsPredicate<TItem> sameItemsPredicate;
@Nullable
private final ChangePayloadProducer<TItem> changePayloadProducer;
public DiffCollectionsChangesCalculator(@NonNull final List<TItem> oldList,
@NonNull final List<TItem> newList,
final boolean detectMoves,
@NonNull final SameItemsPredicate<TItem> sameItemsPredicate,
@Nullable final ChangePayloadProducer<TItem> changePayloadProducer) {
super();
this.oldList = oldList;
this.newList = newList;
this.detectMoves = detectMoves;
this.sameItemsPredicate = sameItemsPredicate;
this.changePayloadProducer = changePayloadProducer;
}
@NonNull
@Override
public List<Change> calculateChanges() {
final List<Change> changes = new ArrayList<>();
DiffUtil.calculateDiff(this, detectMoves).dispatchUpdatesTo(new ListUpdateCallback() {
@Override
public void onInserted(final int position, final int count) {
changes.add(new Change.Inserted(position, count));
}
@Override
public void onRemoved(final int position, final int count) {
changes.add(new Change.Removed(position, count));
}
@Override
public void onMoved(final int fromPosition, final int toPosition) {
changes.add(new Change.Moved(fromPosition, toPosition));
}
@Override
public void onChanged(final int position, final int count, @Nullable final Object payload) {
changes.add(new Change.Changed(position, count, payload));
}
});
return changes;
}
@NonNull
@Override
public List<TItem> calculateInsertedItems() {
final List<TItem> insertedItems = new ArrayList<>();
for (final TItem newItem : newList) {
if (!containsByPredicate(newItem, oldList)) {
insertedItems.add(newItem);
}
}
return insertedItems;
}
@NonNull
@Override
public List<TItem> calculateRemovedItems() {
final List<TItem> removedItems = new ArrayList<>();
for (final TItem oldItem : oldList) {
if (!containsByPredicate(oldItem, newList)) {
removedItems.add(oldItem);
}
}
return removedItems;
}
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(final int oldItemPosition, final int newItemPosition) {
return sameItemsPredicate.areSame(oldList.get(oldItemPosition), newList.get(newItemPosition));
}
@Override
public boolean areContentsTheSame(final int oldItemPosition, final int newItemPosition) {
return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
}
@Nullable
@Override
public Object getChangePayload(final int oldItemPosition, final int newItemPosition) {
return changePayloadProducer != null
? changePayloadProducer.getChangePayload(oldList.get(oldItemPosition), newList.get(newItemPosition)) : null;
}
private boolean containsByPredicate(@NonNull final TItem searchedItem, @NonNull final List<TItem> items) {
for (final TItem item : items) {
if (sameItemsPredicate.areSame(item, searchedItem)) {
return true;
}
}
return false;
}
}

View File

@ -1,40 +0,0 @@
/*
* Copyright (c) 2017 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.changes;
import android.support.annotation.NonNull;
/**
* Functional interface for determine same objects. Usually this is just the comparison by id.
*
* @param <TItem> Type of objects
*/
public interface SameItemsPredicate<TItem> {
/**
* Function for determine same objects.
*
* @param item1 First object;
* @param item2 Second object;
* @return True if items are same.
*/
boolean areSame(@NonNull TItem item1, @NonNull TItem item2);
}

View File

@ -1,61 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.loadable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.Collection;
/**
* Created by Gavriil Sitnikov on 23/05/16.
* Object represents loaded items with reference to load other parts and info of are there more items to load or not.
*
* @param <TItem> Type of items to load;
* @param <TReference> Type of reference to load other parts of items.
*/
public interface LoadedItems<TItem, TReference> {
int UNKNOWN_ITEMS_COUNT = -1;
/**
* Returns count of items that could be loaded more.
*
* @return Count of items to load more or UNKNOWN_ITEMS_COUNT if it's unknown info.
*/
int getMoreItemsCount();
/**
* Returns loaded items.
*
* @return Loaded items.
*/
@NonNull
Collection<TItem> getItems();
/**
* Returns reference that could be used to load other parts of items.
*
* @return Reference object.
*/
@Nullable
TReference getReference();
}

View File

@ -1,400 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.loadable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.Executors;
import ru.touchin.roboswag.core.log.Lc;
import ru.touchin.roboswag.core.observables.collections.ObservableCollection;
import ru.touchin.roboswag.core.observables.collections.ObservableList;
import ru.touchin.roboswag.core.observables.collections.changes.Change;
import ru.touchin.roboswag.core.observables.collections.changes.CollectionChanges;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Observable;
import rx.Scheduler;
import rx.exceptions.OnErrorThrowable;
import rx.functions.Func0;
import rx.functions.Func1;
import rx.schedulers.Schedulers;
import rx.subjects.BehaviorSubject;
/**
* Created by Gavriil Sitnikov on 23/05/16.
* {@link ObservableCollection} which is loading items more and more by paging/limit-offset/reference-based mechanisms.
* To use this collection {@link MoreItemsLoader} should be created.
* {@link MoreItemsLoader} is an object to load next block of items by info from previous loaded block (last loaded item/reference etc.).
* Use {@link #loadItem(int)} and {@link #loadRange(int, int)} to load items asynchronously.
*
* @param <TItem> Type of collection's items;
* @param <TMoreReference> Type of reference object to help rightly loading next block of items;
* @param <TLoadedItems> Type of loading block of items.
*/
public class LoadingMoreList<TItem, TMoreReference, TLoadedItems extends LoadedItems<TItem, TMoreReference>>
extends ObservableCollection<TItem> {
private static final int RETRY_LOADING_AFTER_CHANGE_COUNT = 5;
private static final LoadedItemsFilter<?> DUPLICATES_REMOVER = (collectionObject, loadedItemsObject) ->
collectionObject.equals(loadedItemsObject) ? FilterAction.REMOVE_FROM_LOADED_ITEMS : FilterAction.DO_NOTHING;
@NonNull
private final Scheduler loaderScheduler = Schedulers.from(Executors.newSingleThreadExecutor());
@NonNull
private Observable<TLoadedItems> loadingMoreObservable;
@NonNull
private final BehaviorSubject<Integer> moreItemsCount = BehaviorSubject.create(LoadedItems.UNKNOWN_ITEMS_COUNT);
@NonNull
private final ObservableList<TItem> innerList = new ObservableList<>();
@Nullable
private LoadedItemsFilter<TItem> loadedItemsFilter;
@Nullable
private TMoreReference moreItemsReference;
public LoadingMoreList(@NonNull final MoreItemsLoader<TItem, TMoreReference, TLoadedItems> moreMoreItemsLoader) {
this(moreMoreItemsLoader, null);
}
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
//ConstructorCallsOverridableMethod: actually it is calling in lambda callback
public LoadingMoreList(@NonNull final MoreItemsLoader<TItem, TMoreReference, TLoadedItems> moreMoreItemsLoader,
@Nullable final LoadedItems<TItem, TMoreReference> initialItems) {
super();
this.loadingMoreObservable = Observable
.switchOnNext(Observable.fromCallable(() -> createLoadRequestBasedObservable(this::createActualRequest, moreMoreItemsLoader::load)))
.single()
.doOnError(throwable -> {
if (throwable instanceof IllegalArgumentException || throwable instanceof NoSuchElementException) {
Lc.assertion(new ShouldNotHappenException("Updates during loading not supported."
+ " MoreItemsLoader should emit only one result.", throwable));
}
})
.doOnNext(loadedItems -> onItemsLoaded(loadedItems, size(), false))
.replay(1)
.refCount();
if (initialItems != null) {
innerOnItemsLoaded(initialItems, 0, false);
}
}
@Nullable
public TMoreReference getMoreItemsReference() {
return moreItemsReference;
}
@NonNull
private MoreLoadRequest<TMoreReference> createActualRequest() {
return new MoreLoadRequest<>(moreItemsReference, Math.max(0, size()));
}
@NonNull
protected <T, TRequest> Observable<T> createLoadRequestBasedObservable(@NonNull final Func0<TRequest> requestCreator,
@NonNull final Func1<TRequest, Observable<T>> observableCreator) {
return Observable
.fromCallable(requestCreator)
.switchMap(loadRequest -> observableCreator.call(loadRequest)
.subscribeOn(Schedulers.io())
.observeOn(loaderScheduler)
.doOnNext(ignored -> {
if (!requestCreator.call().equals(loadRequest)) {
throw OnErrorThrowable.from(new RequestChangedDuringLoadingException());
}
}))
.retry((number, throwable) ->
number <= RETRY_LOADING_AFTER_CHANGE_COUNT && throwable instanceof RequestChangedDuringLoadingException);
}
@NonNull
protected final Scheduler getLoaderScheduler() {
return loaderScheduler;
}
@NonNull
@Override
public Observable<CollectionChanges<TItem>> observeChanges() {
return innerList.observeChanges();
}
@Override
protected void notifyAboutChanges(@NonNull final List<TItem> insertedItems,
@NonNull final List<TItem> removedItems,
@NonNull final Collection<Change> changes) {
Lc.assertion("Illegal operation. Modify getInnerList()");
}
/**
* Returns {@link ObservableList} of already loaded items so you can modify it.
*
* @return {@link ObservableList} of already loaded items.
*/
@NonNull
protected ObservableList<TItem> getInnerList() {
return innerList;
}
/**
* Returns if there are more items to load.
*
* @return True if there are more items to load.
*/
public boolean hasMoreItems() {
return moreItemsCount.getValue() != 0;
}
/**
* Returns {@link Observable} which is providing status of if is there are more items to load or not.
*
* @return {@link Observable} of more items availability status.
*/
@NonNull
public Observable<Boolean> observeHasMoreItems() {
return moreItemsCount.map(count -> count != 0).distinctUntilChanged();
}
/**
* Returns {@link Observable} which is providing count of more items to load.
*
* @return {@link Observable} of more items availability status.
*/
@NonNull
public Observable<Integer> observeMoreItemsCount() {
return moreItemsCount.distinctUntilChanged();
}
/**
* Sets if duplicates (compared by {@link #equals(Object)}) should be removed from loaded part of items right after loading.
*
* @param removeDuplicates True if duplicates should be removed.
*/
@SuppressWarnings("unchecked")
//unchecked: it's OK as we are using private static filter
public void setRemoveDuplicates(final boolean removeDuplicates) {
if (this.loadedItemsFilter != null && this.loadedItemsFilter != DUPLICATES_REMOVER) {
Lc.assertion("Remove old filter manually first");
return;
}
this.loadedItemsFilter = removeDuplicates ? (LoadedItemsFilter<TItem>) DUPLICATES_REMOVER : null;
}
/**
* Sets specific filter object which will remove items from already loaded part or from new loaded items right after loading.
*
* @param loadedItemsFilter {@link LoadedItemsFilter} to make decision of removing items.
*/
public void setLoadedItemsFilter(@Nullable final LoadedItemsFilter<TItem> loadedItemsFilter) {
this.loadedItemsFilter = loadedItemsFilter;
}
private void innerOnItemsLoaded(@NonNull final LoadedItems<TItem, TMoreReference> loadedItems, final int insertPosition, final boolean reset) {
final List<TItem> items = new ArrayList<>(loadedItems.getItems());
final boolean lastPage = reset || insertPosition > size() - 1;
if (reset) {
resetState();
if (insertPosition != 0) {
Lc.assertion("Wrong insert position " + insertPosition);
}
innerList.set(items);
} else {
if (this.loadedItemsFilter != null) {
filterList(items, this.loadedItemsFilter);
}
innerList.addAll(insertPosition, items);
}
if (lastPage) {
moreItemsReference = loadedItems.getReference();
moreItemsCount.onNext(loadedItems.getMoreItemsCount());
}
}
/**
* Calls when any new items part loaded.
*
* @param loadedItems Loaded items;
* @param insertPosition Position to insert loaded items;
* @param reset Flag to reset previously loaded items or not.
*/
protected void onItemsLoaded(@NonNull final TLoadedItems loadedItems, final int insertPosition, final boolean reset) {
innerOnItemsLoaded(loadedItems, insertPosition, reset);
}
private void filterList(@NonNull final List<TItem> items, @NonNull final LoadedItemsFilter<TItem> loadedItemsFilter) {
for (int i = items.size() - 1; i >= 0; i--) {
for (int j = innerList.size() - 1; j >= 0; j--) {
final FilterAction filterAction = loadedItemsFilter.decideFilterAction(innerList.get(j), items.get(i));
if (filterAction == FilterAction.REMOVE_FROM_LOADED_ITEMS) {
items.remove(i);
break;
}
if (filterAction == FilterAction.REMOVE_FROM_COLLECTION) {
innerList.remove(j);
}
if (filterAction == FilterAction.REPLACE_SOURCE_ITEM_WITH_LOADED) {
innerList.update(j, items.get(i));
items.remove(i);
break;
}
}
}
}
@Override
public int size() {
return innerList.size();
}
@NonNull
@Override
public TItem get(final int position) {
return innerList.get(position);
}
@NonNull
@Override
public Collection<TItem> getItems() {
return innerList.getItems();
}
/**
* Returns {@link Observable} that is loading new items.
*
* @return {@link Observable} that is loading new items.
*/
@NonNull
protected Observable<TLoadedItems> getLoadingMoreObservable() {
return loadingMoreObservable;
}
/**
* Returns {@link Observable} which is loading item by position.
* It could return null in onNext callback if there is no item to load for such position.
*
* @param position Position to load item;
* @return {@link Observable} to load item.
*/
@NonNull
public Observable<TItem> loadItem(final int position) {
return Observable
.switchOnNext(Observable
.fromCallable(() -> {
if (position < size()) {
return Observable.just(get(position));
} else if (moreItemsCount.getValue() == 0) {
return Observable.just((TItem) null);
} else {
return loadingMoreObservable.switchMap(ignored -> Observable.<TItem>error(new NotLoadedYetException()));
}
})
.subscribeOn(loaderScheduler))
.retry((number, throwable) -> throwable instanceof NotLoadedYetException);
}
/**
* Returns {@link Observable} which is loading item by range.
* It will return collection of loaded items in onNext callback.
*
* @param first First position of item to load;
* @param last Last position of item to load;
* @return {@link Observable} to load items.
*/
@NonNull
public Observable<Collection<TItem>> loadRange(final int first, final int last) {
final List<Observable<TItem>> itemsRequests = new ArrayList<>();
for (int i = first; i <= last; i++) {
itemsRequests.add(loadItem(i));
}
return Observable.concatEager(itemsRequests)
.filter(loadedItem -> loadedItem != null)
.toList()
.map(Collections::unmodifiableCollection);
}
/**
* Remove all loaded items and resets collection's state.
*/
public void reset() {
innerList.clear();
resetState();
}
/**
* Remove all loaded items and resets collection's state but sets some initial items.
*
* @param initialItems initial items to be set after reset.
*/
public void reset(@NonNull final TLoadedItems initialItems) {
onItemsLoaded(initialItems, 0, true);
}
protected void resetState() {
moreItemsReference = null;
moreItemsCount.onNext(LoadedItems.UNKNOWN_ITEMS_COUNT);
}
/**
* Action to do with some items while new part of items have loaded.
*/
public enum FilterAction {
DO_NOTHING,
REMOVE_FROM_COLLECTION,
REMOVE_FROM_LOADED_ITEMS,
REPLACE_SOURCE_ITEM_WITH_LOADED
}
/**
* Class that is representing object to decide what to do with some items from already loaded and newly loaded part.
* It should remove duplicates or items with changed data.
*
* @param <TItem> Type of collection's items.
*/
public interface LoadedItemsFilter<TItem> {
/**
* Returns action to do based on items: do nothing, remove already loaded item or remove newly loaded item.
*
* @param collectionObject Item from collection of already loaded items;
* @param loadedItemsObject Item from collection of newly loaded items part;
* @return Action to do with items.
*/
@NonNull
FilterAction decideFilterAction(@NonNull TItem collectionObject, @NonNull TItem loadedItemsObject);
}
/**
* Helper exception happens if {@link #loadItem(int)} called with big index and latest loading items part still not reached such item.
*/
protected static class NotLoadedYetException extends Exception {
}
/**
* Exception happens if loading request changed during loading so loaded items are not actual anymore.
*/
protected static class RequestChangedDuringLoadingException extends Exception {
}
}

View File

@ -1,45 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.loadable;
import android.support.annotation.NonNull;
import rx.Observable;
/**
* Created by Gavriil Sitnikov on 02/06/2016.
* Object that is loading next part of items by reference or position.
*
* @param <TItem> Type of items to be loaded;
* @param <TMoreReference> Type of reference to be used to load next part of items;
* @param <TLoadedItems> Type of loaded items part.
*/
public interface MoreItemsLoader<TItem, TMoreReference, TLoadedItems extends LoadedItems<TItem, TMoreReference>> {
/**
* Returns {@link Observable} that could load next part of items.
*
* @param moreLoadRequest Request with info inside to load next part of items;
* @return {@link Observable} of loading items.
*/
@NonNull
Observable<TLoadedItems> load(@NonNull final MoreLoadRequest<TMoreReference> moreLoadRequest);
}

View File

@ -1,74 +0,0 @@
/*
* Copyright (c) 2015 RoboSwag (Gavriil Sitnikov, Vsevolod Ivanov)
*
* This file is part of RoboSwag library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package ru.touchin.roboswag.core.observables.collections.loadable;
import android.support.annotation.Nullable;
import ru.touchin.roboswag.core.utils.ObjectUtils;
/**
* Created by Gavriil Sitnikov on 02/06/2016.
* Request represents request to load next part of items.
*
* @param <TMoreReference> Type of reference to load next part of items.
*/
public class MoreLoadRequest<TMoreReference> {
@Nullable
private final TMoreReference moreReference;
private final int nextPosition;
public MoreLoadRequest(@Nullable final TMoreReference moreReference, final int nextPosition) {
this.moreReference = moreReference;
this.nextPosition = nextPosition;
}
/**
* Returns reference to be used to load next part of items.
*
* @return Reference object.
*/
@Nullable
public TMoreReference getReference() {
return moreReference;
}
/**
* Returns position of next item to load.
*
* @return Position of next item.
*/
public int getNextPosition() {
return nextPosition;
}
@Override
public boolean equals(@Nullable final Object object) {
return object instanceof MoreLoadRequest
&& ObjectUtils.equals(((MoreLoadRequest) object).moreReference, moreReference)
&& ((MoreLoadRequest) object).nextPosition == nextPosition;
}
@Override
public int hashCode() {
return nextPosition + (moreReference != null ? moreReference.hashCode() : 0);
}
}

View File

@ -26,19 +26,17 @@ import java.lang.reflect.Type;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.log.Lc;
import io.reactivex.Completable;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.plugins.RxJavaPlugins;
import io.reactivex.schedulers.Schedulers;
import io.reactivex.subjects.PublishSubject;
import ru.touchin.roboswag.core.log.LcGroup;
import ru.touchin.roboswag.core.observables.OnSubscribeRefCountWithCacheTime;
import ru.touchin.roboswag.core.observables.ObservableRefCountWithCacheTime;
import ru.touchin.roboswag.core.utils.ObjectUtils;
import ru.touchin.roboswag.core.utils.Optional;
import rx.Completable;
import rx.Observable;
import rx.Scheduler;
import rx.Single;
import rx.exceptions.OnErrorThrowable;
import rx.functions.Actions;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
@ -123,7 +121,8 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
}
@Nullable
private Optional<TStoreObject> returnDefaultValueIfNull(@NonNull final Optional<TStoreObject> storeObject, @Nullable final TObject defaultValue) {
private Optional<TStoreObject> returnDefaultValueIfNull(@NonNull final Optional<TStoreObject> storeObject, @Nullable final TObject defaultValue)
throws Converter.ConversionException {
if (storeObject.get() != null || defaultValue == null) {
return storeObject;
}
@ -133,7 +132,7 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
} catch (final Converter.ConversionException exception) {
STORABLE_LC_GROUP.w(exception, "Exception while converting default value of '%s' from '%s' from store %s",
key, defaultValue, store);
throw OnErrorThrowable.from(exception);
throw exception;
}
}
@ -160,7 +159,7 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
.concatWith(newStoreValueEvent)
.map(storeObject -> returnDefaultValueIfNull(storeObject, defaultValue));
return observeStrategy == ObserveStrategy.CACHE_STORE_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE
? Observable.unsafeCreate(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
? RxJavaPlugins.onAssembly(new ObservableRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
: result;
}
@ -175,11 +174,11 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
} catch (final Converter.ConversionException exception) {
STORABLE_LC_GROUP.w(exception, "Exception while trying to converting value of '%s' from store %s by %s",
key, storeObject, store, converter);
throw OnErrorThrowable.from(exception);
throw exception;
}
});
return observeStrategy == ObserveStrategy.CACHE_ACTUAL_VALUE || observeStrategy == ObserveStrategy.CACHE_STORE_AND_ACTUAL_VALUE
? Observable.unsafeCreate(new OnSubscribeRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
? RxJavaPlugins.onAssembly(new ObservableRefCountWithCacheTime<>(result.replay(1), cacheTimeMillis, TimeUnit.MILLISECONDS))
: result;
}
@ -235,7 +234,7 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
@NonNull
private Completable internalSet(@Nullable final TObject newValue, final boolean checkForEqualityBeforeSet) {
return (checkForEqualityBeforeSet ? storeValueObservable.take(1).toSingle() : Single.just(new Optional<>(null)))
return (checkForEqualityBeforeSet ? storeValueObservable.firstOrError() : Single.just(new Optional<>(null)))
.observeOn(scheduler)
.flatMapCompletable(oldStoreValue -> {
final TStoreObject newStoreValue;
@ -274,10 +273,9 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
* @param newValue Value to set;
* @return Observable of setting process.
*/
//COMPATIBILITY NOTE: it is not Completable to prevent migration of old code
@NonNull
public Observable<?> forceSet(@Nullable final TObject newValue) {
return internalSet(newValue, false).toObservable();
public Completable forceSet(@Nullable final TObject newValue) {
return internalSet(newValue, false);
}
/**
@ -290,16 +288,9 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
* @param newValue Value to set;
* @return Observable of setting process.
*/
//COMPATIBILITY NOTE: it is not Completable to prevent migration of old code
@NonNull
public Observable<?> set(@Nullable final TObject newValue) {
return internalSet(newValue, true).toObservable();
}
@Deprecated
//COMPATIBILITY NOTE: it is deprecated as it's execution not bound to Android lifecycle objects
public void setCalm(@Nullable final TObject newValue) {
set(newValue).subscribe(Actions.empty(), Lc::assertion);
public Completable set(@Nullable final TObject newValue) {
return internalSet(newValue, true);
}
/**
@ -310,7 +301,7 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
@Deprecated
//deprecation: it should be used for debug only and in very rare cases.
public void setSync(@Nullable final TObject newValue) {
set(newValue).toBlocking().subscribe();
set(newValue).blockingAwait();
}
@NonNull
@ -334,9 +325,8 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
* @return Returns observable of value.
*/
@NonNull
//COMPATIBILITY NOTE: it is not Single to prevent migration of old code
public Observable<TReturnObject> get() {
return observe().take(1);
public Single<TReturnObject> get() {
return observe().firstOrError();
}
/**
@ -348,7 +338,7 @@ public abstract class BaseStorable<TKey, TObject, TStoreObject, TReturnObject> {
//deprecation: it should be used for debug only and in very rare cases.
@Nullable
public TReturnObject getSync() {
return get().toBlocking().first();
return get().blockingGet();
}
/**

View File

@ -38,7 +38,7 @@ public interface Converter<TObject, TStoreObject> {
*
* @param objectType Type of object;
* @param storeObjectType Type of store object allowed to store;
* @param object Object to be converted to store object;
* @param object Object to be converted to store object;
* @return Object that is allowed to store into specific {@link Store};
* @throws ConversionException Exception during conversion. Usually it indicates illegal state.
*/
@ -51,7 +51,7 @@ public interface Converter<TObject, TStoreObject> {
*
* @param objectType Type of object;
* @param storeObjectType Type of store object allowed to store;
* @param storeObject Object from specific {@link Store};
* @param storeObject Object from specific {@link Store};
* @return Object converted from store object;
* @throws ConversionException Exception during conversion. Usually it indicates illegal state.
*/

View File

@ -24,10 +24,9 @@ import android.support.annotation.NonNull;
import java.util.Arrays;
import java.util.List;
import rx.Completable;
import rx.Observable;
import rx.Single;
import rx.exceptions.OnErrorThrowable;
import io.reactivex.Completable;
import io.reactivex.Flowable;
import io.reactivex.Single;
/**
* Created by Gavriil Sitnikov on 06/10/2015.
@ -91,17 +90,18 @@ public class Migration<TKey> {
return makeMigrationChain(key, versionUpdater)
.doOnSuccess(lastUpdatedVersion -> {
if (lastUpdatedVersion < latestVersion) {
throw OnErrorThrowable.from(new NextLoopMigrationException());
throw new NextLoopMigrationException();
}
if (versionUpdater.initialVersion == versionUpdater.oldVersion) {
throw new MigrationException(String.format("Version of '%s' not updated from %s",
key, versionUpdater.initialVersion));
}
})
.retryWhen(attempts -> attempts.switchMap(throwable -> throwable instanceof NextLoopMigrationException
? Observable.just(null) : Observable.error(throwable)));
.retryWhen(attempts -> attempts
.switchMap(throwable -> throwable instanceof NextLoopMigrationException
? Flowable.just(new Object()) : Flowable.error(throwable)));
})
.toCompletable()
.ignoreElement()
.andThen(versionsStore.storeObject(Long.class, key, latestVersion))
.onErrorResumeNext(throwable -> {
if (throwable instanceof MigrationException) {

View File

@ -21,7 +21,7 @@ package ru.touchin.roboswag.core.observables.storable;
import android.support.annotation.NonNull;
import rx.Single;
import io.reactivex.Single;
/**
* Created by Gavriil Sitnikov on 05/10/2015.

View File

@ -17,19 +17,16 @@
*
*/
package ru.touchin.roboswag.core.observables.storable.concrete;
package ru.touchin.roboswag.core.observables.storable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.observables.storable.BaseStorable;
import ru.touchin.roboswag.core.observables.storable.Migration;
import ru.touchin.roboswag.core.observables.storable.Storable;
import ru.touchin.roboswag.core.utils.ShouldNotHappenException;
import rx.Observable;
import rx.Scheduler;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
/**
* Created by Gavriil Sitnikov on 04/10/2015.

View File

@ -25,10 +25,9 @@ import android.support.annotation.Nullable;
import java.lang.reflect.Type;
import java.util.concurrent.TimeUnit;
import ru.touchin.roboswag.core.observables.storable.concrete.NonNullStorable;
import ru.touchin.roboswag.core.utils.Optional;
import rx.Observable;
import rx.Scheduler;
import io.reactivex.Observable;
import io.reactivex.Scheduler;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
@ -43,8 +42,7 @@ import rx.Scheduler;
* @param <TObject> Type of actual object;
* @param <TStoreObject> Type of store object. Could be same as {@link TObject}.
*/
//COMPATIBILITY NOTE: in RxJava2 it should extends BaseStorable<TKey, TObject, TStoreObject, Optional<TObject>>
public class Storable<TKey, TObject, TStoreObject> extends BaseStorable<TKey, TObject, TStoreObject, TObject> {
public class Storable<TKey, TObject, TStoreObject> extends BaseStorable<TKey, TObject, TStoreObject, Optional<TObject>> {
public Storable(@NonNull final BuilderCore<TKey, TObject, TStoreObject> builderCore) {
super(builderCore);
@ -52,8 +50,8 @@ public class Storable<TKey, TObject, TStoreObject> extends BaseStorable<TKey, TO
@NonNull
@Override
public Observable<TObject> observe() {
return observeOptionalValue().map(Optional::get);
public Observable<Optional<TObject>> observe() {
return observeOptionalValue();
}
/**

View File

@ -25,8 +25,8 @@ import android.support.annotation.Nullable;
import java.lang.reflect.Type;
import ru.touchin.roboswag.core.utils.Optional;
import rx.Completable;
import rx.Single;
import io.reactivex.Completable;
import io.reactivex.Single;
/**
* Created by Gavriil Sitnikov on 04/10/2015.

View File

@ -27,8 +27,6 @@ import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import ru.touchin.roboswag.core.observables.collections.ObservableCollection;
/**
* Created by Gavriil Sitnikov on 04/10/2015.
* Some utilities related to objects.
@ -163,16 +161,6 @@ public final class ObjectUtils {
return collection == null || collection.isEmpty();
}
/**
* Returns true if ObservableCollection is null or empty.
*
* @param observableCollection observableCollection to check;
* @return True if observableCollection is null or empty.
*/
public static boolean isNullOrEmpty(@Nullable final ObservableCollection<?> observableCollection) {
return observableCollection == null || observableCollection.isEmpty();
}
/**
* Returns true if map is null or empty.
*

View File

@ -23,7 +23,8 @@ import android.support.annotation.NonNull;
import java.security.MessageDigest;
import rx.functions.Func1;
import io.reactivex.functions.Function;
import ru.touchin.roboswag.core.log.Lc;
/**
* Created by Gavriil Sitnikov on 29/08/2016.
@ -65,11 +66,15 @@ public final class StringUtils {
* @param condition Condition of symbol;
* @return True if some character satisfies condition.
*/
public static boolean containsCharLike(@NonNull final String text, @NonNull final Func1<Character, Boolean> condition) {
for (int i = 0; i < text.length(); i++) {
if (condition.call(text.charAt(i))) {
return true;
public static boolean containsCharLike(@NonNull final String text, @NonNull final Function<Character, Boolean> condition) {
try {
for (int i = 0; i < text.length(); i++) {
if (condition.apply(text.charAt(i))) {
return true;
}
}
} catch (final Exception exception) {
Lc.assertion(exception);
}
return false;
}

View File

@ -32,7 +32,7 @@ import ru.touchin.roboswag.core.utils.ObjectUtils;
* Both arguments are not null.
* Note that if you want to save this pair in state, you need make TFirst and TSecond Serializable too.
*
* @param <TFirst> type of the first nonnull argument.
* @param <TFirst> type of the first nonnull argument.
* @param <TSecond> type of the second nonnull argument.
*/
public class NonNullPair<TFirst, TSecond> implements Serializable {