com.owncloud.android.services.observer.InstantUploadsHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.owncloud.android.services.observer.InstantUploadsHandler.java

Source

/**
 *   ownCloud Android client application
 *
 *   @author David A. Velasco
 *   Copyright (C) 2016 ownCloud GmbH.
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License version 2,
 *   as published by the Free Software Foundation.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package com.owncloud.android.services.observer;

import android.Manifest;
import android.accounts.Account;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.provider.MediaStore;
import android.support.v4.content.ContextCompat;

import com.owncloud.android.authentication.AccountUtils;
import com.owncloud.android.db.PreferenceManager.InstantUploadsConfiguration;
import com.owncloud.android.files.services.FileUploader;
import com.owncloud.android.lib.common.utils.Log_OC;
import com.owncloud.android.operations.UploadFileOperation;
import com.owncloud.android.utils.MimetypeIconUtil;

import java.io.File;
import java.util.HashSet;
import java.util.Set;

/**
 * Request the upload of a possible new picture or video taken by the camera app
 * if matches the criteria of the received configuration for instant uploads.
 */
public class InstantUploadsHandler {

    private static String TAG = InstantUploadsHandler.class.getName();

    /**
     * Because new pictures or videos are detected both from NEW_xxx_ACTION broadcast intents and
     * from a FileObserver watching the camera folder, a single picture or video might produce two
     * calls to a handleXXX method in two different instances of {@link InstantUploadsHandler}.
     *
     * {@link FileUploader} filters upload requests that are already in the queue of files to be uploaded.
     * This is enough to prevent duplications of instant uploads when network is available, since the first
     * upload request will be still in the queue or being uploaded when the second one arrives.
     *
     * Nevertheless, when network is not available, the first upload request might be retired from the
     * queue and archived as failed/delayed before the second upload request arrives to the service.
     * That will lead to two failed uploads for the same file, and this will be uploaded twice when
     * the network is recovered. Real case detected in
     * https://github.com/owncloud/android/issues/1795#issuecomment-245263247.
     *
     * Even worse: due to multithreading scheduling, it's not granted that the two events corresponding
     * to a single picture/video are received one after the other. If some pictures/videos are taken in a row,
     * FileObserver thread can call 'handleNewMediaFile(...)' action for some of them before than
     * InstantUploadsBroadcastReceiver calls 'handleNewXXXAction(...)' for the first of them. Real case
     * detected while fixing https://github.com/owncloud/android/issues/1795#issuecomment-245263247.
     *
     * Due to that, remembering just the last file detected is not enough to correctly filter out duplicated
     * detections. Next static field saves last recent requests to filter duplicated detections for a while.
     */
    private static int MAX_RECENTS = 30;
    private static Set<String> sRecentlyUploadedFilePaths = new HashSet<>(MAX_RECENTS);

    public boolean handleNewPictureAction(Intent intent, InstantUploadsConfiguration configuration,
            Context context) {
        Log_OC.i(TAG, "New photo received");

        if (!configuration.isEnabledForPictures()) {
            Log_OC.d(TAG, "Instant upload disabled for images, ignoring new picture");
            return false;
        }

        /// retrieve file data from MediaStore
        String[] CONTENT_PROJECTION = { MediaStore.Images.Media.DATA, MediaStore.Images.Media.DISPLAY_NAME,
                MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.SIZE };

        Cursor c = context.getContentResolver().query(intent.getData(), CONTENT_PROJECTION, null, null, null);
        if (c == null || !c.moveToFirst()) {
            Log_OC.e(TAG, "Couldn't resolve given uri: " + intent.getDataString());
            if (c != null) {
                c.close();
            }
            return false;
        }
        String localPath = c.getString(c.getColumnIndex(MediaStore.Images.Media.DATA));
        String fileName = c.getString(c.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME));
        String mimeType = c.getString(c.getColumnIndex(MediaStore.Images.Media.MIME_TYPE));
        c.close();

        Log_OC.d(TAG, "Local path: " + localPath);
        return handleNewMediaFile(fileName, localPath, mimeType, true, configuration, context);

    }

    public boolean handleNewVideoAction(Intent intent, InstantUploadsConfiguration configuration, Context context) {
        Log_OC.i(TAG, "New video received");

        if (!configuration.isEnabledForVideos()) {
            Log_OC.d(TAG, "Instant upload disabled for videos, ignoring new video");
            return false;
        }

        /// retrieve file data from MediaStore
        String[] CONTENT_PROJECTION = { MediaStore.Video.Media.DATA, MediaStore.Video.Media.DISPLAY_NAME,
                MediaStore.Video.Media.MIME_TYPE, MediaStore.Video.Media.SIZE };

        Cursor c = context.getContentResolver().query(intent.getData(), CONTENT_PROJECTION, null, null, null);
        if (c == null || !c.moveToFirst()) {
            Log_OC.e(TAG, "Couldn't resolve given uri: " + intent.getDataString());
            if (c != null) {
                c.close();
            }
            return false;
        }
        String localPath = c.getString(c.getColumnIndex(MediaStore.Video.Media.DATA));
        String fileName = c.getString(c.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME));
        String mimeType = c.getString(c.getColumnIndex(MediaStore.Video.Media.MIME_TYPE));
        c.close();

        Log_OC.d(TAG, "Local path: " + localPath);
        return handleNewMediaFile(fileName, localPath, mimeType, false, configuration, context);
    }

    /**
     * Request the upload of a file just created if matches the criteria of the current
     * configuration for instant uploads.
     *
     * @param fileName          Name of the file just created.
     * @param configuration     User configuration for instant uploads.
     * @param context           Valid Context, used to request to uploads service.
     * @return                  'True' if an upload was requested, 'false' otherwise.
     */
    public boolean handleNewFile(String fileName, InstantUploadsConfiguration configuration, Context context) {
        Log_OC.d(TAG, "New file " + fileName);

        /// check file type
        String mimeType = MimetypeIconUtil.getBestMimeTypeByFilename(fileName);
        boolean isImage = mimeType.startsWith("image/");
        boolean isVideo = mimeType.startsWith("video/");

        if (!isImage && !isVideo) {
            Log_OC.d(TAG, "Ignoring " + fileName);
            return false;
        }

        if (isImage && !configuration.isEnabledForPictures()) {
            Log_OC.d(TAG, "Instant upload disabled for images, ignoring " + fileName);
            return false;
        }

        if (isVideo && !configuration.isEnabledForVideos()) {
            Log_OC.d(TAG, "Instant upload disabled for videos, ignoring " + fileName);
            return false;
        }

        String localPath = configuration.getSourcePath() + File.separator + fileName;
        Log_OC.d(TAG, "Local path: " + localPath);

        return handleNewMediaFile(fileName, localPath, mimeType, isImage, configuration, context);
    }

    /**
     * Request the upload of a file just created if matches the criteria of the current
     * configuration for instant uploads.
     *
     * @param fileName          Name of the file just created.
     * @param configuration     User configuration for instant uploads.
     * @param context           Valid Context, used to request to uploads service.
     * @return                  'True' if an upload was requested, 'false' otherwise.
     */
    private synchronized boolean handleNewMediaFile(String fileName, String localPath, String mimeType,
            boolean isImage, InstantUploadsConfiguration configuration, Context context) {

        /// check duplicated detection
        if (sRecentlyUploadedFilePaths.contains(localPath)) {
            Log_OC.i(TAG, "Duplicate detection of " + localPath + ", ignoring");
            return false;
        }

        /// check permission to read
        int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE);
        if (android.content.pm.PackageManager.PERMISSION_GRANTED != permissionCheck) {
            Log_OC.w(TAG, "Read external storage permission isn't granted, aborting");
            return false;
        }

        /// check the file is **still** there and really has something inside (*)
        File localFile = new File(localPath);
        if (!localFile.exists() || localFile.length() <= 0) {
            Log_OC.w(TAG, "Camera app saved an empty or temporary file, ignoring " + fileName);
            // Google Camera renames video files right after stop and save
            // the recording; uploading the video upload with the original
            // name would fail; this prevents it
            return false;
        }

        /// check existence of target account
        Account account = AccountUtils.getOwnCloudAccountByName(context, configuration.getUploadAccountName());
        if (account == null) {
            Log_OC.w(TAG, "No account found for instant upload, aborting upload");
            return false;
        }

        /// upload!
        String remotePath = (isImage ? configuration.getUploadPathForPictures()
                : configuration.getUploadPathForVideos()) + fileName;
        int createdBy = isImage ? UploadFileOperation.CREATED_AS_INSTANT_PICTURE
                : UploadFileOperation.CREATED_AS_INSTANT_VIDEO;

        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
        requester.uploadNewFile(context, account, localPath, remotePath, configuration.getBehaviourAfterUpload(),
                mimeType, true, // create parent folder if not existent
                createdBy);

        if (sRecentlyUploadedFilePaths.size() >= MAX_RECENTS) {
            // remove first path inserted
            sRecentlyUploadedFilePaths.remove(sRecentlyUploadedFilePaths.iterator().next());
        }
        sRecentlyUploadedFilePaths.add(localPath);

        Log_OC.i(TAG, String.format("Requested upload of %1s to %2s in %3s", localPath, remotePath, account.name));
        return true;
    }

}