I am using RxJava2.
i have some observable, and few subscribers that can be subscribed for it.
each time when new subscribers arrive, some job should be done and each of subscribers should be notified.
for this i decide to use PublishSubject. but when doOnSubscribe received from firs subscriber, myPublishSubject.hasObservers() return false...
any idea why it happens and how can i fix this?
private val myPublishSubject = PublishSubject.create<Boolean>()
fun getPublishObservable():Observable<Boolean> {
return myPublishSubject.doOnSubscribe {
//do some job when new subscriber arrived and notify all subscribers
//via
myPublishSubject.onNext(true)
}
}
Do I understand it correct, that when doOnSubscribe called it mean that there is at least one subscribers already present?
i did not find ready answer, so i create my own version of subject and call it RefreshSubject.
it based on PublishSubject but with one difference: if you would like to return observable and be notified when new subscriber arrives and ready to receive some data you should use method getSubscriberReady.
here a small example:
private RefreshSubject<Boolean> refreshSubject = RefreshSubject.create();
//ordinary publish behavior
public Observable<Boolean> getObservable(){
return refreshSubject;
}
//refreshSubject behaviour
public Observable<Boolean> getRefreshObserver(){
return refreshSubject.getSubscriberReady(new Action() {
#Override
public void run() throws Exception {
//new subscriber arrives and ready to receive some data
//so you can make some data request and all your subscribers (with new one just arrived)
//will receive new content
}
});
}
and here is full class:
public class RefreshSubject<T> extends Subject<T> {
/** The terminated indicator for the subscribers array. */
#SuppressWarnings("rawtypes")
private static final RefreshSubject.RefreshDisposable[] TERMINATED = new RefreshSubject.RefreshDisposable[0];
/** An empty subscribers array to avoid allocating it all the time. */
#SuppressWarnings("rawtypes")
private static final RefreshSubject.RefreshDisposable[] EMPTY = new RefreshSubject.RefreshDisposable[0];
/** The array of currently subscribed subscribers. */
private final AtomicReference<RefreshDisposable<T>[]> subscribers;
/** The error, write before terminating and read after checking subscribers. */
Throwable error;
/**
* Constructs a RefreshSubject.
* #param <T> the value type
* #return the new RefreshSubject
*/
#CheckReturnValue
public static <T> RefreshSubject<T> create() {
return new RefreshSubject<T>();
}
/**
* Constructs a RefreshSubject.
* #since 2.0
*/
#SuppressWarnings("unchecked")
private RefreshSubject() {
subscribers = new AtomicReference<RefreshSubject.RefreshDisposable<T>[]>(EMPTY);
}
#Override
public void subscribeActual(Observer<? super T> t) {
RefreshSubject.RefreshDisposable<T> ps = new RefreshSubject.RefreshDisposable<T>(t, RefreshSubject.this);
t.onSubscribe(ps);
if (add(ps)) {
// if cancellation happened while a successful add, the remove() didn't work
// so we need to do it again
if (ps.isDisposed()) {
remove(ps);
}
} else {
Throwable ex = error;
if (ex != null) {
t.onError(ex);
} else {
t.onComplete();
}
}
}
public Observable<T> getSubscriberReady(final Action onReady){
return Observable.create(new ObservableOnSubscribe<T>() {
#Override
public void subscribe(ObservableEmitter<T> e) throws Exception {
add(new RefreshDisposable(e, RefreshSubject.this));
onReady.run();
}
});
}
/**
* Tries to add the given subscriber to the subscribers array atomically
* or returns false if the subject has terminated.
* #param ps the subscriber to add
* #return true if successful, false if the subject has terminated
*/
private boolean add(RefreshSubject.RefreshDisposable<T> ps) {
for (;;) {
RefreshSubject.RefreshDisposable<T>[] a = subscribers.get();
if (a == TERMINATED) {
return false;
}
int n = a.length;
#SuppressWarnings("unchecked")
RefreshSubject.RefreshDisposable<T>[] b = new RefreshSubject.RefreshDisposable[n + 1];
System.arraycopy(a, 0, b, 0, n);
b[n] = ps;
if (subscribers.compareAndSet(a, b)) {
return true;
}
}
}
/**
* Atomically removes the given subscriber if it is subscribed to the subject.
* #param ps the subject to remove
*/
#SuppressWarnings("unchecked")
private void remove(RefreshSubject.RefreshDisposable<T> ps) {
for (;;) {
RefreshSubject.RefreshDisposable<T>[] a = subscribers.get();
if (a == TERMINATED || a == EMPTY) {
return;
}
int n = a.length;
int j = -1;
for (int i = 0; i < n; i++) {
if (a[i] == ps) {
j = i;
break;
}
}
if (j < 0) {
return;
}
RefreshSubject.RefreshDisposable<T>[] b;
if (n == 1) {
b = EMPTY;
} else {
b = new RefreshSubject.RefreshDisposable[n - 1];
System.arraycopy(a, 0, b, 0, j);
System.arraycopy(a, j + 1, b, j, n - j - 1);
}
if (subscribers.compareAndSet(a, b)) {
return;
}
}
}
#Override
public void onSubscribe(Disposable s) {
if (subscribers.get() == TERMINATED) {
s.dispose();
}
}
#Override
public void onNext(T t) {
if (subscribers.get() == TERMINATED) {
return;
}
if (t == null) {
onError(new NullPointerException("onNext called with null. Null values are generally not allowed in 2.x operators and sources."));
return;
}
for (RefreshSubject.RefreshDisposable<T> s : subscribers.get()) {
s.onNext(t);
}
}
#SuppressWarnings("unchecked")
#Override
public void onError(Throwable t) {
if (subscribers.get() == TERMINATED) {
RxJavaPlugins.onError(t);
return;
}
if (t == null) {
t = new NullPointerException("onError called with null. Null values are generally not allowed in 2.x operators and sources.");
}
error = t;
for (RefreshSubject.RefreshDisposable<T> s : subscribers.getAndSet(TERMINATED)) {
s.onError(t);
}
}
#SuppressWarnings("unchecked")
#Override
public void onComplete() {
if (subscribers.get() == TERMINATED) {
return;
}
for (RefreshSubject.RefreshDisposable<T> s : subscribers.getAndSet(TERMINATED)) {
s.onComplete();
}
}
#Override
public boolean hasObservers() {
return subscribers.get().length != 0;
}
#Override
public Throwable getThrowable() {
if (subscribers.get() == TERMINATED) {
return error;
}
return null;
}
#Override
public boolean hasThrowable() {
return subscribers.get() == TERMINATED && error != null;
}
#Override
public boolean hasComplete() {
return subscribers.get() == TERMINATED && error == null;
}
/**
* Wraps the actualEmitter subscriber, tracks its requests and makes cancellation
* to remove itself from the current subscribers array.
*
* #param <T> the value type
*/
private static final class RefreshDisposable<T> extends AtomicBoolean implements Disposable {
private static final long serialVersionUID = 3562861878281475070L;
/** The actualEmitter subscriber. */
final Emitter<? super T> actualEmitter;
/** The actualEmitter subscriber. */
final Observer<? super T> actualObserver;
/** The subject state. */
final RefreshSubject<T> parent;
/**
* Constructs a PublishSubscriber, wraps the actualEmitter subscriber and the state.
* #param actualEmitter the actualEmitter subscriber
* #param parent the parent RefreshProcessor
*/
RefreshDisposable(Emitter<? super T> actualEmitter, RefreshSubject<T> parent) {
this.actualEmitter = actualEmitter;
this.parent = parent;
actualObserver = null;
}
/**
* Constructs a PublishSubscriber, wraps the actualEmitter subscriber and the state.
* #param actualObserver the actualObserver subscriber
* #param parent the parent RefreshProcessor
*/
RefreshDisposable(Observer<? super T> actualObserver, RefreshSubject<T> parent) {
this.actualObserver = actualObserver;
this.parent = parent;
actualEmitter = null;
}
public void onNext(T t) {
if (!get()) {
if (actualEmitter != null)
actualEmitter.onNext(t);
if (actualObserver != null)
actualObserver.onNext(t);
}
}
public void onError(Throwable t) {
if (get()) {
RxJavaPlugins.onError(t);
} else {
if (actualEmitter != null)
actualEmitter.onError(t);
if (actualObserver != null)
actualObserver.onError(t);
}
}
public void onComplete() {
if (!get()) {
if (actualEmitter != null)
actualEmitter.onComplete();
if (actualObserver != null)
actualObserver.onComplete();
}
}
#Override
public void dispose() {
if (compareAndSet(false, true)) {
parent.remove(this);
}
}
#Override
public boolean isDisposed() {
return get();
}
}
}
Related
In my android app I have an option to backup the database to Google Drive. For that I am using DriveServiceHelper class, but I just noticed, in Android 11 the Task.call is deprecated.
public Task<FileList> queryFiles() {
return Tasks.call(mExecutor, () ->
mDriveService.files().list().setSpaces("drive").execute());
}
From my BackupActivity then I call queryFiles from backup method:
public void backup(View v) {
driveServiceHelper.queryFiles()
.addOnSuccessListener(fileList -> {
// another code
})
.addOnFailureListener(e -> showMsgSnack(getString(R.string.uploaderror)));
I did not find any solution how to deal with this to avoid complete rework of that class.
What I tried:
I tried to replace with runnable, also callable, but it doesn't work as Task is expected to be returned, not Filelist.
also I tried to use TaskCompletionSource:
public Task<FileList> queryFiles(int delay) throws IOException, ExecutionException, InterruptedException {
new Thread(
new Runnable() {
#Override
public void run() {
TaskCompletionSource<FileList> taskCompletionSource = new TaskCompletionSource<>();
FileList result = null;
try {
result = mDriveService.files().list().setSpaces("drive").execute();
} catch (IOException e) {
e.printStackTrace();
}
FileList finalResult = result;
new Handler().postDelayed(() -> taskCompletionSource.setResult(finalResult), delay);
return taskCompletionSource.getTask();
}
}).start();
}
but the return works not from a method of void type.
Ok, after hours of testing I tried this solution and this seems working for now: (using executorService, and in Handler a Looper is needed.)
public Task<FileList> queryFiles() {
final TaskCompletionSource<FileList> tcs = new TaskCompletionSource<FileList>();
ExecutorService service = Executors.newFixedThreadPool(1);
service.execute(
new Runnable() {
#Override
public void run() {
FileList result = null;
try {
result = mDriveService.files().list().setSpaces("drive").execute();
} catch (IOException e) {
e.printStackTrace();
}
FileList finalResult = result;
new Handler(Looper.getMainLooper()).postDelayed(() -> tcs.setResult(finalResult), 1000);
}
});
return tcs.getTask();
}
I meant something like this:
public Task<FileList> queryFiles(int delay) throws IOException {
Task<FileList> retVal;
final FutureValue<Task<FileList>> future = new FutureValue<>();
// now run this bit in a runnable
/*
TaskCompletionSource<FileList> taskCompletionSource = new TaskCompletionSource<>();
FileList result = mDriveService.files().list().setSpaces("drive").execute();
new Handler().postDelayed(() -> taskCompletionSource.setResult(result), delay);
return taskCompletionSource.getTask();
*/
new Thread(
new Runnable() {
#Override
public void run() {
TaskCompletionSource<FileList> taskCompletionSource = new TaskCompletionSource<>();
FileList result = mDriveService.files().list().setSpaces("drive").execute();
new Handler().postDelayed(() -> taskCompletionSource.setResult(result), delay);
// and we replace the return statement with something else
// return taskCompletionSource.getTask();
future.set(taskCompletionSource.getTask());
}
}).start();
// And block (wait) for future to finish so we can return it, deadlocking the main thread...
// return future.get();
//FIXME do either this
// retVal = future.get();
// For bonus points, we'll do a timed wait instead -- OR THIS
try {
retVal = future.get(30, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
Log.d(LOG_TAG, "Exception "+e+" happened!", e);
} catch (InterruptedException | ExecutionException e) {
Log.d(LOG_TAG, "Exception "+e+" happened!", e);
}
return retVal;
}
and that should set you on some path to solving the problem.
However, if the only reason for using the Task<> is just so you can add success/fail listeners to these methods - i strongly suggest you come up with something better that actually runs on background threads instead of the thread you're calling them on.
The FutureValue class:
/**
* Implementation of {#link Future}, allowing waiting for value to be set (from another thread).
* Use {#link #set(Object)} to set value, {#link #get()} or {#link #get(long, TimeUnit)} to retrieve
* value.
* TODO: tests
*
* #param <T> type of awaited value
*/
public class FutureValue<T> implements Future<T> {
private static final String LOGTAG = "FutureValue";
private static final long NANOS_IN_MILLI = TimeUnit.MILLISECONDS.toNanos(1);
private volatile T value;
private volatile boolean isDone = false;
private volatile boolean isCanceled = false;
/**
* Sets value awaited by this future.
*
* #param value value
*/
public synchronized void set(T value) {
this.value = value;
isDone = true;
notifyAll();
}
/** {#inheritDoc} */
#Override
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
isCanceled = true;
notifyAll();
return !isDone;
}
/** {#inheritDoc} */
#Override
public boolean isCancelled() {
return isCanceled;
}
/** {#inheritDoc} */
#Override
public boolean isDone() {
return isDone;
}
/** {#inheritDoc} */
#Override
public synchronized T get() {
while (!isDone) {
if (isCanceled) {
return value;
}
try {
wait();
} catch (InterruptedException ignored) {
Log.w(LOGTAG, "We're just gonna ignore this exception: " + ignored, ignored);
}
}
return value;
}
/** {#inheritDoc} */
#Override
public synchronized T get(long timeout, #NonNull TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
final long targetTime = System.nanoTime() + unit.toNanos(timeout);
while (!isDone && !isCanceled) {
try {
final long waitTimeNanos = targetTime - System.nanoTime();
if (waitTimeNanos <= 0) {
throw new TimeoutException();
}
wait(waitTimeNanos / NANOS_IN_MILLI, (int) (waitTimeNanos % NANOS_IN_MILLI));
} catch (InterruptedException ignored) {
Log.w(LOGTAG, "We're just gonna ignore this exception: " + ignored, ignored);
}
}
return value;
}
}
Currently, I am implementing google Speech to Text in my project. The sample code referred is this: Click Here.
I have used the SpeechService and Voice Recorder class from this project.
public class SpeechService extends Service {
public static final List<String> SCOPE =
Collections.singletonList("https://www.googleapis.com/auth/cloud-platform");
private static final String TAG = "SpeechService";
private static final String PREFS = "SpeechService";
private static final String PREF_ACCESS_TOKEN_VALUE = "access_token_value";
private static final String PREF_ACCESS_TOKEN_EXPIRATION_TIME = "access_token_expiration_time";
/**
* We reuse an access token if its expiration time is longer than this.
*/
private static final int ACCESS_TOKEN_EXPIRATION_TOLERANCE = 30 * 60 * 1000; // thirty minutes
/**
* We refresh the current access token before it expires.
*/
private static final int ACCESS_TOKEN_FETCH_MARGIN = 60 * 1000; // one minute
private static final String HOSTNAME = "speech.googleapis.com";
private static final int PORT = 443;
private static Handler mHandler;
private final SpeechBinder mBinder = new SpeechBinder();
private final ArrayList<Listener> mListeners = new ArrayList<>();
private final StreamObserver<StreamingRecognizeResponse> mResponseObserver
= new StreamObserver<StreamingRecognizeResponse>() {
#Override
public void onNext(StreamingRecognizeResponse response) {
Log.e("Speech", "Recognized");
String text = null;
boolean isFinal = false;
if (response.getResultsCount() > 0) {
System.out.println("result count....."+String.valueOf(response.getResultsCount()));
final StreamingRecognitionResult result = response.getResults(0);
isFinal = result.getIsFinal();
if (result.getAlternativesCount() > 0) {
final SpeechRecognitionAlternative alternative = result.getAlternatives(0);
text = alternative.getTranscript();
}
}
if (text != null && isFinal) {
for (Listener listener : mListeners) {
listener.onSpeechRecognized(text, isFinal);
}
} else {
for (Listener listener : mListeners) {
listener.onRandomStupidity();
}
}
}
#Override
public void onError(Throwable t) {
Log.e(TAG, "Error calling the API.", t);
for(Listener listener : mListeners){
listener.onErrorRecognizing();
}
}
#Override
public void onCompleted() {
Log.i(TAG, "API completed.");
}
};
private volatile AccessTokenTask mAccessTokenTask;
private final Runnable mFetchAccessTokenRunnable = new Runnable() {
#Override
public void run() {
fetchAccessToken();
}
};
private SpeechGrpc.SpeechStub mApi;
private StreamObserver<StreamingRecognizeRequest> mRequestObserver;
public static SpeechService from(IBinder binder) {
return ((SpeechBinder) binder).getService();
}
#Override
public void onCreate() {
super.onCreate();
mHandler = new Handler();
fetchAccessToken();
}
#Override
public void onDestroy() {
super.onDestroy();
mHandler.removeCallbacks(mFetchAccessTokenRunnable);
mHandler = null;
// Release the gRPC channel.
if (mApi != null) {
final ManagedChannel channel = (ManagedChannel) mApi.getChannel();
if (channel != null && !channel.isShutdown()) {
try {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Log.e(TAG, "Error shutting down the gRPC channel.", e);
}
}
mApi = null;
}
}
private void fetchAccessToken() {
if (mAccessTokenTask != null) {
return;
}
mAccessTokenTask = new AccessTokenTask();
mAccessTokenTask.execute();
}
private String getDefaultLanguageCode() {
final LangInnerResponse languageToLearn = MemoryCache.getLanguageToLearn();
if(languageToLearn != null) {
Log.e("Test Lang", languageToLearn.getCode());
return languageToLearn.getCode();
} else {
final Locale locale = Locale.getDefault();
final StringBuilder language = new StringBuilder(locale.getLanguage());
final String country = locale.getCountry();
if (!TextUtils.isEmpty(country)) {
language.append("-");
language.append(country);
}
return language.toString();
}
}
#Nullable
#Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public void addListener(#NonNull Listener listener) {
mListeners.add(listener);
}
public void removeListener(#NonNull Listener listener) {
mListeners.remove(listener);
}
/**
** Starts recognizing speech audio.
*
* #param sampleRate The sample rate of the audio.
*/
public void startRecognizing(int sampleRate) {
if (mApi == null) {
Log.w(TAG, "API not ready. Ignoring the request.");
return;
}
System.out.println("calling api....");
// Configure the API
mRequestObserver = mApi.streamingRecognize(mResponseObserver);
mRequestObserver.onNext(StreamingRecognizeRequest.newBuilder()
.setStreamingConfig(StreamingRecognitionConfig.newBuilder()
.setConfig(RecognitionConfig.newBuilder()
.setLanguageCode(getDefaultLanguageCode())
.setEncoding(RecognitionConfig.AudioEncoding.LINEAR16)
.setSampleRateHertz(sampleRate)
.build())
.setInterimResults(true)
.setSingleUtterance(true)
.build())
.build());
}
/**
* Recognizes the speech audio. This method should be called every time a chunk of byte buffer
* is ready.
*
* #param data The audio data.
* #param size The number of elements that are actually relevant in the {#code data}.
*/
public void recognize(byte[] data, int size) {
if (mRequestObserver == null) {
return;
}
// Call the streaming recognition API
mRequestObserver.onNext(StreamingRecognizeRequest.newBuilder()
.setAudioContent(ByteString.copyFrom(data, 0, size))
.build());
}
/**
* Finishes recognizing speech audio.
*/
public void finishRecognizing() {
if (mRequestObserver == null) {
return;
}
mRequestObserver.onCompleted();
mRequestObserver = null;
}
public interface Listener {
/**
* Called when a new piece of text was recognized by the Speech API.
*
* #param text The text.
* #param isFinal {#code true} when the API finished processing audio.
*/
void onSpeechRecognized(String text, boolean isFinal);
void onErrorRecognizing();
void onRandomStupidity();
}
/**
* Authenticates the gRPC channel using the specified {#link GoogleCredentials}.
*/
private static class GoogleCredentialsInterceptor implements ClientInterceptor {
private final Credentials mCredentials;
private Metadata mCached;
private Map<String, List<String>> mLastMetadata;
GoogleCredentialsInterceptor(Credentials credentials) {
mCredentials = credentials;
}
private static Metadata toHeaders(Map<String, List<String>> metadata) {
Metadata headers = new Metadata();
if (metadata != null) {
for (String key : metadata.keySet()) {
Metadata.Key<String> headerKey = Metadata.Key.of(
key, Metadata.ASCII_STRING_MARSHALLER);
for (String value : metadata.get(key)) {
headers.put(headerKey, value);
}
}
}
return headers;
}
#Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
final MethodDescriptor<ReqT, RespT> method, CallOptions callOptions,
final Channel next) {
return new ClientInterceptors.CheckedForwardingClientCall<ReqT, RespT>(
next.newCall(method, callOptions)) {
#Override
protected void checkedStart(Listener<RespT> responseListener, Metadata headers)
throws StatusException {
Metadata cachedSaved;
URI uri = serviceUri(next, method);
synchronized (this) {
Map<String, List<String>> latestMetadata = getRequestMetadata(uri);
if (mLastMetadata == null || mLastMetadata != latestMetadata) {
mLastMetadata = latestMetadata;
mCached = toHeaders(mLastMetadata);
}
cachedSaved = mCached;
}
headers.merge(cachedSaved);
delegate().start(responseListener, headers);
}
};
}
/**
* Generate a JWT-specific service URI. The URI is simply an identifier with enough
* information for a service to know that the JWT was intended for it. The URI will
* commonly be verified with a simple string equality check.
*/
private URI serviceUri(Channel channel, MethodDescriptor<?, ?> method)
throws StatusException {
String authority = channel.authority();
if (authority == null) {
throw Status.UNAUTHENTICATED
.withDescription("Channel has no authority")
.asException();
}
// Always use HTTPS, by definition.
final String scheme = "https";
final int defaultPort = 443;
String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName());
URI uri;
try {
uri = new URI(scheme, authority, path, null, null);
} catch (URISyntaxException e) {
throw Status.UNAUTHENTICATED
.withDescription("Unable to construct service URI for auth")
.withCause(e).asException();
}
// The default port must not be present. Alternative ports should be present.
if (uri.getPort() == defaultPort) {
uri = removePort(uri);
}
return uri;
}
private URI removePort(URI uri) throws StatusException {
try {
return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */,
uri.getPath(), uri.getQuery(), uri.getFragment());
} catch (URISyntaxException e) {
throw Status.UNAUTHENTICATED
.withDescription("Unable to construct service URI after removing port")
.withCause(e).asException();
}
}
private Map<String, List<String>> getRequestMetadata(URI uri) throws StatusException {
try {
return mCredentials.getRequestMetadata(uri);
} catch (IOException e) {
throw Status.UNAUTHENTICATED.withCause(e).asException();
}
}
}
private class SpeechBinder extends Binder {
SpeechService getService() {
return SpeechService.this;
}
}
private class CreateApiSingle implements SingleOnSubscribe<SpeechGrpc.SpeechStub> {
#Override
public void subscribe(SingleEmitter<SpeechGrpc.SpeechStub> emitter) throws Exception {
final AccessToken accessToken = generateCredentials();
final SpeechGrpc.SpeechStub api = generateApi(accessToken);
emitter.onSuccess(api);
}
private AccessToken generateCredentials() throws IOException {
final SharedPreferences prefs =
getSharedPreferences(PREFS, Context.MODE_PRIVATE);
String tokenValue = prefs.getString(PREF_ACCESS_TOKEN_VALUE, null);
long expirationTime = prefs.getLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, -1);
// Check if the current token is still valid for a while
if (tokenValue != null && expirationTime > 0) {
if (expirationTime
> System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TOLERANCE) {
return new AccessToken(tokenValue, new Date(expirationTime));
}
}
// ***** WARNING *****
// In this sample, we load the credential from a JSON file stored in a raw resource
// folder of this client app. You should never do this in your app. Instead, store
// the file in your server and obtain an access token from there.
// *******************
final InputStream stream = getResources().openRawResource(R.raw.credential);
final GoogleCredentials credentials = GoogleCredentials.fromStream(stream)
.createScoped(SCOPE);
final AccessToken token = credentials.refreshAccessToken();
prefs.edit()
.putString(PREF_ACCESS_TOKEN_VALUE, token.getTokenValue())
.putLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME,
token.getExpirationTime().getTime())
.apply();
stream.close();
return token;
}
private SpeechGrpc.SpeechStub generateApi(AccessToken accessToken) {
final ManagedChannel channel = new OkHttpChannelProvider()
.builderForAddress(HOSTNAME, PORT)
.nameResolverFactory(new DnsNameResolverProvider())
.intercept(new GoogleCredentialsInterceptor(new GoogleCredentials(accessToken)
.createScoped(SCOPE)))
.build();
return SpeechGrpc.newStub(channel);
}
}
private class AccessTokenTask extends AsyncTask<Void, Void, AccessToken> {
#Override
protected AccessToken doInBackground(Void... voids) {
final SharedPreferences prefs =
getSharedPreferences(PREFS, Context.MODE_PRIVATE);
String tokenValue = prefs.getString(PREF_ACCESS_TOKEN_VALUE, null);
long expirationTime = prefs.getLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME, -1);
// Check if the current token is still valid for a while
if (tokenValue != null && expirationTime > 0) {
if (expirationTime
> System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TOLERANCE) {
return new AccessToken(tokenValue, new Date(expirationTime));
}
}
// ***** WARNING *****
// In this sample, we load the credential from a JSON file stored in a raw resource
// folder of this client app. You should never do this in your app. Instead, store
// the file in your server and obtain an access token from there.
// *******************
final InputStream stream = getResources().openRawResource(R.raw.credential);
try {
final GoogleCredentials credentials = GoogleCredentials.fromStream(stream)
.createScoped(SCOPE);
final AccessToken token = credentials.refreshAccessToken();
prefs.edit()
.putString(PREF_ACCESS_TOKEN_VALUE, token.getTokenValue())
.putLong(PREF_ACCESS_TOKEN_EXPIRATION_TIME,
token.getExpirationTime().getTime())
.apply();
return token;
} catch (IOException e) {
Log.e(TAG, "Failed to obtain access token.", e);
}
return null;
}
#Override
protected void onPostExecute(AccessToken accessToken) {
mAccessTokenTask = null;
final ManagedChannel channel = new OkHttpChannelProvider()
.builderForAddress(HOSTNAME, PORT)
.nameResolverFactory(new DnsNameResolverProvider())
.intercept(new GoogleCredentialsInterceptor(new GoogleCredentials(accessToken)
.createScoped(SCOPE)))
.build();
mApi = SpeechGrpc.newStub(channel);
// Schedule access token refresh before it expires
if (mHandler != null) {
mHandler.postDelayed(mFetchAccessTokenRunnable,
Math.max(accessToken.getExpirationTime().getTime()
- System.currentTimeMillis()
- ACCESS_TOKEN_FETCH_MARGIN, ACCESS_TOKEN_EXPIRATION_TOLERANCE));
}
}
}}
public class VoiceRecorder {
private static final int[] SAMPLE_RATE_CANDIDATES = new int[]{48000, 44100};
private static final int CHANNEL = AudioFormat.CHANNEL_IN_MONO;
private static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
private static final int AMPLITUDE_THRESHOLD = 1500;
private static final int SPEECH_TIMEOUT_MILLIS = 2000;
private static final int MAX_SPEECH_LENGTH_MILLIS = 30 * 1000;
public static abstract class Callback {
/**
* Called when the recorder starts hearing voice.
*/
public void onVoiceStart() {
}
/**
* Called when the recorder is hearing voice.
*
* #param data The audio data in {#link AudioFormat#ENCODING_PCM_16BIT}.
* #param size The size of the actual data in {#code data}.
*/
public void onVoice(byte[] data, int size) {
}
/**
* Called when the recorder stops hearing voice.
*/
public void onVoiceEnd() {
}
}
private final Callback mCallback;
private AudioRecord mAudioRecord;
private Thread mThread;
private byte[] mBuffer;
private final Object mLock = new Object();
/** The timestamp of the last time that voice is heard. */
private long mLastVoiceHeardMillis = Long.MAX_VALUE;
/** The timestamp when the current voice is started. */
private long mVoiceStartedMillis;
public VoiceRecorder(#NonNull Callback callback) {
mCallback = callback;
}
/**
* Starts recording audio.
*
* <p>The caller is responsible for calling {#link #stop()} later.</p>
*/
public void start() {
// Stop recording if it is currently ongoing.
stop();
// Try to create a new recording session.
mAudioRecord = createAudioRecord();
if (mAudioRecord == null) {
throw new RuntimeException("Cannot instantiate VoiceRecorder");
}
// Start recording.
mAudioRecord.startRecording();
// Start processing the captured audio.
mThread = new Thread(new ProcessVoice());
mThread.start();
}
/**
* Stops recording audio.
*/
public void stop() {
synchronized (mLock) {
System.out.println("stop audio record....");
dismiss();
if (mThread != null) {
mThread.interrupt();
mThread = null;
}
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
mBuffer = null;
System.out.println("stop audio record....2");
}
}
/**
* Dismisses the currently ongoing utterance.
*/
public void dismiss() {
if (mLastVoiceHeardMillis != Long.MAX_VALUE) {
mLastVoiceHeardMillis = Long.MAX_VALUE;
mCallback.onVoiceEnd();
}
}
/**
* Retrieves the sample rate currently used to record audio.
*
* #return The sample rate of recorded audio.
*/
public int getSampleRate() {
if (mAudioRecord != null) {
return mAudioRecord.getSampleRate();
}
return 0;
}
/**
* Creates a new {#link AudioRecord}.
*
* #return A newly created {#link AudioRecord}, or null if it cannot be created (missing
* permissions?).
*/
private AudioRecord createAudioRecord() {
for (int sampleRate : SAMPLE_RATE_CANDIDATES) {
final int sizeInBytes = AudioRecord.getMinBufferSize(sampleRate, CHANNEL, ENCODING);
if (sizeInBytes == AudioRecord.ERROR_BAD_VALUE) {
continue;
}
final AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
sampleRate, CHANNEL, ENCODING, sizeInBytes);
if (audioRecord.getState() == AudioRecord.STATE_INITIALIZED) {
mBuffer = new byte[sizeInBytes];
return audioRecord;
} else {
audioRecord.release();
}
}
return null;
}
/**
* Continuously processes the captured audio and notifies {#link #mCallback} of corresponding
* events.
*/
private class ProcessVoice implements Runnable {
#Override
public void run() {
while (true) {
synchronized (mLock) {
if (Thread.currentThread().isInterrupted()) {
break;
}
final int size = mAudioRecord.read(mBuffer, 0, mBuffer.length);
final long now = System.currentTimeMillis();
if (isHearingVoice(mBuffer, size)) {
if (mLastVoiceHeardMillis == Long.MAX_VALUE) {
mVoiceStartedMillis = now;
mCallback.onVoiceStart();
}
mCallback.onVoice(mBuffer, size);
mLastVoiceHeardMillis = now;
if (now - mVoiceStartedMillis > MAX_SPEECH_LENGTH_MILLIS) {
end();
}
} else if (mLastVoiceHeardMillis != Long.MAX_VALUE) {
mCallback.onVoice(mBuffer, size);
if (now - mLastVoiceHeardMillis > SPEECH_TIMEOUT_MILLIS) {
end();
}
}
}
}
}
private void end() {
mLastVoiceHeardMillis = Long.MAX_VALUE;
mCallback.onVoiceEnd();
System.out.println("end...");
}
private boolean isHearingVoice(byte[] buffer, int size) {
for (int i = 0; i < size - 1; i += 2) {
// The buffer has LINEAR16 in little endian.
int s = buffer[i + 1];
if (s < 0) s *= -1;
s <<= 8;
s += Math.abs(buffer[i]);
if (s > AMPLITUDE_THRESHOLD) {
return true;
}
}
return false;
}
}}
Then I implemented the Speech Service & Voice Recorder callback as follows:
private VoiceRecorder voiceRecorder;
private final SpeechService.Listener speechServiceListener = new SpeechService.Listener() {
#Override
public void onSpeechRecognized(final String text, final boolean isFinal) {
if (isFinal) {
System.out.println("ui thread...");
if (!TextUtils.isEmpty(text)) {
runOnUiThread(() -> {
showMessage(text);
flingAnswer(text);
});
}
}
}
#Override
public void onErrorRecognizing() {
showMessage("Please try again. Could not detect.");
}
#Override
public void onRandomStupidity() {
}
};
private SpeechService speechService;
private final VoiceRecorder.Callback voiceCallback = new VoiceRecorder.Callback() {
#Override
public void onVoiceStart() {
if (speechService != null) {
System.out.println("voice start....");
speechService.startRecognizing(voiceRecorder.getSampleRate());
}
}
#Override
public void onVoice(byte[] data, int size) {
if (speechService != null) {
speechService.recognize(data, size);
}
}
#Override
public void onVoiceEnd() {
if (speechService != null) {
speechService.finishRecognizing();
}
}
};
private final ServiceConnection serviceConnection = new ServiceConnection() {
#Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
speechService = SpeechService.from(binder);
speechService.addListener(speechServiceListener);
}
#Override
public void onServiceDisconnected(ComponentName componentName) {
speechService = null;
}
};
For voice input this is the code:
#Override
public void stopRecognizing() {
stopVoiceRecorder();
Log.e("Recording", "Stopped");
}
#Override
public void startRecognizing() {
if (permissionManager != null && permissionManager.askForPermissions()) {
startVoiceRecorder();
vibrate.vibrate(50);//Providing haptic feedback to user on press.
}
Log.e("Recording", "Started");
}
binding.imgVoice.setOnTouchListener((v, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
System.out.println("up...");
mCallback.stopRecognizing();
binding.imgVoice
.animate()
.scaleX(1.0f)
.scaleY(1.0f);
binding.imgVoice.setVisibility(View.GONE);
binding.progressBar.setVisibility(View.VISIBLE);
break;
case MotionEvent.ACTION_DOWN:
System.out.println("down...");
binding.imgVoice
.animate()
.scaleX(1.8f)
.scaleY(1.8f);
mCallback.startRecognizing();
break;
}
return true;
});
}
When I press the mic, event registered as Action_Down, I start the voice recorder and on releasing the mic , voice recorder is stopped. Also, with the Action_Down I am scaling up the mic icon which needs to be scaled down on Action_Up . But the ui freezes as a whole most of the times. I find that the onNext() callback for StreamObserver is continuously being invoked before the isFinal becomes true.
private void startVoiceRecorder() {
if (voiceRecorder != null) {
voiceRecorder.stop();
}
voiceRecorder = new VoiceRecorder(voiceCallback);
voiceRecorder.start();
}
private void stopVoiceRecorder() {
if (voiceRecorder != null) {
voiceRecorder.stop();
voiceRecorder = null;
}
}
But I want the mic to scale down as soon as I release the mic(on Action up event) which is not happening.
So if anyone can help me over this?
Thanks in Advance.
Trying to get all the channels from Twilio chat using the twilio SDK. Want to wait for the channel list to load(using Observables) and then display it in my UI. Below is a rough idea of what i'm trying to do:
private List<Paginator<ChannelDescriptor> getAllChannels() {
ChatClient.Properties props = new ChatClient.Properties.Builder()
.createProperties();
ChatClient chatClient = ChatClient.create(context.getApplicationContext(),
accessToken,
props,
null);
List<Paginator<ChannelDescriptor> channelList = new ArrayList<>()
chatClient.getChannels().getUserChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> firstPaginator) {
channelList.add(firstPaginator);
Paginator<ChannelDescriptor> nextPaginator = firstPaginator;
while (nextPaginator != null && nextPaginator.hasNextPage()) {
nextPaginator = loadNextChatChannelPage(firstPaginator);
if(nextPaginator != null) {
channelList.add(nextPaginator);
}
}
}
});
return channelList;
}
public Paginator<ChannelDescriptor> loadNextChatChannelPage(Paginator<ChannelDescriptor> paginator) {
paginator.requestNextPage(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
return channelDescriptorPaginator;
}
#Override
public void onError(ErrorInfo errorInfo) {
return null.
}
}));
}
What i ended up doing is this:
/**
* Start loading the Twilio chat channel pages
*
* #return Single containing a list of all the chat channel pages
*/
public Single<List<Paginator<ChannelDescriptor>>> loadAllChatChannelPages(ChatClientManager chatClientManager) {
return Single.create(emitter -> chatClientManager.getChatClient().getChannels().getUserChannelsList(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
if(channelDescriptorPaginator != null) {
emitter.onSuccess(channelDescriptorPaginator);
}
}
#Override
public void onError(ErrorInfo errorInfo) {
String errorMessage = "";
if(errorInfo != null) {
errorMessage = errorInfo.getMessage();
}
emitter.onError(new Throwable(errorMessage));
}
})).subscribeOn(Schedulers.io())
.flatMap(firstPaginator -> {
if(firstPaginator != null) {
return loadChannelPaginator((Paginator<ChannelDescriptor>) firstPaginator).toList()
.subscribeOn(Schedulers.io());
} else {
return Single.error(new Throwable("Could not get chat channels"));
}
});
}
/**
* Recursively loads the chat channel pages and returns them as a single observable
*
* #param paginator this needs to be the first chat channel paginator from the chat client
* #return Observable containing a flattened version of all the available chat channel paginators
*/
private Observable<Paginator<ChannelDescriptor>> loadChannelPaginator(Paginator<ChannelDescriptor> paginator) {
if (paginator.hasNextPage()) {
return Observable.mergeDelayError(
Observable.just(paginator),
loadNextChatChannelPage(paginator)
.flatMap(this::loadChannelPaginator));
}
return Observable.just(paginator);
}
/**
* Loads a single chat channel page
*
* #param previousPage the previous page of chat channels
* #return Observable containing the next chat channel page
*/
private Observable<Paginator<ChannelDescriptor>> loadNextChatChannelPage(Paginator<ChannelDescriptor> previousPage) {
return Observable.create(emitter -> previousPage.requestNextPage(new CallbackListener<Paginator<ChannelDescriptor>>() {
#Override
public void onSuccess(Paginator<ChannelDescriptor> channelDescriptorPaginator) {
if(channelDescriptorPaginator != null) {
emitter.onNext(channelDescriptorPaginator);
}
emitter.onComplete();
}
#Override
public void onError(ErrorInfo errorInfo) {
if(errorInfo != null) {
String errorMessage = errorInfo.getMessage();
Timber.e(errorMessage);
}
// emitter.onError(new Throwable(errorMessage));
emitter.onComplete();
}
}));
}
In the above code loadAllChatChannelPages loads the first paginator.
If that's not null then loadChannelPaginator takes over and recursively grabs each next paginator by executing loadNextChatChannelPage, a method that returns an observable for each single paginator.
Then mergeDelayError flattens all the paginators and returns them as one single Observable.
Finally in getAllChannels i apply Observable.toList(), this return a Single containing the list of Paginated chat channels that i needed.
I wanted to create a wrapper for api calls in retrofit so I can display ProgressDialog at common place & handle common response.
I achieved this by creating wrapper like this
public static <T> Observable<T> callApiWrapper(final Context context,
final boolean shouldShowProgress,
final String message,
final Observable<T> source) {
final ProgressDialog progressDialog = new ProgressDialog(context);
if (shouldShowProgress) {
if (!TextUtils.isEmpty(message))
progressDialog.setMessage(message);
else
progressDialog.setMessage(context.getString(R.string.please_wait));
}
return source.lift(new Observable.Operator<T, T>() {
#Override
public Subscriber<? super T> call(final Subscriber<? super T> child) {
return new Subscriber<T>() {
#Override
public void onStart() {
super.onStart();
if (shouldShowProgress) {
new Handler(Looper.getMainLooper()).post(new Runnable() {
#Override
public void run() {
progressDialog.show();
}
});
}
child.onStart();
}
#Override
public void onCompleted() {
if (shouldShowProgress && progressDialog.isShowing())
progressDialog.dismiss();
child.onCompleted();
}
#Override
public void onError(Throwable e) {
if (shouldShowProgress && progressDialog.isShowing())
progressDialog.dismiss();
child.onError(e);
}
#Override
public void onNext(T t) {
/*
Handle Invalid API response
*/
if (((BaseResponse) t).getStatus() == RestParams.Codes.INVALID_API_KEY) {
mCommonDataModel.setApiKey("");
getCommonApiService().getApiKey()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<ResponseBody>() {
#Override
public void onCompleted() {
}
#Override
public void onError(Throwable e) {
}
#Override
public void onNext(ResponseBody responseBody) {
try {
String response = responseBody.string();
JSONObject jsonObject = new JSONObject(response);
String key = jsonObject.optString("KEY");
if (!TextUtils.isEmpty(key))
mCommonDataModel.setApiKey(key);
callApiWrapper(context, shouldShowProgress,
message, source)
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe();
} catch (Exception e) {
e.printStackTrace();
}
}
});
} else {
if (shouldShowProgress && progressDialog.isShowing())
progressDialog.dismiss();
child.onNext(t);
}
}
};
}
});
}
In the above code, I check that if I get specific status code like Invalid API KEY, then I am calling an API to get the new API key instead of giving the status directly to original subscriber.
Once I get the new API key successfully, I call the wrapper recursively & try to give the response to original subscriber. But the problem is Original Subscriber is not getting onNext callback
What am I missing here? Is there any other way of achieving what I am trying to do?
You need to add some retry logic in case you get an invalid key failure so something like
source.flatMap(
t ->
{
if (((BaseResponse) t).getStatus() == RestParams.Codes.INVALID_API_KEY) {
return Observable.error(new InvalidKeyException("The key is not valid"));
}
else {
return Observable.just(t);
}
}
)
.retryWhen(
errors ->
errors.flatMap(error -> {
if (error instanceof InvalidKeyException()) {
return getCommonApiService().getApiKey()
.flatMap(
responseBody -> {
String response = responseBody.string();
JSONObject jsonObject = new JSONObject(response);
String key = jsonObject.optString("KEY");
if (TextUtils.isEmpty(key))
return Observable.error();
else {
return Observable.just(key);
}})
.doOnNext( key -> mCommonDataModel.setApiKey(key));
}
// For anything else, don't retry
return Observable.error(error);
}))
.subscribe(/* do what you need to do with the results*/)
In order to add the side effects i.e. enable progress bar when you start the subscription and dismiss it when you've finished something like
modifiedSource.doOnSubscribe(/* progress bar show logic */)
.doOnTerminate(/* progress bar dismiss logic */)
Finally I managed to create a wrapper which handles for me the common Progressbar & retry logic in case of Invalid Key API Response. This kind of wrapper might be useful if in many cases. Thanks to #JohnWowUs for his answer which helped me to understand things & implement this wrapper.
Here's the working code
private static final int MAX_RETRIES = 2;
private static int sCurrentRetryAttempt = 0;
/**
* Common Wrapper for calling API.
*
* #param context context for showing progress dialog
* #param shouldShowProgress boolean which indicated if progress dialog should be shown or not
* #param message message to be shown in progress dialog. if null passed, then "Please wait..." will be shown
* #param source original observable
* #return observable to which observer can subscribe
*/
public static <T> Observable<T> callApiWrapper(final Context context,
final boolean shouldShowProgress,
final String message,
final Observable<T> source) {
// Progress Dialog
final ProgressDialog progressDialog = setupProgressDialog(context, shouldShowProgress, message);
if (progressDialog != null) progressDialog.show();
return source
.flatMap(new Func1<T, Observable<T>>() {
#Override
public Observable<T> call(T t) {
/*
* Check if the response contains invalid key status code.
*/
if (t instanceof BaseResponse) {
if (((BaseResponse) t).getStatus() == RestParams.Codes.INVALID_API_KEY) {
return Observable.error(new InvalidKeyException("Invalid key"));
}
}
/*
* We are here, that means, there wasn't invalid key status code.
* So we wouldn't like to handle it so just return to original subscriber
*/
if (progressDialog != null && progressDialog.isShowing())
progressDialog.dismiss();
return Observable.just(t);
}
}).retryWhen(new Func1<Observable<? extends Throwable>, Observable<?>>() {
#Override
public Observable<?> call(Observable<? extends Throwable> observable) {
return observable.flatMap(new Func1<Throwable, Observable<?>>() {
#Override
public Observable<?> call(final Throwable throwable) {
if (throwable instanceof InvalidKeyException) {
/*
* Check for retry limit. if we already have retried enough, then
* we should tell the original subscriber about the error as it
* doesn't seems recoverable.
*/
if (sCurrentRetryAttempt >= MAX_RETRIES) {
if (progressDialog != null && progressDialog.isShowing())
progressDialog.dismiss();
//Resetting the attempts to 0
sCurrentRetryAttempt = 0;
return Observable.error(throwable);
}
//Increase the attempt counter
sCurrentRetryAttempt += 1;
return getCommonApiService().getApiKey()
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.flatMap(new Func1<ResponseBody, Observable<?>>() {
#Override
public Observable<?> call(ResponseBody responseBody) {
try {
/*
* Check if we succeed in our attempt to handle
* invalid key
*/
if (processApiKey(responseBody)) {
/*
* We succeeded in our attempts to handle
* invalid api key, so we will return the
* original subscriber what it wanted.
*/
return callApiWrapper(context,
shouldShowProgress, message, source);
} else
return Observable.just(throwable);
} catch (Exception e) {
/*
* We are here that means something went wrong,
* so we will retry silently.
*/
return Observable.just(throwable);
}
}
});
} else {
/*
* For any other error, we are not going to handle right now,
* so just return
*/
return Observable.error(throwable);
}
}
});
}
});
}
& using this is same as normal like:
RestClient.callApiWrapper(mContext, true, null,
RestClient.getAuthenticationApiService().socialLogIn(name, email, singInVia, mobile, "android", deviceToken))
.subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Subscriber<BaseResponse<RegistrationResponse>>() {
//...
}
Is there some way to interrupt AsyncTaskLoader's loadInBackground() thread when I call cancelLoad()? I believe that AsyncTask.cancel() does this but the task variables are private and cannot be accessed.
Use stopLoading() or abandon() or reset() for that
Article: https://plus.google.com/117981280628062796190/posts/8b9RmQvxudb
It's relatively simple to add that functionality, this is the Gist of it:
public abstract class InterruptibleAsyncTaskLoader<D> extends AsyncTaskLoader<D> {
private volatile Thread thread;
public InterruptibleAsyncTaskLoader(Context context) {
super(context);
}
public boolean isLoadInBackgroundRunning() {
return thread != null;
}
#Override
public void cancelLoadInBackground() {
Thread t = thread;
if (t != null) t.interrupt();
}
#Override
public final D loadInBackground() {
try {
thread = Thread.currentThread();
return doLoadInBackground();
} catch (InterruptedException e) {
OperationCanceledException oce = new OperationCanceledException(e.toString());
oce.initCause(e);
throw oce;
} finally {
Thread.interrupted();
thread = null;
}
}
public abstract D doLoadInBackground() throws InterruptedException;
}
I've checked the available methods, and it seems none really interrupts the thread that runs in the background.
Looking at the code, it seems that AsyncTask is being used under the hood. I've tried finding a "hole" that I could use to interrupt the asyncTask, but I couldn't find (unless you are fine with reflection).
I think you could make your own loader that will have this functionality.
I think you can modify the code to your needs. Maybe add "forceCancel", which will check the value of the asyncTask, and if it's not null, call "cancel(true)" on it. Here's the code I've looked at:
AsyncTaskLoader.java
package android.support.v4.content;
import android.content.Context;
import android.os.Handler;
import android.os.SystemClock;
import android.support.v4.util.TimeUtils;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.concurrent.CountDownLatch;
/**
* Static library support version of the framework's {#link android.content.AsyncTaskLoader}.
* Used to write apps that run on platforms prior to Android 3.0. When running
* on Android 3.0 or above, this implementation is still used; it does not try
* to switch to the framework's implementation. See the framework SDK
* documentation for a class overview.
*/
public abstract class AsyncTaskLoader<D> extends Loader<D> {
static final String TAG = "AsyncTaskLoader";
static final boolean DEBUG = false;
final class LoadTask extends ModernAsyncTask<Void, Void, D> implements Runnable {
D result;
boolean waiting;
private CountDownLatch done = new CountDownLatch(1);
/* Runs on a worker thread */
#Override
protected D doInBackground(Void... params) {
if (DEBUG) Log.v(TAG, this + " >>> doInBackground");
result = AsyncTaskLoader.this.onLoadInBackground();
if (DEBUG) Log.v(TAG, this + " <<< doInBackground");
return result;
}
/* Runs on the UI thread */
#Override
protected void onPostExecute(D data) {
if (DEBUG) Log.v(TAG, this + " onPostExecute");
try {
AsyncTaskLoader.this.dispatchOnLoadComplete(this, data);
} finally {
done.countDown();
}
}
#Override
protected void onCancelled() {
if (DEBUG) Log.v(TAG, this + " onCancelled");
try {
AsyncTaskLoader.this.dispatchOnCancelled(this, result);
} finally {
done.countDown();
}
}
#Override
public void run() {
waiting = false;
AsyncTaskLoader.this.executePendingTask();
}
}
volatile LoadTask mTask;
volatile LoadTask mCancellingTask;
long mUpdateThrottle;
long mLastLoadCompleteTime = -10000;
Handler mHandler;
public AsyncTaskLoader(Context context) {
super(context);
}
/**
* Set amount to throttle updates by. This is the minimum time from
* when the last {#link #onLoadInBackground()} call has completed until
* a new load is scheduled.
*
* #param delayMS Amount of delay, in milliseconds.
*/
public void setUpdateThrottle(long delayMS) {
mUpdateThrottle = delayMS;
if (delayMS != 0) {
mHandler = new Handler();
}
}
#Override
protected void onForceLoad() {
super.onForceLoad();
cancelLoad();
mTask = new LoadTask();
if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask);
executePendingTask();
}
/**
* Attempt to cancel the current load task. See {#link android.os.AsyncTask#cancel(boolean)}
* for more info. Must be called on the main thread of the process.
*
* <p>Cancelling is not an immediate operation, since the load is performed
* in a background thread. If there is currently a load in progress, this
* method requests that the load be cancelled, and notes this is the case;
* once the background thread has completed its work its remaining state
* will be cleared. If another load request comes in during this time,
* it will be held until the cancelled load is complete.
*
* #return Returns <tt>false</tt> if the task could not be cancelled,
* typically because it has already completed normally, or
* because {#link #startLoading()} hasn't been called; returns
* <tt>true</tt> otherwise.
*/
public boolean cancelLoad() {
if (DEBUG) Log.v(TAG, "cancelLoad: mTask=" + mTask);
if (mTask != null) {
if (mCancellingTask != null) {
// There was a pending task already waiting for a previous
// one being canceled; just drop it.
if (DEBUG) Log.v(TAG,
"cancelLoad: still waiting for cancelled task; dropping next");
if (mTask.waiting) {
mTask.waiting = false;
mHandler.removeCallbacks(mTask);
}
mTask = null;
return false;
} else if (mTask.waiting) {
// There is a task, but it is waiting for the time it should
// execute. We can just toss it.
if (DEBUG) Log.v(TAG, "cancelLoad: task is waiting, dropping it");
mTask.waiting = false;
mHandler.removeCallbacks(mTask);
mTask = null;
return false;
} else {
boolean cancelled = mTask.cancel(false);
if (DEBUG) Log.v(TAG, "cancelLoad: cancelled=" + cancelled);
if (cancelled) {
mCancellingTask = mTask;
}
mTask = null;
return cancelled;
}
}
return false;
}
/**
* Called if the task was canceled before it was completed. Gives the class a chance
* to properly dispose of the result.
*/
public void onCanceled(D data) {
}
void executePendingTask() {
if (mCancellingTask == null && mTask != null) {
if (mTask.waiting) {
mTask.waiting = false;
mHandler.removeCallbacks(mTask);
}
if (mUpdateThrottle > 0) {
long now = SystemClock.uptimeMillis();
if (now < (mLastLoadCompleteTime+mUpdateThrottle)) {
// Not yet time to do another load.
if (DEBUG) Log.v(TAG, "Waiting until "
+ (mLastLoadCompleteTime+mUpdateThrottle)
+ " to execute: " + mTask);
mTask.waiting = true;
mHandler.postAtTime(mTask, mLastLoadCompleteTime+mUpdateThrottle);
return;
}
}
if (DEBUG) Log.v(TAG, "Executing: " + mTask);
mTask.executeOnExecutor(ModernAsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
}
}
void dispatchOnCancelled(LoadTask task, D data) {
onCanceled(data);
if (mCancellingTask == task) {
if (DEBUG) Log.v(TAG, "Cancelled task is now canceled!");
rollbackContentChanged();
mLastLoadCompleteTime = SystemClock.uptimeMillis();
mCancellingTask = null;
executePendingTask();
}
}
void dispatchOnLoadComplete(LoadTask task, D data) {
if (mTask != task) {
if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel");
dispatchOnCancelled(task, data);
} else {
if (isAbandoned()) {
// This cursor has been abandoned; just cancel the new data.
onCanceled(data);
} else {
commitContentChanged();
mLastLoadCompleteTime = SystemClock.uptimeMillis();
mTask = null;
if (DEBUG) Log.v(TAG, "Delivering result");
deliverResult(data);
}
}
}
/**
*/
public abstract D loadInBackground();
/**
* Called on a worker thread to perform the actual load. Implementations should not deliver the
* result directly, but should return them from this method, which will eventually end up
* calling {#link #deliverResult} on the UI thread. If implementations need to process
* the results on the UI thread they may override {#link #deliverResult} and do so
* there.
*
* #return Implementations must return the result of their load operation.
*/
protected D onLoadInBackground() {
return loadInBackground();
}
/**
* Locks the current thread until the loader completes the current load
* operation. Returns immediately if there is no load operation running.
* Should not be called from the UI thread: calling it from the UI
* thread would cause a deadlock.
* <p>
* Use for testing only. <b>Never</b> call this from a UI thread.
*
* #hide
*/
public void waitForLoader() {
LoadTask task = mTask;
if (task != null) {
try {
task.done.await();
} catch (InterruptedException e) {
// Ignore
}
}
}
#Override
public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
super.dump(prefix, fd, writer, args);
if (mTask != null) {
writer.print(prefix); writer.print("mTask="); writer.print(mTask);
writer.print(" waiting="); writer.println(mTask.waiting);
}
if (mCancellingTask != null) {
writer.print(prefix); writer.print("mCancellingTask="); writer.print(mCancellingTask);
writer.print(" waiting="); writer.println(mCancellingTask.waiting);
}
if (mUpdateThrottle != 0) {
writer.print(prefix); writer.print("mUpdateThrottle=");
TimeUtils.formatDuration(mUpdateThrottle, writer);
writer.print(" mLastLoadCompleteTime=");
TimeUtils.formatDuration(mLastLoadCompleteTime,
SystemClock.uptimeMillis(), writer);
writer.println();
}
}
}
EDIT: after 3 years, I've decided to post my solution for this:
/**
* makes it a bit easier to use AsyncTaskLoader. based on https://github.com/alexjlockwood/AppListLoader
*/
abstract class AsyncTaskLoaderEx<T>(context: Context) : AsyncTaskLoader<T>(context) {
#JvmField
var hasResult = false
#Suppress("MemberVisibilityCanBePrivate")
#JvmField
var isCanceled = false
var result: T? = null
private set
private var _currentThread: Thread? = null
init {
onContentChanged()
}
override fun onStartLoading() {
if (takeContentChanged())
forceLoad()
}
override fun deliverResult(data: T?) {
result = data
hasResult = true
super.deliverResult(data)
}
override fun onLoadInBackground(): T? {
_currentThread = Thread.currentThread()
return super.onLoadInBackground()
}
open fun interrupt() {
isCanceled = true
_currentThread?.interrupt()
}
override fun onReset() {
super.onReset()
onStopLoading()
if (hasResult) {
onReleaseResources(result)
result = null
hasResult = false
}
}
protected open fun onReleaseResources(data: T?) {
//nothing to do.
}
companion object {
private val sCurrentUniqueId = AtomicInteger(0)
#JvmStatic
val newUniqueLoaderId: Int
get() = sCurrentUniqueId.getAndIncrement()
}
}
And usage sample:
class MainActivity : AppCompatActivity() {
companion object {
val TASK_LOADER_ID = AsyncTaskLoaderEx.newUniqueLoaderId
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val loaderManager = LoaderManager.getInstance(this)
loaderManager.initLoader(TASK_LOADER_ID, null, object : LoaderManager.LoaderCallbacks<Boolean?> {
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Boolean?> {
return ImageLoadingTask(this#MainActivity)
}
override fun onLoadFinished(loader: Loader<Boolean?>, result: Boolean?) {
Log.d("AppLog", "finished without being interrupted?:$result")
if (result == null)
return
//TODO use result
}
override fun onLoaderReset(loader: Loader<Boolean?>) {
}
})
val runnable = Runnable {
Log.d("AppLog", "trying to stop loader")
(loaderManager.getLoader<Loader<Boolean?>>(TASK_LOADER_ID) as AsyncTaskLoaderEx?)?.interrupt()
}
val handler = Handler()
handler.postDelayed(runnable, 2000L)
Log.d("AppLog", "will try to interrupt in 2 seconds")
lifecycle.addObserver(object : LifecycleObserver {
#Suppress("unused")
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
handler.removeCallbacks(runnable)
}
})
}
private class ImageLoadingTask(context: Context) : AsyncTaskLoaderEx<Boolean?>(context) {
override fun loadInBackground(): Boolean? {
try {
for (i in 0..10) {
Log.d("AppLog", "loadInBackground: $i")
Thread.sleep(1000L)
}
return true
} catch (e: InterruptedException) {
Log.d("AppLog", "INTERRUPTED!!!")
}
return false
}
}
}