diff --git a/src/main/java/ru/touchin/templates/TouchinActivity.java b/src/main/java/ru/touchin/templates/TouchinActivity.java index ee59241..1565aec 100644 --- a/src/main/java/ru/touchin/templates/TouchinActivity.java +++ b/src/main/java/ru/touchin/templates/TouchinActivity.java @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates; import ru.touchin.roboswag.components.navigation.activities.ViewControllerActivity; diff --git a/src/main/java/ru/touchin/templates/chat/Chat.java b/src/main/java/ru/touchin/templates/chat/Chat.java new file mode 100644 index 0000000..af3e09f --- /dev/null +++ b/src/main/java/ru/touchin/templates/chat/Chat.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.chat; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import ru.touchin.roboswag.core.utils.android.RxAndroidUtils; +import rx.Observable; +import rx.Scheduler; +import rx.subjects.BehaviorSubject; +import rx.subjects.PublishSubject; + +/** + * Created by Gavriil Sitnikov on 12/05/16. + */ +public abstract class Chat { + + @NonNull + private final PublishSubject messageToSendEvent = PublishSubject.create(); + private final BehaviorSubject> sendingMessages = BehaviorSubject.create(new ArrayList<>()); + @NonNull + private final BehaviorSubject isActivated = BehaviorSubject.create(false); + @NonNull + private final PublishSubject retrySendingEvent = PublishSubject.create(); + @NonNull + private final BehaviorSubject isSendingInError = BehaviorSubject.create(false); + + public Chat(@Nullable final Collection messagesToSend) { + final Scheduler sendingScheduler = RxAndroidUtils.createLooperScheduler(); + messageToSendEvent + .observeOn(sendingScheduler) + .doOnNext(message -> { + final List messages = new ArrayList<>(sendingMessages.getValue()); + messages.add(0, message); + sendingMessages.onNext(messages); + }) + .concatMap(message -> isActivated + .filter(activated -> activated) + .first() + .switchMap(ignored -> Observable + .combineLatest(isMessageInCacheObservable(message), isMessageInActualObservable(message), + (messageInCache, messageInActual) -> !messageInCache && !messageInActual) + .observeOn(sendingScheduler) + .switchMap(shouldSendMessage -> { + if (!shouldSendMessage) { + return Observable.empty(); + } + return sendMessageObservable(message) + .doOnSubscribe(() -> isSendingInError.onNext(false)) + .retryWhen(attempts -> attempts.map(throwable -> { + isSendingInError.onNext(true); + return retrySendingEvent; + })) + .doOnCompleted(() -> { + final List messages = new ArrayList<>(sendingMessages.getValue()); + messages.remove(message); + sendingMessages.onNext(messages); + }); + }))) + .subscribe(); + + if (messagesToSend != null) { + for (final TOutgoingMessage message : messagesToSend) { + messageToSendEvent.onNext(message); + } + } + } + + @NonNull + public Observable> observeSendingMessages() { + return sendingMessages; + } + + @NonNull + protected abstract Observable isMessageInCacheObservable(@NonNull final TOutgoingMessage message); + + @NonNull + protected abstract Observable isMessageInActualObservable(@NonNull final TOutgoingMessage message); + + @NonNull + protected abstract Observable sendMessageObservable(@NonNull final TOutgoingMessage message); + + public void sendMessage(@NonNull final TOutgoingMessage message) { + messageToSendEvent.onNext(message); + } + + public void activate() { + isActivated.onNext(true); + } + + public void retrySend() { + isSendingInError.onNext(false); + } + + public void deactivate() { + isActivated.onNext(false); + } + +} diff --git a/src/main/java/ru/touchin/templates/model/GoogleJsonModel.java b/src/main/java/ru/touchin/templates/model/GoogleJsonModel.java index 5b448bb..aca7412 100644 --- a/src/main/java/ru/touchin/templates/model/GoogleJsonModel.java +++ b/src/main/java/ru/touchin/templates/model/GoogleJsonModel.java @@ -1,3 +1,22 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.model; import android.support.annotation.Nullable; diff --git a/src/main/java/ru/touchin/templates/model/increasing/IncreasingItem.java b/src/main/java/ru/touchin/templates/model/increasing/IncreasingItem.java new file mode 100644 index 0000000..b811f4d --- /dev/null +++ b/src/main/java/ru/touchin/templates/model/increasing/IncreasingItem.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.model.increasing; + +import android.support.annotation.NonNull; + +/** + * Created by Gavriil Sitnikov on 18/05/16. + */ +public interface IncreasingItem { + + @NonNull + String getItemId(); + +} diff --git a/src/main/java/ru/touchin/templates/model/increasing/IncreasingItemsPage.java b/src/main/java/ru/touchin/templates/model/increasing/IncreasingItemsPage.java new file mode 100644 index 0000000..fe397fa --- /dev/null +++ b/src/main/java/ru/touchin/templates/model/increasing/IncreasingItemsPage.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.model.increasing; + +import android.support.annotation.NonNull; + +import java.util.Collection; + +/** + * Created by Gavriil Sitnikov on 18/05/16. + */ +public interface IncreasingItemsPage { + + boolean isLast(); + + @NonNull + Collection getItems(); + +} diff --git a/src/main/java/ru/touchin/templates/model/increasing/IncreasingItemsProvider.java b/src/main/java/ru/touchin/templates/model/increasing/IncreasingItemsProvider.java new file mode 100644 index 0000000..66283a6 --- /dev/null +++ b/src/main/java/ru/touchin/templates/model/increasing/IncreasingItemsProvider.java @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2016 Touch Instinct + * + * 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.templates.model.increasing; + +import android.os.SystemClock; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import ru.touchin.roboswag.components.listing.ItemsProvider; +import ru.touchin.roboswag.core.utils.android.RxAndroidUtils; +import rx.Observable; +import rx.Scheduler; +import rx.exceptions.OnErrorThrowable; +import rx.schedulers.Schedulers; +import rx.subjects.BehaviorSubject; +import rx.subjects.PublishSubject; + +/** + * Created by Gavriil Sitnikov on 13/05/16. + */ +public class IncreasingItemsProvider extends ItemsProvider { + + private static final long MIN_UPDATE_TIME = TimeUnit.SECONDS.toMillis(5); + + @NonNull + private final Scheduler scheduler = RxAndroidUtils.createLooperScheduler(); + @NonNull + private final BehaviorSubject haveNewItems = BehaviorSubject.create(false); + @NonNull + private final BehaviorSubject haveHistoryItems = BehaviorSubject.create(true); + @NonNull + private final PublishSubject refreshRequestEvent = PublishSubject.create(); + + @NonNull + private final List items = new ArrayList<>(); + @NonNull + private final LoaderRequestCreator newItemsLoader; + @NonNull + private final LoaderRequestCreator historyLoader; + @NonNull + private final Observable needRefreshObservable; + @Nullable + private Long lastNewItemsUpdate; + + @Nullable + private Observable newItemsConcreteObservable; + @Nullable + private Observable historyItemsConcreteObservable; + + public IncreasingItemsProvider(@NonNull final LoaderRequestCreator historyLoader, + @NonNull final LoaderRequestCreator newItemsLoader) { + super(); + this.newItemsLoader = newItemsLoader; + this.historyLoader = historyLoader; + this.needRefreshObservable = createNeedRefreshObservable(); + } + + @NonNull + private Observable createNeedRefreshObservable() { + return observeIsHaveNewItems() + .switchMap(haveNewItems -> haveNewItems + ? Observable.just(true) + : Observable.just(isNeedUpdate()) + .concatWith(Observable + .merge(Observable.interval(MIN_UPDATE_TIME, TimeUnit.MILLISECONDS).map(ignored -> true), + refreshRequestEvent.map(ignored -> true)))) + .replay(1) + .refCount(); + } + + @NonNull + public Observable getNeedRefreshObservable() { + return needRefreshObservable; + } + + @NonNull + public Observable observeIsHaveNewItems() { + return haveNewItems.distinctUntilChanged(); + } + + @NonNull + public Observable observeIsHaveHistoryItems() { + return haveHistoryItems.distinctUntilChanged(); + } + + private boolean isNeedUpdate() { + return lastNewItemsUpdate == null || SystemClock.uptimeMillis() - lastNewItemsUpdate < MIN_UPDATE_TIME; + } + + @NonNull + @Override + public TItem getItem(final int position) { + return items.get(position); + } + + @NonNull + private Observable loadNewItems() { + return Observable + .>create(subscriber -> { + if (items.isEmpty()) { + subscriber.onNext(loadHistory()); + subscriber.onCompleted(); + return; + } + if (newItemsConcreteObservable == null) { + newItemsConcreteObservable = newItemsLoader.getByItemId(items.get(0).getItemId()) + .subscribeOn(Schedulers.io()) + .observeOn(scheduler) + .doOnNext(page -> { + newItemsConcreteObservable = null; + items.addAll(0, page.getItems()); + notifyChanges(Collections.singletonList(new Change(Change.Type.INSERTED, 0, page.getItems().size()))); + lastNewItemsUpdate = SystemClock.uptimeMillis(); + haveNewItems.onNext(!page.isLast()); + if (!page.isLast()) { + throw OnErrorThrowable.from(new NotLastException()); + } + }) + .replay(1) + .refCount(); + } + subscriber.onNext(newItemsConcreteObservable); + subscriber.onCompleted(); + }) + .subscribeOn(scheduler) + .switchMap(observable -> observable); + } + + @NonNull + private Observable loadHistory() { + return Observable + .>create(subscriber -> { + if (historyItemsConcreteObservable == null) { + final TItem fromMessage = !items.isEmpty() ? items.get(items.size() - 1) : null; + historyItemsConcreteObservable = historyLoader.getByItemId(fromMessage != null ? fromMessage.getItemId() : null) + .subscribeOn(Schedulers.io()) + .observeOn(scheduler) + .doOnNext(page -> { + historyItemsConcreteObservable = null; + items.addAll(page.getItems()); + final int newItemsCount = page.getItems().size(); + final Change change = new Change(Change.Type.INSERTED, items.size() - newItemsCount, newItemsCount); + notifyChanges(Collections.singletonList(change)); + haveHistoryItems.onNext(!page.isLast()); + }) + .replay(1) + .refCount(); + } + subscriber.onNext(historyItemsConcreteObservable); + subscriber.onCompleted(); + }) + .subscribeOn(scheduler) + .switchMap(observable -> observable); + } + + public void requestRefresh() { + refreshRequestEvent.onNext(null); + } + + @NonNull + public Observable refresh() { + return retryIfNotLast(loadNewItems()); + } + + @NonNull + private Observable loadFromHistory(final int position) { + if (position < items.size()) { + return Observable.just(items.get(position)); + } + if (!haveHistoryItems.getValue()) { + return Observable.just(null); + } + return retryIfNotLast(loadHistory() + .observeOn(scheduler) + .map(ignored -> { + if (position < items.size()) { + return items.get(position); + } + throw OnErrorThrowable.from(new NotLastException()); + })); + } + + @SuppressWarnings("PMD.SimplifiedTernary") + //TODO: looks like PMD thinking that false is part of ternary + @NonNull + @Override + public Observable loadItem(final int position) { + return (position == 0 ? needRefreshObservable : Observable.just(false)) + .observeOn(scheduler) + .switchMap(needRefresh -> { + if (!needRefresh) { + return loadFromHistory(position); + } + return loadNewItems() + .observeOn(scheduler) + .switchMap(ignored -> loadFromHistory(position)); + }); + } + + @NonNull + protected Observable retryIfNotLast(@NonNull final Observable observable) { + return observable + .retryWhen(attempts -> attempts + .map(throwable -> throwable instanceof NotLastException + ? Observable.just(null) + : Observable.error(throwable))); + } + + @Override + public int getSize() { + return items.size(); + } + + private static class NotLastException extends Exception { + } + + public interface LoaderRequestCreator { + + @NonNull + Observable> getByItemId(@Nullable final String itemId); + + } + +}