Browse Source

WIP storage picker
improved icons/layout for storage path chooser
use dir name for external sdcard, add only if exists
ignore findbugs regarding hardcoded storage path and use better var names
improve codacy sccore, make interla storage paths a set
setup path chooser in picker action bar when storage location has been chosen
add missing licenses header, set correct year
show local storage path picker dialog if folder to navigate into is not readable
fix crashing app due to open dialog

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>

tobiasKaminsky 6 years ago
parent
commit
860e8e7ff4

+ 1 - 0
drawable_resources/ic_document.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="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M15,18V16H6V18H15M18,14V12H6V14H18Z" /></svg>

+ 1 - 0
drawable_resources/ic_download.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="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" /></svg>

+ 1 - 0
drawable_resources/ic_file-document-outline.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="M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z" /></svg>

+ 1 - 0
drawable_resources/ic_image.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="M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z" /></svg>

+ 1 - 0
drawable_resources/ic_movie.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="M18,4L20,8H17L15,4H13L15,8H12L10,4H8L10,8H7L5,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V4H18Z" /></svg>

+ 1 - 0
drawable_resources/ic_music.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="M21,3V15.5A3.5,3.5 0 0,1 17.5,19A3.5,3.5 0 0,1 14,15.5A3.5,3.5 0 0,1 17.5,12C18.04,12 18.55,12.12 19,12.34V6.47L9,8.6V17.5A3.5,3.5 0 0,1 5.5,21A3.5,3.5 0 0,1 2,17.5A3.5,3.5 0 0,1 5.5,14C6.04,14 6.55,14.12 7,14.34V6L21,3Z" /></svg>

+ 1 - 0
drawable_resources/ic_sd.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="M18,8H16V4H18M15,8H13V4H15M12,8H10V4H12M18,2H10L4,8V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" /></svg>

+ 55 - 7
src/main/java/com/owncloud/android/ui/activity/UploadFilesActivity.java

@@ -46,10 +46,12 @@ 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.adapter.StoragePathAdapter;
 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;
+import com.owncloud.android.ui.dialog.LocalStoragePathPickerDialogFragment;
 import com.owncloud.android.ui.dialog.SortingOrderDialogFragment;
 import com.owncloud.android.ui.fragment.ExtendedListFragment;
 import com.owncloud.android.ui.fragment.LocalFileListFragment;
@@ -78,7 +80,7 @@ import androidx.fragment.app.FragmentTransaction;
 public class UploadFilesActivity extends FileActivity implements
     LocalFileListFragment.ContainerActivity, ActionBar.OnNavigationListener,
     OnClickListener, ConfirmationDialogFragmentListener, SortingOrderDialogFragment.OnSortingOrderListener,
-    CheckAvailableSpaceTask.CheckAvailableSpaceListener {
+    CheckAvailableSpaceTask.CheckAvailableSpaceListener, StoragePathAdapter.StoragePathAdapterListener {
 
     private static final String SORT_ORDER_DIALOG_TAG = "SORT_ORDER_DIALOG";
     private static final int SINGLE_DIR = 1;
@@ -116,6 +118,7 @@ public class UploadFilesActivity extends FileActivity implements
     private static final String QUERY_TO_MOVE_DIALOG_TAG = "QUERY_TO_MOVE";
     public static final String REQUEST_CODE_KEY = "requestCode";
     private int requestCode;
+    private LocalStoragePathPickerDialogFragment dialog;
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -152,12 +155,7 @@ public class UploadFilesActivity extends FileActivity implements
 
         // Drop-down navigation
         mDirectories = new CustomArrayAdapter<>(this, R.layout.support_simple_spinner_dropdown_item);
-        File currDir = mCurrentDir;
-        while(currDir != null && currDir.getParentFile() != null) {
-            mDirectories.add(currDir.getName());
-            currDir = currDir.getParentFile();
-        }
-        mDirectories.add(File.separator);
+        fillDirectoryDropdown();
 
         // Inflate and set the layout view
         setContentView(R.layout.upload_files_layout);
@@ -224,6 +222,15 @@ public class UploadFilesActivity extends FileActivity implements
         Log_OC.d(TAG, "onCreate() end");
     }
 
+    private void fillDirectoryDropdown() {
+        File currentDir = mCurrentDir;
+        while (currentDir != null && currentDir.getParentFile() != null) {
+            mDirectories.add(currentDir.getName());
+            currentDir = currentDir.getParentFile();
+        }
+        mDirectories.add(File.separator);
+    }
+
     /**
      * Helper to launch the UploadFilesActivity for which you would like a result when it finished.
      * Your onActivityResult() method will be called with the given requestCode.
@@ -306,6 +313,9 @@ public class UploadFilesActivity extends FileActivity implements
                 }
                 break;
             }
+            case R.id.action_choose_storage_path: {
+                showLocalStoragePathPickerDialog();
+            }
             default:
                 retval = super.onOptionsItemSelected(item);
                 break;
@@ -313,6 +323,14 @@ public class UploadFilesActivity extends FileActivity implements
         return retval;
     }
 
+    private void showLocalStoragePathPickerDialog() {
+        FragmentManager fm = getSupportFragmentManager();
+        FragmentTransaction ft = fm.beginTransaction();
+        ft.addToBackStack(null);
+        dialog = LocalStoragePathPickerDialogFragment.newInstance();
+        dialog.show(ft, LocalStoragePathPickerDialogFragment.LOCAL_STORAGE_PATH_PICKER_FRAGMENT);
+    }
+
     @Override
     public void onSortingOrderChosen(FileSortOrder selection) {
         mFileListFragment.sortFiles(selection);
@@ -354,6 +372,13 @@ public class UploadFilesActivity extends FileActivity implements
                 finish();
                 return;
             }
+
+            File parentFolder = mCurrentDir.getParentFile();
+            if (!parentFolder.canRead()) {
+                showLocalStoragePathPickerDialog();
+                return;
+            }
+
             popDirname();
             mFileListFragment.onNavigateUp();
             mCurrentDir = mFileListFragment.getCurrentDirectory();
@@ -488,6 +513,20 @@ public class UploadFilesActivity extends FileActivity implements
         }
     }
 
+    @Override
+    public void chosenPath(String path) {
+        if (getListOfFilesFragment() instanceof LocalFileListFragment) {
+            File file = new File(path);
+            ((LocalFileListFragment) getListOfFilesFragment()).listDirectory(file);
+            onDirectoryClick(file);
+
+            mCurrentDir = new File(path);
+            mDirectories.clear();
+
+            fillDirectoryDropdown();
+        }
+    }
+
     /**
      * Custom array adapter to override text colors
      */
@@ -659,4 +698,13 @@ public class UploadFilesActivity extends FileActivity implements
         Log_OC.e(TAG, "Access to unexisting list of files fragment!!");
         return null;
     }
+
+    @Override
+    protected void onStop() {
+        if (dialog != null) {
+            dialog.dismiss();
+        }
+
+        super.onStop();
+    }
 }

+ 96 - 0
src/main/java/com/owncloud/android/ui/adapter/StoragePathAdapter.java

@@ -0,0 +1,96 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2019 Andy Scherzinger
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.owncloud.android.R;
+
+import java.util.List;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+
+public class StoragePathAdapter extends RecyclerView.Adapter<StoragePathAdapter.StoragePathViewHolder> {
+    private List<StoragePathItem> pathList;
+    private StoragePathAdapterListener storagePathAdapterListener;
+
+    public StoragePathAdapter(List<StoragePathItem> pathList, StoragePathAdapterListener storagePathAdapterListener) {
+        this.pathList = pathList;
+        this.storagePathAdapterListener = storagePathAdapterListener;
+    }
+
+    @NonNull
+    @Override
+    public StoragePathViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+        View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.storage_path_item, parent, false);
+        return new StoragePathAdapter.StoragePathViewHolder(v);
+    }
+
+    @Override
+    public void onBindViewHolder(@NonNull StoragePathViewHolder holder, int position) {
+        if (pathList != null && pathList.size() > position) {
+            StoragePathItem storagePathItem = pathList.get(position);
+
+            holder.icon.setImageResource(storagePathItem.getIcon());
+            holder.name.setText(storagePathItem.getName());
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        return pathList.size();
+    }
+
+    public interface StoragePathAdapterListener {
+        /**
+         * sets the chosen path.
+         *
+         * @param path chosen path
+         */
+        void chosenPath(String path);
+    }
+
+    class StoragePathViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+        @BindView(R.id.icon)
+        ImageView icon;
+        @BindView(R.id.name)
+        TextView name;
+
+        public StoragePathViewHolder(@NonNull View itemView) {
+            super(itemView);
+            ButterKnife.bind(this, itemView);
+            itemView.setOnClickListener(this);
+        }
+
+        @Override
+        public void onClick(View view) {
+            storagePathAdapterListener.chosenPath(pathList.get(getAdapterPosition()).getPath());
+        }
+    }
+}

+ 37 - 0
src/main/java/com/owncloud/android/ui/adapter/StoragePathItem.java

@@ -0,0 +1,37 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2019 Andy Scherzinger
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.adapter;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * UI POJO for the storage path list.
+ */
+@Getter
+@Setter
+@AllArgsConstructor
+public class StoragePathItem {
+    private int icon;
+    private String name;
+    private String path;
+}

+ 194 - 0
src/main/java/com/owncloud/android/ui/dialog/LocalStoragePathPickerDialogFragment.java

@@ -0,0 +1,194 @@
+/*
+ * Nextcloud Android client application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2019 Andy Scherzinger
+ *
+ * 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.dialog;
+
+import android.annotation.SuppressLint;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.owncloud.android.R;
+import com.owncloud.android.ui.adapter.StoragePathAdapter;
+import com.owncloud.android.ui.adapter.StoragePathItem;
+import com.owncloud.android.utils.FileStorageUtils;
+import com.owncloud.android.utils.ThemeUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AlertDialog;
+import androidx.fragment.app.DialogFragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.Unbinder;
+
+/**
+ * Picker dialog for choosing a (storage) path.
+ */
+public class LocalStoragePathPickerDialogFragment extends DialogFragment
+    implements DialogInterface.OnClickListener, StoragePathAdapter.StoragePathAdapterListener {
+
+    public static final String LOCAL_STORAGE_PATH_PICKER_FRAGMENT = "LOCAL_STORAGE_PATH_PICKER_FRAGMENT";
+
+    private static Set<String> internalStoragePaths = new HashSet<>();
+
+    static {
+        internalStoragePaths.add("/storage/emulated/legacy");
+        internalStoragePaths.add("/storage/emulated/0");
+        internalStoragePaths.add("/mnt/sdcard");
+    }
+
+    private Unbinder unbinder;
+
+    @BindView(R.id.storage_path_recycler_view)
+    RecyclerView recyclerView;
+
+    public static LocalStoragePathPickerDialogFragment newInstance() {
+        return new LocalStoragePathPickerDialogFragment();
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        int color = ThemeUtils.primaryAccentColor(getContext());
+
+        AlertDialog alertDialog = (AlertDialog) getDialog();
+
+        alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(color);
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+    }
+
+    @NonNull
+    @Override
+    public Dialog onCreateDialog(Bundle savedInstanceState) {
+        if (!(getActivity() instanceof StoragePathAdapter.StoragePathAdapterListener)) {
+            throw new IllegalArgumentException("Calling activity must implement " +
+                "StoragePathAdapter.StoragePathAdapterListener");
+        }
+
+        int accentColor = ThemeUtils.primaryAccentColor(getContext());
+
+        // Inflate the layout for the dialog
+        LayoutInflater inflater = requireActivity().getLayoutInflater();
+        @SuppressLint("InflateParams") View view = inflater.inflate(R.layout.storage_path_dialog, null, false);
+
+        StoragePathAdapter adapter = new StoragePathAdapter(getPathList(), this);
+
+        unbinder = ButterKnife.bind(this, view);
+        recyclerView.setAdapter(adapter);
+        recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
+
+        // Build the dialog
+        AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
+        builder.setView(view)
+            .setNegativeButton(R.string.common_cancel, this)
+            .setTitle(ThemeUtils.getColoredTitle(getResources().getString(R.string.storage_choose_location),
+                accentColor));
+
+        return builder.create();
+    }
+
+    @Override
+    public void onStop() {
+        unbinder.unbind();
+
+        super.onStop();
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        if (which == AlertDialog.BUTTON_NEGATIVE) {
+            dismissAllowingStateLoss();
+        }
+    }
+
+    private List<StoragePathItem> getPathList() {
+        List<StoragePathItem> storagePathItems = new ArrayList<>();
+
+        addIfExists(storagePathItems, new StoragePathItem(R.drawable.ic_image_grey600,
+                                                          getString(R.string.storage_pictures),
+                                                          Environment.getExternalStoragePublicDirectory(
+                                                              Environment.DIRECTORY_PICTURES).getAbsolutePath()));
+        addIfExists(storagePathItems, new StoragePathItem(R.drawable.ic_camera, getString(R.string.storage_camera),
+                                                          Environment.getExternalStoragePublicDirectory(
+                                                              Environment.DIRECTORY_DCIM).getAbsolutePath()));
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            addIfExists(storagePathItems, new StoragePathItem(R.drawable.ic_document_grey600,
+                                                              getString(R.string.storage_documents),
+                                                              Environment.getExternalStoragePublicDirectory(
+                                                                  Environment.DIRECTORY_DOCUMENTS).getAbsolutePath()));
+        }
+        addIfExists(storagePathItems, new StoragePathItem(R.drawable.ic_download_grey600,
+                                                          getString(R.string.storage_downloads),
+                                                          Environment.getExternalStoragePublicDirectory(
+                                                              Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()));
+        addIfExists(storagePathItems, new StoragePathItem(R.drawable.ic_movie_grey600,
+                                                          getString(R.string.storage_movies),
+                                                          Environment.getExternalStoragePublicDirectory(
+                                                              Environment.DIRECTORY_MOVIES).getAbsolutePath()));
+        addIfExists(storagePathItems, new StoragePathItem(R.drawable.ic_music_grey600,
+                                                          getString(R.string.storage_music),
+                                                          Environment.getExternalStoragePublicDirectory(
+                                                              Environment.DIRECTORY_MUSIC).getAbsolutePath()));
+
+        String sdCard = getString(R.string.storage_internal_storage);
+        for (String dir : FileStorageUtils.getStorageDirectories(requireActivity())) {
+            if (internalStoragePaths.contains(dir)) {
+                addIfExists(storagePathItems, new StoragePathItem(R.drawable.ic_sd_grey600, sdCard, dir));
+            } else {
+                addIfExists(storagePathItems, new StoragePathItem(R.drawable.ic_sd_grey600, new File(dir).getName(), dir));
+            }
+        }
+
+        return storagePathItems;
+    }
+
+    private void addIfExists(List<StoragePathItem> storagePathItems, StoragePathItem item) {
+        File path = new File(item.getPath());
+        if (path.exists() && path.canRead()) {
+            storagePathItems.add(item);
+        }
+    }
+
+    @Override
+    public void chosenPath(String path) {
+        if (getActivity() != null) {
+            ((StoragePathAdapter.StoragePathAdapterListener) getActivity()).chosenPath(path);
+        }
+        dismissAllowingStateLoss();
+    }
+}

+ 140 - 3
src/main/java/com/owncloud/android/utils/FileStorageUtils.java

@@ -19,9 +19,16 @@
 
 package com.owncloud.android.utils;
 
+import android.Manifest;
 import android.accounts.Account;
+import android.annotation.TargetApi;
+import android.app.Activity;
 import android.content.Context;
+import android.content.pm.PackageManager;
 import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.text.TextUtils;
 import android.util.Log;
 import android.webkit.MimeTypeMap;
 
@@ -39,14 +46,18 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
 import java.util.TimeZone;
 
+import androidx.core.app.ActivityCompat;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
+import static android.os.Build.VERSION.SDK_INT;
+
 
 /**
  * Static methods to help in access to local file system.
@@ -54,7 +65,8 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 public final class FileStorageUtils {
     private static final String TAG = FileStorageUtils.class.getSimpleName();
 
-    public static final String PATTERN_YYYY_MM = "yyyy/MM/";
+    private static final String PATTERN_YYYY_MM = "yyyy/MM/";
+    private static final String DEFAULT_FALLBACK_STORAGE_PATH = "/storage/sdcard0";
 
     private FileStorageUtils() {
         // utility class -> private constructor
@@ -129,7 +141,7 @@ public final class FileStorageUtils {
      * @param date: date in microseconds since 1st January 1970
      * @return string: yyyy/mm/
      */
-    private static String getSubpathFromDate(long date, Locale currentLocale) {
+    private static String getSubPathFromDate(long date, Locale currentLocale) {
         if (date == 0) {
             return "";
         }
@@ -156,7 +168,7 @@ public final class FileStorageUtils {
                                                   Boolean subfolderByDate) {
         String subPath = "";
         if (subfolderByDate) {
-            subPath = getSubpathFromDate(dateTaken, current);
+            subPath = getSubPathFromDate(dateTaken, current);
         }
 
         return remotePath + OCFile.PATH_SEPARATOR + subPath + (fileName == null ? "" : fileName);
@@ -422,4 +434,129 @@ public final class FileStorageUtils {
         }
         return false;
     }
+
+    /**
+     * Taken from https://github.com/TeamAmaze/AmazeFileManager/blob/54652548223d151f089bdc6fc868b13ca5ab20a9/app/src
+     * /main/java/com/amaze/filemanager/activities/MainActivity.java#L620 on 14.02.2019
+     */
+    @SuppressFBWarnings(value = "DMI_HARDCODED_ABSOLUTE_FILENAME", 
+        justification = "Default Android fallback storage path")
+    public static List<String> getStorageDirectories(Activity activity) {
+        // Final set of paths
+        final List<String> rv = new ArrayList<>();
+        // Primary physical SD-CARD (not emulated)
+        final String rawExternalStorage = System.getenv("EXTERNAL_STORAGE");
+        // All Secondary SD-CARDs (all exclude primary) separated by ":"
+        final String rawSecondaryStoragesStr = System.getenv("SECONDARY_STORAGE");
+        // Primary emulated SD-CARD
+        final String rawEmulatedStorageTarget = System.getenv("EMULATED_STORAGE_TARGET");
+        if (TextUtils.isEmpty(rawEmulatedStorageTarget)) {
+            // Device has physical external storage; use plain paths.
+            if (TextUtils.isEmpty(rawExternalStorage)) {
+                // EXTERNAL_STORAGE undefined; falling back to default.
+                // Check for actual existence of the directory before adding to list
+                if (new File(DEFAULT_FALLBACK_STORAGE_PATH).exists()) {
+                    rv.add(DEFAULT_FALLBACK_STORAGE_PATH);
+                } else {
+                    //We know nothing else, use Environment's fallback
+                    rv.add(Environment.getExternalStorageDirectory().getAbsolutePath());
+                }
+            } else {
+                rv.add(rawExternalStorage);
+            }
+        } else {
+            // Device has emulated storage; external storage paths should have
+            // userId burned into them.
+            final String rawUserId;
+            if (SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+                rawUserId = "";
+            } else {
+                final String path = Environment.getExternalStorageDirectory().getAbsolutePath();
+                final String[] folders = OCFile.PATH_SEPARATOR.split(path);
+                final String lastFolder = folders[folders.length - 1];
+                boolean isDigit = false;
+                try {
+                    Integer.valueOf(lastFolder);
+                    isDigit = true;
+                } catch (NumberFormatException ignored) {
+                }
+                rawUserId = isDigit ? lastFolder : "";
+            }
+            // /storage/emulated/0[1,2,...]
+            if (TextUtils.isEmpty(rawUserId)) {
+                rv.add(rawEmulatedStorageTarget);
+            } else {
+                rv.add(rawEmulatedStorageTarget + File.separator + rawUserId);
+            }
+        }
+        // Add all secondary storages
+        if (!TextUtils.isEmpty(rawSecondaryStoragesStr)) {
+            // All Secondary SD-CARDs splited into array
+            final String[] rawSecondaryStorages = rawSecondaryStoragesStr.split(File.pathSeparator);
+            Collections.addAll(rv, rawSecondaryStorages);
+        }
+        if (SDK_INT >= Build.VERSION_CODES.M && checkStoragePermission(activity)) {
+            rv.clear();
+        }
+        if (SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            String strings[] = getExtSdCardPathsForActivity(activity);
+            File f;
+            for (String s : strings) {
+                f = new File(s);
+                if (!rv.contains(s) && canListFiles(f)) {
+                    rv.add(s);
+                }
+            }
+        }
+
+        return rv;
+    }
+
+    /**
+     * Taken from https://github.com/TeamAmaze/AmazeFileManager/blob/d11e0d2874c6067910e58e059859431a31ad6aee/app/src
+     * /main/java/com/amaze/filemanager/activities/superclasses/PermissionsActivity.java#L47 on
+     * 14.02.2019
+     */
+    private static boolean checkStoragePermission(Activity activity) {
+        // Verify that all required contact permissions have been granted.
+        return ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            == PackageManager.PERMISSION_GRANTED;
+    }
+
+    /**
+     * Taken from https://github.com/TeamAmaze/AmazeFileManager/blob/616f2a696823ab0e64ea7a017602dc08e783162e/app/src
+     * /main/java/com/amaze/filemanager/filesystem/FileUtil.java#L764 on 14.02.2019
+     */
+    @TargetApi(Build.VERSION_CODES.KITKAT)
+    private static String[] getExtSdCardPathsForActivity(Context context) {
+        List<String> paths = new ArrayList<>();
+        for (File file : context.getExternalFilesDirs("external")) {
+            if (file != null) {
+                int index = file.getAbsolutePath().lastIndexOf("/Android/data");
+                if (index < 0) {
+                    Log_OC.w(TAG, "Unexpected external file dir: " + file.getAbsolutePath());
+                } else {
+                    String path = file.getAbsolutePath().substring(0, index);
+                    try {
+                        path = new File(path).getCanonicalPath();
+                    } catch (IOException e) {
+                        // Keep non-canonical path.
+                    }
+                    paths.add(path);
+                }
+            }
+        }
+        if (paths.isEmpty()) {
+            paths.add("/storage/sdcard1");
+        }
+        return paths.toArray(new String[0]);
+    }
+
+    /**
+     * Taken from https://github.com/TeamAmaze/AmazeFileManager/blob/9cf1fd5ff1653c692cb54cf6bc71b572c19a11cd/app/src
+     * /main/java/com/amaze/filemanager/utils/files/FileUtils.java#L754 on 14.02.2019
+     */
+    private static boolean canListFiles(File f) {
+        return f.canRead() && f.isDirectory();
+    }
 }

+ 18 - 3
src/main/res/drawable/ic_camera.xml

@@ -1,8 +1,23 @@
-<!-- drawable/camera.xml -->
+<!--
+    @author Google LLC
+    Copyright (C) 2019 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
 <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>
+    <path android:fillColor="#757575" 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>

+ 26 - 0
src/main/res/drawable/ic_document_grey600.xml

@@ -0,0 +1,26 @@
+<!--
+    @author Austin Andrews
+    Copyright (C) 2019 Austin Andrews
+
+    This Font Software is licensed under the SIL Open Font License, Version 1.1.
+	This license is available with a FAQ at:
+
+    https://scripts.sil.org/OFL
+
+    THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+    OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+    COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+    INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+    DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+    OTHER DEALINGS IN THE FONT SOFTWARE.
+-->
+<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="#757575" android:pathData="M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M15,18V16H6V18H15M18,14V12H6V14H18Z" />
+</vector>

+ 23 - 0
src/main/res/drawable/ic_download_grey600.xml

@@ -0,0 +1,23 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2019 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<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="#757575" android:pathData="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" />
+</vector>

+ 23 - 0
src/main/res/drawable/ic_image_grey600.xml

@@ -0,0 +1,23 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2019 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<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="#757575" android:pathData="M8.5,13.5L11,16.5L14.5,12L19,18H5M21,19V5C21,3.89 20.1,3 19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19Z" />
+</vector>

+ 23 - 0
src/main/res/drawable/ic_movie_grey600.xml

@@ -0,0 +1,23 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2019 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<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="#757575" android:pathData="M18,4L20,8H17L15,4H13L15,8H12L10,4H8L10,8H7L5,4H4A2,2 0 0,0 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V4H18Z" />
+</vector>

+ 26 - 0
src/main/res/drawable/ic_music_grey600.xml

@@ -0,0 +1,26 @@
+<!--
+    @author Austin Andrews
+    Copyright (C) 2019 Austin Andrews
+
+    This Font Software is licensed under the SIL Open Font License, Version 1.1.
+	This license is available with a FAQ at:
+
+    https://scripts.sil.org/OFL
+
+    THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+    EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+    MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+    OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+    COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+    INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+    DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+    FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+    OTHER DEALINGS IN THE FONT SOFTWARE.
+-->
+<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="#757575" android:pathData="M21,3V15.5A3.5,3.5 0 0,1 17.5,19A3.5,3.5 0 0,1 14,15.5A3.5,3.5 0 0,1 17.5,12C18.04,12 18.55,12.12 19,12.34V6.47L9,8.6V17.5A3.5,3.5 0 0,1 5.5,21A3.5,3.5 0 0,1 2,17.5A3.5,3.5 0 0,1 5.5,14C6.04,14 6.55,14.12 7,14.34V6L21,3Z" />
+</vector>

+ 23 - 0
src/main/res/drawable/ic_sd.xml

@@ -0,0 +1,23 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2019 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<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="#fff" android:pathData="M18,8H16V4H18M15,8H13V4H15M12,8H10V4H12M18,2H10L4,8V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
+</vector>

+ 23 - 0
src/main/res/drawable/ic_sd_grey600.xml

@@ -0,0 +1,23 @@
+<!--
+    @author Google LLC
+    Copyright (C) 2019 Google LLC
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<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="#757575" android:pathData="M18,8H16V4H18M15,8H13V4H15M12,8H10V4H12M18,2H10L4,8V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
+</vector>

+ 32 - 0
src/main/res/layout/storage_path_dialog.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  @author Andy Scherzinger
+  Copyright (C) 2019 Andy Scherzinger
+
+  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/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical"
+    android:padding="@dimen/standard_padding">
+
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/storage_path_recycler_view"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"/>
+
+</LinearLayout>

+ 56 - 0
src/main/res/layout/storage_path_item.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2019 Andy Scherzinger
+
+  This program is free software; you can redistribute it and/or
+  modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+  License as published by the Free Software Foundation; either
+  version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+
+  You should have received a copy of the GNU Affero General Public
+  License along with this program. If not, see <http://www.gnu.org/licenses/>.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="horizontal"
+    android:paddingTop="@dimen/standard_half_padding"
+    android:paddingBottom="@dimen/standard_half_padding"
+    android:weightSum="1"
+    tools:ignore="UseCompoundDrawables">
+
+    <ImageView
+        android:id="@+id/icon"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_vertical"
+        android:background="@color/white"
+        android:contentDescription="@string/user_icon"
+        android:paddingStart="@dimen/standard_padding"
+        android:paddingLeft="@dimen/standard_padding"
+        android:paddingEnd="@dimen/standard_padding"
+        android:paddingRight="@dimen/standard_padding"
+        android:src="@drawable/ic_user" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_width="0dp"
+        android:layout_height="match_parent"
+        android:layout_marginEnd="@dimen/standard_margin"
+        android:layout_marginRight="@dimen/standard_margin"
+        android:layout_weight="1"
+        android:ellipsize="end"
+        android:gravity="center_vertical"
+        android:singleLine="true"
+        android:textColor="@color/black"
+        android:textSize="@dimen/file_details_username_text_size"
+        tools:text="DCIM" />
+</LinearLayout>

+ 9 - 4
src/main/res/menu/upload_files_picker.xml

@@ -20,11 +20,16 @@
       xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <item android:id="@+id/action_search"
-          android:icon="@drawable/ic_search"
-          android:title="@string/actionbar_search"
-          android:contentDescription="@string/actionbar_search"
+        android:contentDescription="@string/actionbar_search"
+        android:icon="@drawable/ic_search"
+        android:title="@string/actionbar_search"
         app:actionViewClass="androidx.appcompat.widget.SearchView"
-          app:showAsAction="ifRoom"/>
+        app:showAsAction="ifRoom" />
+    <item android:id="@+id/action_choose_storage_path"
+        android:icon="@drawable/ic_sd"
+        android:title="@string/actionbar_search"
+        android:contentDescription="@string/actionbar_search"
+        app:showAsAction="ifRoom"/>
     <item
         android:id="@+id/action_select_all"
         android:checkable="true"

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

@@ -862,4 +862,12 @@
     <string name="notification_action_failed">Failed to execute action.</string>
     <string name="remove_push_notification">Remove</string>
     <string name="new_notification">New Notification</string>
+    <string name="storage_choose_location">Choose storage location</string>
+    <string name="storage_internal_storage">Internal storage</string>
+    <string name="storage_camera">Camera</string>
+    <string name="storage_pictures">Pictures</string>
+    <string name="storage_movies">Movies</string>
+    <string name="storage_music">Music</string>
+    <string name="storage_documents">Documents</string>
+    <string name="storage_downloads">Downloads</string>
 </resources>