At E-gineering, we’ve been using RxJava to help us handle networking (with Retrofit) and threading in Android apps due to the ease with which it handles these tasks. As a learning exercise, Nate Ridderman and I wanted to challenge ourselves to dig deeper with RxJava and get to know some additional operators. Knowing that one of the primary raison d’être and abilities of RxJava is to handle streams of events, we decided to try using it to handle touch inputs.
We came up with the idea to create a simple app that would let us record a secret knock pattern and then evaluate attempts to recreate it. The app lets users input a message that they will then lock away with their secret knock. To reveal the hidden message, the user must enter the secret knock again. In this post, we will examine the implementation we used for recording and evaluating the secret knock with RxJava. We used RxJava version 1 since version 2 is quite new and not well documented yet. You can view the entire project on github and the relevant RxJava portions are shown below.
Observable.Transformer<Void, List<TimeInterval<Void>>> collectTapSequence() { return observable -> observable .observeOn(Schedulers.io()) .doOnNext(clickEvent -> Timber.d("tap")) .timeInterval() .skip(1) .timeout(timeInterval -> Observable.timer(1500, TimeUnit.MILLISECONDS), Observable.empty()) .toList() .observeOn(AndroidSchedulers.mainThread()); } private void initializeRecorder() { clickSubscription = RxView.clicks(recordingMessage) .compose(collectTapSequence()) .subscribe(new Subscriber<List<TimeInterval<Void>>>() { @Override public void onCompleted() { Timber.v("tapsSubscriber onCompleted"); } @Override public void onError(Throwable e) { Timber.v(e, "ruh roh"); } @Override public void onNext(List<TimeInterval<Void>> durations) { Timber.v("onNext"); if (patternIsValid(durations)) { storePattern(durations); activateLock(); } else { Timber.v("pattern invalid"); Toast.makeText(MainActivity.this, R.string.pattern_invalid, Toast.LENGTH_SHORT).show(); Handler handler = new Handler(); handler.post(()-> initializeRecorder()); } } } ); } public void initializeTapToUnlock() { clickSubscription = RxView.clicks(hiddenMessage) .doOnNext(clickEvent -> { Timber.v("unlock tap"); showBorderAnimation(); }) .compose(collectTapSequence()) .subscribe(new Subscriber<List<TimeInterval<Void>>>() { @Override public void onCompleted() { hideBorderAnimation(); } @Override public void onError(Throwable e) { Timber.v(e, "ruh roh"); } @Override public void onNext(List<TimeInterval<Void>> durations) { Timber.v("onNext"); if (patternMatches(durations)) { Timber.v("lock deactivated"); deactivateLock(); } else { Timber.v("pattern incorrect"); Toast.makeText(MainActivity.this, R.string.pattern_incorrect, Toast.LENGTH_SHORT).show(); hiddenMessage.setBackgroundResource(R.drawable.message_background); Handler handler = new Handler(); handler.post(()-> initializeTapToUnlock()); } } }); }
DRYer RxJava
First of all, it must be noted that recording the initial secret knock pattern and recording the unlock attempts are doing the same thing. Since we want to reuse a fair portion of our RxJava chain for these actions, we utilized the Transformer class (thanks Dan Lew) to create a method that contains our core logic that we can call with the compose()
operator. Let’s take a closer look at what we are pulling into our Transformer. With observeOn(Schedulers.io())
, we observe the input stream on an IO thread to avoid blocking the UI. We then have a doOnNext()
with some logging.
Next, we make use of the timeInterval()
operator which just produces the delta in duration between two items emitted from the observable. Initially during development, we had been emitting the timestamp of each item and then using the buffer(2,1)
operator. With count=2 and skip=1, this emits a stream of overlapping tuples where the last element of each tuple is the first element of the next tuple. This allowed us to calculate the difference between each pair of taps and emit the durations downstream with a map()
operator. Using the timeInterval()
operator is a cleaner solution in this case. This operator’s first item emitted is the time between when the Observable was subscribed to and when the first item is emitted. We are not interested in this time so we use the skip(1)
operator to ignore it. We then are left with a sequence of durations between knocks.
End of the stream
Now, the question becomes “How do we end our stream?” For the purposes of this app, we just want to time out once the user has finished entering their secret knock. There are many versions of the timeout operator. After trying out several of them, we settled on this one:
timeout(Func1<? super T, ? extends Observable<V>> timeoutSelector, Observable<? extends T> other)
Per the javadoc, it “returns an Observable that mirrors the source Observable, but that switches to a fallback Observable if an item emitted by the source Observable doesn’t arrive within a window of time after the emission of the previous item, where that period of time is measured by an Observable that is a function of the previous item.” That’s a mouthful. Let’s break it down.
First of all, it’s returning a mirror of the source Observable so it’s just passing through whatever was being emitted. Good. As for the fallback Observable, we are providing Observable.empty()
because we don’t want anything additional, we just want our source Observable to complete when we timeout. And as for the time window, although we’re sure you can do more powerful things based on the previous item, all we are interested in here is the passage of time so we provide Observable.timer(1500, TimeUnit.MILLISECONDS)
to set a timeout of 1.5 seconds. You should be aware that some versions of timeout terminate with a call to onError()
; we couldn’t use those because it unsubscribed our upstream operators.
Once we have timed out, we convert all the emitted items into a list with the toList()
operator. And one more subtlety, in our Subscriber, we are going to be performing UI operations. In Android, this must be done on the main thread so we call observeOn(AndroidSchedulers.mainThread())
to jump back to the main thread and avoid CalledFromWrongThreadExceptions when manipulating views. And there we have our Transformer ready to be used whenever we need it. Next we need to set up our click listener.
Listen to that click
In order to easily use RxJava as a click listener, we made use of the RxBinding library. RxView.clicks()
is the method we call. In the javadoc there, we see the warning about a strong reference so that is why we assign our Observable to a Subscription field called clickSubscription
. Then, in onDestroy()
we have a reference we can explicitly call unsubscribe on to avoid memory leaks. We add all the steps in the chain we created with our Transformer by adding the compose()
operator. Then we subscribe to our Observable.
I Subscribe to that notion
In our Subscriber’s onNext()
, we get the list of durations and check for validity. A secret knock of only two raps is pretty boring so we require at least three. If the pattern is valid, we activate our lock and hide the message. If not, we indicate the rules to the user and reset the UI for them to try again. Note the use of a Handler to ensure that our call to initializeRecorder()
gets added to the end of the UI thread’s message queue so any calls already in process complete before the subsequent call is started. That wraps up initializerRecorder()
.
Next, we need to set up listening for the secret knock to unlock the message. We do that with a method called initializeTapToUnlock()
which will be quite similar to initializeRecorder()
. Again, we register our click listener with RxView.clicks()
. We want the border animation to show again once we start inputting the secret knock so we use the doOnNext()
operator to accomplish that here. Note that we are still on the main thread so we can manipulate the views.
We then use the compose()
operator to reuse the Observable chain we already created for capturing the secret knock in the first place and then subscribe to our Observable. In the Subscriber’s onNext()
, we compare the secret knock we just received to the one we recorded earlier. If it matches, we show the hidden message. If it doesn’t, we indicate the user’s unworthiness and reset the UI to allow them to make another attempt.
To help the user out a little, our pattern match test does a couple things. 1) We scale the entire pattern to be the same length as the original recorded pattern so it doesn’t matter if their tempo is off a little as long as the rhythm is accurate. 2) We allow up to 100 millisecond variation for each segment of the pattern. In our (non-rigorous) testing, this seems to be a reasonable amount of error to allow.
Whether the user succeeded in unlocking the hidden message or not, we stop the border animation in onCompleted()
. On a successful unlock, you are returned to the initial view and can modify your message and hide it away again if you choose.
We also wanted to note that this isn’t an example of actually securing the the hidden message since we are just conveniently storing it in SharedPreferences.
While initially quite intimidating, once you force yourself to dig in and begin to understand it, RxJava is quite powerful. We don’t want to gloss over that a lot of time was spent on our part reading about the available operators searching for the ones that would best accomplish our goals. It was great exposure for us to see what is possible with RxJava. We look forward to seeing through the new eyes of experience how others are also using RxJava in their code and also using it ourselves where it makes sense in helping our clients realize their next Android apps.