Procházet zdrojové kódy

Direct camera upload

Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
AndyScherzinger před 6 roky
rodič
revize
8ae93db343

+ 1 - 0
drawable_resources/ic_camera.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" /></svg>

+ 0 - 2
gradle.properties

@@ -1,5 +1,3 @@
-# workaround since lombok and desugering have an issue
-# to be fixed with gradle Android plugin 3.3.0
 android.enableJetifier=true
 android.useAndroidX=true
 android.debug.obsoleteApi=true

+ 50 - 12
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -96,6 +96,7 @@ import com.owncloud.android.operations.UpdateShareViaLinkOperation;
 import com.owncloud.android.operations.UploadFileOperation;
 import com.owncloud.android.providers.UsersAndGroupsSearchProvider;
 import com.owncloud.android.syncadapter.FileSyncAdapter;
+import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask;
 import com.owncloud.android.ui.dialog.SendShareDialog;
 import com.owncloud.android.ui.dialog.ShareLinkToDialog;
 import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
@@ -180,6 +181,7 @@ public class FileDisplayActivity extends HookActivity
     public static final int REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM = REQUEST_CODE__LAST_SHARED + 2;
     public static final int REQUEST_CODE__MOVE_FILES = REQUEST_CODE__LAST_SHARED + 3;
     public static final int REQUEST_CODE__COPY_FILES = REQUEST_CODE__LAST_SHARED + 4;
+    public static final int REQUEST_CODE__UPLOAD_FROM_CAMERA = REQUEST_CODE__LAST_SHARED + 5;
 
     protected static final long DELAY_TO_REQUEST_REFRESH_OPERATION_LATER = DELAY_TO_REQUEST_OPERATIONS_LATER + 350;
 
@@ -967,6 +969,33 @@ public class FileDisplayActivity extends HookActivity
 
             requestUploadOfFilesFromFileSystem(data, resultCode);
 
+        } else if (requestCode == REQUEST_CODE__UPLOAD_FROM_CAMERA &&
+                (resultCode == RESULT_OK || resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE)) {
+
+            new CheckAvailableSpaceTask(new CheckAvailableSpaceTask.CheckAvailableSpaceListener() {
+                @Override
+                public void onCheckAvailableSpaceStart() {
+                    Log_OC.d(this, "onCheckAvailableSpaceStart");
+                }
+
+                @Override
+                public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String[] filesToUpload) {
+                    Log_OC.d(this, "onCheckAvailableSpaceFinish");
+
+                    if (hasEnoughSpaceAvailable) {
+                        File file = new File(filesToUpload[0]);
+                        File renamedFile = new File(file.getParent() + "/" + FileOperationsHelper.getCapturedImageName());
+
+                        if (!file.renameTo(renamedFile)) {
+                            DisplayUtils.showSnackMessage(getActivity(), "Fail to upload taken image!");
+                            return;
+                        }
+
+                        requestUploadOfFilesFromFileSystem(new String[]{renamedFile.getAbsolutePath()},
+                                                           FileUploader.LOCAL_BEHAVIOUR_MOVE);
+                    }
+                }
+            }, new String[]{FileOperationsHelper.createImageFile(getActivity()).getAbsolutePath()}).execute();
         } else if (requestCode == REQUEST_CODE__MOVE_FILES && resultCode == RESULT_OK) {
             exitSelectionMode();
             final Intent fData = data;
@@ -992,11 +1021,9 @@ public class FileDisplayActivity extends HookActivity
                     },
                     DELAY_TO_REQUEST_OPERATIONS_LATER
             );
-
         } else {
             super.onActivityResult(requestCode, resultCode, data);
         }
-
     }
 
     private void exitSelectionMode() {
@@ -1008,6 +1035,17 @@ public class FileDisplayActivity extends HookActivity
 
     private void requestUploadOfFilesFromFileSystem(Intent data, int resultCode) {
         String[] filePaths = data.getStringArrayExtra(UploadFilesActivity.EXTRA_CHOSEN_FILES);
+        int behaviour;
+
+        if (resultCode == UploadFilesActivity.RESULT_OK_AND_MOVE) {
+            behaviour = FileUploader.LOCAL_BEHAVIOUR_MOVE;
+        } else {
+            behaviour = FileUploader.LOCAL_BEHAVIOUR_COPY;
+        }
+        requestUploadOfFilesFromFileSystem(filePaths, behaviour);
+    }
+
+    private void requestUploadOfFilesFromFileSystem(String[] filePaths, int resultCode) {
         if (filePaths != null) {
             String[] remotePaths = new String[filePaths.length];
             String remotePathBase = getCurrentDir().getRemotePath();
@@ -1036,16 +1074,16 @@ public class FileDisplayActivity extends HookActivity
 
             FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
             requester.uploadNewFile(
-                    this,
-                    getAccount(),
-                    filePaths,
-                    remotePaths,
-                    null,           // MIME type will be detected from file name
-                    behaviour,
-                    false,          // do not create parent folder if not existent
-                    UploadFileOperation.CREATED_BY_USER,
-                    false,
-                    false
+                this,
+                getAccount(),
+                filePaths,
+                remotePaths,
+                null,           // MIME type will be detected from file name
+                behaviour,
+                false,          // do not create parent folder if not existent
+                UploadFileOperation.CREATED_BY_USER,
+                false,
+                false
             );
 
         } else {

+ 80 - 102
src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java

@@ -27,7 +27,6 @@ import android.content.Intent;
 import android.content.res.ColorStateList;
 import android.graphics.PorterDuff;
 import android.graphics.drawable.Drawable;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Environment;
 import android.view.Menu;
@@ -45,7 +44,9 @@ import android.widget.TextView;
 import com.google.android.material.button.MaterialButton;
 import com.owncloud.android.R;
 import com.owncloud.android.db.PreferenceManager;
+import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.asynctasks.CheckAvailableSpaceTask;
 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment;
 import com.owncloud.android.ui.dialog.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
 import com.owncloud.android.ui.dialog.IndeterminateProgressDialog;
@@ -53,7 +54,6 @@ import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
 import com.owncloud.android.ui.fragment.ExtendedListFragment;
 import com.owncloud.android.ui.fragment.LocalFileListFragment;
 import com.owncloud.android.utils.FileSortOrder;
-import com.owncloud.android.utils.FileStorageUtils;
 import com.owncloud.android.utils.ThemeUtils;
 
 import java.io.File;
@@ -77,7 +77,8 @@ import androidx.fragment.app.FragmentTransaction;
  */
 public class UploadFilesActivity extends FileActivity implements
     LocalFileListFragment.ContainerActivity, ActionBar.OnNavigationListener,
-        OnClickListener, ConfirmationDialogFragmentListener, SortingOrderDialogFragment.OnSortingOrderListener {
+    OnClickListener, ConfirmationDialogFragmentListener, SortingOrderDialogFragment.OnSortingOrderListener,
+    CheckAvailableSpaceTask.CheckAvailableSpaceListener {
 
     private static final String SORT_ORDER_DIALOG_TAG = "SORT_ORDER_DIALOG";
     private static final int SINGLE_DIR = 1;
@@ -113,6 +114,8 @@ public class UploadFilesActivity extends FileActivity implements
     private static final String TAG = "UploadFilesActivity";
     private static final String WAIT_DIALOG_TAG = "WAIT";
     private static final String QUERY_TO_MOVE_DIALOG_TAG = "QUERY_TO_MOVE";
+    public static final String REQUEST_CODE_KEY = "requestCode";
+    private int requestCode;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -122,6 +125,7 @@ public class UploadFilesActivity extends FileActivity implements
         Bundle extras = getIntent().getExtras();
         if (extras != null) {
             mLocalFolderPickerMode = extras.getBoolean(KEY_LOCAL_FOLDER_PICKER_MODE, false);
+            requestCode = (int) extras.get(REQUEST_CODE_KEY);
         }
 
         if (savedInstanceState != null) {
@@ -182,12 +186,12 @@ public class UploadFilesActivity extends FileActivity implements
 
         List<String> behaviours = new ArrayList<>();
         behaviours.add(getString(R.string.uploader_upload_files_behaviour_move_to_nextcloud_folder,
-                ThemeUtils.getDefaultDisplayNameForRootFolder(this)));
+                                 ThemeUtils.getDefaultDisplayNameForRootFolder(this)));
         behaviours.add(getString(R.string.uploader_upload_files_behaviour_only_upload));
         behaviours.add(getString(R.string.uploader_upload_files_behaviour_upload_and_delete_from_source));
 
         ArrayAdapter<String> behaviourAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item,
-                behaviours);
+                                                                   behaviours);
         behaviourAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
         mBehaviourSpinner.setAdapter(behaviourAdapter);
         mBehaviourSpinner.setSelection(localBehaviour);
@@ -197,8 +201,7 @@ public class UploadFilesActivity extends FileActivity implements
 
         // Action bar setup
         ActionBar actionBar = getSupportActionBar();
-        actionBar.setHomeButtonEnabled(true);   // mandatory since Android ICS, according to the
-                                                // official documentation
+        actionBar.setHomeButtonEnabled(true);   // mandatory since Android ICS, according to the official documentation
         actionBar.setDisplayHomeAsUpEnabled(mCurrentDir != null && mCurrentDir.getName() != null);
         actionBar.setDisplayShowTitleEnabled(false);
         actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
@@ -232,6 +235,7 @@ public class UploadFilesActivity extends FileActivity implements
     public static void startUploadActivityForResult(Activity activity, Account account, int requestCode) {
         Intent action = new Intent(activity, UploadFilesActivity.class);
         action.putExtra(EXTRA_ACCOUNT, account);
+        action.putExtra(REQUEST_CODE_KEY, requestCode);
         activity.startActivityForResult(action, requestCode);
     }
 
@@ -417,6 +421,73 @@ public class UploadFilesActivity extends FileActivity implements
         }
     }
 
+    @Override
+    public void onCheckAvailableSpaceStart() {
+        if (requestCode == FileDisplayActivity.REQUEST_CODE__SELECT_FILES_FROM_FILE_SYSTEM) {
+            mCurrentDialog = IndeterminateProgressDialog.newInstance(R.string.wait_a_moment, false);
+            mCurrentDialog.show(getSupportFragmentManager(), WAIT_DIALOG_TAG);
+        }
+    }
+
+    /**
+     * Updates the activity UI after the check of space is done. If there is not space enough. shows a new dialog to
+     * query the user if wants to move the files instead of copy them.
+     *
+     * @param hasEnoughSpaceAvailable 'True' when there is space enough to copy all the selected files.
+     */
+    @Override
+    public void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String[] filesToUpload) {
+        if (mCurrentDialog != null) {
+            mCurrentDialog.dismiss();
+            mCurrentDialog = null;
+        }
+
+        if (hasEnoughSpaceAvailable) {
+            // return the list of files (success)
+            Intent data = new Intent();
+
+            if (requestCode == FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_CAMERA) {
+                data.putExtra(EXTRA_CHOSEN_FILES, new String[]{filesToUpload[0]});
+                setResult(RESULT_OK_AND_MOVE, data);
+
+                PreferenceManager.setUploaderBehaviour(getApplicationContext(), FileUploader.LOCAL_BEHAVIOUR_MOVE);
+            } else {
+                data.putExtra(EXTRA_CHOSEN_FILES, mFileListFragment.getCheckedFilePaths());
+
+                // set result code
+                switch (mBehaviourSpinner.getSelectedItemPosition()) {
+                    case 0: // move to nextcloud folder
+                        setResult(RESULT_OK_AND_MOVE, data);
+                        break;
+
+                    case 1: // only upload
+                        setResult(RESULT_OK_AND_DO_NOTHING, data);
+                        break;
+
+                    case 2: // upload and delete from source
+                        setResult(RESULT_OK_AND_DELETE, data);
+                        break;
+                }
+
+                // store behaviour
+                PreferenceManager.setUploaderBehaviour(getApplicationContext(),
+                                                       mBehaviourSpinner.getSelectedItemPosition());
+            }
+
+            finish();
+        } else {
+            // show a dialog to query the user if wants to move the selected files
+            // to the ownCloud folder instead of copying
+            String[] args = {getString(R.string.app_name)};
+            ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(
+                R.string.upload_query_move_foreign_files, args, 0, R.string.common_yes, -1,
+                R.string.common_no
+            );
+            dialog.setOnConfirmationListener(UploadFilesActivity.this);
+            dialog.show(getSupportFragmentManager(), QUERY_TO_MOVE_DIALOG_TAG);
+        }
+    }
+
     /**
      * Custom array adapter to override text colors
      */
@@ -531,101 +602,8 @@ public class UploadFilesActivity extends FileActivity implements
 
                 finish();
             } else {
-                new CheckAvailableSpaceTask().execute(mBehaviourSpinner.getSelectedItemPosition() == 0);
-            }
-        }
-    }
-
-    /**
-     * Asynchronous task checking if there is space enough to copy all the files chosen
-     * to upload into the ownCloud local folder.
-     *
-     * Maybe an AsyncTask is not strictly necessary, but who really knows.
-     */
-    private class CheckAvailableSpaceTask extends AsyncTask<Boolean, Void, Boolean> {
-
-        /**
-         * Updates the UI before trying the movement.
-         */
-        @Override
-        protected void onPreExecute () {
-            /// progress dialog and disable 'Move' button
-            mCurrentDialog = IndeterminateProgressDialog.newInstance(R.string.wait_a_moment, false);
-            mCurrentDialog.show(getSupportFragmentManager(), WAIT_DIALOG_TAG);
-        }
-
-        /**
-         * Checks the available space.
-         *
-         * @param params boolean flag if storage calculation should be done.
-         * @return 'True' if there is space enough or doesn't have to be calculated
-         */
-        @Override
-        protected Boolean doInBackground(Boolean... params) {
-            if(params[0]) {
-                String[] checkedFilePaths = mFileListFragment.getCheckedFilePaths();
-                long total = 0;
-                for (int i = 0; checkedFilePaths != null && i < checkedFilePaths.length; i++) {
-                    String localPath = checkedFilePaths[i];
-                    File localFile = new File(localPath);
-                    total += localFile.length();
-                }
-                return FileStorageUtils.getUsableSpace() >= total;
-            }
-
-            return true;
-        }
-
-        /**
-         * Updates the activity UI after the check of space is done.
-         *
-         * If there is not space enough. shows a new dialog to query the user if wants to move the
-         * files instead of copy them.
-         *
-         * @param result        'True' when there is space enough to copy all the selected files.
-         */
-        @Override
-        protected void onPostExecute(Boolean result) {
-            if(mCurrentDialog != null) {
-                mCurrentDialog.dismiss();
-                mCurrentDialog = null;
-            }
-
-            if (result) {
-                // return the list of selected files (success)
-                Intent data = new Intent();
-                data.putExtra(EXTRA_CHOSEN_FILES, mFileListFragment.getCheckedFilePaths());
-
-                // set result code
-                switch (mBehaviourSpinner.getSelectedItemPosition()) {
-                    case 0: // move to nextcloud folder
-                        setResult(RESULT_OK_AND_MOVE, data);
-                        break;
-
-                    case 1: // only upload
-                        setResult(RESULT_OK_AND_DO_NOTHING, data);
-                        break;
-
-                    case 2: // upload and delete from source
-                        setResult(RESULT_OK_AND_DELETE, data);
-                        break;
-                }
-
-                // store behaviour
-                PreferenceManager.setUploaderBehaviour(getApplicationContext(),
-                        mBehaviourSpinner.getSelectedItemPosition());
-
-                finish();
-            } else {
-                // show a dialog to query the user if wants to move the selected files
-                // to the ownCloud folder instead of copying
-                String[] args = {getString(R.string.app_name)};
-                ConfirmationDialogFragment dialog = ConfirmationDialogFragment.newInstance(
-                    R.string.upload_query_move_foreign_files, args, 0, R.string.common_yes, -1,
-                        R.string.common_no
-                );
-                dialog.setOnConfirmationListener(UploadFilesActivity.this);
-                dialog.show(getSupportFragmentManager(), QUERY_TO_MOVE_DIALOG_TAG);
+                new CheckAvailableSpaceTask(this, mFileListFragment.getCheckedFilePaths())
+                    .execute(mBehaviourSpinner.getSelectedItemPosition() == 0);
             }
         }
     }

+ 79 - 0
src/main/java/com/owncloud/android/ui/asynctasks/CheckAvailableSpaceTask.java

@@ -0,0 +1,79 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2019 Tobias Kaminsky
+ * Copyright (C) 2019 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * 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 <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.asynctasks;
+
+import android.os.AsyncTask;
+
+import com.owncloud.android.utils.FileStorageUtils;
+
+import java.io.File;
+
+/**
+ * Asynchronous task checking if there is space enough to copy all the files chosen to upload into the ownCloud local
+ * folder. Maybe an AsyncTask is not strictly necessary, but who really knows.
+ */
+public class CheckAvailableSpaceTask extends AsyncTask<Boolean, Void, Boolean> {
+
+    private String[] paths;
+    private CheckAvailableSpaceListener callback;
+
+    public CheckAvailableSpaceTask(CheckAvailableSpaceListener callback, String[] paths) {
+        this.paths = paths;
+        this.callback = callback;
+    }
+
+    /**
+     * Updates the UI before trying the movement.
+     */
+    @Override
+    protected void onPreExecute() {
+        callback.onCheckAvailableSpaceStart();
+    }
+
+    /**
+     * Checks the available space.
+     *
+     * @param params boolean flag if storage calculation should be done.
+     * @return 'True' if there is space enough or doesn't have to be calculated
+     */
+    @Override
+    protected Boolean doInBackground(Boolean... params) {
+        long total = 0;
+        for (int i = 0; paths != null && i < paths.length; i++) {
+            String localPath = paths[i];
+            File localFile = new File(localPath);
+            total += localFile.length();
+        }
+        return FileStorageUtils.getUsableSpace() >= total;
+    }
+
+    @Override
+    protected void onPostExecute(Boolean result) {
+        callback.onCheckAvailableSpaceFinish(result, paths);
+    }
+
+    public interface CheckAvailableSpaceListener {
+        void onCheckAvailableSpaceStart();
+
+        void onCheckAvailableSpaceFinish(boolean hasEnoughSpaceAvailable, String[] filesToUpload);
+    }
+}

+ 81 - 78
src/main/java/com/owncloud/android/ui/dialog/ConfirmationDialogFragment.java

@@ -1,44 +1,42 @@
-/**
- *   ownCloud Android client application
+/*
+ * ownCloud Android client application
  *
- *   Copyright (C) 2012 Bartek Przybylski
- *   Copyright (C) 2015 ownCloud Inc.
+ * Copyright (C) 2012 Bartek Przybylski Copyright (C) 2015 ownCloud Inc.
  *
- *   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 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/>.
+ * 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.ui.dialog;
 
+import android.app.Activity;
 import android.app.Dialog;
-import android.content.DialogInterface;
 import android.os.Bundle;
 
 import com.owncloud.android.R;
+import com.owncloud.android.utils.ThemeUtils;
 
+import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 import androidx.fragment.app.DialogFragment;
 
 
 public class ConfirmationDialogFragment extends DialogFragment {
 
-    public final static String ARG_MESSAGE_RESOURCE_ID = "resource_id";
-    public final static String ARG_MESSAGE_ARGUMENTS = "string_array";
+    final static String ARG_MESSAGE_RESOURCE_ID = "resource_id";
+    final static String ARG_MESSAGE_ARGUMENTS = "string_array";
     private static final String ARG_TITLE_ID = "title_id";
 
-    public final static String ARG_POSITIVE_BTN_RES = "positive_btn_res";
-    public final static String ARG_NEUTRAL_BTN_RES = "neutral_btn_res";
-    public final static String ARG_NEGATIVE_BTN_RES = "negative_btn_res";
+    final static String ARG_POSITIVE_BTN_RES = "positive_btn_res";
+    final static String ARG_NEUTRAL_BTN_RES = "neutral_btn_res";
+    final static String ARG_NEGATIVE_BTN_RES = "negative_btn_res";
 
     public static final String FTAG_CONFIRMATION = "CONFIRMATION_FRAGMENT";
 
@@ -47,24 +45,16 @@ public class ConfirmationDialogFragment extends DialogFragment {
     /**
      * Public factory method to create new ConfirmationDialogFragment instances.
      *
-     * @param messageResId      Resource id for a message to show in the dialog.
-     * @param messageArguments  Arguments to complete the message, if it's a format string. May be null.
-     * @param titleResId        Resource id for a text to show in the title.
-     *                          0 for default alert title, -1 for no title.
-     * @param posBtn            Resource id for the text of the positive button. -1 for no positive button.
-     * @param neuBtn            Resource id for the text of the neutral button. -1 for no neutral button.
-     * @param negBtn            Resource id for the text of the negative button. -1 for no negative button.
-     * @return                  Dialog ready to show.
+     * @param messageResId     Resource id for a message to show in the dialog.
+     * @param messageArguments Arguments to complete the message, if it's a format string. May be null.
+     * @param titleResId       Resource id for a text to show in the title. 0 for default alert title, -1 for no title.
+     * @param posBtn           Resource id for the text of the positive button. -1 for no positive button.
+     * @param neuBtn           Resource id for the text of the neutral button. -1 for no neutral button.
+     * @param negBtn           Resource id for the text of the negative button. -1 for no negative button.
+     * @return Dialog ready to show.
      */
-    public static ConfirmationDialogFragment newInstance(
-        int messageResId,
-        String[] messageArguments,
-        int titleResId,
-        int posBtn,
-        int neuBtn,
-        int negBtn
-    ) {
-
+    public static ConfirmationDialogFragment newInstance(int messageResId, String[] messageArguments, int titleResId,
+                                                         int posBtn, int neuBtn, int negBtn) {
         if (messageResId == -1) {
             throw new IllegalStateException("Calling confirmation dialog without message resource");
         }
@@ -81,27 +71,52 @@ public class ConfirmationDialogFragment extends DialogFragment {
         return frag;
     }
 
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        int color = ThemeUtils.primaryAccentColor(getContext());
+
+        AlertDialog alertDialog = (AlertDialog) getDialog();
+
+        alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(color);
+        alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(color);
+    }
+
     public void setOnConfirmationListener(ConfirmationDialogFragmentListener listener) {
         mListener = listener;
     }
 
+    @NonNull
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
-        Object[] messageArguments = getArguments().getStringArray(ARG_MESSAGE_ARGUMENTS);
-        int messageId = getArguments().getInt(ARG_MESSAGE_RESOURCE_ID, -1);
-        int titleId = getArguments().getInt(ARG_TITLE_ID, -1);
-        int posBtn = getArguments().getInt(ARG_POSITIVE_BTN_RES, -1);
-        int neuBtn = getArguments().getInt(ARG_NEUTRAL_BTN_RES, -1);
-        int negBtn = getArguments().getInt(ARG_NEGATIVE_BTN_RES, -1);
+        Bundle arguments = getArguments();
+
+        if (arguments == null) {
+            throw new IllegalArgumentException("Arguments may not be null");
+        }
+
+        Object[] messageArguments = arguments.getStringArray(ARG_MESSAGE_ARGUMENTS);
+        int messageId = arguments.getInt(ARG_MESSAGE_RESOURCE_ID, -1);
+        int titleId = arguments.getInt(ARG_TITLE_ID, -1);
+        int posBtn = arguments.getInt(ARG_POSITIVE_BTN_RES, -1);
+        int neuBtn = arguments.getInt(ARG_NEUTRAL_BTN_RES, -1);
+        int negBtn = arguments.getInt(ARG_NEGATIVE_BTN_RES, -1);
 
         if (messageArguments == null) {
             messageArguments = new String[]{};
         }
 
-        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), R.style.Theme_ownCloud_Dialog)
-                .setIcon(R.drawable.ic_warning)
-                .setIconAttribute(android.R.attr.alertDialogIcon)
-                .setMessage(String.format(getString(messageId), messageArguments));
+        Activity activity = getActivity();
+
+        if (activity == null) {
+            throw new IllegalArgumentException("Activity may not be null");
+        }
+
+        AlertDialog.Builder builder = new AlertDialog.Builder(activity, R.style.Theme_ownCloud_Dialog)
+            .setIcon(R.drawable.ic_warning)
+            .setIconAttribute(android.R.attr.alertDialogIcon)
+            .setMessage(String.format(getString(messageId), messageArguments));
 
         if (titleId == 0) {
             builder.setTitle(android.R.string.dialog_alert_title);
@@ -110,43 +125,32 @@ public class ConfirmationDialogFragment extends DialogFragment {
         }
 
         if (posBtn != -1) {
-            builder.setPositiveButton(posBtn,
-                    new DialogInterface.OnClickListener() {
-                        public void onClick(DialogInterface dialog, int whichButton) {
-                            if (mListener != null) {
-                                mListener.onConfirmation(getTag());
-                            }
-                            dialog.dismiss();
-                        }
-                    });
+            builder.setPositiveButton(posBtn, (dialog, whichButton) -> {
+                if (mListener != null) {
+                    mListener.onConfirmation(getTag());
+                }
+                dialog.dismiss();
+            });
         }
         if (neuBtn != -1) {
-            builder.setNeutralButton(neuBtn,
-                    new DialogInterface.OnClickListener() {
-                        public void onClick(DialogInterface dialog, int whichButton) {
-                            if (mListener != null) {
-                                mListener.onNeutral(getTag());
-                            }
-                            dialog.dismiss();
-                        }
-                    });
+            builder.setNeutralButton(neuBtn, (dialog, whichButton) -> {
+                if (mListener != null) {
+                    mListener.onNeutral(getTag());
+                }
+                dialog.dismiss();
+            });
         }
         if (negBtn != -1) {
-            builder.setNegativeButton(negBtn,
-                    new DialogInterface.OnClickListener() {
-                        @Override
-                        public void onClick(DialogInterface dialog, int which) {
-                            if (mListener != null) {
-                                mListener.onCancel(getTag());
-                            }
-                            dialog.dismiss();
-                        }
-                    });
+            builder.setNegativeButton(negBtn, (dialog, which) -> {
+                if (mListener != null) {
+                    mListener.onCancel(getTag());
+                }
+                dialog.dismiss();
+            });
         }
-      return builder.create();
+        return builder.create();
     }
 
-
     public interface ConfirmationDialogFragmentListener {
         void onConfirmation(String callerTag);
 
@@ -154,6 +158,5 @@ public class ConfirmationDialogFragment extends DialogFragment {
 
         void onCancel(String callerTag);
     }
-
 }
 

+ 5 - 0
src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetActions.java

@@ -54,4 +54,9 @@ public interface OCFileListBottomSheetActions {
      * opens template selection for presentations
      */
     void newPresentation();
+
+    /**
+     * offers direct camera upload to the current folder.
+     */
+    void directCameraUpload();
 }

+ 9 - 1
src/main/java/com/owncloud/android/ui/fragment/OCFileListBottomSheetDialog.java

