@ThreadSafe class Example_11_ViewModel_Address implements IViewModel<Example_11_Model> { public final BehaviorSubject<Boolean> vm2v_edit = BehaviorSubject.create(); public final BehaviorSubject<String> v2vm_address = BehaviorSubject.create(); public final BehaviorSubject<String> vm2v_address = BehaviorSubject.create(); public final BehaviorSubject<Boolean> vm2v_dirty = BehaviorSubject.create(false /* initial state */); public Example_11_ViewModel_Address() { wireInternally(); } private void wireInternally() { v2vm_address .map(anschriftGemässView -> !anschriftGemässView.equals(vm2v_address.getValue())) .subscribe(vm2v_dirty); // Simulating some data vm2v_address.onNext("Koch Peti\nLadengasse 42\n4242 Vordemberg\nSchweiz"); } @Override public void connectTo(final Example_11_Model model) { // NO-OP } }
public DownloadManager( Context context, SourceManager sourceManager, PreferencesHelper preferences) { this.context = context; this.sourceManager = sourceManager; this.preferences = preferences; gson = new Gson(); queue = new DownloadQueue(); downloadsQueueSubject = PublishSubject.create(); threadsNumber = BehaviorSubject.create(); runningSubject = BehaviorSubject.create(); }
public Subscription subscribeData(Observer<List<GankBeauty>> observer) { if (mCache == null) { mCache = BehaviorSubject.create(); Observable.create( new Observable.OnSubscribe<List<GankBeauty>>() { @Override public void call(Subscriber<? super List<GankBeauty>> subscriber) { List<GankBeauty> itemList = getDataFromDisk(DATA_FILE_NAME); if (ListUtils.isEmpty(itemList)) { Logger.d("Load data from network"); setDataSource(DATA_SOURCE_NETWORK); loadFromNetwork(); } else { Logger.d("Load data from disk"); setDataSource(DATA_SOURCE_DISK); subscriber.onNext(itemList); } } }) .subscribeOn(Schedulers.io()) .subscribe(mCache); } else { Logger.d("Load data from memory."); setDataSource(DATA_SOURCE_MEMORY); } return mCache.observeOn(AndroidSchedulers.mainThread()).subscribe(observer); }
public class RepositoryViewModel extends AbstractViewModel { private static final String TAG = RepositoryViewModel.class.getSimpleName(); private final DataFunctions.GetUserSettings getUserSettings; private final DataFunctions.FetchAndGetGitHubRepository fetchAndGetGitHubRepository; private final BehaviorSubject<GitHubRepository> repository = BehaviorSubject.create(); public RepositoryViewModel( @NonNull DataFunctions.GetUserSettings getUserSettings, @NonNull DataFunctions.FetchAndGetGitHubRepository fetchAndGetGitHubRepository) { Preconditions.checkNotNull(getUserSettings, "Gey User Settings cannot be null."); Preconditions.checkNotNull( fetchAndGetGitHubRepository, "Fetch And Get GitHub Repository cannot be null."); this.getUserSettings = getUserSettings; this.fetchAndGetGitHubRepository = fetchAndGetGitHubRepository; Log.v(TAG, "RepositoryViewModel"); } @Override public void subscribeToDataStoreInternal(@NonNull CompositeSubscription compositeSubscription) { compositeSubscription.add( getUserSettings .call() .map(UserSettings::getSelectedRepositoryId) .switchMap(fetchAndGetGitHubRepository::call) .subscribe(repository)); } @NonNull public Observable<GitHubRepository> getRepository() { return repository.asObservable(); } }
/** Created by chanx on 16/5/20. */ public class DataStore { private static DataStore INSTANCE; // actual data store could be files, databases, or ContentProvider etc private final BehaviorSubject<List<GithubUser>> mUserListDataStoreStream = BehaviorSubject.create(); private DataStore() {} public static DataStore getInstance() { if (INSTANCE == null) { synchronized (DataStore.class) { if (INSTANCE == null) { INSTANCE = new DataStore(); } } } return INSTANCE; } public Observable<List<GithubUser>> getGithubUserListDataStore() { return mUserListDataStoreStream.asObservable(); } public void fetchRandomGithubUsersToDataStore() { Intent intent = new Intent(GithubNetworkService.NETWORK_REQUEST_RANDOM_GITHUB_USERS); intent.setClass(DemoApplication.getContext(), GithubNetworkService.class); DemoApplication.getContext().startService(intent); Logger.d("send intent to NetworkService:" + intent); } public void setUserList(List<GithubUser> list) { mUserListDataStoreStream.onNext(list); } }
@ThreadSafe class Example_10_ViewModel_Address implements IViewModel<Example_10_Model> { public final BehaviorSubject<Boolean> vm2v_edit = BehaviorSubject.create(); public final BehaviorSubject<String> v2vm_address = BehaviorSubject.create(); public final BehaviorSubject<String> vm2v_address = BehaviorSubject.create(); public Example_10_ViewModel_Address() { wireInternally(); } private void wireInternally() { // Simulating some data vm2v_address.onNext("Koch Peti\nLadengasse 42\n4242 Vordemberg\nSchweiz"); } @Override public void connectTo(final Example_10_Model model) { // NO-OP } }
public class AppRouter extends Router { private final BehaviorSubject<RouteChange> screenChangeSubject = BehaviorSubject.create(); public AppRouter(ScreenScooper screenScooper) { super(screenScooper); } public Observable<RouteChange> observeScreenChange() { return screenChangeSubject.asObservable(); } @Override protected void onScoopChanged(RouteChange change) { screenChangeSubject.onNext(change); } }
public class BaseActivity extends FragmentActivity { @Nonnull private final BehaviorSubject<LifecycleEvent> lifecycleSubject = BehaviorSubject.create(); protected final LifecycleMainObservable lifecycleMainObservable = new LifecycleMainObservable( new LifecycleMainObservable.LifecycleProviderActivity(lifecycleSubject, this)); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleSubject.onNext(LifecycleEvent.CREATE); } @Override protected void onStart() { super.onStart(); lifecycleSubject.onNext(LifecycleEvent.START); } @Override protected void onResume() { super.onResume(); lifecycleSubject.onNext(LifecycleEvent.RESUME); } @Override protected void onPause() { lifecycleSubject.onNext(LifecycleEvent.PAUSE); super.onPause(); } @Override protected void onStop() { lifecycleSubject.onNext(LifecycleEvent.STOP); super.onStop(); } @Override protected void onDestroy() { lifecycleSubject.onNext(LifecycleEvent.DESTROY); super.onDestroy(); } }
public abstract class BaseActivity extends RoboActionBarActivity { private final BehaviorSubject<LifecycleEvent> lifecycleSubject = BehaviorSubject.create(); public Observable<LifecycleEvent> lifecycle() { return lifecycleSubject.asObservable(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleSubject.onNext(LifecycleEvent.CREATE); } @Override protected void onStart() { super.onStart(); lifecycleSubject.onNext(LifecycleEvent.START); } @Override protected void onResume() { super.onResume(); lifecycleSubject.onNext(LifecycleEvent.RESUME); } @Override protected void onPause() { lifecycleSubject.onNext(LifecycleEvent.PAUSE); super.onPause(); } @Override protected void onStop() { lifecycleSubject.onNext(LifecycleEvent.STOP); super.onStop(); } @Override protected void onDestroy() { lifecycleSubject.onNext(LifecycleEvent.DESTROY); super.onDestroy(); } }
/** Created by Administrator on 2016/3/18. */ public class RxBus { // PublishSubject仅会向Observer释放在订阅之后Observable释放的数据。 private final Subject<Object, Object> publishBus = new SerializedSubject<>(PublishSubject.create()); // AsyncSubject仅释放Observable释放的最后一个数据,并且仅在Observable完成之后。然而如果当Observable因为异常而终止,AsyncSubject将不会释放任何数据,但是会向Observer传递一个异常通知。 private final Subject<Object, Object> asyncBus = new SerializedSubject<>(AsyncSubject.create()); // 当Observer订阅了一个BehaviorSubject,它一开始就会释放Observable最近释放的一个数据对象,当还没有任何数据释放时,它则是一个默认值。接下来就会释放Observable释放的所有数据。 // 如果Observable因异常终止,BehaviorSubject将不会向后续的Observer释放数据,但是会向Observer传递一个异常通知。 private final Subject<Object, Object> behaviorBus = new SerializedSubject<>(BehaviorSubject.create()); // 不管Observer何时订阅ReplaySubject,ReplaySubject会向所有Observer释放Observable释放过的数据。 // 有不同类型的ReplaySubject,它们是用来限定Replay的范围,例如设定Buffer的具体大小,或者设定具体的时间范围。 // 如果使用ReplaySubject作为Observer,注意不要在多个线程中调用onNext、onComplete和onError方法,因为这会导致顺序错乱,这个是违反了Observer规则的。 private final Subject<Object, Object> replayBus = new SerializedSubject<>(ReplaySubject.create()); }
/** Created by Christoph on 05.10.2015. */ public class RacletteRxFragment<V extends ViewModel, B extends ViewDataBinding> extends RacletteFragment<V, B> implements FragmentLifecycleProvider { protected final BehaviorSubject<FragmentEvent> lifecycleSubject = BehaviorSubject.create(); @Override @NonNull @CheckResult public final Observable<FragmentEvent> lifecycle() { return lifecycleSubject.asObservable(); } @Override @NonNull @CheckResult public final <T> Observable.Transformer<T, T> bindUntilEvent(@NonNull FragmentEvent event) { return RxLifecycle.bindUntilEvent(lifecycleSubject, event); } @Override @NonNull @CheckResult public final <T> Observable.Transformer<T, T> bindToLifecycle() { return RxLifecycle.bindFragment(lifecycleSubject); } @Override @CallSuper public void onAttach(Context context) { super.onAttach(context); lifecycleSubject.onNext(FragmentEvent.ATTACH); } @Override @CallSuper public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleSubject.onNext(FragmentEvent.CREATE); } @Override @CallSuper public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); lifecycleSubject.onNext(FragmentEvent.CREATE_VIEW); } @Override @CallSuper public void onStart() { super.onStart(); lifecycleSubject.onNext(FragmentEvent.START); } @Override @CallSuper public void onResume() { super.onResume(); lifecycleSubject.onNext(FragmentEvent.RESUME); } @Override @CallSuper public void onPause() { lifecycleSubject.onNext(FragmentEvent.PAUSE); super.onPause(); } @Override @CallSuper public void onStop() { lifecycleSubject.onNext(FragmentEvent.STOP); super.onStop(); } @Override @CallSuper public void onDestroyView() { lifecycleSubject.onNext(FragmentEvent.DESTROY_VIEW); super.onDestroyView(); } @Override @CallSuper public void onDestroy() { lifecycleSubject.onNext(FragmentEvent.DESTROY); super.onDestroy(); } @Override @CallSuper public void onDetach() { lifecycleSubject.onNext(FragmentEvent.DETACH); super.onDetach(); } }
public class RxFragment extends Fragment implements FragmentLifecycleProvider { private final BehaviorSubject<FragmentEvent> lifecycleSubject = BehaviorSubject.create(); @Override public final Observable<FragmentEvent> lifecycle() { return lifecycleSubject.asObservable(); } @Override public final <T> Observable.Transformer<T, T> bindUntilEvent(FragmentEvent event) { return RxLifecycle.bindUntilFragmentEvent(lifecycleSubject, event); } @Override public final <T> Observable.Transformer<T, T> bindToLifecycle() { return RxLifecycle.bindFragment(lifecycleSubject); } @Override @CallSuper public void onAttach(android.app.Activity activity) { super.onAttach(activity); lifecycleSubject.onNext(FragmentEvent.ATTACH); } @Override @CallSuper public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleSubject.onNext(FragmentEvent.CREATE); } @Override @CallSuper public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); lifecycleSubject.onNext(FragmentEvent.CREATE_VIEW); } @Override @CallSuper public void onStart() { super.onStart(); lifecycleSubject.onNext(FragmentEvent.START); } @Override @CallSuper public void onResume() { super.onResume(); lifecycleSubject.onNext(FragmentEvent.RESUME); } @Override @CallSuper public void onPause() { lifecycleSubject.onNext(FragmentEvent.PAUSE); super.onPause(); } @Override @CallSuper public void onStop() { lifecycleSubject.onNext(FragmentEvent.STOP); super.onStop(); } @Override @CallSuper public void onDestroyView() { lifecycleSubject.onNext(FragmentEvent.DESTROY_VIEW); super.onDestroyView(); } @Override @CallSuper public void onDestroy() { lifecycleSubject.onNext(FragmentEvent.DESTROY); super.onDestroy(); } @Override @CallSuper public void onDetach() { lifecycleSubject.onNext(FragmentEvent.DETACH); super.onDetach(); } }
/** * Maintains a stream of distributions for a given Command. There is a rolling window abstraction on * this stream. The latency distribution object is calculated over a window of t1 milliseconds. This * window has b buckets. Therefore, a new set of counters is produced every t2 (=t1/b) milliseconds * t1 = metricsRollingPercentileWindowInMilliseconds() b = metricsRollingPercentileBucketSize() * * <p>These values are stable - there's no peeking into a bucket until it is emitted * * <p>These values get produced and cached in this class. */ public class RollingDistributionStream<Event extends HystrixEvent> { private AtomicReference<Subscription> rollingDistributionSubscription = new AtomicReference<Subscription>(null); private final BehaviorSubject<CachedValuesHistogram> rollingDistribution = BehaviorSubject.create( CachedValuesHistogram.backedBy(CachedValuesHistogram.getNewHistogram())); private final Observable<CachedValuesHistogram> rollingDistributionStream; private static final Func2<Histogram, Histogram, Histogram> distributionAggregator = new Func2<Histogram, Histogram, Histogram>() { @Override public Histogram call(Histogram initialDistribution, Histogram distributionToAdd) { initialDistribution.add(distributionToAdd); return initialDistribution; } }; private static final Func1<Observable<Histogram>, Observable<Histogram>> reduceWindowToSingleDistribution = new Func1<Observable<Histogram>, Observable<Histogram>>() { @Override public Observable<Histogram> call(Observable<Histogram> window) { return window.reduce(distributionAggregator); } }; private static final Func1<Histogram, CachedValuesHistogram> cacheHistogramValues = new Func1<Histogram, CachedValuesHistogram>() { @Override public CachedValuesHistogram call(Histogram histogram) { return CachedValuesHistogram.backedBy(histogram); } }; private static final Func1< Observable<CachedValuesHistogram>, Observable<List<CachedValuesHistogram>>> convertToList = new Func1<Observable<CachedValuesHistogram>, Observable<List<CachedValuesHistogram>>>() { @Override public Observable<List<CachedValuesHistogram>> call( Observable<CachedValuesHistogram> windowOf2) { return windowOf2.toList(); } }; protected RollingDistributionStream( final HystrixEventStream<Event> stream, final int numBuckets, final int bucketSizeInMs, final Func2<Histogram, Event, Histogram> addValuesToBucket) { final List<Histogram> emptyDistributionsToStart = new ArrayList<Histogram>(); for (int i = 0; i < numBuckets; i++) { emptyDistributionsToStart.add(CachedValuesHistogram.getNewHistogram()); } final Func1<Observable<Event>, Observable<Histogram>> reduceBucketToSingleDistribution = new Func1<Observable<Event>, Observable<Histogram>>() { @Override public Observable<Histogram> call(Observable<Event> bucket) { return bucket.reduce(CachedValuesHistogram.getNewHistogram(), addValuesToBucket); } }; rollingDistributionStream = Observable.defer( new Func0<Observable<CachedValuesHistogram>>() { @Override public Observable<CachedValuesHistogram> call() { return stream .observe() .window( bucketSizeInMs, TimeUnit.MILLISECONDS) // stream of unaggregated buckets .flatMap( reduceBucketToSingleDistribution) // stream of aggregated Histograms .startWith( emptyDistributionsToStart) // stream of aggregated Histograms that // starts with n empty .window( numBuckets, 1) // windowed stream: each OnNext is a stream of n Histograms .flatMap( reduceWindowToSingleDistribution) // reduced stream: each OnNext is a // single Histogram .map(cacheHistogramValues); // convert to CachedValueHistogram // (commonly-accessed values are cached) } }) .share(); // multicast } public Observable<CachedValuesHistogram> observe() { return rollingDistributionStream; } public int getLatestMean() { CachedValuesHistogram latest = getLatest(); if (latest != null) { return latest.getMean(); } else { return 0; } } public int getLatestPercentile(double percentile) { CachedValuesHistogram latest = getLatest(); if (latest != null) { return latest.getValueAtPercentile(percentile); } else { return 0; } } public void startCachingStreamValuesIfUnstarted() { if (rollingDistributionSubscription.get() == null) { // the stream is not yet started Subscription candidateSubscription = observe().subscribe(rollingDistribution); if (rollingDistributionSubscription.compareAndSet(null, candidateSubscription)) { // won the race to set the subscription } else { // lost the race to set the subscription, so we need to cancel this one candidateSubscription.unsubscribe(); } } } CachedValuesHistogram getLatest() { startCachingStreamValuesIfUnstarted(); if (rollingDistribution.hasValue()) { return rollingDistribution.getValue(); } else { return null; } } public void unsubscribe() { Subscription s = rollingDistributionSubscription.get(); if (s != null) { s.unsubscribe(); rollingDistributionSubscription.compareAndSet(s, null); } } }
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final MainPresenter presenter = getRetentionFragment(savedInstanceState).presenter(); setContentView(R.layout.main_activity); final RecyclerView recyclerView = (RecyclerView) findViewById(R.id.main_activity_recycler_view); final LinearLayoutManager layout = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false); recyclerView.setLayoutManager(layout); final MainAdapter adapter = new MainAdapter(); recyclerView.setAdapter(adapter); recyclerView.setItemAnimator(new DefaultItemAnimator()); final BehaviorSubject<Boolean> isLastSubject = BehaviorSubject.create(true); recyclerView.setOnScrollListener( new RecyclerView.OnScrollListener() { boolean manualScrolling = false; @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_DRAGGING) { manualScrolling = true; } if (manualScrolling && newState == RecyclerView.SCROLL_STATE_IDLE) { manualScrolling = false; final int lastVisibleItemPosition = layout.findLastVisibleItemPosition(); final int previousItemsCount = adapter.getItemCount(); final boolean isLast = previousItemsCount - 1 == lastVisibleItemPosition; isLastSubject.onNext(isLast); } } }); subs = new CompositeSubscription( isLastSubject.subscribe(presenter.lastItemInViewObserver()), presenter .itemsWithScrollObservable() .subscribe( new Action1<MainPresenter.ItemsWithScroll>() { @Override public void call(final MainPresenter.ItemsWithScroll itemsWithScroll) { adapter.call(itemsWithScroll.items()); if (itemsWithScroll.shouldScroll()) { recyclerView.post( new Runnable() { @Override public void run() { recyclerView.smoothScrollToPosition( itemsWithScroll.scrollToPosition()); } }); } } }), presenter .connectButtonEnabledObservable() .subscribe(ViewActions.setEnabled(findViewById(R.id.main_activity_connect_button))), presenter .disconnectButtonEnabledObservable() .subscribe( ViewActions.setEnabled(findViewById(R.id.macin_activity_disconnect_button))), presenter .sendButtonEnabledObservable() .subscribe(ViewActions.setEnabled(findViewById(R.id.main_activity_send_button))), ViewObservable.clicks(findViewById(R.id.main_activity_connect_button)) .subscribe(presenter.connectClickObserver()), ViewObservable.clicks(findViewById(R.id.macin_activity_disconnect_button)) .subscribe(presenter.disconnectClickObserver()), ViewObservable.clicks(findViewById(R.id.main_activity_send_button)) .subscribe(presenter.sendClickObserver())); }
/** Created by Mr.Jude on 2015/6/12. 关于账户的信息。主要用于账户类型:商家/用户,和权限管理。 会发送AccountInfo事件 */ public class AccountModel extends AbsModel { private static final String ACCOUNTFILE = "account"; public static AccountModel getInstance() { return getInstance(AccountModel.class); } public boolean isUser = true; public UserAccountData userAccountData; public BehaviorSubject<UserAccountData> userAccountDataBehaviorSubject = BehaviorSubject.create(); @Override protected void onAppCreate(Context ctx) { super.onAppCreate(ctx); Activity a = new Activity(); Window.Callback callback = a; View.OnCreateContextMenuListener menuListener = a; ComponentCallbacks2 componentCallbacks = a; userAccountData = (UserAccountData) Utils.readObjectFromFile( FileManager.getInstance().getChild(FileManager.Dir.Object, ACCOUNTFILE)); if (userAccountData != null) { applyToken(userAccountData.getTokenApp()); } else { applyToken(""); } updateAccountData(); } public boolean isUser() { return isUser; } /** @return return null if not login */ public AccountData getAccount() { if (isUser) return userAccountData; else return null; } public UserAccountData getUserAccount() { return userAccountData; } public void updateAccountData() { RequestManager.getInstance() .post( API.URL.GetUserData, null, new DataCallback<UserAccountData>() { @Override public void success(String info, UserAccountData data) { setUserAccountData(data); } }); } public Subscription registerUserAccountUpdate(Action1<UserAccountData> user) { return userAccountDataBehaviorSubject.subscribe(user); } public void setUserAccountData(UserAccountData userAccountData) { isUser = true; this.userAccountData = userAccountData; saveAccount(); applyToken(userAccountData.getTokenApp()); userAccountDataBehaviorSubject.onNext(userAccountData); Utils.Log("f**k"); } public void UserLoginOut() { this.userAccountData = null; FileManager.getInstance().getChild(FileManager.Dir.Object, ACCOUNTFILE).delete(); applyToken(""); RongYunModel.getInstance().loginOut(); userAccountDataBehaviorSubject.onNext(null); } public void LoginOut() { if (isUser()) UserLoginOut(); } public void saveAccount() { if (isUser) { Utils.writeObjectToFile( userAccountData, FileManager.getInstance().getChild(FileManager.Dir.Object, ACCOUNTFILE)); } else { } } private void applyToken(String token) { HashMap<String, String> map = new HashMap(); JSONObject json = new JSONObject(); try { json.put("token", token); json.put("type", "android"); json.put("version", Utils.getAppVersionCode() + ""); } catch (JSONException e) { e.printStackTrace(); } map.put("token", json.toString()); RequestManager.getInstance().setHeader(map); Utils.Log("setToken:" + json.toString()); } public void userRegister( String name, String tel, String password, String verify, StatusCallback callback) { RequestMap params = new RequestMap(); params.put("name", name); params.put("tel", tel); params.put("pass", Utils.MD5(password.getBytes())); params.put("code", verify); RequestManager.getInstance().post(API.URL.Register, params, callback); } public void isRegistered(String tel, StatusCallback callback) { RequestMap params = new RequestMap(); params.put("tel", tel); RequestManager.getInstance().post(API.URL.IsRegistered, params, callback); } public void userLogin(String tel, String password, StatusCallback callback) { RequestMap params = new RequestMap(); params.put("tel", tel); params.put("pass", Utils.MD5(password.getBytes())); RequestManager.getInstance() .post( API.URL.Login, params, callback.add( new DataCallback<UserAccountData>() { @Override public void success(String info, UserAccountData data) { setUserAccountData(data); } @Override public void error(String errorInfo) {} })); } public void userLoginThroughQQ(String openId, String face, String name, StatusCallback callback) { RequestMap params = new RequestMap(); params.put("type", "0"); params.put("openId", openId); params.put("face", face); params.put("name", name); RequestManager.getInstance() .post( API.URL.ThirdLogin, params, callback.add( new DataCallback<UserAccountData>() { @Override public void success(String info, UserAccountData data) { setUserAccountData(data); } @Override public void error(String errorInfo) {} })); } public void userLoginThroughSina( String openId, String face, String name, StatusCallback callback) { RequestMap params = new RequestMap(); params.put("type", "1"); params.put("openId", openId); params.put("face", face); params.put("name", name); RequestManager.getInstance() .post( API.URL.ThirdLogin, params, callback.add( new DataCallback<UserAccountData>() { @Override public void success(String info, UserAccountData data) { setUserAccountData(data); } @Override public void error(String errorInfo) {} })); } public void userLoginThroughWeChat( String openId, String face, String name, StatusCallback callback) { RequestMap params = new RequestMap(); params.put("type", "2"); params.put("openId", openId); params.put("face", face); params.put("name", name); RequestManager.getInstance() .post( API.URL.ThirdLogin, params, callback.add( new DataCallback<UserAccountData>() { @Override public void success(String info, UserAccountData data) { setUserAccountData(data); } @Override public void error(String errorInfo) {} })); } /** * 修改密码 * * @param tel * @param password * @param verify * @param callback */ public void modifyPassword(String tel, String password, String verify, StatusCallback callback) { RequestMap params = new RequestMap(); params.put("tel", tel); params.put("newP", Utils.MD5(password.getBytes())); params.put("code", verify); RequestManager.getInstance().post(API.URL.ModifyPassword, params, callback); } /** * 绑定手机 * * @param * @param code */ public void boundTel( String oldTel, String newTel, String oldPassword, String newPassword, String code, StatusCallback callback) { RequestMap params = new RequestMap(); params.put("oldTel", oldTel); params.put("newTel", newTel); params.put("oldPassword", Utils.MD5(oldPassword.getBytes())); params.put("newPassword", Utils.MD5(newPassword.getBytes())); params.put("code", code); RequestManager.getInstance().post(API.URL.BindTel, params, callback); } /** * 商家登录 * * @param email * @param password * @param callback */ public void bizLogin(String email, String password, DataCallback callback) { RequestMap param = new RequestMap(); param.put("email", email); param.put("pass", password); RequestManager.getInstance().post(API.URL.BizLogin, param, callback); } }
@Inject public AuthDataRepository(AuthApi authApi, AccessTokenMapper accessTokenMapper) { this.authApi = authApi; this.accessTokenMapper = accessTokenMapper; this.accessTokenSubject = BehaviorSubject.create(); }
/** * This is an extension of {@link Presenter} which provides RxJava functionality. * * @param <V> a type of view. */ public class RxPresenter<V> extends Presenter<V> { private static final String REQUESTED_KEY = RxPresenter.class.getName() + "#requested"; private final BehaviorSubject<V> views = BehaviorSubject.create(); private final SubscriptionList subscriptions = new SubscriptionList(); private final HashMap<Integer, Func0<Subscription>> restartables = new HashMap<>(); private final HashMap<Integer, Subscription> restartableSubscriptions = new HashMap<>(); private final ArrayList<Integer> requested = new ArrayList<>(); /** {@inheritDoc} */ @CallSuper @Override protected void onCreate(Bundle savedState) { if (savedState != null) requested.addAll(savedState.getIntegerArrayList(REQUESTED_KEY)); } /** {@inheritDoc} */ @CallSuper @Override protected void onTakeView(V view) { views.onNext(view); } /** {@inheritDoc} */ @CallSuper @Override protected void onSave(Bundle state) { for (int i = requested.size() - 1; i >= 0; i--) { int restartableId = requested.get(i); Subscription subscription = restartableSubscriptions.get(restartableId); if (subscription != null && subscription.isUnsubscribed()) requested.remove(i); } state.putIntegerArrayList(REQUESTED_KEY, requested); } /** {@inheritDoc} */ @CallSuper @Override protected void onDropView() { views.onNext(null); } /** {@inheritDoc} */ @CallSuper @Override protected void onDestroy() { views.onCompleted(); subscriptions.unsubscribe(); for (Map.Entry<Integer, Subscription> entry : restartableSubscriptions.entrySet()) entry.getValue().unsubscribe(); } /** * Returns an {@link Observable} that emits the current attached view or null. See {@link * BehaviorSubject} for more information. * * @return an observable that emits the current attached view or null. */ public Observable<V> view() { return views; } /** * Registers a subscription to automatically unsubscribe it during onDestroy. See {@link * SubscriptionList#add(Subscription) for details.} * * @param subscription a subscription to add. */ public void add(Subscription subscription) { subscriptions.add(subscription); } /** * Removes and unsubscribes a subscription that has been registered with {@link #add} previously. * See {@link SubscriptionList#remove(Subscription)} for details. * * @param subscription a subscription to remove. */ public void remove(Subscription subscription) { subscriptions.remove(subscription); } /** * A restartable is any RxJava observable that can be started (subscribed) and should be * automatically restarted (re-subscribed) after a process restart if it was still subscribed at * the moment of saving presenter's state. * * <p>Registers a factory. Re-subscribes the restartable after the process restart. * * @param restartableId id of the restartable * @param factory factory of the restartable */ public void restartable(int restartableId, Func0<Subscription> factory) { restartables.put(restartableId, factory); if (requested.contains(restartableId)) start(restartableId); } /** * Starts the given restartable. * * @param restartableId id of the restartable */ public void start(int restartableId) { stop(restartableId); requested.add(restartableId); restartableSubscriptions.put(restartableId, restartables.get(restartableId).call()); } /** * Unsubscribes a restartable * * @param restartableId id of a restartable. */ public void stop(int restartableId) { requested.remove((Integer) restartableId); Subscription subscription = restartableSubscriptions.get(restartableId); if (subscription != null) subscription.unsubscribe(); } /** * Checks if a restartable is unsubscribed. * * @param restartableId id of the restartable. * @return true if the subscription is null or unsubscribed, false otherwise. */ public boolean isUnsubscribed(int restartableId) { Subscription subscription = restartableSubscriptions.get(restartableId); return subscription == null || subscription.isUnsubscribed(); } /** * This is a shortcut that can be used instead of combining together {@link #restartable(int, * Func0)}, {@link #deliverFirst()}, {@link #split(Action2, Action2)}. * * @param restartableId an id of the restartable. * @param observableFactory a factory that should return an Observable when the restartable should * run. * @param onNext a callback that will be called when received data should be delivered to view. * @param onError a callback that will be called if the source observable emits onError. * @param <T> the type of the observable. */ public <T> void restartableFirst( int restartableId, final Func0<Observable<T>> observableFactory, final Action2<V, T> onNext, @Nullable final Action2<V, Throwable> onError) { restartable( restartableId, new Func0<Subscription>() { @Override public Subscription call() { return observableFactory .call() .compose(RxPresenter.this.<T>deliverFirst()) .subscribe(split(onNext, onError)); } }); } /** * This is a shortcut for calling {@link #restartableFirst(int, Func0, Action2, Action2)} with the * last parameter = null. */ public <T> void restartableFirst( int restartableId, final Func0<Observable<T>> observableFactory, final Action2<V, T> onNext) { restartableFirst(restartableId, observableFactory, onNext, null); } /** * This is a shortcut that can be used instead of combining together {@link #restartable(int, * Func0)}, {@link #deliverLatestCache()}, {@link #split(Action2, Action2)}. * * @param restartableId an id of the restartable. * @param observableFactory a factory that should return an Observable when the restartable should * run. * @param onNext a callback that will be called when received data should be delivered to view. * @param onError a callback that will be called if the source observable emits onError. * @param <T> the type of the observable. */ public <T> void restartableLatestCache( int restartableId, final Func0<Observable<T>> observableFactory, final Action2<V, T> onNext, @Nullable final Action2<V, Throwable> onError) { restartable( restartableId, new Func0<Subscription>() { @Override public Subscription call() { return observableFactory .call() .compose(RxPresenter.this.<T>deliverLatestCache()) .subscribe(split(onNext, onError)); } }); } /** * This is a shortcut for calling {@link #restartableLatestCache(int, Func0, Action2, Action2)} * with the last parameter = null. */ public <T> void restartableLatestCache( int restartableId, final Func0<Observable<T>> observableFactory, final Action2<V, T> onNext) { restartableLatestCache(restartableId, observableFactory, onNext, null); } /** * This is a shortcut that can be used instead of combining together {@link #restartable(int, * Func0)}, {@link #deliverReplay()}, {@link #split(Action2, Action2)}. * * @param restartableId an id of the restartable. * @param observableFactory a factory that should return an Observable when the restartable should * run. * @param onNext a callback that will be called when received data should be delivered to view. * @param onError a callback that will be called if the source observable emits onError. * @param <T> the type of the observable. */ public <T> void restartableReplay( int restartableId, final Func0<Observable<T>> observableFactory, final Action2<V, T> onNext, @Nullable final Action2<V, Throwable> onError) { restartable( restartableId, new Func0<Subscription>() { @Override public Subscription call() { return observableFactory .call() .compose(RxPresenter.this.<T>deliverReplay()) .subscribe(split(onNext, onError)); } }); } /** * This is a shortcut for calling {@link #restartableReplay(int, Func0, Action2, Action2)} with * the last parameter = null. */ public <T> void restartableReplay( int restartableId, final Func0<Observable<T>> observableFactory, final Action2<V, T> onNext) { restartableReplay(restartableId, observableFactory, onNext, null); } /** * Returns an {@link Observable.Transformer} that couples views with data that has been emitted by * the source {@link Observable}. * * <p>{@link #deliverLatestCache} keeps the latest onNext value and emits it each time a new view * gets attached. If a new onNext value appears while a view is attached, it will be delivered * immediately. * * @param <T> the type of source observable emissions */ public <T> DeliverLatestCache<V, T> deliverLatestCache() { return new DeliverLatestCache<>(views); } /** * Returns an {@link Observable.Transformer} that couples views with data that has been emitted by * the source {@link Observable}. * * <p>{@link #deliverFirst} delivers only the first onNext value that has been emitted by the * source observable. * * @param <T> the type of source observable emissions */ public <T> DeliverFirst<V, T> deliverFirst() { return new DeliverFirst<>(views); } /** * Returns an {@link Observable.Transformer} that couples views with data that has been emitted by * the source {@link Observable}. * * <p>{@link #deliverReplay} keeps all onNext values and emits them each time a new view gets * attached. If a new onNext value appears while a view is attached, it will be delivered * immediately. * * @param <T> the type of source observable emissions */ public <T> DeliverReplay<V, T> deliverReplay() { return new DeliverReplay<>(views); } /** * Returns a method that can be used for manual restartable chain build. It returns an Action1 * that splits a received {@link Delivery} into two {@link Action2} onNext and onError calls. * * @param onNext a method that will be called if the delivery contains an emitted onNext value. * @param onError a method that will be called if the delivery contains an onError throwable. * @param <T> a type on onNext value. * @return an Action1 that splits a received {@link Delivery} into two {@link Action2} onNext and * onError calls. */ public <T> Action1<Delivery<V, T>> split( final Action2<V, T> onNext, @Nullable final Action2<V, Throwable> onError) { return new Action1<Delivery<V, T>>() { @Override public void call(Delivery<V, T> delivery) { delivery.split(onNext, onError); } }; } /** * This is a shortcut for calling {@link #split(Action2, Action2)} when the second parameter is * null. */ public <T> Action1<Delivery<V, T>> split(Action2<V, T> onNext) { return split(onNext, null); } }
/** Fragment which exposes useful lifecycle callbacks as an {@link rx.Observable} */ abstract class RxFragment extends Fragment { private final BehaviorSubject<FragmentEvent> lifecycleSubject = BehaviorSubject.create(); public Observable<FragmentEvent> lifecycle() { return lifecycleSubject.asObservable(); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); lifecycleSubject.onNext(FragmentEvent.CREATE); } @SuppressWarnings("deprecation") @Override public void onAttach(android.app.Activity activity) { super.onAttach(activity); lifecycleSubject.onNext(FragmentEvent.ATTACH); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); lifecycleSubject.onNext(FragmentEvent.CREATE_VIEW); } @Override public void onStart() { super.onStart(); lifecycleSubject.onNext(FragmentEvent.START); } @Override public void onResume() { super.onResume(); lifecycleSubject.onNext(FragmentEvent.RESUME); } @Override public void onPause() { lifecycleSubject.onNext(FragmentEvent.PAUSE); super.onPause(); } @Override public void onStop() { lifecycleSubject.onNext(FragmentEvent.STOP); super.onStop(); } @Override public void onDestroyView() { lifecycleSubject.onNext(FragmentEvent.DESTROY_VIEW); super.onDestroyView(); } @Override public void onDestroy() { lifecycleSubject.onNext(FragmentEvent.DESTROY); super.onDestroy(); } @Override public void onDetach() { lifecycleSubject.onNext(FragmentEvent.DETACH); super.onDetach(); } }
/** * A list fragment representing a list of Candidates. This fragment also supports tablet devices by * allowing list items to be given an 'activated' state upon selection. This helps indicate which * item is currently being viewed in a {@link CandidateDetailFragment}. * * <p>Activities containing this fragment MUST implement the {@link Callbacks} interface. */ public class CandidateListFragment extends ListFragment { /** * The serialization (saved instance state) Bundle key representing the activated item position. * Only used on tablets. */ private static final String STATE_ACTIVATED_POSITION = "activated_position"; /** The fragment's current callback object, which is notified of list item clicks. */ private Callbacks mCallbacks = sDummyCallbacks; /** The current activated item position. Only used on tablets. */ private int mActivatedPosition = ListView.INVALID_POSITION; private BehaviorSubject<FragmentEvent> lifecycle$ = BehaviorSubject.create(); private BehaviorSubject<Integer> selectedItemPos$ = BehaviorSubject.create(); /** * A callback interface that all activities containing this fragment must implement. This * mechanism allows activities to be notified of item selections. */ public interface Callbacks { /** Callback for when an item has been selected. */ public void onItemSelected(String pos); } /** * A dummy implementation of the {@link Callbacks} interface that does nothing. Used only when * this fragment is not attached to an activity. */ private static Callbacks sDummyCallbacks = new Callbacks() { @Override public void onItemSelected(String id) {} }; /** * Mandatory empty constructor for the fragment manager to instantiate the fragment (e.g. upon * screen orientation changes). */ public CandidateListFragment() {} @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Observable<List<CandidateBuzz>> candidateBuzzes$ = App.i().streams.candidateBuzzes$; CandidateBuzzAdapter adapter = new CandidateBuzzAdapter(getActivity(), R.layout.candidate_list_item); setListAdapter(adapter); syncListAdapter(candidateBuzzes$, adapter); lifecycle$.onNext(FragmentEvent.CREATE); } private void syncListAdapter( Observable<List<CandidateBuzz>> candidateBuzzes$, final CandidateBuzzAdapter adapter) { Observable<List<CandidateBuzz>> composed = candidateBuzzes$ .observeOn(AndroidSchedulers.mainThread()) .compose(RxLifecycle.bindFragment(lifecycle$)); composed.subscribe( candidateBuzzes -> { adapter.clear(); adapter.addAll(candidateBuzzes); }); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); // Restore the previously serialized activated item position. if (savedInstanceState != null && savedInstanceState.containsKey(STATE_ACTIVATED_POSITION)) { setActivatedPosition(savedInstanceState.getInt(STATE_ACTIVATED_POSITION)); } lifecycle$.onNext(FragmentEvent.CREATE_VIEW); } @Override public void onAttach(Activity activity) { super.onAttach(activity); // Activities containing this fragment must implement its callbacks. if (!(activity instanceof Callbacks)) { throw new IllegalStateException("Activity must implement fragment's callbacks."); } mCallbacks = (Callbacks) activity; lifecycle$.onNext(FragmentEvent.ATTACH); } @Override public void onResume() { super.onResume(); getListView().setDivider(null); App.i().loadCandidateBuzzes(); lifecycle$.onNext(FragmentEvent.RESUME); } @Override public void onDetach() { super.onDetach(); // Reset the active callbacks interface to the dummy implementation. mCallbacks = sDummyCallbacks; lifecycle$.onNext(FragmentEvent.DETACH); } @Override public void onDestroyView() { super.onDestroyView(); lifecycle$.onNext(FragmentEvent.DESTROY_VIEW); } @Override public void onDestroy() { super.onDestroy(); lifecycle$.onNext(FragmentEvent.DESTROY); } @Override public void onListItemClick(ListView listView, View view, int position, long id) { super.onListItemClick(listView, view, position, id); mCallbacks.onItemSelected(((CandidateBuzz) getListAdapter().getItem(position)).candidate.id); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mActivatedPosition != ListView.INVALID_POSITION) { // Serialize and persist the activated item position. outState.putInt(STATE_ACTIVATED_POSITION, mActivatedPosition); } } /** * Turns on activate-on-click mode. When this mode is on, list items will be given the 'activated' * state when touched. */ public void setActivateOnItemClick(boolean activateOnItemClick) { // When setting CHOICE_MODE_SINGLE, ListView will automatically // give items the 'activated' state when touched. getListView() .setChoiceMode( activateOnItemClick ? ListView.CHOICE_MODE_SINGLE : ListView.CHOICE_MODE_NONE); } private void setActivatedPosition(int position) { if (position == ListView.INVALID_POSITION) { getListView().setItemChecked(mActivatedPosition, false); } else { getListView().setItemChecked(position, true); } mActivatedPosition = position; } private static class CandidateBuzzAdapter extends ArrayAdapter<CandidateBuzz> { private final int layoutId; public CandidateBuzzAdapter(Context ctx, int layoutId) { super(ctx, layoutId); this.layoutId = layoutId; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) convertView = LayoutInflater.from(getContext()).inflate(this.layoutId, parent, false); CandidateBuzz item = getItem(position); ((TextView) convertView.findViewById(R.id.candidate_name)).setText(item.candidate.name); ((TextView) convertView.findViewById(R.id.candidate_location)) .setText(item.candidate.province); ((TextView) convertView.findViewById(R.id.candidate_buzz)).setText(item.buzz + " buzz"); Picasso.with(getContext()) .load(item.candidate.profpic) .fit() .centerCrop() .into((ImageView) convertView.findViewById(R.id.candidate_img)); return convertView; } } }
public class RxPresenter<View> extends Presenter<View> { private static final String REQUESTED_KEY = RxPresenter.class.getName() + "#requested"; private final ArrayList<Integer> requested = new ArrayList(); private final HashMap<Integer, Subscription> restartableSubscriptions = new HashMap(); private final HashMap<Integer, Func0<Subscription>> restartables = new HashMap(); private final SubscriptionList subscriptions = new SubscriptionList(); private final BehaviorSubject<View> views = BehaviorSubject.create(); public void add(Subscription paramSubscription) { this.subscriptions.add(paramSubscription); } public <T> DeliverFirst<View, T> deliverFirst() { return new DeliverFirst(this.views); } public <T> DeliverLatestCache<View, T> deliverLatestCache() { return new DeliverLatestCache(this.views); } public <T> DeliverReplay<View, T> deliverReplay() { return new DeliverReplay(this.views); } @CallSuper protected void onCreate(Bundle paramBundle) { if (paramBundle != null) { this.requested.addAll(paramBundle.getIntegerArrayList(REQUESTED_KEY)); } } @CallSuper protected void onDestroy() { super.onDestroy(); this.views.onCompleted(); this.subscriptions.unsubscribe(); Iterator localIterator = this.restartableSubscriptions.entrySet().iterator(); while (localIterator.hasNext()) { ((Subscription) ((Map.Entry) localIterator.next()).getValue()).unsubscribe(); } } @CallSuper protected void onDropView() { super.onDropView(); this.views.onNext(null); } @CallSuper protected void onSave(Bundle paramBundle) { super.onSave(paramBundle); int i = this.requested.size() - 1; while (i >= 0) { int j = ((Integer) this.requested.get(i)).intValue(); Subscription localSubscription = (Subscription) this.restartableSubscriptions.get(Integer.valueOf(j)); if ((localSubscription != null) && (localSubscription.isUnsubscribed())) { this.requested.remove(i); } i -= 1; } paramBundle.putIntegerArrayList(REQUESTED_KEY, this.requested); } @CallSuper protected void onTakeView(View paramView) { super.onTakeView(paramView); this.views.onNext(paramView); } public void remove(Subscription paramSubscription) { this.subscriptions.remove(paramSubscription); } public void restartable(int paramInt, Func0<Subscription> paramFunc0) { this.restartables.put(Integer.valueOf(paramInt), paramFunc0); if (this.requested.contains(Integer.valueOf(paramInt))) { start(paramInt); } } public <T> void restartableFirst( int paramInt, Func0<Observable<T>> paramFunc0, Action2<View, T> paramAction2) { restartableFirst(paramInt, paramFunc0, paramAction2, null); } public <T> void restartableFirst( int paramInt, final Func0<Observable<T>> paramFunc0, final Action2<View, T> paramAction2, @Nullable final Action2<View, Throwable> paramAction21) { restartable( paramInt, new Func0() { public Subscription call() { return ((Observable) paramFunc0.call()) .compose(RxPresenter.this.deliverFirst()) .subscribe(RxPresenter.this.split(paramAction2, paramAction21)); } }); } public <T> void restartableLatestCache( int paramInt, Func0<Observable<T>> paramFunc0, Action2<View, T> paramAction2) { restartableLatestCache(paramInt, paramFunc0, paramAction2, null); } public <T> void restartableLatestCache( int paramInt, final Func0<Observable<T>> paramFunc0, final Action2<View, T> paramAction2, @Nullable final Action2<View, Throwable> paramAction21) { restartable( paramInt, new Func0() { public Subscription call() { return ((Observable) paramFunc0.call()) .compose(RxPresenter.this.deliverLatestCache()) .subscribe(RxPresenter.this.split(paramAction2, paramAction21)); } }); } public <T> void restartableReplay( int paramInt, Func0<Observable<T>> paramFunc0, Action2<View, T> paramAction2) { restartableReplay(paramInt, paramFunc0, paramAction2, null); } public <T> void restartableReplay( int paramInt, final Func0<Observable<T>> paramFunc0, final Action2<View, T> paramAction2, @Nullable final Action2<View, Throwable> paramAction21) { restartable( paramInt, new Func0() { public Subscription call() { return ((Observable) paramFunc0.call()) .compose(RxPresenter.this.deliverReplay()) .subscribe(RxPresenter.this.split(paramAction2, paramAction21)); } }); } public <T> Action1<Delivery<View, T>> split(Action2<View, T> paramAction2) { return split(paramAction2, null); } public <T> Action1<Delivery<View, T>> split( final Action2<View, T> paramAction2, @Nullable final Action2<View, Throwable> paramAction21) { new Action1() { public void call(Delivery<View, T> paramAnonymousDelivery) { paramAnonymousDelivery.split(paramAction2, paramAction21); } }; } public void start(int paramInt) { stop(paramInt); this.requested.add(Integer.valueOf(paramInt)); this.restartableSubscriptions.put( Integer.valueOf(paramInt), ((Func0) this.restartables.get(Integer.valueOf(paramInt))).call()); } public void stop(int paramInt) { this.requested.remove(Integer.valueOf(paramInt)); Subscription localSubscription = (Subscription) this.restartableSubscriptions.get(Integer.valueOf(paramInt)); if (localSubscription != null) { localSubscription.unsubscribe(); } } public Observable<View> view() { return this.views; } }
public class MainActivity extends RxAppCompatActivity implements SwipeRefreshLayout.OnRefreshListener { private static final String REDDIT_BASE_URL = "http://www.reddit.com"; @InjectView(R.id.toolbar) Toolbar mToolbar; @InjectView(R.id.swipe) SwipeRefreshLayout mSwipeRefreshLayout; @InjectView(R.id.list) RecyclerView mRecyclerView; private final BehaviorSubject<String> mFetchAfterSubject = BehaviorSubject.create(); private RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); int itemCount = mLayoutManager.getItemCount(); int lastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition(); if (lastVisibleItemPosition == itemCount - 1 && PhotosValidator.hasAfter(mPhotos)) { mFetchAfterSubject.onNext(mPhotos.getAfter()); } } }; RedditPhotoService mPhotoService; Photos mPhotos; PhotosAdapter mPhotosAdapter; LinearLayoutManager mLayoutManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // Init UI components ButterKnife.inject(this); setSupportActionBar(mToolbar); mLayoutManager = new LinearLayoutManager(this); mRecyclerView.setLayoutManager(mLayoutManager); mRecyclerView.addOnScrollListener(mOnScrollListener); mSwipeRefreshLayout.setOnRefreshListener(this); mSwipeRefreshLayout.setColorSchemeColors( getResources().getColor(R.color.accent), getResources().getColor(R.color.flipagram_red)); initServices(); initData(); } @Override protected void onDestroy() { super.onDestroy(); mRecyclerView.removeOnScrollListener(mOnScrollListener); } @Override public void onRefresh() { initData(); } private void initData() { // Initial load mPhotoService .getHotPhotos(null) .doOnNext(__ -> runOnUiThread(() -> mSwipeRefreshLayout.setRefreshing(true))) .compose(bindToLifecycle()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::bind, this::onErrorDownloading); // Observable for loading more pages mFetchAfterSubject .filter(after -> !StringUtils.isNullOrEmpty(after)) .distinctUntilChanged() .switchMap(mPhotoService::getHotPhotos) .doOnNext(__ -> runOnUiThread(() -> mSwipeRefreshLayout.setRefreshing(true))) .compose(bindUntilEvent(ActivityEvent.DESTROY)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::merge, this::onErrorDownloading); } private void merge(Photos photos) { mSwipeRefreshLayout.setRefreshing(false); mPhotos.mergeWith(photos); mPhotosAdapter.notifyDataSetChanged(); } private void onErrorDownloading(Throwable throwable) { mSwipeRefreshLayout.setRefreshing(false); Toast.makeText(this, throwable.getMessage(), Toast.LENGTH_SHORT).show(); } private void initServices() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(Photos.class, new PhotosDeserializer()); RestAdapter restAdapter = new RestAdapter.Builder() .setEndpoint(REDDIT_BASE_URL) .setConverter(new GsonConverter(gsonBuilder.create())) .build(); mPhotoService = restAdapter.create(RedditPhotoService.class); } private void bind(Photos photos) { mSwipeRefreshLayout.setRefreshing(false); mPhotos = photos; mPhotosAdapter = new PhotosAdapter(mPhotos.getPhotos()); mRecyclerView.setAdapter(mPhotosAdapter); Observable.merge( mPhotosAdapter .getThumbnailClickedSubject() .filter(PhotosValidator::isSourceValid) .map(Photo::getSourceUrl), mPhotosAdapter .getItemClickedSubject() .filter(photo -> StringUtils.isValidUrl(photo.getPermalink())) .map(Photo::getPermalink)) .compose(bindUntilEvent(ActivityEvent.DESTROY)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(OnlyNextObserver.forAction(this::view)); } private void view(String url) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse(url)); startActivity(intent); } }
/** This fragment shows the content of one post. */ public class PostFragment extends BaseFragment implements NewTagDialogFragment.OnAddNewTagsListener, CommentsAdapter.CommentActionListener, InfoLineView.OnDetailClickedListener { private static final String ARG_FEED_ITEM = "PostFragment.post"; private static final String ARG_COMMENT_DRAFT = "PostFragment.comment-draft"; private FeedItem feedItem; private MediaView viewer; @Inject FeedService feedService; @Inject VoteService voteService; @Inject Settings settings; @Inject SeenService seenService; @Inject DownloadManager downloadManager; @Inject SingleShotService singleShotService; @Inject InMemoryCacheService inMemoryCacheService; @Inject UserService userService; @Inject DownloadService downloadService; @Inject FavedCommentService favedCommentService; @Bind(R.id.refresh) SwipeRefreshLayout swipeRefreshLayout; @Bind(R.id.player_container) ViewGroup playerContainer; @Bind(R.id.post_content) RecyclerView content; @Bind(R.id.vote_indicator) TextView voteAnimationIndicator; @Bind(R.id.repost_hint) View repostHint; private InfoLineView infoLineView; // start with an empty adapter here private MergeRecyclerAdapter adapter; private CommentsAdapter commentsAdapter; private Optional<Long> autoScrollTo; private RecyclerView.OnScrollListener scrollHandler; private final LoginActivity.DoIfAuthorizedHelper doIfAuthorizedHelper = LoginActivity.helper(this); private PreviewInfo previewInfo; private List<Tag> tags; private List<Comment> comments; private boolean rewindOnLoad; private boolean tabletLayout; private final BehaviorSubject<Boolean> activeStateSubject = BehaviorSubject.create(false); @Override public void onCreate(Bundle savedState) { super.onCreate(savedState); setHasOptionsMenu(true); // get the item that is to be displayed. feedItem = getArguments().getParcelable(ARG_FEED_ITEM); if (savedState != null) { tags = Parceler.get(TagListParceler.class, savedState, "PostFragment.tags"); comments = Parceler.get(CommentListParceler.class, savedState, "PostFragment.comments"); } autoScrollTo = Optional.absent(); activeState() .compose(bindToLifecycleForeground()) .subscribe( active -> { if (viewer != null) { if (active) { viewer.playMedia(); } else { viewer.stopMedia(); } } if (!active) { exitFullscreenAnimated(false); } }); } private Observable<Boolean> activeState() { Observable<FragmentEvent> startStopLifecycle = lifecycle().filter(ev -> ev == FragmentEvent.START || ev == FragmentEvent.STOP); // now combine with the activeStateSubject and return a new observable with // the "active state". Observable<Boolean> combined = combineLatest( startStopLifecycle, activeStateSubject, (ev, active) -> active && ev == FragmentEvent.START); return combined.distinctUntilChanged(); } @Override protected void injectComponent(ActivityComponent activityComponent) { activityComponent.inject(this); } @Override public View onCreateView( LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { ViewGroup view = (ViewGroup) inflater.inflate(R.layout.fragment_post, container, false); addWarnOverlayIfNecessary(inflater, view); return view; } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); this.tabletLayout = view.findViewById(R.id.infoview) != null; if (!(getActivity() instanceof ToolbarActivity)) { throw new IllegalStateException("Fragment must be child of a ToolbarActivity."); } ToolbarActivity activity = (ToolbarActivity) getActivity(); activity.getScrollHideToolbarListener().reset(); int abHeight = AndroidUtility.getActionBarContentOffset(getActivity()); if (tabletLayout) { View tabletLayout = ButterKnife.findById(view, R.id.tabletlayout); tabletLayout.setPadding(0, abHeight, 0, 0); ((FrameLayout.LayoutParams) voteAnimationIndicator.getLayoutParams()).gravity = Gravity.CENTER; scrollHandler = new NoopOnScrollListener(); } else { // use height of the toolbar to configure swipe refresh layout. swipeRefreshLayout.setProgressViewOffset(false, 0, (int) (1.5 * abHeight)); scrollHandler = new ScrollHandler(activity); } content.addOnScrollListener(scrollHandler); swipeRefreshLayout.setColorSchemeResources(R.color.primary); swipeRefreshLayout.setOnRefreshListener( () -> { if (!isVideoFullScreen()) { rewindOnLoad = true; loadPostDetails(); } }); swipeRefreshLayout.setKeepScreenOn(settings.keepScreenOn()); adapter = new MergeRecyclerAdapter(); content.setItemAnimator(null); content.setLayoutManager(new LinearLayoutManager(getActivity())); content.setAdapter(adapter); initializeMediaView(); initializeInfoLine(); initializeCommentPostLine(); commentsAdapter = new CommentsAdapter(userService.getName().or("")); commentsAdapter.setCommentActionListener(this); commentsAdapter.setPrioritizeOpComments(settings.prioritizeOpComments()); commentsAdapter.setShowFavCommentButton(userService.isAuthorized()); adapter.addAdapter(commentsAdapter); // restore the postInfo, if possible. if (tags != null && comments != null) { displayTags(tags); displayComments(comments); } loadPostDetails(); // show the repost badge if this is a repost repostHint.setVisibility(isRepost() ? View.VISIBLE : View.GONE); } private boolean isRepost() { return inMemoryCacheService.isRepost(feedItem); } @Override public void onDestroyView() { content.removeOnScrollListener(scrollHandler); // restore orientation if the user closes this view Screen.unlockOrientation(getActivity()); adapter = null; super.onDestroyView(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putParcelable("PostFragment.tags", new TagListParceler(tags)); outState.putParcelable("PostFragment.comments", new CommentListParceler(comments)); } private void addWarnOverlayIfNecessary(LayoutInflater inflater, ViewGroup view) { // add a view over the main view, if the post is not visible now if (!settings.getContentType().contains(feedItem.getContentType())) { View overlay = inflater.inflate(R.layout.warn_post_can_not_be_viewed, view, false); view.addView(overlay); // link the hide button View button = overlay.findViewById(R.id.hide_warning_button); button.setOnClickListener(v -> AndroidUtility.removeView(overlay)); } } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); doIfAuthorizedHelper.onActivityResult(requestCode, resultCode); if (requestCode == RequestCodes.WRITE_COMMENT && resultCode == Activity.RESULT_OK) { onNewComments(WriteMessageActivity.getNewComment(data)); } } public void setPreviewInfo(PreviewInfo previewInfo) { this.previewInfo = previewInfo; } private void initializeCommentPostLine() { ViewGroup.MarginLayoutParams layoutParams = new ViewGroup.MarginLayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); CommentPostLine line = new CommentPostLine(getActivity()); line.setLayoutParams(layoutParams); adapter.addAdapter(SingleViewAdapter.ofView(line)); line.setCommentDraft(getArguments().getString(ARG_COMMENT_DRAFT, "")); line.textChanges().subscribe(text -> getArguments().putString(ARG_COMMENT_DRAFT, text)); line.comments() .subscribe( text -> { Runnable action = () -> { line.clear(); writeComment(text); }; doIfAuthorizedHelper.run(action, action); }); } private void writeComment(String text) { voteService .postComment(feedItem, 0, text) .compose(bindToLifecycle()) .lift(busyDialog(this)) .subscribe(this::onNewComments, defaultOnError()); } /** * Scroll the th given comment * * @param commentId The comment id to scroll to */ private void scrollToComment(long commentId) { Optional<Integer> offset = adapter.getOffset(commentsAdapter); if (!offset.isPresent()) return; for (int idx = 0; idx < commentsAdapter.getItemCount(); idx++) { if (commentsAdapter.getItemId(idx) == commentId) { content.scrollToPosition(offset.get() + idx); break; } } commentsAdapter.setSelectedCommentId(commentId); } public void autoScrollToComment(long commentId) { if (commentId > 0) { autoScrollTo = Optional.of(commentId); } else { autoScrollTo = Optional.absent(); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.menu_post, menu); } @Override public void onPrepareOptionsMenu(Menu menu) { boolean isImage = isStaticImage(feedItem); boolean isRotated = getActivity().getWindowManager().getDefaultDisplay().getRotation() != Surface.ROTATION_0; MenuItem item; if ((item = menu.findItem(R.id.action_refresh)) != null) item.setVisible(settings.showRefreshButton() && !isVideoFullScreen()); if ((item = menu.findItem(R.id.action_zoom)) != null) item.setVisible(!isVideoFullScreen() && (isImage || !isRotated)); if ((item = menu.findItem(R.id.action_share_image)) != null) item.setVisible(ShareProvider.canShare(getActivity(), feedItem)); if ((item = menu.findItem(R.id.action_search_image)) != null) item.setVisible(isImage && settings.showGoogleImageButton()); } @OnOptionsItemSelected(R.id.action_zoom) public void enterFullscreen() { FragmentActivity activity = getActivity(); if (activity == null) return; if (isStaticImage(feedItem)) { boolean hq = settings.loadHqInZoomView(); Intent intent = ZoomViewActivity.newIntent(activity, feedItem, hq); startActivity(intent); } else { FullscreenParams params = new FullscreenParams(); ObjectAnimator.ofPropertyValuesHolder( viewer, ofFloat(View.ROTATION, params.rotation), ofFloat(View.TRANSLATION_Y, params.trY), ofFloat(View.SCALE_X, params.scale), ofFloat(View.SCALE_Y, params.scale)) .setDuration(500) .start(); repostHint.setVisibility(View.GONE); // hide content below swipeRefreshLayout.setVisibility(View.GONE); if (activity instanceof ToolbarActivity) { // hide the toolbar if required necessary ((ToolbarActivity) activity).getScrollHideToolbarListener().hide(); } viewer.setClipBoundsCompat(null); activity.supportInvalidateOptionsMenu(); registerExitFullscreenListener(); // forbid orientation changes while in fullscreen Screen.lockOrientation(activity); } } private void realignFullScreen() { FullscreenParams params = new FullscreenParams(); viewer.setTranslationY(params.trY); viewer.setScaleX(params.scale); viewer.setScaleY(params.scale); } private void registerExitFullscreenListener() { // add a listener to show/hide the fullscreen. View view = getView(); if (view != null) { // get the focus for the back button view.setFocusableInTouchMode(true); view.requestFocus(); view.setOnKeyListener( (v, keyCode, event) -> { if (isVideoFullScreen()) { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { // remove listener view.setOnKeyListener(null); view.setOnFocusChangeListener(null); // and move back to normal state exitFullscreen(); return true; } } return false; }); view.setOnFocusChangeListener( (v, hasFocus) -> { if (!hasFocus && isVideoFullScreen()) { view.requestFocus(); } }); } } public void exitFullscreen() { exitFullscreenAnimated(true); } private void exitFullscreenAnimated(boolean animated) { if (!isVideoFullScreen()) return; swipeRefreshLayout.setVisibility(View.VISIBLE); // reset the values correctly viewer.setRotation(0.f); viewer.setScaleX(1.f); viewer.setScaleY(1.f); viewer.setTranslationX(0.f); // simulate scrolling to fix the clipping and translationY simulateScroll(); // go back to normal! FragmentActivity activity = getActivity(); activity.supportInvalidateOptionsMenu(); if (activity instanceof ToolbarActivity) { // show the toolbar again ((ToolbarActivity) activity).getScrollHideToolbarListener().reset(); } Screen.unlockOrientation(activity); } private boolean isVideoFullScreen() { return swipeRefreshLayout != null && swipeRefreshLayout.getVisibility() != View.VISIBLE; } @OnOptionsItemSelected(MainActivity.ID_FAKE_HOME) public boolean onHomePressed() { if (isVideoFullScreen()) { exitFullscreen(); return true; } return false; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.action_search_image: ShareHelper.searchImage(getActivity(), feedItem); return true; case R.id.action_share_post: ShareHelper.sharePost(getActivity(), feedItem); return true; case R.id.action_share_direct_link: ShareHelper.shareDirectLink(getActivity(), feedItem); return true; case R.id.action_share_image: ShareHelper.shareImage(getActivity(), feedItem); return true; default: return OptionMenuHelper.dispatch(this, item) || super.onOptionsItemSelected(item); } } @OnOptionsItemSelected(R.id.action_refresh) public void refreshWithIndicator() { if (swipeRefreshLayout.isRefreshing()) return; rewindOnLoad = true; swipeRefreshLayout.setRefreshing(true); swipeRefreshLayout.postDelayed(this::loadPostDetails, 500); } @OnOptionsItemSelected(R.id.action_download) public void downloadPostMedia() { ((PermissionHelperActivity) getActivity()) .requirePermission(WRITE_EXTERNAL_STORAGE) .compose(bindUntilEvent(FragmentEvent.DESTROY)) .subscribe(ignored -> downloadPostWithPermissionGranted(), defaultOnError()); } private void downloadPostWithPermissionGranted() { Optional<String> error = downloadService.download(feedItem); if (error.isPresent()) showErrorString(getFragmentManager(), error.get()); } @Override public void onStart() { super.onStart(); favedCommentService .favedCommentIds() .compose(bindToLifecycle()) .subscribe(commentsAdapter::setFavedComments); } @Override public void onResume() { super.onResume(); // set ordering commentsAdapter.setPrioritizeOpComments(settings.prioritizeOpComments()); } /** Loads the information about the post. This includes the tags and the comments. */ private void loadPostDetails() { int delay = Sdk.isAtLeastKitKat() ? 500 : 100; feedService .loadPostDetails(feedItem.getId()) .delay(delay, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread()) .compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW)) .subscribe(this::onPostReceived, defaultOnError()); } @SuppressWarnings("CodeBlock2Expr") private void initializeInfoLine() { // get the vote from the service Observable<Vote> cachedVote = voteService.getVote(feedItem).compose(bindToLifecycle()); //noinspection ConstantConditions infoLineView = ButterKnife.findById(getView(), R.id.infoview); if (infoLineView == null) { infoLineView = new InfoLineView(getActivity()); adapter.addAdapter(SingleViewAdapter.ofView(infoLineView)); } boolean isSelfPost = userService .getName() .transform(name -> name.equalsIgnoreCase(feedItem.getUser())) .or(false); // display the feed item in the view infoLineView.setFeedItem(feedItem, isSelfPost, cachedVote); infoLineView.setOnDetailClickedListener(this); // register the vote listener infoLineView.setOnVoteListener( vote -> { Runnable action = () -> { showPostVoteAnimation(vote); voteService .vote(feedItem, vote) .compose(bindToLifecycle()) .subscribe(Actions.empty(), defaultOnError()); }; Runnable retry = () -> infoLineView.getVoteView().setVote(vote); return doIfAuthorizedHelper.run(action, retry); }); // and a vote listener vor voting tags. infoLineView.setTagVoteListener( (tag, vote) -> { Runnable action = () -> { voteService .vote(tag, vote) .compose(bindToLifecycle()) .subscribe(Actions.empty(), defaultOnError()); }; return doIfAuthorizedHelper.run(action, action); }); infoLineView.setOnAddTagClickedListener( () -> { NewTagDialogFragment dialog = new NewTagDialogFragment(); dialog.show(getChildFragmentManager(), null); }); } private void showPostVoteAnimation(Vote vote) { if (vote == null || vote == Vote.NEUTRAL) return; // quickly center the vote button simulateScroll(); String text = vote == Vote.UP ? "+" : (vote == Vote.DOWN ? "-" : "*"); voteAnimationIndicator.setText(text); voteAnimationIndicator.setVisibility(View.VISIBLE); voteAnimationIndicator.setAlpha(0); voteAnimationIndicator.setScaleX(0.7f); voteAnimationIndicator.setScaleY(0.7f); ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( voteAnimationIndicator, ofFloat(View.ALPHA, 0, 0.6f, 0.7f, 0.6f, 0), ofFloat(View.SCALE_X, 0.7f, 1.3f), ofFloat(View.SCALE_Y, 0.7f, 1.3f)); animator.start(); animator.addListener( new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { View view = PostFragment.this.voteAnimationIndicator; if (view != null) { view.setVisibility(View.GONE); } } }); } private void initializeMediaView() { int padding = AndroidUtility.getActionBarContentOffset(getActivity()); // initialize a new viewer fragment MediaUri uri = MediaUri.of(feedItem.getId(), UriHelper.of(getContext()).media(feedItem)); if (!uri.isLocal() && AndroidUtility.isOnMobile(getActivity())) { Settings.ConfirmOnMobile confirmOnMobile = settings.confirmPlayOnMobile(getContext()); if (confirmOnMobile == Settings.ConfirmOnMobile.ALL) { uri = uri.withDelay(true); } else if (confirmOnMobile == Settings.ConfirmOnMobile.VIDEO && uri.getMediaType() != MediaUri.MediaType.IMAGE) { uri = uri.withDelay(true); } } viewer = MediaViews.newInstance( getActivity(), uri, () -> { // mark this item seen. We do that in a background thread seenService.markAsSeen(feedItem); }); // inform viewer over fragment lifecycle events! MediaViews.adaptFragmentLifecycle(lifecycle(), viewer); registerTapListener(viewer); PreviewInfo previewInfo = this.previewInfo != null ? this.previewInfo : getPreviewInfoFromCache(); if (previewInfo != null) { viewer.setPreviewImage(previewInfo, "TransitionTarget-" + feedItem.getId()); if (Sdk.isAtLeastLollipop()) { viewer.postDelayed(this::onTransitionEnds, 350); } else { viewer.post(this::onTransitionEnds); } } else { viewer.post(this::onTransitionEnds); } // add views in the correct order int idx = playerContainer.indexOfChild(voteAnimationIndicator); playerContainer.addView(viewer, idx); if (tabletLayout) { viewer.setLayoutParams( new FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); } else { viewer.setPadding(0, padding, 0, 0); // we add a placeholder to the first element of the recycler view. // this placeholder will mirror the size of the viewer. PlaceholderView placeholder = new PlaceholderView(); adapter.addAdapter(SingleViewAdapter.ofView(placeholder)); RxView.layoutChanges(viewer) .subscribe( event -> { int newHeight = viewer.getMeasuredHeight(); if (newHeight != placeholder.fixedHeight) { placeholder.fixedHeight = newHeight; if (Sdk.isAtLeastKitKat()) { placeholder.requestLayout(); } else { // it looks like a requestLayout is not honored on pre kitkat devices // if already in a layout pass. placeholder.post(placeholder::requestLayout); } if (isVideoFullScreen()) { realignFullScreen(); } } }); RxView.layoutChanges(placeholder) .subscribe( event -> { // simulate scroll after layouting the placeholder to // reflect changes to the viewers clipping. simulateScroll(); }); } } private void onTransitionEnds() { if (viewer != null && content != null) { viewer.onTransitionEnds(); } } private void simulateScroll() { if (scrollHandler instanceof ScrollHandler) { scrollHandler.onScrolled(content, 0, 0); } else { // simulate a scroll to "null" offsetMediaView(true, 0.0f); } } /** * Registers a tap listener on the given viewer instance. The listener is used to handle * double-tap-to-vote events from the view. * * @param viewer The viewer to register the tap listener to. */ private void registerTapListener(MediaView viewer) { viewer.setTapListener( new MediaView.TapListener() { final boolean isImage = isStaticImage(feedItem); @Override public boolean onSingleTap() { if (isImage && settings.singleTapForFullscreen()) { enterFullscreen(); } return true; } @Override public boolean onDoubleTap() { if (settings.doubleTapToUpvote()) { infoLineView.getVoteView().triggerUpVoteClicked(); } return true; } }); } /** * Called with the downloaded post information. * * @param post The post information that was downloaded. */ private void onPostReceived(Post post) { swipeRefreshLayout.setRefreshing(false); // update from post displayTags(post.getTags()); displayComments(post.getComments()); if (rewindOnLoad) { rewindOnLoad = false; viewer.rewind(); } } private void displayTags(List<Tag> tags_) { List<Tag> tags = inMemoryCacheService.enhanceTags(feedItem.getId(), tags_); this.tags = ImmutableList.copyOf(tags); // show tags now infoLineView.setTags(toMap(tags, tag -> Vote.NEUTRAL)); // and update tags with votes later. voteService .getTagVotes(tags) .filter(votes -> !votes.isEmpty()) .onErrorResumeNext(Observable.<Map<Long, Vote>>empty()) .compose(bindToLifecycle()) .subscribe( votes -> infoLineView.setTags( toMap(tags, tag -> firstNonNull(votes.get(tag.getId()), Vote.NEUTRAL)))); hideProgressIfLoop(tags); } /** * If the current post is a loop, we'll check if it is a loop. If it is, we will hide the little * video progress bar. */ private void hideProgressIfLoop(List<Tag> tags) { MediaView actualView = viewer != null ? viewer.getActualMediaView() : null; if (actualView instanceof AbstractProgressMediaView) { if (Iterables.any(tags, tag -> isLoopTag(tag.getTag()))) { ((AbstractProgressMediaView) actualView).setProgressEnabled(false); } } } /** * Displays the given list of comments combined with the voting for those comments. * * @param comments The list of comments to display. */ private void displayComments(List<Comment> comments) { this.comments = ImmutableList.copyOf(comments); // show now commentsAdapter.set(comments, emptyMap(), feedItem.getUser()); if (autoScrollTo.isPresent()) { scrollToComment(autoScrollTo.get()); autoScrollTo = Optional.absent(); } // load the votes for the comments and update, when we found any voteService .getCommentVotes(comments) .filter(votes -> !votes.isEmpty()) .onErrorResumeNext(empty()) .compose(bindToLifecycle()) .subscribe(votes -> commentsAdapter.set(comments, votes, feedItem.getUser())); } /** Returns the feed item that is displayed in this {@link PostFragment}. */ public FeedItem getFeedItem() { return feedItem; } /** * Called from the {@link PostPagerFragment} if this fragment is currently the active/selected * fragment - or if it is not the active fragment anymore. * * @param active The new active status. */ public void setActive(boolean active) { activeStateSubject.onNext(active); } @Override public void onAddNewTags(List<String> tags) { voteService .tag(feedItem, tags) .compose(bindToLifecycle()) .lift(busyDialog(this)) .subscribe(this::displayTags, defaultOnError()); } /** Creates a new instance of a {@link PostFragment} displaying the given {@link FeedItem}. */ public static PostFragment newInstance(FeedItem item) { checkNotNull(item, "Item must not be null"); Bundle arguments = new Bundle(); arguments.putParcelable(ARG_FEED_ITEM, item); PostFragment fragment = new PostFragment(); fragment.setArguments(arguments); return fragment; } @SuppressWarnings("CodeBlock2Expr") @Override public boolean onCommentVoteClicked(Comment comment, Vote vote) { return doIfAuthorizedHelper.run( () -> { voteService .vote(comment, vote) .compose(bindToLifecycle()) .subscribe(Actions.empty(), defaultOnError()); }); } @SuppressWarnings("CodeBlock2Expr") @Override public void onAnswerClicked(Comment comment) { Runnable retry = () -> onAnswerClicked(comment); doIfAuthorizedHelper.run( () -> { startActivityForResult( WriteMessageActivity.answerToComment(getActivity(), feedItem, comment), RequestCodes.WRITE_COMMENT); }, retry); } @Override public void onCommentAuthorClicked(Comment comment) { onUserClicked(comment.getName()); } @Override public void onCommentMarkAsFavoriteClicked(Comment comment, boolean markAsFavorite) { Observable<Void> result; if (markAsFavorite) { result = favedCommentService.save( ImmutableFavedComment.builder() .id(comment.getId()) .name(comment.getName()) .content(comment.getContent()) .created(comment.getCreated()) .up(comment.getUp()) .down(comment.getDown()) .mark(comment.getMark()) .thumb(feedItem.getThumb()) .itemId(feedItem.getId()) .flags(feedItem.getFlags()) .build()); } else { result = favedCommentService.delete(comment.getId()); } result .compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW)) .subscribe(Actions.empty(), defaultOnError()); if (singleShotService.isFirstTime("kfav-userscript-hint")) { DialogBuilder.start(getContext()) .content(R.string.hint_kfav_userscript) .positive( R.string.open_website, di -> { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://goo.gl/py7xNW")); getContext().startActivity(intent); }) .negative(R.string.ignore) .show(); } } @Override public void onTagClicked(Tag tag) { if (getParentFragment() instanceof PostPagerFragment) ((PostPagerFragment) getParentFragment()).onTagClicked(tag); } @Override public void onUserClicked(String username) { if (getParentFragment() instanceof PostPagerFragment) ((PostPagerFragment) getParentFragment()).onUsernameClicked(username); } private void onNewComments(NewComment response) { autoScrollToComment(response.getCommentId()); displayComments(response.getComments()); Snackbar.make(content, R.string.comment_written_successful, Snackbar.LENGTH_LONG).show(); hideSoftKeyboard(); } @SuppressWarnings("ConstantConditions") private void hideSoftKeyboard() { try { InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(getView().getWindowToken(), 0); } catch (Exception ignored) { } } @Nullable private PreviewInfo getPreviewInfoFromCache() { Uri previewUri = UriHelper.of(getActivity()).thumbnail(feedItem); return inMemoryCacheService .getSizeInfo(feedItem.getId()) .transform( info -> new PreviewInfo(info.getId(), previewUri, info.getWidth(), info.getHeight())) .orNull(); } public void mediaHorizontalOffset(int offset) { viewer.setTranslationX(offset); } private class PlaceholderView extends View { int fixedHeight = AndroidUtility.dp(getActivity(), 150); public PlaceholderView() { super(PostFragment.this.getContext()); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); setMeasuredDimension(width, fixedHeight); } @Override public boolean onTouchEvent(@NonNull MotionEvent event) { return viewer.onTouchEvent(event); } } private class ScrollHandler extends RecyclerView.OnScrollListener { private final ToolbarActivity activity; public ScrollHandler(ToolbarActivity activity) { this.activity = activity; } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (isVideoFullScreen()) return; // get our facts straight int recyclerHeight = recyclerView.getHeight(); Optional<Integer> scrollEstimate = estimateRecyclerViewScrollY(recyclerView); boolean viewerVisible = scrollEstimate.isPresent(); int scrollY = scrollEstimate.or(viewer.getHeight()); int viewerHeight = viewer.getHeight(); boolean doFancyScroll = viewerHeight < recyclerHeight; ScrollHideToolbarListener toolbar = activity.getScrollHideToolbarListener(); if (!doFancyScroll || dy < 0 || scrollY > toolbar.getToolbarHeight()) { toolbar.onScrolled(dy); } float scroll = doFancyScroll ? 0.7f * scrollY : scrollY; if (doFancyScroll) { int clipTop = (int) (scroll + 0.5f); int clipBottom = viewer.getHeight() - (int) (scrollY - scroll + 0.5f); if (clipTop < clipBottom) { viewer.setClipBoundsCompat(new Rect(0, clipTop, viewer.getRight(), clipBottom)); } else { viewerVisible = false; } } else { // reset bounds. we might have set some previously and want // to clear those bounds now. viewer.setClipBoundsCompat(null); } offsetMediaView(viewerVisible, scroll); // position the vote indicator float remaining = viewerHeight - scrollY; int tbVisibleHeight = toolbar.getVisibleHeight(); float voteIndicatorY = Math.min((remaining - tbVisibleHeight) / 2, (recyclerHeight - tbVisibleHeight) / 2) + tbVisibleHeight; voteAnimationIndicator.setTranslationY(voteIndicatorY); } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (!isVideoFullScreen() && newState == RecyclerView.SCROLL_STATE_IDLE) { int y = estimateRecyclerViewScrollY(recyclerView).or(Integer.MAX_VALUE); activity.getScrollHideToolbarListener().onScrollFinished(y); } } } /** Positions the media view using the given offset (on the y axis) */ private void offsetMediaView(boolean viewerVisible, float offset) { if (viewerVisible) { // finally position the viewer viewer.setTranslationY(-offset); viewer.setVisibility(View.VISIBLE); // position the repost badge, if it is visible if (repostHint.getVisibility() == View.VISIBLE) { repostHint.setTranslationY(viewer.getPaddingTop() - repostHint.getPivotY() - offset); } } else { viewer.setVisibility(View.INVISIBLE); } } private class FullscreenParams { private final float scale; private final float trY; private final float rotation; FullscreenParams() { int windowWidth = swipeRefreshLayout.getWidth(); float windowHeight = swipeRefreshLayout.getHeight(); //noinspection UnnecessaryLocalVariable int viewerWidth = windowWidth; int viewerHeight = viewer.getHeight() - viewer.getPaddingTop(); viewer.setPivotY(viewer.getHeight() - 0.5f * viewerHeight); viewer.setPivotX(viewerWidth / 2.f); trY = (windowHeight / 2.f - viewer.getPivotY()); float scaleRot = Math.min(windowHeight / (float) viewerWidth, windowWidth / (float) viewerHeight); float scaleNoRot = Math.min(windowHeight / (float) viewerHeight, windowWidth / (float) viewerWidth); // check if rotation is necessary if (scaleRot > scaleNoRot) { rotation = 90.f; scale = scaleRot; } else { rotation = 0.f; scale = scaleNoRot; } } } /** Returns true, if the given tag looks like some "loop" tag. */ private static boolean isLoopTag(String tag) { tag = tag.toLowerCase(); return tag.contains("loop") && !(tag.contains("verschenkt") || tag.contains("verkackt")); } /** * Returns true, if the given url links to a static image. This does only a check on the filename * and not on the data. * * @param image The url of the image to check */ private static boolean isStaticImage(FeedItem image) { return image.getImage().toLowerCase().matches(".*\\.(jpg|jpeg|png)"); } }
public class UpdateProcess { private static final Logger log = LoggerFactory.getLogger(UpdateProcess.class); private static final List<ECPoint> UPDATE_SIGNING_KEYS = Crypto.decode("029EF2D0D33A2546CB15FB10D969B7D65CAFB811CB3AC902E8D9A46BE847B1DA21"); private static final String UPDATES_BASE_URL = "https://bitsquare.io/updateFX/v03"; private static final int UPDATE_SIGNING_THRESHOLD = 1; private static final Path ROOT_CLASS_PATH = UpdateFX.findCodePath(BitsquareAppMain.class); private final BitsquareEnvironment environment; public enum State { CHECK_FOR_UPDATES, UPDATE_AVAILABLE, UP_TO_DATE, NEW_RELEASE, // if a new minor release is out we inform the user to download the new binary FAILURE } public final ObjectProperty<State> state = new SimpleObjectProperty<>(State.CHECK_FOR_UPDATES); private String releaseUrl; private final Subject<State, State> process = BehaviorSubject.create(); private Timer timeoutTimer; @Inject public UpdateProcess(BitsquareEnvironment environment) { this.environment = environment; } public void restart() { UpdateFX.restartApp(); } public Observable<State> getProcess() { return process.asObservable(); } public void init() { log.info("UpdateFX current version " + Version.PATCH_VERSION); // process.timeout() will cause an error state back but we don't want to break startup in case // of an timeout timeoutTimer = Utilities.setTimeout( 10000, () -> { log.error("Timeout reached for UpdateFX"); process.onCompleted(); }); String userAgent = environment.getProperty(BitsquareEnvironment.APP_NAME_KEY) + Version.VERSION; // Check if there is a new minor version release out. The release_url should be empty if no // release is available, otherwise the download url. try { releaseUrl = Utilities.readTextFileFromServer(UPDATES_BASE_URL + "/release_url", userAgent); if (releaseUrl != null && releaseUrl.length() > 0) { log.info("New release available at: " + releaseUrl); state.set(State.NEW_RELEASE); timeoutTimer.cancel(); return; } else { // All ok. Empty file if we have no new release. } } catch (IOException e) { // ignore. File might be missing } Updater updater = new Updater( UPDATES_BASE_URL, userAgent, Version.PATCH_VERSION, Paths.get(environment.getProperty(BitsquareEnvironment.APP_DATA_DIR_KEY)), ROOT_CLASS_PATH, UPDATE_SIGNING_KEYS, UPDATE_SIGNING_THRESHOLD) { @Override protected void updateProgress(long workDone, long max) { // log.trace("updateProgress " + workDone + "/" + max); super.updateProgress(workDone, max); } }; /* updater.progressProperty().addListener((observableValue, oldValue, newValue) -> { log.trace("progressProperty newValue = " + newValue); });*/ log.info("Checking for updates!"); updater.setOnSucceeded( event -> { try { UpdateSummary summary = updater.get(); log.info("summary " + summary.toString()); if (summary.descriptions != null && summary.descriptions.size() > 0) { log.info("One liner: {}", summary.descriptions.get(0).getOneLiner()); log.info("{}", summary.descriptions.get(0).getDescription()); } if (summary.highestVersion > Version.PATCH_VERSION) { log.info("UPDATE_AVAILABLE"); state.set(State.UPDATE_AVAILABLE); // We stop the timeout and treat it not completed. // The user should click the restart button manually if there are updates available. timeoutTimer.cancel(); } else if (summary.highestVersion == Version.PATCH_VERSION) { log.info("UP_TO_DATE"); state.set(State.UP_TO_DATE); timeoutTimer.cancel(); process.onCompleted(); } } catch (Throwable e) { log.error("Exception at processing UpdateSummary: " + e.getMessage()); // we treat errors as update not as critical errors to prevent startup, // so we use state.onCompleted() instead of state.onError() state.set(State.FAILURE); timeoutTimer.cancel(); process.onCompleted(); } }); updater.setOnFailed( event -> { log.error("Update failed: " + updater.getException()); updater.getException().printStackTrace(); // we treat errors as update not as critical errors to prevent startup, // so we use state.onCompleted() instead of state.onError() state.set(State.FAILURE); timeoutTimer.cancel(); process.onCompleted(); }); Thread thread = new Thread(updater, "Online update check"); thread.setDaemon(true); thread.start(); } public String getReleaseUrl() { return releaseUrl; } }
public Observable<String> getObservable() { if (mSubject == null) { mSubject = BehaviorSubject.create(get()); } return mSubject.asObservable(); }