Skip to content
This repository was archived by the owner on Jun 14, 2022. It is now read-only.

Conversation

jsoberg
Copy link
Contributor

@jsoberg jsoberg commented Jan 30, 2021

Ok, so I added an ASyncTask to run the authentication code, with a callback to the Activity when it completes (still finishing the Activity on the main thread). While the task is running, I added an indeterminate progress bar that replaces the "unlock" button to provide some feedback to the user:

andOTPUnlockProgress_2021-1-29.mp4

One small caveat here is that I call .get() on the task if the Activity is being paused. I did this to prevent the task from calling back to a dead activity for the case where the user initiates the unlock process and rotates their phone (or some other config change).

If this situation happens, the task will complete and the Activity will finish as expected, however the main thread will be locked while .get() is called (as it was before). Locking the main thread here isn't ideal, but seeing as this is a short-lived (1-2 second) task and the situation is unlikely to happen it's probably not a big issue.

To get around this we'd probably have to start using a retained fragment within the Activity (to store the task), and then in onCreate check for that retained fragment and tie the newly created Activity into it for a callback. This is definitely doable, however it'd probably make more sense to wait and see if we implement a general background execution scheme for the app before doing that work.

Let me know if you see any issues and I'll make necessary changes. I tested this on a Samsung device and on an emulator for both the cases of regular login and changing/upgrading authentication, with configuration changes in between and didn't run into any issues with the changes.

Get rid of the need for separate checks for setting layout hints and input type. Add some clarifying comments.
…icating

Began disabling the password input and unlock buttons while the task is running, for some UI feedback. Right now it's just waiting for the task to finish if the activity gets paused while it's running; this isn't great since there's potential there to lock the main thread, but the task is so short-lived that it may not be an issue.
…ability to show/hide the password text while unlocking)
@flocke flocke self-assigned this Jan 30, 2021
@flocke flocke added this to the v0.8.1 milestone Jan 30, 2021
@flocke
Copy link
Member

flocke commented Jan 30, 2021

Thank you a lot, I hope I find the time to review this soon.

@flocke
Copy link
Member

flocke commented Feb 1, 2021

I haven't found the time yet to review the PR in detail, but I noticed you used an AsyncTask. I looked into that as well for the creating backups in a background process and noticed that it is set to be deprecated in API level 30: https://developer.android.com/reference/android/os/AsyncTask

Do you think it would be feasible to use something else to do this? I decided on using a native Java Runnable and an ExecutorService. You can take look at it in this commit: feeeba7

If you want to put in the extra work I would appreciate it if you could move it of AsyncTask yourself. But I can understand if you don't have the time or don't want to. In that case I will merge it as it is (after a more detailed review) and port it myself later on when I find some time.

@jsoberg
Copy link
Contributor Author

jsoberg commented Feb 1, 2021

