Skip to content
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 Service 4 Alpha 2 #463

Closed
gotev opened this issue Oct 20, 2019 · 5 comments
Closed

Upload Service 4 Alpha 2 #463

gotev opened this issue Oct 20, 2019 · 5 comments
Assignees
Labels
request for comments Need for comments and suggestions from the community
Milestone

Comments

@gotev
Copy link
Owner

gotev commented Oct 20, 2019

Fasten your seatbelt, Upload Service 4 Alpha 02 is here! #450 checkmarks reflects what you will find in this version. Use this issue to provide feedback about this release.

BEAR IN MIND THIS IS A DEVELOPER PREVIEW AND NOT PRODUCTION GRADE VERSION. IF YOU NEED SOMETHING BATTLE TESTED FOR PRODUCTION, USE 3.5.2 UNTIL 4.0.0 IS CERTIFIED STABLE.

For those who wants to experience the new Upload Service, here we go. Upgrade your Gradle (by adding only the artifacts you need):

def uploadServiceVersion = "4.0.0-alpha02"
implementation "net.gotev:uploadservice:$uploadServiceVersion"
implementation "net.gotev:uploadservice-okhttp:$uploadServiceVersion"
implementation "net.gotev:uploadservice-ftp:$uploadServiceVersion"

Docs are not ready yet because APIs have not reached their final form and may be changed based on feedback, but the demo app example is fully functional and can be used as reference. If you're coming from 3.x version, prepare a cup of coffee and enter in refactoring mood.

Feel free to tell here anything about this version.

Changelog since 4.0 Alpha 1

Implemented delegates substitutes. Example usage:

@Override
public void onDone(String httpMethod, String serverUrl, UploadItemUtils uploadItemUtils) {
try {
final String uploadId = UUID.randomUUID().toString();
final MultipartUploadRequest request =
new MultipartUploadRequest(this, serverUrl)
.setUploadID(uploadId)
.setMethod(httpMethod)
.setNotificationConfig(getNotificationConfig(uploadId, R.string.multipart_upload))
.setMaxRetries(MAX_RETRIES)
//.setAutoDeleteFilesAfterSuccessfulUpload(true)
//.setCustomUserAgent(getUserAgent())
.setUsesFixedLengthStreamingMode(FIXED_LENGTH_STREAMING_MODE);
uploadItemUtils.forEach(new UploadItemUtils.ForEachDelegate() {
@Override
public void onHeader(UploadItem item) {
try {
request.addHeader(item.getTitle(), item.getSubtitle());
} catch (IllegalArgumentException exc) {
Toast.makeText(MultipartUploadActivity.this,
exc.getMessage(), Toast.LENGTH_LONG).show();
}
}
@Override
public void onParameter(UploadItem item) {
request.addParameter(item.getTitle(), item.getSubtitle());
}
@Override
public void onFile(UploadItem item) {
try {
request.addFileToUpload(item.getSubtitle(), item.getTitle());
} catch (IOException exc) {
Toast.makeText(MultipartUploadActivity.this,
getString(R.string.file_not_found, item.getSubtitle()),
Toast.LENGTH_LONG).show();
}
}
});
request.subscribe(this, new RequestObserverDelegate() {
@Override
public void onProgress(@NotNull Context context, @NotNull UploadInfo uploadInfo) {
Log.e("LIFECYCLE", "Progress " + uploadInfo.getProgressPercent());
}
@Override
public void onSuccess(@NotNull Context context, @NotNull UploadInfo uploadInfo, @NotNull ServerResponse serverResponse) {
Log.e("LIFECYCLE", "Success " + uploadInfo.getProgressPercent());
}
@Override
public void onError(@NotNull Context context, @NotNull UploadInfo uploadInfo, @NotNull Throwable exception) {
Log.e("LIFECYCLE", "Error " + exception.getMessage());
}
@Override
public void onCompleted(@NotNull Context context, @NotNull UploadInfo uploadInfo) {
Log.e("LIFECYCLE", "Completed ");
finish();
}
@Override
public void onCompletedWhileNotObserving() {
Log.e("LIFECYCLE", "Completed while not observing");
finish();
}
});
} catch (Exception exc) {
Toast.makeText(this, exc.getMessage(), Toast.LENGTH_LONG).show();
}
}

This uses the newly introduced RequestObserverDelegate:

interface RequestObserverDelegate {
/**
* Called when the upload progress changes.
*
* @param context context
* @param uploadInfo upload status information
*/
fun onProgress(context: Context, uploadInfo: UploadInfo)
/**
* Called when the upload is completed successfully.
*
* @param context context
* @param uploadInfo upload status information
* @param serverResponse response got from the server
*/
fun onSuccess(context: Context, uploadInfo: UploadInfo, serverResponse: ServerResponse)
/**
* Called when an error happens during the upload.
*
* @param context context
* @param uploadInfo upload status information
* @param exception exception that caused the error
*/
fun onError(context: Context, uploadInfo: UploadInfo, exception: Throwable)
/**
* Called when the upload is completed wither with success or error.
*
* @param context context
* @param uploadInfo upload status information
*/
fun onCompleted(context: Context, uploadInfo: UploadInfo)
/**
* Called only when listening to a single upload ID and you register the request observer,
* but the upload ID is not present in UploadService's task list, meaning it has completed.
* In this case, you cannot know with which state it finished (success or error).
*
* Useful when used in activities and the following scenario applies:
* - user triggers an upload in an activity which shows the progress
* - user navigates away from that activity and comes back later after the upload completed and
* you need to to some stuff to adjust UI properly
*/
fun onCompletedWhileNotObserving()
}

Now the delegate is automatically wrapped inside a standard BroadcastReceiver, called RequestObserver which is lifecycle aware (using AndroidX lifecycle) and registers and unregisters itself automatically. As a bonus, you can also intercept the case when you get back to the activity from background and the observed task already completed.

@ChristinaGit
Copy link

Sorry for my bad English.

I checked the current version from the master branch. And as I understand, there is still no way to localize notifications content and placeholders.

As I can see in

fun replace(string: String?, uploadInfo: UploadInfo): String {
val safeString = string ?: return ""
return safeString.replace(ELAPSED_TIME, uploadInfo.elapsedTimeString)
.replace(PROGRESS, "${uploadInfo.progressPercent}%")
.replace(UPLOAD_RATE, uploadInfo.uploadRateString)
.replace(UPLOADED_FILES, uploadInfo.successfullyUploadedFiles.toString())
.replace(TOTAL_FILES, uploadInfo.files.size.toString())
}

you use wrong way to format values.

toString() and kotlin's {} do not use locale to format numbers. You should use Java's String.format or java.text.NumberFormat since different locales use different group/decimal separators (even for integers).

Also % sign, Mb/s, etc. usually should be separated by space or even &nbsp from number value.

elapsedTimeString and uploadRateString simply contain hardcoded text that cannot be localized (like Mb/s).

I read wiki, but I still don’t understand whether these values can be localized.

Because of this, I can not use placeholders (-> no usefull information), because non-localized text does not look professional.

Forgive me if I didn’t find something, and this can be done.

@gotev
Copy link
Owner Author

gotev commented Oct 22, 2019

Hi there @ChristinaGit and thanks for the detailed explanation!

Currently, to use localised messages, you can do something like the following (extracted from the demo app).

strings.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="uploading">Uploaded [[UPLOADED_FILES]] of [[TOTAL_FILES]] at [[UPLOAD_RATE]] - [[PROGRESS]]</string>
    <string name="upload_success">Upload completed successfully in [[ELAPSED_TIME]]</string>
    <string name="upload_error">Error while uploading</string>
    <string name="upload_cancelled">Upload has been cancelled</string>
</resources>
 protected UploadNotificationConfig getNotificationConfig(final String uploadId, @StringRes int title) {
    UploadNotificationConfig config = new UploadNotificationConfig(App.CHANNEL);

    config.getProgress().setMessage(getString(R.string.uploading));
    config.getProgress().setIconResourceID(R.drawable.ic_upload);
    config.getProgress().setIconColorResourceID(Color.BLUE);
    config.getProgress().getActions().add(new UploadNotificationAction(
            R.drawable.ic_cancelled,
            getString(R.string.cancel_upload),
            NotificationActions.getCancelUploadAction(this, 1, uploadId)));

    config.getCompleted().setMessage(getString(R.string.upload_success));
    config.getCompleted().setIconResourceID(R.drawable.ic_upload_success);
    config.getCompleted().setIconColorResourceID(Color.GREEN);

    config.getError().setMessage(getString(R.string.upload_error));
    config.getError().setIconResourceID(R.drawable.ic_upload_error);
    config.getError().setIconColorResourceID(Color.RED);

    config.getCancelled().setMessage(getString(R.string.upload_cancelled));
    config.getCancelled().setIconResourceID(R.drawable.ic_cancelled);
    config.getCancelled().setIconColorResourceID(Color.YELLOW);

    return config;
}

Example usage

final BinaryUploadRequest request = new BinaryUploadRequest(this, serverUrl)
                    .setUploadID(uploadId)
                    .setMethod(httpMethod)
                    .setNotificationConfig(getNotificationConfig(uploadId, R.string.binary_upload))
                    .setMaxRetries(MAX_RETRIES)
                    .setUsesFixedLengthStreamingMode(FIXED_LENGTH_STREAMING_MODE);

However, contents of Placeholders are not localisable as you pointed out. I'd love to see a PR with your additions to make them truly localisable!

@ChristinaGit
Copy link

I may not have been able to do the localization, but I have a couple more suggestions that I would like to mention.

HttpStack for demo

I think many will want to see how notifications and callbacks works. But making real requests to a real server (event for local) looks too complicated.

So I think it's worth mentioning on the wiki page, or even adding an implementation of HttpStack into the library for UI tests and demonstration purposes.

I did this for the old version. Don't know how to do it for a new one.

class TestHttpStack(
    private val _uploadingTime: Long,
    private val _shouldThrowError: (String?) -> Boolean
) : HttpStack {
    override fun createNewConnection(
        method: String?,
        url: String?
    ): HttpConnection = object : HttpConnection {
        override fun getResponse(delegate: HttpConnection.RequestBodyDelegate?): ServerResponse {
            Thread.sleep(_uploadingTime)

            return if (!_shouldThrowError(url)) {
                ServerResponse(200, "".toByteArray(Charsets.UTF_8), null)
            } else {
                ServerResponse(400, "".toByteArray(Charsets.UTF_8), null)
            }
        }

        override fun setTotalBodyBytes(
            totalBodyBytes: Long,
            isFixedLengthStreamingMode: Boolean
        ): HttpConnection = this

        override fun setHeaders(requestHeaders: MutableList<NameValue>?): HttpConnection = this
        override fun close() {
        }
    }
}

Custom thread pool

Many libraries use their own thread pools.
At the moment, I have already 4 of them in my current project:

  1. android.os.AsyncTask.THREAD_POOL_EXECUTOR
  2. RxJava IO thread pool
  3. GlideExecutor from Glide
  4. AsyncDifferConfig.mBackgroundThreadExecutor from PagedLibrary.

And I would like to avoid increasing their number. Perhaps you should add the ability to specify custom thread pool, not just its size.

@gotev
Copy link
Owner Author

gotev commented Oct 25, 2019

@ChristinaGit this is exactly the purpose of this issue.

Docs are not ready yet for lack of time, however there are 2 out of the box implementations ready to be used (exactly as previous versions). First one is based in HttpUrlConnection and is the default if you don't override anything. Second one needs the okhttp artifact and is as easy as OkHttpStack(yourOkHttpInstance). It's actually what I recommend, so you have a single OkHttpClient in your app (I use the same for Retrofit, UploadService, Image loaders)

Testing is a good point, because tests are lacking in current implementation. For convenience and acquired knowledge, I would use OkHttp together with its MockWebServer which is easy, stable and already used by many people. Your example may be part of a new uploadservice-test package which serves only as testImplementation and accomodates unit testing.

Custom ThreadPool is another good point. Definitely going to add it!

gotev added a commit that referenced this issue Oct 29, 2019
This was referenced Oct 29, 2019
@gotev
Copy link
Owner Author

gotev commented Oct 29, 2019

Alpha 2 stage is now complete, Alpha 3 is now open #465

@gotev gotev closed this as completed Oct 29, 2019
@gotev gotev unpinned this issue Oct 29, 2019
@gotev gotev changed the title Upload Service 4 Alpha 2 Feedback Upload Service 4 Alpha 2 Oct 29, 2019
@gotev gotev added the request for comments Need for comments and suggestions from the community label Oct 29, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
request for comments Need for comments and suggestions from the community
Projects
None yet
Development

No branches or pull requests

2 participants