Подтвердить что ты не робот

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

Фон

У меня есть App-Manager app, что позволяет отправлять файлы 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. Вот о чем я думал:

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

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

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

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

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

Вопрос

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

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

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

4b9b3361

Ответ 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" , с другой стороны, является необязательным, но должен сделать его более эффективным.

Ответ 2

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

  • работает от API 14, а не от API 10, не уверен в промежутках между ними.
  • использует отражение, поэтому может не работать в будущем и на некоторых устройствах.
  • должен создавать символические ссылки на пути к "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);