Skip to content

Conversation

@ezaquarii
Copy link
Collaborator

@ezaquarii ezaquarii commented May 12, 2020

New download service is a complete rewrite of a legacy FileDownloader service.

Sync code uses old solution - this PR does not replace it as it would be too much work and risk to swallow.

Small comparison of old vs new downloader design:

Old New
Using broadcast intents for state notifications Plain listeners
UI must track broadcast history No UI state
State transferred via type unsafe Intents Immutable, strongly typed data model
No test coverage Full test coverage
Multi-user state management Every user has private downloader instance
Single threaded downloads Multi-threaded downloads
No single source of truth Single source of truth regarding all downlaods
Downloader async logic depends on Android API Async logic uses generic concurrency API

LiveData<T> - not used in the service anymore

LiveData<T> is an observable value of type T that integrates with Android life-cycle mess. It's a relatively new API introduced by Google in androidx that is meant to cut the chores of lifecycle management. It works pretty well and the only issue I have with it is that unit testing is a bit more involving.

Update: I removed dependency on LiveData<T> - it was a bad idea o have it there.

LocalConnection<S> and LocalBinder<S>

LocalConnection and LocalBinder are generic implementations of platform ServiceConnection and Binder that everybody re-implements over and over again in the same form. It also provies a good dumping ground for typed service interaction API in general, so we can address #5866 for services this way. We already used this approach in PlayerServiceConnection.kt (extracted from legacy Java inline implementation).

Apart from that, we need a connection object as a facade for Service, because Service instance is created asynchonously and might (will) not be available when UI wants to interact with it.

Downloader interface

I managed to extract public downloader API and calls can be nicely delegated through all layers -> connector -> binder -> user downloader processor. All layers use the same interface.

DownloaderConnection

This component expsoses very vanilla API to the underlying Android Service mess, hiding all asynchronous issues behind a boring interface.

Updates are delivered using vanilla listener API (addListener()/removeListener(), so every
developer should be familiar with them. API solves problematic missed updates issue caused by asynchronous start/binding races, so no more any need for messaging bus pattern to deliver updates reliably.

If we'd like to expose LiveData<T> interface to the UI, this is the place to do it.

DownloaderService

This is a proposed replacement for FileDownloader service. Apart from cleaner code, it will
keep per-user Downloader instance and automatically terminate when all downloads are finished.

AsyncRunner

AsyncRunner is a replacement for AsyncTask that uses generic Java concurrency API and does not require any subclassing (tasks are just lambdas or functions so can be tested easily, contrary to AsyncTask being virtually untestable piece of global state). Also, AsyncTask API is officially deprecated by Google in favour of `java.util.concurrent' APIs.

This PR extends the API by adding progress and task cancelation, as those will be needed to provide adequate UX.

MockK

MockK is a Mockito, but in Kotlin. I introduced it because I find Mockito unable to mock Kotlin classes on Dalvik (it works in test, but not in androidTest). There is some weird issue with bytecode generator and I'm still investigating it. MockK has some nice properties, like support for co-routines, but the community behind it is smaller. Mockito is more mature, but the android support seems to still be rather poorly maintained. I do not commit myself to use it - I needed something to move forward with unit tests.

DexOpener

Mocking final classes Android below P (API <= 27) is not possible, which makes testing very troublesome. Android 28+ has out-of-the-box support for mocking, but our CI runs tests on older versions.

DexOpener is installed by instrumentation test runner and removes final attribute, enabling mocking of Kotlin classes.

ContactListFragment - proof of concept use

Contacts imports has been migrated to a new API. To test it:

  1. backup your contacts
  2. Settings->Show hidden files
  3. navigate to contacts backup dir and delete backup locally
  4. go to Settings->Back up contacts->Choose date

Contacts will be downloaded using proposed downloader service.

MockUser

Sadly, using mockito to mock User interface will blow up when actually trying to parcel it into Intent (mock does no contain proper Parcelable implementation, obviously).

This litle helper provides a true implementation mock to be used in connected tests.

EtmDownloaderFragment and test mode

ETM page to test the downloader. Any download request can be scheduled with a test flag true - this triggers "fake" download task that just Thread.sleep() and pushes some progress. One can use it for logic testing or UI work.

NotificationsManager

Downloader service must be switched to foreground and that requires a notifcation. I ported some notification code that is smeared in FileDownloader and placed it behind an interface.

Notifications requires some more work, as the UX is not great - it contains too much redundant text and not enough interesting information. Also, new downloader architecture allows us potentially to show multiple downloads, download transfer rate, number of pending files, etc, etc... We can do more.

@ezaquarii
Copy link
Collaborator Author

ezaquarii commented May 12, 2020

This work is a looong overdue attempt to tackle the sync issues, but I believe that this require a bit better foundations than the current uploader/downloader infrastructure we have in place.

Since this area is very complex and drowning in accumulated legacy issues, I don't think we can do any sensible work there without better building materials first.

I identified contacts import/export area as being relatively simple and suitable to test the new approach.

@tobiasKaminsky @AndyScherzinger Please take a look and let me know what you think.

@nextcloud-android-bot
Copy link
Collaborator

Unit test failed, but no output was generated. Maybe a preliminary stage failed.

@AndyScherzinger
Copy link
Member

I don't quite understand all the new concepts/structures like live data but the code/structure look good to me 👍

@ezaquarii ezaquarii force-pushed the ezaquarii/new-download-manager branch 2 times, most recently from a724833 to 8bf48f8 Compare May 16, 2020 23:39
@ezaquarii
Copy link
Collaborator Author

@AndyScherzinger @tobiasKaminsky Partial rewrite to eliminate some issues I found when trying to use it in contacts import. I have updated the changes description as well: #6048 (comment)

@ezaquarii ezaquarii force-pushed the ezaquarii/new-download-manager branch 4 times, most recently from 6718a38 to 2bd5cf7 Compare May 18, 2020 23:05
@nextcloud-android-bot
Copy link
Collaborator

Unit test failed, but no output was generated. Maybe a preliminary stage failed.

@ezaquarii ezaquarii force-pushed the ezaquarii/new-download-manager branch from 2bd5cf7 to 3570a7d Compare May 19, 2020 19:12
@ezaquarii ezaquarii changed the title [WIP] New download manager New download manager May 19, 2020
@ezaquarii
Copy link
Collaborator Author

@tobiasKaminsky @AndyScherzinger ready for review

@nextcloud-android-bot
Copy link
Collaborator

Unit test failed, but no output was generated. Maybe a preliminary stage failed.

@nextcloud-android-bot
Copy link
Collaborator

@ezaquarii
Copy link
Collaborator Author

Getting rid of broadcast-based progress updates is required to solve issues similar to this one: #4205

@ezaquarii ezaquarii force-pushed the ezaquarii/new-download-manager branch from 3570a7d to 70ab37d Compare May 19, 2020 22:42
@nextcloud-android-bot
Copy link
Collaborator

Unit test failed, but no output was generated. Maybe a preliminary stage failed.

@nextcloud-android-bot
Copy link
Collaborator

Copy link
Member

@AndyScherzinger AndyScherzinger left a comment

Choose a reason for hiding this comment

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

Needs a rebase, other than that I'd say let's give a go :)

Copy link
Member

@tobiasKaminsky tobiasKaminsky left a comment

Choose a reason for hiding this comment

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

Some small questions

val op = DownloadFileOperation(
request.user.toPlatformAccount(),
request.file,
OCFileListFragment.DOWNLOAD_SEND,
Copy link
Member

Choose a reason for hiding this comment

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

This and the next 2 lines is only needed in one special case (FDA:2255), but it should not be hardcoded.

Copy link
Collaborator Author

@ezaquarii ezaquarii Jun 4, 2020

Choose a reason for hiding this comment

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

@tobiasKaminsky
The thing is that DownloadFileOperation does not use those values for anything.
It seems to me that it just carries them around so FileDownloader can action finished operation.

There is nothing wrong with that approach, but since new downloader implementation does not support sending downloaded files anywhere at this moment, we don't need those values.

I'm also pretty sure that we won't use them in the future, because new implementation splits request, download state and download code into separate entitites and it doesn't have access to DownloadFileOperation instance, becuase download mechanism is hidden behind opaque function/lambda interface.

At first glance, information about post-download actions should be carried by Request and actioned in DownloaderService when downloader signals completion or failure.

However, it's pretty pointless to put those hardcoded values here, so I'll add an overloaded constructor for this case.

Copy link
Collaborator Author

@ezaquarii ezaquarii Jun 4, 2020

Choose a reason for hiding this comment

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

@tobiasKaminsky Let me know if you'd like to migrate more use cases to the downloader as part of this PR - in such case, we'll need to add this functionality. If this is not the case, I think we can skip it. I'm confident that the design is ready to support this feature with minimal effort when we decide migrate preview functionality to the new API.

String downloadedRemotePath = download.getRequest().getFile().getRemotePath();
FileDataStorageManager storageManager = new FileDataStorageManager(user.toPlatformAccount(),
activity.getContentResolver());
ocFile = storageManager.getFileByPath(downloadedRemotePath);
Copy link
Member

Choose a reason for hiding this comment

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

With the new approach, is this roundtrip via FDSM needed, as we already have the OCFile?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OCFile in download status is part of the Request, so it's not updated.

DownloadStatus does not carry result OCFile, but this can be changed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@tobiasKaminsky Downloaded OCFile is now exposed in the download status.

@tobiasKaminsky tobiasKaminsky force-pushed the ezaquarii/new-download-manager branch from 70ab37d to a3cba24 Compare May 28, 2020 09:46
@tobiasKaminsky
Copy link
Member

This is just amazing! This is a giant step forward!
Thanks Chris for all your work ❤️ ❤️ ❤️ ❤️

@tobiasKaminsky
Copy link
Member

@ezaquarii unit tests with TestEtmViewModel fail…

@ezaquarii ezaquarii force-pushed the ezaquarii/new-download-manager branch 2 times, most recently from 71f398f to aba9554 Compare July 16, 2020 21:49
@ezaquarii ezaquarii requested a review from tobiasKaminsky July 16, 2020 21:50
@ezaquarii
Copy link
Collaborator Author

@tobiasKaminsky test should be ok now

@nextcloud-android-bot
Copy link
Collaborator

APK file: https://www.kaminsky.me/nc-dev/android-artifacts/14970.apk

qrcode

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.

@nextcloud-android-bot
Copy link
Collaborator

@nextcloud-android-bot
Copy link
Collaborator

@nextcloud-android-bot
Copy link
Collaborator

Codacy

396

Lint

TypemasterPR
Warnings8384
Errors00

SpotBugs (new)

Warning TypeNumber
Bad practice Warnings27
Correctness Warnings61
Internationalization Warnings9
Multithreaded correctness Warnings9
Performance Warnings73
Security Warnings44
Dodgy code Warnings106
Total329

SpotBugs (master)

Warning TypeNumber
Bad practice Warnings27
Correctness Warnings61
Internationalization Warnings9
Multithreaded correctness Warnings9
Performance Warnings73
Security Warnings44
Dodgy code Warnings106
Total329

Lint increased!

@ezaquarii ezaquarii force-pushed the ezaquarii/new-download-manager branch from aba9554 to d20e246 Compare July 17, 2020 07:17
@nextcloud-android-bot
Copy link
Collaborator

APK file: https://www.kaminsky.me/nc-dev/android-artifacts/14986.apk

qrcode

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.

@nextcloud-android-bot
Copy link
Collaborator

Codacy

396

Lint

TypemasterPR
Warnings8384
Errors00

SpotBugs (new)

Warning TypeNumber
Bad practice Warnings27
Correctness Warnings61
Internationalization Warnings9
Multithreaded correctness Warnings9
Performance Warnings73
Security Warnings44
Dodgy code Warnings106
Total329

SpotBugs (master)

Warning TypeNumber
Bad practice Warnings27
Correctness Warnings61
Internationalization Warnings9
Multithreaded correctness Warnings9
Performance Warnings73
Security Warnings44
Dodgy code Warnings106
Total329

Lint increased!

@nextcloud-android-bot
Copy link
Collaborator

APK file: https://www.kaminsky.me/nc-dev/android-artifacts/14990.apk

qrcode

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.

@nextcloud-android-bot
Copy link
Collaborator

Codacy

396

Lint

TypemasterPR
Warnings8384
Errors00

SpotBugs (new)

Warning TypeNumber
Bad practice Warnings27
Correctness Warnings61
Internationalization Warnings9
Multithreaded correctness Warnings9
Performance Warnings73
Security Warnings44
Dodgy code Warnings106
Total329

SpotBugs (master)

Warning TypeNumber
Bad practice Warnings27
Correctness Warnings61
Internationalization Warnings9
Multithreaded correctness Warnings9
Performance Warnings73
Security Warnings44
Dodgy code Warnings106
Total329

Lint increased!

ezaquarii and others added 2 commits July 20, 2020 12:28
Signed-off-by: Chris Narkiewicz <hello@ezaquarii.com>
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
@tobiasKaminsky tobiasKaminsky force-pushed the ezaquarii/new-download-manager branch from d20e246 to 5a9d578 Compare July 20, 2020 10:29
@nextcloud-android-bot
Copy link
Collaborator

APK file: https://www.kaminsky.me/nc-dev/android-artifacts/15024.apk

qrcode

To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.

@nextcloud-android-bot
Copy link
Collaborator

Codacy

396

Lint

TypemasterPR
Warnings8383
Errors00

SpotBugs (new)

Warning TypeNumber
Bad practice Warnings27
Correctness Warnings61
Internationalization Warnings9
Multithreaded correctness Warnings9
Performance Warnings73
Security Warnings44
Dodgy code Warnings106
Total329

SpotBugs (master)

Warning TypeNumber
Bad practice Warnings27
Correctness Warnings61
Internationalization Warnings9
Multithreaded correctness Warnings9
Performance Warnings73
Security Warnings44
Dodgy code Warnings106
Total329

@tobiasKaminsky tobiasKaminsky merged commit 4f97110 into master Jul 20, 2020
@delete-merged-branch delete-merged-branch bot deleted the ezaquarii/new-download-manager branch July 20, 2020 10:52
@AndyScherzinger AndyScherzinger added this to the Nextcloud App 3.13.0 milestone Jul 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants