Как отправить большой файл или несколько файлов в другие приложения и знать, когда их удалять?

Задний план

У меня есть приложение App-Manager , которое позволяет отправлять файлы APK в другие приложения.

До тех пор, пока Android 4.4 (в том числе), все, что мне нужно было сделать для этой задачи, – это отправить пути к исходным файлам APK (все они были в разделе «/ data / app / …», доступный даже без root).

Это код для отправки файлов (документы доступны здесь ):

intent=new Intent(Intent.ACTION_SEND_MULTIPLE); intent.setType("*/*"); final ArrayList<Uri> uris=new ArrayList<>(); for(...) uris.add(Uri.fromFile(new File(...))); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,uris); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NO_HISTORY|Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET|Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 

Проблема

То, что я делал, так как все APK-файлы приложений имели уникальное имя (это было имя пакета).

С тех пор, как Lollipop (5.0), все APK-файлы приложений просто называются «base.APK», что делает другие приложения неспособными понять их.

Это означает, что у меня есть некоторые опции для отправки файлов APK. Это то, о чем я думал:

  1. Скопируйте их все в папку, переименуйте их все в уникальные имена и затем отправьте их.

  2. Сжимайте их все в один файл, а затем отправляйте. Уровень сжатия может быть минимальным, поскольку файлы APK уже сжаты.

Проблема в том, что мне пришлось бы отправлять файлы как можно быстрее, и если мне действительно нужно иметь эти временные файлы (если только не существует другого решения), они также должны утилизировать их как можно быстрее.

Дело в том, что я не получаю уведомления, когда сторонние приложения закончили обработку временного файла, и я также думаю, что для выбора нескольких файлов потребуется некоторое время для подготовки независимо от того, что я выбираю.

Другая проблема заключается в том, что некоторые приложения (например, Gmail) фактически запрещают отправку файлов APK.

Вопрос

Есть ли альтернатива решениям, о которых я думал? Может быть, есть способ решить эту проблему со всеми преимуществами, которые у меня были раньше (быстро и без ненужных файлов осталось)?

Может быть, какой-то способ контролировать файл? Или создать поток вместо реального файла?

Поможет ли временный файл в папке с кешем?

Solutions Collecting From Web of "Как отправить большой файл или несколько файлов в другие приложения и знать, когда их удалять?"

Любое приложение, зарегистрированное для этого намерения, должно иметь возможность обрабатывать файлы с тем же именем файла, но с разными путями. Чтобы иметь возможность справиться с тем, что доступ к файлам, предоставляемым другими приложениями, может быть доступен только во время работы приема (см. « Исключение безопасности» при попытке получить доступ к изображению 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.

Проблемы, которые необходимо решить:

  1. Как включить путь файла в Uri, чтобы извлечь тот же самый путь на более позднем этапе (в ContentProvider)?
  2. Как создать уникальное имя файла, которое вы можете вернуть в 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); 

Поставщик услуг

 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 _- =] в соответствии с этим https://stackoverflow.com/a/6102233/534471 ), действительны в Путь Uri (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", с другой стороны, является необязательным, но должен сделать его более эффективным.

Вот работающее решение для использования SymLinks. Недостатки:

  1. Работает от API 14, а не от API 10, не уверен в промежутках между ними.
  2. Использует отражение, поэтому может не работать в будущем и на некоторых устройствах.
  3. Должен создавать символические ссылки на пути «getFilesDir», поэтому вам нужно управлять ими самостоятельно и при необходимости создавать уникальные имена файлов.

Образец разделяет APK текущего приложения.

Код:

 public class SymLinkActivity extends Activity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(lb.com.myapplication2.R.layout.activity_main); final Intent intent=new Intent(Intent.ACTION_SEND_MULTIPLE); intent.setType(MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk")); final String filePath; try { final android.content.pm.ApplicationInfo applicationInfo=getPackageManager().getApplicationInfo(getPackageName(),0); filePath=applicationInfo.sourceDir; } catch(NameNotFoundException e) { e.printStackTrace(); finish(); return; } final File file=new File(filePath); final String symcLinksFolderPath=getFilesDir().getAbsolutePath(); findViewById(R.id.button).setOnClickListener(new android.view.View.OnClickListener(){ @Override public void onClick(final android.view.View v) { final File symlink=new File(symcLinksFolderPath,"CustomizedNameOfApkFile-"+System.currentTimeMillis()+".apk"); symlink.getParentFile().mkdirs(); File[] oldSymLinks=new File(symcLinksFolderPath).listFiles(); if(oldSymLinks!=null) { for(java.io.File child : oldSymLinks) if(child.getName().endsWith(".apk")) child.delete(); } symlink.delete(); // do some dirty reflection to create the symbolic link try { final Class<?> libcore=Class.forName("libcore.io.Libcore"); final java.lang.reflect.Field fOs=libcore.getDeclaredField("os"); fOs.setAccessible(true); final Object os=fOs.get(null); final java.lang.reflect.Method method=os.getClass().getMethod("symlink",String.class,String.class); method.invoke(os,file.getAbsolutePath(),symlink.getAbsolutePath()); final ArrayList<Uri> uris=new ArrayList<>(); uris.add(Uri.fromFile(symlink)); intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,uris); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NO_HISTORY|Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET|Intent.FLAG_ACTIVITY_MULTIPLE_TASK); startActivity(intent); android.widget.Toast.makeText(SymLinkActivity.this,"succeeded ?",android.widget.Toast.LENGTH_SHORT).show(); } catch(Exception e) { android.widget.Toast.makeText(SymLinkActivity.this,"failed :(",android.widget.Toast.LENGTH_SHORT).show(); e.printStackTrace(); // TODO handle the exception } } }); } } 

EDIT: для части symlink для Android API 21 и выше вы можете использовать это вместо отражения:

  Os.symlink(originalFilePath,symLinkFilePath);