package io.filepicker.services; import android.app.IntentService; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.util.Log; import android.webkit.MimeTypeMap; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import de.greenrobot.event.EventBus; import io.filepicker.api.FpApiClient; import io.filepicker.events.ApiErrorEvent; import io.filepicker.events.FileExportedEvent; import io.filepicker.events.FpFilesReceivedEvent; import io.filepicker.events.GotContentEvent; import io.filepicker.events.UploadFileErrorEvent; import io.filepicker.events.UploadProgressEvent; import io.filepicker.models.FPFile; import io.filepicker.models.Folder; import io.filepicker.models.Node; import io.filepicker.models.UploadLocalFileResponse; import io.filepicker.utils.FilesUtils; import io.filepicker.utils.Utils; import java.util.concurrent.CountDownLatch; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; /** * An {@link IntentService} subclass for handling asynchronous task requests in * a service on a separate handler thread. * <p> */ public class ContentService extends IntentService { private static final String LOG_TAG = ContentService.class.getSimpleName(); private static final String ACTION_GET_CONTENT = "io.filepicker.services.action.get_content"; private static final String ACTION_UPLOAD_FILE = "io.filepicker.services.action.upload_file"; private static final String ACTION_PICK_FILES = "io.filepicker.services.action.pick_files"; private static final String ACTION_EXPORT_FILE = "io.filepicker.services.action.export_file"; private static final String EXTRA_BACK_PRESSED = "io.filepicker.services.extra.back_pressed"; private static final String EXTRA_NODE = "io.filepicker.services.extra.node"; private static final String EXTRA_FILENAME = "io.filepicker.services.extra.filename"; // Used for upload file action and uri looks like content://<path to local file> private static final String EXTRA_FILE_URI = "io.filepicker.services.extra.file_uri"; private static final int RETRY_COUNT = 10; private static final int RETRY_INTERVAL = 2000; FilepickerListener filepickerListener; public interface FilepickerListener { void onLocalFileUploaded(List<FPFile> files); } public ContentService() { super("ContentService"); } public static void getContent(Context context, Node node, boolean backPressed) { if (context == null) { return; } Intent intent = new Intent(context, ContentService.class); intent.setAction(ACTION_GET_CONTENT); intent.putExtra(EXTRA_NODE, node); intent.putExtra(EXTRA_BACK_PRESSED, backPressed); context.startService(intent); } public static void pickFiles(Context context, ArrayList<Node> files) { if (context == null) { return; } Intent intent = new Intent(context, ContentService.class); intent.setAction(ACTION_PICK_FILES); intent.putParcelableArrayListExtra(EXTRA_NODE, files); context.startService(intent); } public static void uploadFile(Context context, Uri fileUri) { if (context == null) { return; } Intent intent = new Intent(context, ContentService.class); intent.setAction(ACTION_UPLOAD_FILE); intent.putExtra(EXTRA_FILE_URI, fileUri); context.startService(intent); } public static void exportFile(Context context, Node node, Uri fileUri, String filename) { if (context == null) { return; } Intent intent = new Intent(context, ContentService.class); intent.setAction(ACTION_EXPORT_FILE); intent.putExtra(EXTRA_NODE, node); intent.putExtra(EXTRA_FILENAME, filename); intent.putExtra(EXTRA_FILE_URI, fileUri); context.startService(intent); } public static void cancelAll() { FpApiClient.cancelAll(); } @Override protected void onHandleIntent(Intent intent) { if (intent != null) { final String action = intent.getAction(); Node node; switch (action) { case ACTION_GET_CONTENT: node = intent.getParcelableExtra(EXTRA_NODE); boolean backPressed = intent.getBooleanExtra(EXTRA_BACK_PRESSED, false); handleActionGetContent(node, backPressed); break; case ACTION_UPLOAD_FILE: Uri uri = intent.getParcelableExtra(EXTRA_FILE_URI); handleActionUploadFile(uri); break; case ACTION_PICK_FILES: ArrayList<Node> files = intent.getParcelableArrayListExtra(EXTRA_NODE); handleActionPickFiles(files); break; case ACTION_EXPORT_FILE: node = intent.getParcelableExtra(EXTRA_NODE); String filename = intent.getStringExtra(EXTRA_FILENAME); Uri fileUri = intent.getParcelableExtra(EXTRA_FILE_URI); handleActionExportFile(node, fileUri, filename); break; default: break; } } } private void handleActionGetContent(Node node, final boolean backPressed) { FpApiClient.getFpApiClient(this) .getFolder(node.linkPath, "info", FpApiClient.getJsSession(this)) .enqueue(new Callback<Folder>() { @Override public void onResponse(Call<Folder> call, Response<Folder> response) { if (response.isSuccessful()) { EventBus.getDefault().post(new GotContentEvent(response.body(), backPressed)); } else { handleApiError(getErrorType(response)); } } @Override public void onFailure(Call<Folder> call, Throwable throwable) { handleApiError(ApiErrorEvent.ErrorType.UNKNOWN_ERROR); } }); } private void handleActionPickFiles(ArrayList<Node> nodes) { final ArrayList<FPFile> results = new ArrayList<>(); try { final CountDownLatch countDownLatch = new CountDownLatch(nodes.size()); for (Node node : nodes) { FpApiClient.getFpApiClient(this) .pickFile(URLDecoder.decode(node.linkPath, "utf-8"), "fpurl", FpApiClient.getJsSession(this)) .enqueue(new Callback<FPFile>() { @Override public void onResponse(Call<FPFile> call, Response<FPFile> response) { if (response.isSuccessful()) { results.add(response.body()); } else { Log.w(LOG_TAG, "Error: " + response.code()); } countDownLatch.countDown(); } @Override public void onFailure(Call<FPFile> call, Throwable throwable) { Log.e(LOG_TAG, "Error", throwable); countDownLatch.countDown(); } }); } countDownLatch.await(); if (results.isEmpty()) { Log.e(LOG_TAG, "handleActionPickFiles: Failed to retrieve FpFiles from the response"); handleApiError(ApiErrorEvent.ErrorType.UNKNOWN_ERROR); } else { EventBus.getDefault().post(new FpFilesReceivedEvent(results)); } } catch (Exception syntaxException) { handleApiError(ApiErrorEvent.ErrorType.WRONG_RESPONSE); } } private void handleActionUploadFile(final Uri uri) { ApiErrorEvent.ErrorType errorType = null; RequestBody requestBody = null; String filePath = FilesUtils.getPath(this, uri); if (filePath == null) { handleUploadFileError(uri, ApiErrorEvent.ErrorType.INVALID_FILE); return; } waitForFile(filePath); try { requestBody = FilesUtils.getRequestBodyFromUri(this, filePath, uri); } catch (SecurityException e) { errorType = ApiErrorEvent.ErrorType.LOCAL_FILE_PERMISSION_DENIAL; } if (requestBody == null && errorType == null) { errorType = ApiErrorEvent.ErrorType.INVALID_FILE; } if (errorType != null) { handleUploadFileError(uri, errorType); return; } File file = new File(filePath); requestBody = new ProgressRequestBody(file, uri.getPath(), new ProgressRequestBody.Listener() { @Override public void onProgress(float progress) { EventBus.getDefault().post(new UploadProgressEvent(uri, progress)); } }); FpApiClient.getFpApiClient(this).uploadFile( Utils.getUploadedFilename(file.getName()), FpApiClient.getJsSession(this), requestBody ).enqueue(uploadLocalFileCallback(uri)); } private static void waitForFile(String filePath) { int retires = RETRY_COUNT; while (new File(filePath).length() == 0 && retires-- > 0) { try { Thread.sleep(RETRY_INTERVAL); } catch (InterruptedException e) { Log.e(LOG_TAG, "Error while waiting for the local file", e); } } } private Callback<UploadLocalFileResponse> uploadLocalFileCallback(final Uri uri) { return new Callback<UploadLocalFileResponse>() { @Override public void onResponse(Call<UploadLocalFileResponse> call, Response<UploadLocalFileResponse> response) { if (response.isSuccessful()) { onFileUploadSuccess(response.body(), uri); } else { handleUploadFileError(uri, getErrorType(response)); } } @Override public void onFailure(Call<UploadLocalFileResponse> call, Throwable throwable) { handleUploadFileError(uri, ApiErrorEvent.ErrorType.UNKNOWN_ERROR); } }; } private void onFileUploadSuccess(UploadLocalFileResponse response, Uri fileUri) { final FPFile fpFile = response.parseToFpFile(); if (fpFile != null) { fpFile.setLocalPath(fileUri.toString()); ArrayList<FPFile> fpFiles = new ArrayList<>(); fpFiles.add(fpFile); EventBus.getDefault().post(new FpFilesReceivedEvent(fpFiles)); } else { Log.e(LOG_TAG, "onFileUploadSuccess: Failed to retrieve FpFile from the response"); handleApiError(ApiErrorEvent.ErrorType.UNKNOWN_ERROR); } } /** Exports file to service node - destination node fileUri - uri to file on device filename - new filename given by user */ private void handleActionExportFile(Node node, Uri fileUri, String filename) { String fileExtension = FilesUtils.getFileExtension(this, fileUri); final String path = FilesUtils.getFilePath(node, filename, fileExtension); RequestBody content = FilesUtils.buildRequestBody(this, fileUri); FpApiClient.getFpApiClient(this) .exportFile(path, FpApiClient.getJsSession(this), content) .enqueue(new Callback<FPFile>() { @Override public void onResponse(Call<FPFile> call, Response<FPFile> response) { if (response.isSuccessful()) { EventBus.getDefault().post(new FileExportedEvent(path, response.body())); Log.d(LOG_TAG, "success"); } else { Log.d(LOG_TAG, "failure"); } } @Override public void onFailure(Call<FPFile> call, Throwable throwable) { Log.e(LOG_TAG, "failure", throwable); } }); } private void handleApiError(ApiErrorEvent.ErrorType errorType) { EventBus.getDefault().post(new ApiErrorEvent(errorType)); } private void handleUploadFileError(Uri uri, ApiErrorEvent.ErrorType errorType) { EventBus.getDefault().post(new UploadFileErrorEvent(uri, errorType)); } public ApiErrorEvent.ErrorType getErrorType(Response error) { if (error != null) { return ApiErrorEvent.ErrorType.UNAUTHORIZED; } return ApiErrorEvent.ErrorType.UNKNOWN_ERROR; } private static class ProgressRequestBody extends RequestBody { private static final int BUFFER_SIZE = 4096; private File mFile; private String mPath; private final Listener mListener; public ProgressRequestBody(File file, String path, Listener listener) { mFile = file; mPath = path; mListener = listener; } @Override public MediaType contentType() { String extension = MimeTypeMap.getFileExtensionFromUrl(mPath); String type = ""; if (extension != null) { type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); } return MediaType.parse(type); } @Override public void writeTo(BufferedSink sink) throws IOException { long fileLength = mFile.length(); byte[] buffer = new byte[BUFFER_SIZE]; FileInputStream in = new FileInputStream(mFile); long uploaded = 0; try { int read; while ((read = in.read(buffer)) != -1 && !Thread.interrupted()) { uploaded += read; sink.write(buffer, 0, read); mListener.onProgress(((float) uploaded) / fileLength); } } finally { in.close(); } } interface Listener { void onProgress(float progress); } } }