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

Java tutorial

Introduction

Here is the source code for com.owncloud.android.services.observer.InstantUploadsObserver.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.os.FileObserver;
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.HashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Observer watching a folder to request the upload of new pictures or videos inside it.
 */
public class InstantUploadsObserver extends FileObserver {

    private static final String TAG = InstantUploadsObserver.class.getSimpleName();

    private static final int UPDATE_MASK = (FileObserver.CREATE | FileObserver.MODIFY | FileObserver.CLOSE_WRITE
            | FileObserver.MOVED_TO);

    private static final int ALL_EVENTS_EVEN_THOSE_NOT_DOCUMENTED = 0x7fffffff; // NEVER use 0xffffffff
    private static final int IN_IGNORE = 32768;

    private static final ScheduledThreadPoolExecutor mDelayerExecutor = new ScheduledThreadPoolExecutor(1);

    private final Object mLock = new Object(); // to sync mConfiguration, mainly

    private InstantUploadsConfiguration mConfiguration;
    private Context mContext;
    private HashMap<String, Boolean> mObservedChildren;

    /**
     * Constructor.
     *
     * Initializes the observer to receive events about files created in the source folder
     * included in parameter 'configuration'.
     *
     *
     * @param configuration     Full configuration for instant uploads to apply, including folder to watch.
     * @param context           Used to start an operation to upload a file, when needed.
     */
    public InstantUploadsObserver(InstantUploadsConfiguration configuration, Context context) {
        super(configuration.getSourcePath(), ALL_EVENTS_EVEN_THOSE_NOT_DOCUMENTED);

        if (context == null) {
            throw new IllegalArgumentException("NULL context argument received");
        }

        // TODO - work if camera folder doesn't exist, but is created later?

        mConfiguration = configuration;
        mContext = context;
        mObservedChildren = new HashMap<>();
    }

    /**
     * Receives and processes events about updates of the monitored folder.
     *
     * This is almost heuristic. Do no expect it works magically with any camera.
     *
     * For instance, Google Camera creates a new video file when the user enters in "video mode", before
     * start to record, and saves it empty if the user leaves recording nothing. True store. Life is magic.
     *
     * @param event     Kind of event occurred.
     * @param path      Relative path of the file referred by the event.
     */
    @Override
    public void onEvent(int event, String path) {
        Log_OC.d(TAG, "Got event " + event + " on FOLDER " + mConfiguration.getSourcePath() + " about "
                + ((path != null) ? path : "") + " (in thread '" + Thread.currentThread().getName() + "')");

        if (path != null && path.length() > 0) {
            synchronized (mLock) {
                if ((event & FileObserver.CREATE) != 0) {
                    // new file created, let's watch it; false -> not modified yet
                    mObservedChildren.put(path, false);
                }
                if (((event & FileObserver.MODIFY) != 0) && mObservedChildren.containsKey(path)
                        && !mObservedChildren.get(path)) {
                    // watched file was written for the first time after creation
                    mObservedChildren.put(path, true);
                }
                if ((event & FileObserver.CLOSE_WRITE) != 0 && mObservedChildren.containsKey(path)
                        && mObservedChildren.get(path)) {
                    // a file that was previously created and written has been closed;
                    // testing for FileObserver.MODIFY is needed because some apps
                    // close the video file right after creating it when the recording
                    // is started, and reopen it to write with the first chunk of video
                    // to save; for instance, Camera MX does so.
                    mObservedChildren.remove(path);
                    handleNewFile(path);
                }
                if ((event & FileObserver.MOVED_TO) != 0) {
                    // a file has been moved or renamed into the folder;
                    // for instance, Google Camera does so right after
                    // saving a video recording
                    handleNewFile(path);
                }
            }
        }

        if ((event & IN_IGNORE) != 0 && (path == null || path.length() == 0)) {
            Log_OC.d(TAG, "Stopping the observance on " + mConfiguration.getSourcePath());
        }
    }

    /**
     * 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
     */
    private void handleNewFile(final String fileName) {
        Log_OC.d(TAG, "New file " + fileName);

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

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

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

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

        /// check permission to read
        int permissionCheck = ContextCompat.checkSelfPermission(mContext,
                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;
        }

        /// delay a bit final checks and upload request
        mDelayerExecutor.schedule(new Runnable() {
            @Override
            public void run() {
                /// check the file is **still** there and really has something inside (*)
                String localPath = mConfiguration.getSourcePath() + File.separator + fileName;
                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;
                }

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

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

                FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
                requester.uploadNewFile(mContext, account, localPath, remotePath,
                        mConfiguration.getBehaviourAfterUpload(), mimeType, true, // create parent folder if not existent
                        createdBy);
                Log_OC.i(TAG, String.format("Requested upload of %1s to %2s in %3s", localPath, remotePath,
                        account.name));
            }
        },

                200, TimeUnit.MILLISECONDS);

    }

    /**
     * Returns the absolute path to the folder observed
     *
     * @return      Absolute path to folder observed
     */
    public String getSourcePath() {
        synchronized (mLock) {
            return mConfiguration.getSourcePath();
        }
    }

    /**
     * Updates the configuration for instant uploads with the one received.
     *
     * Source path of both the new and the current configurations must be the same.
     *
     * @param configuration     New configuration for instant uploads to replace the current one.
     */
    public void updateConfiguration(InstantUploadsConfiguration configuration) {
        if (configuration == null) {
            throw new IllegalArgumentException("NULL configuration argument received");
        }
        synchronized (mLock) {
            if (!mConfiguration.getSourcePath().equals(configuration.getSourcePath())) {
                throw new IllegalArgumentException(
                        "Source path in new configuration must match source path in the current one");
            }
            mConfiguration = configuration;
        }
    }
}