Skip to content

Commit c3d4b6d

Browse files
committed
Fix for multiple subscribers on Observables. Fixed bug which did not handle Exceptions after successful resolution. Added tests for resolution handling with multiple subscribers. Added Transformer to help with resuming with another RxFit Observable after an Exception, but propagate an unsuccessful resolution.
1 parent f10893e commit c3d4b6d

File tree

9 files changed

+252
-50
lines changed

9 files changed

+252
-50
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## Version 1.1.1
4+
5+
* Fix for multiple subscribers on RxFit Observables.
6+
* Fixed bug which did not handle Exceptions after successful resolution.
7+
* Added RxFit.OnExceptionResumeNext Transformer.
8+
39
## Version 1.1.0
410

511
* BREAKING CHANGE: Removed PermissionRequiredException in favor of SecurityException

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Reactive Fit API Library for Android
22

3-
[![Build Status](https://travis-ci.org/patloew/RxFit.svg?branch=master)](https://travis-ci.org/patloew/RxFit) [![API](https://img.shields.io/badge/API-9%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=9)
3+
[![Build Status](https://travis-ci.org/patloew/RxFit.svg?branch=master)](https://travis-ci.org/patloew/RxFit) [ ![Download](https://api.bintray.com/packages/patloew/maven/com.patloew.rxfit/images/download.svg) ](https://bintray.com/patloew/maven/com.patloew.rxfit/_latestVersion) [![API](https://img.shields.io/badge/API-9%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=9)
44

55
This library wraps the Fit API in [RxJava](https://github.com/ReactiveX/RxJava) Observables. No more managing GoogleApiClients! Also, the authorization process for using fitness data is handled by the lib.
66

library/build.gradle

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ apply plugin: 'com.jfrog.bintray'
33
apply plugin: 'com.github.dcendents.android-maven'
44

55
group = 'com.patloew.rxfit'
6-
version = '1.1.0'
6+
version = '1.1.1'
77
project.archivesBaseName = 'rxfit'
88

99
android {
@@ -13,8 +13,8 @@ android {
1313
defaultConfig {
1414
minSdkVersion 9
1515
targetSdkVersion 23
16-
versionCode 2
17-
versionName "1.1.0"
16+
versionCode 3
17+
versionName "1.1.1"
1818
}
1919
buildTypes {
2020
release {

library/src/main/java/com/patloew/rxfit/BaseObservable.java

+33-25
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
import com.google.android.gms.common.api.ResultCallback;
1515
import com.google.android.gms.common.api.Scope;
1616

17-
import java.util.ArrayList;
18-
import java.util.List;
17+
import java.util.HashMap;
18+
import java.util.HashSet;
19+
import java.util.Map;
20+
import java.util.Set;
1921
import java.util.concurrent.TimeUnit;
2022

2123
import rx.Observable;
@@ -44,17 +46,16 @@
4446
*
4547
*/
4648
public abstract class BaseObservable<T> implements Observable.OnSubscribe<T> {
47-
private static final List<BaseObservable> observableList = new ArrayList<>();
49+
private static final Set<BaseObservable> observableSet = new HashSet<>();
4850

4951
private final Context ctx;
5052
private final Api<? extends Api.ApiOptions.NotRequiredOptions>[] services;
5153
private final Scope[] scopes;
5254
private final boolean handleResolution;
53-
private GoogleApiClient apiClient;
54-
Subscriber<? super T> subscriber;
55+
private final Long timeoutTime;
56+
private final TimeUnit timeoutUnit;
5557

56-
protected final Long timeoutTime;
57-
protected final TimeUnit timeoutUnit;
58+
private final HashMap<GoogleApiClient, Subscriber<? super T>> subscriptionInfoHashMap = new HashMap<>();
5859

5960
protected BaseObservable(@NonNull RxFit rxFit, Long timeout, TimeUnit timeUnit) {
6061
this.ctx = rxFit.getContext();
@@ -63,11 +64,11 @@ protected BaseObservable(@NonNull RxFit rxFit, Long timeout, TimeUnit timeUnit)
6364
handleResolution = true;
6465

6566
if(timeout != null && timeUnit != null) {
66-
this.timeoutTime = RxFit.getTimeout(timeout);
67-
this.timeoutUnit = RxFit.getTimeoutUnit(timeUnit);
67+
this.timeoutTime = timeout;
68+
this.timeoutUnit = timeUnit;
6869
} else {
69-
this.timeoutTime = RxFit.getTimeout(null);
70-
this.timeoutUnit = RxFit.getTimeoutUnit(null);
70+
this.timeoutTime = RxFit.getDefaultTimeout();
71+
this.timeoutUnit = RxFit.getDefaultTimeoutUnit();
7172
}
7273
}
7374

@@ -80,7 +81,7 @@ protected BaseObservable(@NonNull Context ctx, @NonNull Api<? extends Api.ApiOpt
8081
timeoutUnit = null;
8182
}
8283

83-
protected <T extends Result> void setupFitnessPendingResult(PendingResult<T> pendingResult, ResultCallback<? super T> resultCallback) {
84+
protected final <T extends Result> void setupFitnessPendingResult(PendingResult<T> pendingResult, ResultCallback<? super T> resultCallback) {
8485
if(timeoutTime != null && timeoutUnit != null) {
8586
pendingResult.setResultCallback(resultCallback, timeoutTime, timeoutUnit);
8687
} else {
@@ -89,10 +90,9 @@ protected <T extends Result> void setupFitnessPendingResult(PendingResult<T> pen
8990
}
9091

9192
@Override
92-
public void call(Subscriber<? super T> subscriber) {
93-
this.subscriber = subscriber;
94-
95-
apiClient = createApiClient(subscriber);
93+
public final void call(Subscriber<? super T> subscriber) {
94+
final GoogleApiClient apiClient = createApiClient(subscriber);
95+
subscriptionInfoHashMap.put(apiClient, subscriber);
9696

9797
try {
9898
apiClient.connect();
@@ -107,12 +107,14 @@ public void call() {
107107
onUnsubscribed(apiClient);
108108
apiClient.disconnect();
109109
}
110+
111+
subscriptionInfoHashMap.remove(apiClient);
110112
}
111113
}));
112114
}
113115

114116

115-
protected GoogleApiClient createApiClient(Subscriber<? super T> subscriber) {
117+
GoogleApiClient createApiClient(Subscriber<? super T> subscriber) {
116118

117119
ApiClientConnectionCallbacks apiClientConnectionCallbacks = new ApiClientConnectionCallbacks(subscriber);
118120

@@ -173,7 +175,7 @@ public void onConnectionSuspended(int cause) {
173175
@Override
174176
public void onConnectionFailed(ConnectionResult connectionResult) {
175177
if(handleResolution && connectionResult.hasResolution()) {
176-
observableList.add(BaseObservable.this);
178+
observableSet.add(BaseObservable.this);
177179

178180
if(!ResolutionActivity.isResolutionShown()) {
179181
Intent intent = new Intent(ctx, ResolutionActivity.class);
@@ -192,16 +194,22 @@ public void setClient(GoogleApiClient client) {
192194
}
193195

194196
static void onResolutionResult(int resultCode, ConnectionResult connectionResult) {
195-
for(BaseObservable observable : observableList) {
196-
if(!observable.subscriber.isUnsubscribed()) {
197-
if (resultCode == Activity.RESULT_OK && observable.apiClient != null) {
198-
observable.apiClient.connect();
199-
} else {
200-
observable.subscriber.onError(new GoogleAPIConnectionException("Error connecting to GoogleApiClient, resolution was not successful.", connectionResult));
197+
for(BaseObservable observable : observableSet) {
198+
for (Map.Entry<GoogleApiClient, Subscriber> entry : (Set<Map.Entry<GoogleApiClient, Subscriber>>) observable.subscriptionInfoHashMap.entrySet()) {
199+
if (!entry.getValue().isUnsubscribed()) {
200+
if (resultCode == Activity.RESULT_OK) {
201+
try {
202+
entry.getKey().connect();
203+
} catch (Throwable ex) {
204+
entry.getValue().onError(ex);
205+
}
206+
} else {
207+
entry.getValue().onError(new GoogleAPIConnectionException("Error connecting to GoogleApiClient, resolution was not successful.", connectionResult));
208+
}
201209
}
202210
}
203211
}
204212

205-
observableList.clear();
213+
observableSet.clear();
206214
}
207215
}

library/src/main/java/com/patloew/rxfit/GoogleAPIConnectionException.java

+8
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,12 @@ public class GoogleAPIConnectionException extends RuntimeException {
2727
public ConnectionResult getConnectionResult() {
2828
return connectionResult;
2929
}
30+
31+
public boolean wasResolutionUnsuccessful() {
32+
if(connectionResult != null) {
33+
return connectionResult.hasResolution();
34+
} else {
35+
return false;
36+
}
37+
}
3038
}

library/src/main/java/com/patloew/rxfit/RxFit.java

+40-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import android.app.PendingIntent;
44
import android.content.Context;
55
import android.support.annotation.NonNull;
6-
import android.support.annotation.Nullable;
76
import android.support.annotation.RequiresPermission;
87

98
import com.google.android.gms.common.api.Api;
@@ -34,6 +33,8 @@
3433

3534
import rx.Completable;
3635
import rx.Observable;
36+
import rx.exceptions.Exceptions;
37+
import rx.functions.Func1;
3738

3839
/* Factory for Google Fit API observables. Make sure to include all the APIs
3940
* and Scopes that you need for your app. Also make sure to have the Location
@@ -106,14 +107,12 @@ Scope[] getScopes() {
106107
return scopes;
107108
}
108109

109-
@Nullable
110-
static Long getTimeout(@Nullable Long timeout) {
111-
return timeout != null ? timeout : timeoutTime;
110+
static Long getDefaultTimeout() {
111+
return timeoutTime;
112112
}
113113

114-
@Nullable
115-
static TimeUnit getTimeoutUnit(@Nullable TimeUnit timeUnit) {
116-
return timeUnit != null ? timeUnit : timeoutUnit;
114+
static TimeUnit getDefaultTimeoutUnit() {
115+
return timeoutUnit;
117116
}
118117

119118

@@ -446,4 +445,38 @@ public static Observable<List<Session>> stop(@NonNull String identifier, long ti
446445

447446
}
448447

448+
449+
/* Transformer that behaves like onExceptionResumeNext(Observable o), but propagates
450+
* a GoogleAPIConnectionException, which was caused by an unsuccessful resolution.
451+
* This can be helpful if you want to resume with another RxFit Observable when
452+
* an Exception occurs, but don't want to show the resolution dialog multiple times.
453+
*
454+
* An example use case: Fetch fitness data with server queries enabled, but provide
455+
* a timeout. When an exception occurs (e.g. timeout), switch to cached fitness data.
456+
* Using this Transformer prevents showing the authorization dialog twice, if the user
457+
* denys access for the first read. See MainActivity in sample project.
458+
*/
459+
public static class OnExceptionResumeNext<T, R extends T> implements Observable.Transformer<T, T> {
460+
461+
private final Observable<R> other;
462+
463+
public OnExceptionResumeNext(Observable<R> other) {
464+
this.other = other;
465+
}
466+
467+
@Override
468+
public Observable<T> call(Observable<T> source) {
469+
return source.onErrorResumeNext(new Func1<Throwable, Observable<R>>() {
470+
@Override
471+
public Observable<R> call(Throwable throwable) {
472+
if (!(throwable instanceof Exception) || (throwable instanceof GoogleAPIConnectionException && ((GoogleAPIConnectionException) throwable).wasResolutionUnsuccessful())) {
473+
Exceptions.propagate(throwable);
474+
}
475+
476+
return other;
477+
}
478+
});
479+
}
480+
}
481+
449482
}

0 commit comments

Comments
 (0)