-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upload folder support #4859
Upload folder support #4859
Conversation
cc2a9f4
to
a5803c2
Compare
Codecov Report
@@ Coverage Diff @@
## master #4859 +/- ##
============================================
- Coverage 17.71% 17.58% -0.14%
Complexity 3 3
============================================
Files 384 383 -1
Lines 32650 32624 -26
Branches 4597 4611 +14
============================================
- Hits 5785 5736 -49
- Misses 25942 25974 +32
+ Partials 923 914 -9
|
7eefd80
to
fc718c8
Compare
IT test failed: https://www.kaminsky.me/nc-dev/android-integrationTests/11695 |
91a2213
to
88ecc91
Compare
IT test failed: https://www.kaminsky.me/nc-dev/android-integrationTests/11701 |
IT test fail might be unrelated, the exact same error has happened before on a completely different PR: https://kaminsky.me/nc-dev/android-integrationTests/10775/index.html |
hi @kilves, first of all welcome to the Nextcloud Android client project and thank for taking an interest in it and for picking up this task ❤️ Tests are actually a bit flaky, so no worries. Can you do a rebase against the latest master please. See #4859 (comment) which shows an increase in linbt warnings (breaking the build) which might be due to the not-up-to-date master branch of yours (or needs fixing, hard to tell right now). The rebase would also remove all changes not made within this PR itself 😃 cc @tobiasKaminsky for review |
Also cc @ezaquarii for review :) |
88ecc91
to
805a236
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for dry tone, but I'm finishing my train journey and wanted to wrap it up fast, fast, fast. :)
Anyway, we have quite some things to improve.
Also, would be it be possible to extract all of this logic from Activity
into a separate class and put under unit test? We're suffering from logic mothballing in UI components and that puts a lot of strain on us already so we try to improve the quality rather aggressively.
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java
Outdated
Show resolved
Hide resolved
src/main/java/com/owncloud/android/ui/asynctasks/CheckAvailableSpaceTask.java
Outdated
Show resolved
Hide resolved
Changes in upload menu: * User is now able to select files on different folders at the same time * Made local folders selectable (long-press to select) * When local folder is selected, all files and subfolders are also selected * When user presses upload, app replicates directory structure to upload target (from the point of view of currently open local folder, or common ancestor folder if impossible) * Before upload starts, app automatically creates necessary folders to remote Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
Signed-off-by: Petri <petri.hetta@dicode.fi>
805a236
to
6c3088a
Compare
Signed-off-by: Petri <petri.hetta@dicode.fi>
6c3088a
to
4f50574
Compare
APK file: https://www.kaminsky.me/nc-dev/android-artifacts/11787.apk |
Codacy333Lint
SpotBugs (new)
SpotBugs (master)
SpotBugs increased! |
Ok, so I did some refactoring and the code should be more readable now. I also moved the logic to separate classes. I'm not sure about unit tests in this case. The code is still largely dependent on the existence of a context, because you still need intents to create folders before uploading files. So it would really be an integration test. I can write some though if they are really necessary. I'm also open to suggestions about |
|
||
requester.uploadNewFile( | ||
fileActivity, | ||
account, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not create Account
variables.
We're running a continuous refactoring effort where all calls to toPlatformAccount()
are being chased. Because we have tons of it, it's much easier to browse the analysis report if it's called in point of use rather than calls being coalesced via temporary variables, as IDE can tell us how many items we have in each class.
For the record, toPlaformAccount()
is being retired as we speak/write, it just takes time.
requester.uploadNewFile(..., user.toPlaformAccount(), ...) // makes our refactoring easier
|
||
public class FilesUploader { | ||
|
||
private FileActivity fileActivity; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see any Activity
APIs being used, so we shall not depend on it.
intent.putExtra(OperationsService.EXTRA_ACCOUNT, user.toPlatformAccount()); | ||
intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, folder); | ||
intent.putExtra(OperationsService.EXTRA_CREATE_FULL_PATH, true); | ||
fileActivity.getOperationsServiceBinder().queueNewOperation(intent); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few issues here:
- we inject
FileActivity
only to call a getter. - 2 statements per line
We shall inject the OperationsServiceBinder
instead.
String[] remotePaths = getRemoteTargetPaths(remoteTargets).toArray(new String[]{}); | ||
|
||
requester.uploadNewFile( | ||
fileActivity, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context
is needed here.
@@ -286,6 +288,7 @@ protected void onCreate(Bundle savedInstanceState) { | |||
} | |||
|
|||
mPlayerConnection = new PlayerServiceConnection(this); | |||
filesUploader = new FilesUploader(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's constructor-inject all dependencies:
Context
fromgetApplicationContext()
OperationsServiceBinder
* @param user current user | ||
* @return true if this operation was consumed, false otherwise | ||
*/ | ||
public boolean onCreateFolderOperationFinish(CreateFolderOperation operation, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name (and I know it's a delegate from legacy code, so no blames) suggests that we're creating some mysterious folder operation finish (a finish event?).
JavaDoc tells that it's onFinish<what/noun>
callback.
Let's rename both methods.
false | ||
); | ||
private void uploadFromFileSystem(List<File> files, int resultCode) { | ||
filesUploader.uploadFromFileSystem(files, resultCode, null, getCurrentDir(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When args list is too long, we make it vertical to boost readability.
break; | ||
} | ||
String replicationStartDir = data.getStringExtra(UploadFilesActivity.EXTRA_REPLICATION_START_DIR); | ||
filesUploader.uploadFromFileSystem(files, resultCode, replicationStartDir, getCurrentDir(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When args list is too long, we make it vertical to boost readability.
String targetPath = targetDirectory.getAbsolutePath(); | ||
String currentPath = currentDirectory.getAbsolutePath(); | ||
if (targetPath.startsWith(currentPath)) { | ||
return new File(targetPath.replace(targetPath.replace(currentPath, ""), "")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's generally bad practice to have more than 1 statement per line.
We have 3 here.
return false; | ||
} | ||
|
||
private List<UploadTarget> getRemoteTargets(@NonNull List<File> localFiles, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Javadoc pls
@@ -41,6 +43,15 @@ public CheckAvailableSpaceTask(CheckAvailableSpaceListener callback, String... p | |||
this.callback = callback; | |||
} | |||
|
|||
public CheckAvailableSpaceTask(CheckAvailableSpaceListener callback, Collection<File> files) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be beneficial to kill the legacy constructor that uses String[]
. Is it feasible?
private List<File> localFiles = new ArrayList<>(); | ||
private int resultCode; | ||
|
||
public static class UploadTarget { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Kotlin, this would be 1 line data class. :)
* @param currentDir the directory user currently is in | ||
* @param user current user | ||
*/ | ||
public void uploadFromFileSystem(List<File> localFiles, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
JavaDoc tells me it's places a request.
Name tells me it uploads.
Shall we bring the function name on par with the description?
@ezaquarii did you delete the long comment chain about javadocs and refactoring? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kilves, first of all, I'd like to thank you for the effort taken to bring this feature forward, taking stale PR, and for the refactoring you've done. Before I move forward, please know that you have choosen to work on the most challenging part of the application.
For the time being, let's ignore my inline comments that I put in the code. While I was going through the code line by line over past hours, I realized that we have some fundamental issues to be addressed before we sink more time into marginal code improvements. I'll leave those comments in case we find it useful at some point, but please ignore them for now.
So far (and please correct me if I'm wrong), this is how I see the situation:
- upload is done in the background by a
FileUploader
service (subclass ofandroid.app.Service
) - the service has a sneaky backdoor in form of the
FileUploader.Requester
(that is in fact an intentBuilder
cross-bred with some sort ofServiceConnection
concept) FilesUploader
is a stateful wrapper around this backdoor intoFileUploader
FileDisplayActivity.requestUploadOfFilesFromFileSystem()
is a fire-and-forget enqueue method that uses that builder to enqueue uploads into service in UI-independent way
Here are my initial concerns:
- We introduce new stateful
FilesUploader
concept that is a wrapper around a statelessRequester
backdoor into theFileUploader
service - This
FilesUploader
genuinely just builds a request that is then sent to the service, so it's more of aBuilder
kind of thing again... - ...except that this time the request is partial and continuation is driven by callbacks and we changed queue push operation (
requestUploadOfFilesFromFileSystem
) into a state machine living in the UI, tying it to an unpredictableActivity
lifecycle - for a large folder, partial traversal is still done on UI thread, which means blocking I/O (although it's split into smallers operations, so this might event work for humanely sized folders).
Overall, it looks to me like this design is not going to work for us. The upload driving logicsimply does not belong here (UI) and I'm not surprised that the old author abandoned it. Your refactoring made the code readable to a point where we can actually understand how difficult the situation is, so good work done here. Having said that, I still believe this PR is something we should rethink and get back to the whiteboard.
Before we get too depressed, I'd like to say that the FilesUploader
logic is probably salvageable and I see 3 avenues:
Variant 1a - in ui, stateless
Remove the callbacks and make it stateless - construct list of files to upload by recursively traversing the folder, fire-and-forget gigantic request, respecting old design rules. In this variant we probably want to extend FileUpload.Requester
with another method that builds fancy Intent
with large list of files.
Pros:
- simple(r)
Cons:
- probably won't work with big folders as input size is unbounded
- traversal on UI thread is not feasible due to blocking I/O and ANRs
Variant 1b - in service, stateless
Same as 1a, except that FileUpload
(Service
) will receive a folder path and perform the traversal to construct the list of files to upload in background. I'm not sure if it does not clash with the funky FileDisplayActivity.onActivityResult(...) -> requestUploadOfFilesFromFileSystem(...)
logic, as there is a lot of if
-ology there that I don't fully understand, but at first glance it looks like it just handles externally fired Intent
Pros:
- traversal can be probably done in background easily
Cons:
- not sure, but it looks like a refactoring rabbit hole
- still in UI, which is wrong
Variang 2 - in service, stateful
If we can't have a some sort of quasi-functional solution (because of backend round-trips for example, or we need to check things between uploads), FilesUploader-the-wrapper
should live inside FileUploader-the-service
(with a different name of course), possibly as a properly encoded state machine.
Pros:
- service is the place where this should live actually
Cons:
- basically a rewrite
- service is already suffering from high complexity - this avenue probably induces refactoring rabbit hole around this component
@kilves @tobiasKaminsky @AndyScherzinger IMO this is really tough problem and we should get back to the drawing board.
Sorry, it looks like my review was published before I finished it. This one is final. I moved this comment from attached-to-source-line to a standalone comment, as it mothballed into a summary. :) |
@ezaquarii i totally agree with your analysis, but fear that there is (at least from my side) no time in nearer future to refactor this. So I would go for 1b as a quick win (then we also do not have Android warning if UI takes too long) and we can also add this enhancement. Meanwhile we still can & should discuss how to refactor this (in a new issue) with lots of smaller steps, which can be easily done and tested. |
I'd also say for now 1b seems to be feasable given the circumstances. |
I switched label back to "1. developing", please change it once it is ready for review |
superseeded by #7877 |
#4505 develeper stopped updating his PR, so here's an alternate solution that should implement the requirements mentioned in old PR.
Fixes #184