Oh sorry, I completely forgot that ASyncTask was being deprecated. I can definitely move it into using an executor with a main thread Handler instead. We could also pull in RxJava (though that's adding another dependency), but it has some nice threading utilities built in (default schedulers for background work, Android main thread scheduler for pushing to the main thread, etc). Let me know if you prefer one over the other, it's not a big change either way so if I don't hear back I'll just use an executor.

@flocke
Copy link
Member

flocke commented Feb 1, 2021

I would prefer to avoid additional dependencies if not strictly necessary, so I would prefer an executor.

Even though I posted my commit as example, you probably should not look at it to much. It was my first time doing something like this in an Android app, so it's probably not very clean. You seem to have some more experience in that regard, so I will probably look at your code afterwards trying to learn something from it ;)

@jsoberg
Copy link
Contributor Author

jsoberg commented Feb 1, 2021

That makes sense, I'll stick with an executor service. Haha I have some experience with threading in Android, but any way you go it's never fun to tie background work back to the UI in a clean way (hence my .get() call in the code I put up). We might be able to use a base executor and send a lambda/callable in to do background work that'll make things like this easier in the future, I'll definitely take a look while modifying my code within the next few days

…d make sure that AuthenticateActivity can receive results across configuration changes

Begin storing the task in a retained fragment so that if the task is begun, we will be able to get a callback when the activity is destroyed/recreated.
This ensure that if the user goes home, the callback will be reset when coming back (since onCreate doesn't get called most of the time in this instance).
@jsoberg
Copy link
Contributor Author

jsoberg commented Feb 2, 2021

Added a base UiBackedBackgroundTask class that can hopefully be reused for similar instances where we want to run a background task that calls back to a UI on the main thread with a result, and converted the AuthenticationTask to extend this instead of ASyncTask.

I went ahead and implemented the retained fragment concept that I was talking about earlier while I was at it. This means that on configuration changes (i.e the user rotates their device, goes home, etc), the task will continue to run and if the activity is eventually brought back, we can reconnect to the task and get our callback. I made sure to set the callback to null when pausing so that we wouldn't ever get a callback to a dead activity instance.

All in all, the user can rotate their device and the main thread won't be paused (it will just show progress again when recreated, until the task completes). If the task completes between configuration changes, it retains the result and just calls back immediately when a new callback is set (in the activity's onResume).

@Override
public void onBackPressed() {
finishWithResult(false, null);
super.onBackPressed();
}

/** Retained instance fragment to hold a running @link{{@link AuthenticationTask}} between configuration changes.*/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed this weird double javadoc link, I'll fix that real quick on my lunch break

@flocke
Copy link
Member

flocke commented Feb 3, 2021

I finally found the time to review the code and I didn't really find anything I would change in the implementation.

The only problem I found come from the "Re-lock on screen off" and "Re-lock when going into the background" options.
With your retained fragment approach the unlocking continues in both cases when coming back from the background or unlocking the screen again. I would prefer the unlocking to be canceled in that case so we don't return to an unlocked activity, which would contradict the settings in my opinion. But only if one of the settings is enabled and the corresponding event (screen off or going into background) happens during the unlock task.

Edit: And I'm definitely going to refactor my backup task to use your UiBackedBackgroundTask and the retained fragment approach once this merged.

@jsoberg
Copy link
Contributor Author

jsoberg commented Feb 3, 2021

I finally found the time to review the code and I didn't really find anything I would change in the implementation.

The only problem I found come from the "Re-lock on screen off" and "Re-lock when going into the background" options.
With your retained fragment approach the unlocking continues in both cases when coming back from the background or unlocking the screen again. I would prefer the unlocking to be canceled in that case so we don't return to an unlocked activity, which would contradict the settings in my opinion. But only if one of the settings is enabled and the corresponding event (screen off or going into background) happens during the unlock task.

Edit: And I'm definitely going to refactor my backup task to use your UiBackedBackgroundTask and the retained fragment approach once this merged.

Good point, thank you for catching that. I'll make sure to check that setting for this case and cancel the task if it applies. I'm not sure off the top of my head how to tell when we're entering/coming back from a lock or background event vs a normal config change, but I'll take a look where that setting is being checked currently and add that in.

@jsoberg jsoberg marked this pull request as draft February 3, 2021 20:51
…off if enabled in settings

Note that we can't just remove the fragment from the fragment manager when we get a screen off callback, because Android doesn't allow you to remove fragments after onSaveInstanceState()
@jsoberg jsoberg marked this pull request as ready for review February 4, 2021 03:05
@jsoberg
Copy link
Contributor Author

jsoberg commented Feb 4, 2021

I added the ability to cancel a UiBackedBackgroundTask, and then used the MainActivity as a reference for cancelling the task on screen off/background if set to do so in the settings. I tested this on my device and it seemed to work fine (though in some cases it seems like Android kills the Activity on screen off, which ends with the same result anyway), but let me know if you see any issues with it.

Copy link
Member

@flocke flocke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have one small change request to handle the re-lock cases a little better, after that I think this is ready to be merged.

unlockButton.setOnClickListener(this);
private void checkBackgroundTask() {
TaskFragment taskFragment = findTaskFragment();
if (taskFragment != null && !taskFragment.task.isCanceled()) {
Copy link
Member

@flocke flocke Feb 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you do something like this here instead (not sure if this is the best way but it works):

if (taskFragment != null) {
    if (taskFragment.task.isCanceled()) {
        passwordInput.setText("");
        passwordInput.requestFocus();

        InputMethodManager keyboard = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
        keyboard.showSoftInput(passwordInput, 0);
    } else {
        taskFragment.task.setCallback(this::handleResult);
        setupUiForTaskState(true);
    }
}

Otherwise the password stays entered when returning from background, which makes the re-lock useless.

And could you exclude the AuthenticateActivity from being finished in destroyIfNotMain() in the BaseActivity as well? Otherwise it will show the "Authentication failed, closing" warning after screen off and end the app because the AuthenticateActivity gets killed

The manual showSoftInput seems to be required after screen-off, otherwise it did not show the keyboard again for me.

@jsoberg
Copy link
Contributor Author

jsoberg commented Feb 4, 2021

Thanks for the review, I'll make sure to add all of that in. I noticed the failure toast when the Activity dies and that definitely makes sense to not show when it's killed. Did you think we should also prevent that toast from showing when finishing from onBackPressed()?

@jsoberg jsoberg requested a review from flocke February 5, 2021 01:43
@jsoberg
Copy link
Contributor Author

jsoberg commented Feb 5, 2021

Added the requested changes:

  • Modified BaseActivity to allow for destroyOnScreenOff/destroyIfNotMain functionality to be overridden. I thought Android was just destroying the Activity on screen off here, sorry about that.
  • Made sure to reset the password input and request focus when coming back and seeing our task has been canceled. I made sure to remove the task fragment (when we're able) as well after seeing a canceled task, since otherwise the password input would be reset on each configuration change after canceling a task.

Tested those changes and everything seems to work as expected, but let me know if you see anything else and I'll be happy to change it

@flocke
Copy link
Member

flocke commented Feb 5, 2021

Looks good, thank you for implementing this!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants