Ответ 1
Любое приложение, зарегистрированное для этого намерения, должно иметь возможность обрабатывать файлы с тем же именем файла, но с разными путями. Чтобы иметь возможность справиться с тем, что доступ к файлам, предоставляемым другими приложениями, возможен только при запуске операции получения (см. "Исключение безопасности" при попытке получить доступ к изображению Picasa на запущенном устройстве 4.2 или SecurityException при загрузке изображений с помощью Universal-Image-Downloader), получение приложений должно копировать файлы в каталог, к которому они постоянно имеют доступ. Я предполагаю, что некоторые приложения не реализовали этот процесс копирования для работы с идентичными именами файлов (при копировании путь к файлу, вероятно, будет одинаковым для всех файлов).
Я бы предложил использовать файлы через ContentProvider, а не напрямую из файловой системы. Таким образом вы можете создать уникальное имя файла для каждого файла, который хотите отправить.
Прием приложений "должен" получать файлы более или менее:
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(uri, new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, null, null, null);
// retrieve name and size columns from the cursor...
InputStream in = contentResolver.openInputStream(uri);
// copy file from the InputStream
Так как приложения должны открывать файл, используя contentResolver.openInputStream(), ContentProvider должен/будет работать, а не просто передавать файл uri в Intent. Конечно, могут быть приложения, которые плохо себя ведут, и это нужно тщательно протестировать, но в случае, если некоторые приложения не будут обрабатывать файлы, содержащиеся в ContentProvider, вы можете добавить два разных варианта обмена (одно наследие и обычный).
Для части ContentProvider: https://developer.android.com/reference/android/support/v4/content/FileProvider.html
К сожалению, есть и следующее:
FileProvider может генерировать только URI содержимого для файлов в каталоги, которые вы указываете заранее
Если вы можете определить все каталоги, с которыми вы хотите делиться файлами с момента создания приложения, FileProvider будет вашим лучшим вариантом. Я предполагаю, что ваше приложение захочет обмениваться файлами из любого каталога, поэтому вам понадобится ваша собственная реализация ContentProvider.
Задачами для решения являются:
- Как вы включаете путь к файлу в Uri, чтобы извлечь тот же самый путь на более позднем этапе (в ContentProvider)?
- Как создать уникальное имя файла, которое вы можете вернуть в ContentProvider в принимающее приложение? Это уникальное имя файла должно быть одинаковым для нескольких вызовов ContentProvider, что означает, что вы не можете создавать уникальный идентификатор всякий раз, когда вызывается ContentProvider, или вы получите другой вызов с каждым вызовом.
Проблема 1
ContentProvider Uri состоит из схемы (content://), полномочий и сегмента (ов) пути, например:
Содержание://lb.com.myapplication2.fileprovider/123/base.apk
Существует много решений первой проблемы. Я предлагаю base64 кодировать путь к файлу и использовать его в качестве последнего сегмента в Uri:
Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
Если путь к файлу указан, например:
/data/data/com.google.android.gm/base.apk
то получившийся Uri будет:
Содержание://lb.com.myapplication2.fileprovider/L2RhdGEvZGF0YS9jb20uZ29vZ2xlLmFuZHJvaWQuZ20vYmFzZS5hcGs=
Чтобы получить путь к файлу в ContentProvider, просто выполните:
String lastSegment = uri.getLastPathSegment();
String filePath = new String(Base64.decode(lastSegment, Base64.DEFAULT) );
Проблема 2
Решение довольно просто. Мы включаем уникальный идентификатор в Uri, сгенерированный при создании Intent. Этот идентификатор является частью Uri и может быть извлечен ContentProvider:
String encodedFileName = new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
String uniqueId = UUID.randomUUID().toString();
Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + uniqueId + "/" + encodedFileName );
Если путь к файлу указан, например:
/data/data/com.google.android.gm/base.apk
то получившийся Uri будет:
Содержание://lb.com.myapplication2.fileprovider/d2788038-53da-4e84-b10a-8d4ef95e8f5f/L2RhdGEvZGF0YS9jb20uZ29vZ2xlLmFuZHJvaWQuZ20vYmFzZS5hcGs=
Для получения уникального идентификатора в ContentProvider просто выполните:
List<String> segments = uri.getPathSegments();
String uniqueId = segments.size() > 0 ? segments.get(0) : "";
Единственным именем файла, которое возвращает ContentProvider, будет исходное имя файла (base.apk) плюс уникальный идентификатор, вставленный после имени базового файла. Например. base.apk становится базой < unique id > .apk.
Хотя это может звучать очень абстрактно, это должно стать ясным с полным кодом:
Намерение
intent=new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("*/*");
final ArrayList<Uri> uris=new ArrayList<>();
for(...)
String encodedFileName = new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
String uniqueId = UUID.randomUUID().toString();
Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + uniqueId + "/" + encodedFileName );
uris.add(uri);
}
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,uris);
ContentProvider
public class FileProvider extends ContentProvider {
private static final String[] DEFAULT_PROJECTION = new String[] {
MediaColumns.DATA,
MediaColumns.DISPLAY_NAME,
MediaColumns.SIZE,
};
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
String fileName = getFileName(uri);
if (fileName == null) return null;
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
String fileName = getFileName(uri);
if (fileName == null) return null;
File file = new File(fileName);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String fileName = getFileName(uri);
if (fileName == null) return null;
String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection;
MatrixCursor ret = new MatrixCursor(columnNames);
Object[] values = new Object[columnNames.length];
for (int i = 0, count = columnNames.length; i < count; i++) {
String column = columnNames[i];
if (MediaColumns.DATA.equals(column)) {
values[i] = uri.toString();
}
else if (MediaColumns.DISPLAY_NAME.equals(column)) {
values[i] = getUniqueName(uri);
}
else if (MediaColumns.SIZE.equals(column)) {
File file = new File(fileName);
values[i] = file.length();
}
}
ret.addRow(values);
return ret;
}
private String getFileName(Uri uri) {
String path = uri.getLastPathSegment();
return path != null ? new String(Base64.decode(path, Base64.DEFAULT)) : null;
}
private String getUniqueName(Uri uri) {
String path = getFileName(uri);
List<String> segments = uri.getPathSegments();
if (segments.size() > 0 && path != null) {
String baseName = FilenameUtils.getBaseName(path);
String extension = FilenameUtils.getExtension(path);
String uniqueId = segments.get(0);
return baseName + uniqueId + "." + extension;
}
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0; // not supported
}
@Override
public int delete(Uri uri, String arg1, String[] arg2) {
return 0; // not supported
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null; // not supported
}
}
Примечание:
- В моем примере кода используется библиотека org.apache.commons для манипуляций с именами файлов (FilenameUtils.getXYZ)
- с использованием кодировки base64 для пути к файлу является допустимым подходом, потому что все символы, используемые в base64 ([a-zA-Z0-9 _- =] в соответствии с этим fooobar.com/questions/257029/...) действительны в пути Ури (0-9, az, AZ, _-. ~ '() *,;: $& + =/@- > см. https://developer.android.com/reference/java/net/URI.html)
Ваш манифест должен был бы определить ContentProvider следующим образом:
<provider
android:name="lb.com.myapplication2.fileprovider.FileProvider"
android:authorities="lb.com.myapplication2.fileprovider"
android:exported="true"
android:grantUriPermissions="true"
android:multiprocess="true"/>
Он не будет работать без андроида: grantUriPermissions = "true" и android: exported = "true" , потому что у другого приложения не будет разрешения на доступ к ContentProvider (см. также http://developer.android.com/guide/topics/manifest/provider-element.html#exported). android: multiprocess = "true" , с другой стороны, является необязательным, но должен сделать его более эффективным.