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>
This commit is contained in:
tobiasKaminsky 2019-02-14 17:30:17 +01:00
parent 3e6d01965b
commit 860e8e7ff4
No known key found for this signature in database
GPG Key ID: 0E00D4D47D0C5AF7
24 changed files with 819 additions and 17 deletions

View File

@ -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>

After

Width:  |  Height:  |  Size: 422 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 336 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 411 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 410 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 401 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 515 B

View File

@ -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>

After

Width:  |  Height:  |  Size: 401 B

View File

@ -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();
}
}

View File

@ -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());
}
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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>