@@ -47,7 +47,8 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog {
 
     @BindView(R.id.menu_icon_upload_from_app)
     public ImageView iconUploadFromApp;
-
+    @BindView(R.id.menu_icon_direct_camera_upload)
+    public ImageView iconDirectCameraUpload;
     @BindView(R.id.menu_icon_mkdir)
     public ImageView iconMakeDir;
 
@@ -82,6 +83,7 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog {
         int primaryColor = ThemeUtils.primaryColor(getContext(), true);
         ThemeUtils.tintDrawable(iconUploadFiles.getDrawable(), primaryColor);
         ThemeUtils.tintDrawable(iconUploadFromApp.getDrawable(), primaryColor);
+        ThemeUtils.tintDrawable(iconDirectCameraUpload.getDrawable(), primaryColor);
         ThemeUtils.tintDrawable(iconMakeDir.getDrawable(), primaryColor);
 
         headline.setText(getContext().getResources().getString(R.string.add_to_cloud,
@@ -110,6 +112,12 @@ public class OCFileListBottomSheetDialog extends BottomSheetDialog {
         dismiss();
     }
 
+    @OnClick(R.id.menu_direct_camera_upload)
+    public void directCameraUpload() {
+        actions.directCameraUpload();
+        dismiss();
+    }
+
     @OnClick(R.id.menu_upload_files)
     public void uploadFiles() {
         actions.uploadFiles();

+ 12 - 0
src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java

@@ -417,6 +417,18 @@ public class OCFileListFragment extends ExtendedListFragment implements
         );
     }
 
+    @Override
+    public void directCameraUpload() {
+        FileDisplayActivity fileDisplayActivity = ((FileDisplayActivity) getActivity());
+
+        if (fileDisplayActivity != null) {
+            fileDisplayActivity.getFileOperationsHelper()
+                .uploadFromCamera(fileDisplayActivity, FileDisplayActivity.REQUEST_CODE__UPLOAD_FROM_CAMERA);
+        } else {
+            DisplayUtils.showSnackMessage(getView(), getString(R.string.error_starting_direct_camera_upload));
+        }
+    }
+
     @Override
     public void uploadFiles() {
         UploadFilesActivity.startUploadActivityForResult(

+ 48 - 0
src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java

@@ -24,6 +24,7 @@
 package com.owncloud.android.ui.helpers;
 
 import android.accounts.Account;
+import android.app.Activity;
 import android.content.ActivityNotFoundException;
 import android.content.ComponentName;
 import android.content.Context;
@@ -32,6 +33,8 @@ import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
 import android.util.Log;
 import android.view.View;
 import android.webkit.MimeTypeMap;
@@ -77,10 +80,13 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStreamReader;
 import java.nio.charset.Charset;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -969,4 +975,46 @@ public class FileOperationsHelper {
 
         mFileActivity.showLoadingDialog(mFileActivity.getString(R.string.wait_checking_credentials));
     }
+
+    public void uploadFromCamera(Activity activity, int requestCode) {
+        Intent pictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+
+        deleteOldFiles(activity);
+
+        File photoFile = createImageFile(activity);
+
+        Uri photoUri = FileProvider.getUriForFile(activity.getApplicationContext(),
+                                                  activity.getResources().getString(R.string.file_provider_authority), photoFile);
+        pictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
+
+        if (pictureIntent.resolveActivity(activity.getPackageManager()) != null) {
+            activity.startActivityForResult(pictureIntent, requestCode);
+        } else {
+            DisplayUtils.showSnackMessage(activity, "No Camera found");
+        }
+    }
+
+    private void deleteOldFiles(Activity activity) {
+        File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+
+        if (storageDir != null) {
+            for (File file : storageDir.listFiles()) {
+                if (!file.delete()) {
+                    Log_OC.d(this, "Failed to delete: " + file.getAbsolutePath());
+                }
+            }
+        }
+    }
+
+    public static File createImageFile(Activity activity) {
+        File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+
+        return new File(storageDir + "/directCameraUpload.jpg");
+    }
+
+    public static String getCapturedImageName() {
+        return new SimpleDateFormat("Y-MM-dd_HHmmss", Locale.US).format(new Date()) + ".jpg";
+    }
+
+
 }

+ 8 - 0
src/main/res/drawable/ic_camera.xml

@@ -0,0 +1,8 @@
+<!-- drawable/camera.xml -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:height="24dp"
+    android:width="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+    <path android:fillColor="#000" android:pathData="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" />
+</vector>

+ 52 - 11
src/main/res/layout/file_list_actions_bottom_sheet_fragment.xml

@@ -35,10 +35,10 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
-        android:paddingBottom="@dimen/standard_half_padding"
         android:paddingLeft="@dimen/standard_padding"
-        android:paddingRight="@dimen/standard_padding"
         android:paddingTop="@dimen/standard_half_padding"
+        android:paddingRight="@dimen/standard_padding"
+        android:paddingBottom="@dimen/standard_half_padding"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -53,8 +53,8 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
-            android:layout_marginLeft="@dimen/standard_margin"
             android:layout_marginStart="@dimen/standard_margin"
+            android:layout_marginLeft="@dimen/standard_margin"
             android:text="@string/upload_files"
             android:textColor="@color/black"
             android:textSize="@dimen/bottom_sheet_text_size"/>
@@ -66,10 +66,10 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
-        android:paddingBottom="@dimen/standard_half_padding"
         android:paddingLeft="@dimen/standard_padding"
-        android:paddingRight="@dimen/standard_padding"
         android:paddingTop="@dimen/standard_half_padding"
+        android:paddingRight="@dimen/standard_padding"
+        android:paddingBottom="@dimen/standard_half_padding"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -84,8 +84,8 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
-            android:layout_marginLeft="@dimen/standard_margin"
             android:layout_marginStart="@dimen/standard_margin"
+            android:layout_marginLeft="@dimen/standard_margin"
             android:text="@string/upload_content_from_other_apps"
             android:textColor="@color/black"
             android:textSize="@dimen/bottom_sheet_text_size"/>
@@ -93,13 +93,54 @@
     </LinearLayout>
 
     <View
-        android:id="@+id/divider"
+        android:id="@+id/dividerCameraUpload"
         android:layout_width="match_parent"
         android:layout_height="1dp"
-        android:layout_marginBottom="@dimen/standard_half_margin"
         android:layout_marginLeft="@dimen/standard_margin"
+        android:layout_marginTop="@dimen/standard_half_margin"
         android:layout_marginRight="@dimen/standard_margin"
+        android:layout_marginBottom="@dimen/standard_half_margin"
+        android:background="@color/list_divider_background" />
+
+    <LinearLayout
+        android:id="@+id/menu_direct_camera_upload"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        android:paddingLeft="@dimen/standard_padding"
+        android:paddingTop="@dimen/standard_half_padding"
+        android:paddingRight="@dimen/standard_padding"
+        android:paddingBottom="@dimen/standard_half_padding"
+        tools:ignore="UseCompoundDrawables">
+
+        <ImageView
+            android:id="@+id/menu_icon_direct_camera_upload"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:contentDescription="@null"
+            android:src="@drawable/ic_camera"
+            android:tint="@color/primary" />
+
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:layout_marginStart="@dimen/standard_margin"
+            android:layout_marginLeft="@dimen/standard_margin"
+            android:text="@string/upload_direct_camera_upload"
+            android:textColor="@color/black"
+            android:textSize="@dimen/bottom_sheet_text_size" />
+
+    </LinearLayout>
+
+    <View
+        android:id="@+id/divider"
+        android:layout_width="match_parent"
+        android:layout_height="1dp"
+        android:layout_marginLeft="@dimen/standard_margin"
         android:layout_marginTop="@dimen/standard_half_margin"
+        android:layout_marginRight="@dimen/standard_margin"
+        android:layout_marginBottom="@dimen/standard_half_margin"
         android:background="@color/list_divider_background"/>
 
     <LinearLayout
@@ -107,10 +148,10 @@
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
-        android:paddingBottom="@dimen/standard_padding"
         android:paddingLeft="@dimen/standard_padding"
-        android:paddingRight="@dimen/standard_padding"
         android:paddingTop="@dimen/standard_half_padding"
+        android:paddingRight="@dimen/standard_padding"
+        android:paddingBottom="@dimen/standard_padding"
         tools:ignore="UseCompoundDrawables">
 
         <ImageView
@@ -125,8 +166,8 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_gravity="center_vertical"
-            android:layout_marginLeft="@dimen/standard_margin"
             android:layout_marginStart="@dimen/standard_margin"
+            android:layout_marginLeft="@dimen/standard_margin"
             android:text="@string/create_new_folder"
             android:textColor="@color/black"
             android:textSize="@dimen/bottom_sheet_text_size"/>

+ 2 - 0
src/main/res/values/strings.xml

@@ -762,6 +762,7 @@
     <string name="hint_password">Password</string>
     <string name="add_to_cloud">Add to %1$s</string>
     <string name="upload_files">Upload files</string>
+    <string name="upload_direct_camera_upload">Upload from camera</string>
     <string name="upload_content_from_other_apps">Upload content from other apps</string>
     <string name="create_new_folder">Create new folder</string>
     <string name="uploads_view_upload_status_virus_detected">Virus detected. Upload cannot be completed!</string>
@@ -845,6 +846,7 @@
     <string name="battery_optimization_close">Close</string>
     <string name="battery_optimization_no_setting">Unable to start battery settings directly. Please adjust manually in settings.</string>
     <string name="file_details_no_content">Failed to load details</string>
+    <string name="error_starting_direct_camera_upload">Error starting camera</string>
     <string name="uploader_upload_files_behaviour_not_writable">source folder is read-only; file will only be uploaded</string>
     <string name="auto_upload_file_behaviour_kept_in_folder">kept in original folder, as it is readonly</string>
 </resources>