Важно (9.07.22)

Если картинки в постах не отображаются, зайдите в блог через прокси. РКН заблокировал поддомены blogger.com на которые загружались картинки.

четверг, 30 января 2014 г.

Пишем свой файловый менеджер для Android, #1

Решил попробовать написать простенький файловый менеджер, используя при этом модуль «System.IOUtils», чтобы умел перемещаться по папкам, открывать/удалять файлы. (Как вы могли заметить, в заголовке есть номер части, это значит, что будут ещё статьи, где я буду "допиливать" это приложение)
Описывать весь модуль «System.IOUtils» я не буду, т.к. в интернете и официальной справке вы можете найти достаточно подробное описание этого модуля и его классов, методов.
И так, поехали.

Статья №2 (Пишем свой файловый менеджер для Android, #2)

Upd (21.04.14). Проверил код на Delphi XE6 и добавил исходники для новой версии IDE.


Определяем минимальный функционал приложения:
1) Чтение, перемещение по директориям на устройстве (начинать будем с корня устройства, т.е. «/»)
2) Определять тип пути (папка или файл)
3) Удаление директорий и файлов
4) Открывать файлы с использованием Intent (намерения) в Android
5) Обязательно обрабатывать нажатие на хардварную кнопку «назад», как возврат в предыдущую директорию (до корня устройства)

Структура проекта:

Принцип работы:

Чтение, перемещение по директориям на устройстве
1) TForm1.FormCreate - При запуске приложения будем читать корневую директорию устройства.
2) TForm1.ListBox1ItemClick – При клике по итему, читаем выбранную директорию. (т.е. идём глубже по дереву)
3) TForm1.SpeedButton1Click (и TForm1.FormKeyUp) – Кнопка назад, читаем предыдущую директорию.

Для того чтобы использовать модуль «System.IOUtils», его нужно подключить в Uses.

Т.к. во всех трёх процедурах будет почти идентичный код, вынесем его в отдельную процедуру, я назвал её так «TForm1.TotalWork», а также для удобства создал процедуру для добавления пунктов в ListBox, это «TForm1.AddListItem». Функция «CompareLowerStr» - это условие для сортировки массивов. Ещё нам понадобится глобальная переменная «path», которая будет содержать текущий путь.

Определять тип пути (папка или файл)
Тут всё получилось не очень, но, в конце концов, я сделал всё красиво (как мне кажется…) :).
Я не обнаружил (в модуле System.IOUtils) методов для определения типа (файл или папка) входящего пути, решил попробовать написать решение сам. Прочитал в справке про то, что можно получать атрибуты папки/файла и подумал, что всё классно. Нужно всего-то реагировать на «faDirectory» и «faNormal». Чтобы запросить атрибуты папки используем «TDirectory.GetAttributes» и для файла «TFile.GetAttributes». Далее используя условный оператор, можно было бы определить тип, но не тут-то было… Почему-то, если выбрана не папка, а файл, то выскакивала табличка с ошибкой и дальше ни чего не происходило. Сейчас даже кода не осталось, чтобы показать вам, да и писал я уже ночью, примерно в 2-3 часа, так что может и сам чего накосячил, в итоге я отказался от одного только условного оператора и использовал «try except end;». Что из этого вышло(примерно), смотрите ниже.
Код:
uses
  System.IOUtils;

var
  Attrs: TFileAttributes; // Атрибуты папки или файла
  LItem: string; // Выбранная папка или файл для удаления

begin
  LItem := ListBox1.ListItems[i].ItemData.Detail

  try
    Attrs := TDirectory.GetAttributes(LItem, False);

    if TFileAttribute.faDirectory in Attrs then
    begin
      ...
    end;
  except
    Attrs := TFile.GetAttributes(LItem, False);

    if TFileAttribute.faNormal in Attrs then
    begin
      ...
    end;
  end;

Решение получилось совсем некрасивое и "малость" корявое. Поэтому мне приснилось другое решение. :)
Зачем каждый раз читать атрибуты файлов и папок, чтобы определить тип, если можно сделать это один раз. Т.к. я получаю отдельно список папок (GetDirectories) и список файлов (GetFiles), после чего сортирую и добавляю в ListBox, то мне уже заранее известно, где будут папки, а где файлы, поэтому при добавлении в ListBox я сразу записываю тип сюда «LItem.TagString := itype;». В дальнейшем просто считываю значение и всё.

Удаление директорий и файлов
Чтобы удалить папку или файл, нужно знать тип. В предыдущем шаге, я описал, как получаю тип выбранного пункта в ListBox’е. Не забываем проверять выбранные папки и файлы на существование при помощи TDirectory.Exists(path) и TFile.Exists(path). Удаляем при помощи TDirectory.Delete(path, true) и TFile.Delete(path).

Открывать файлы с использованием Intent (намерения) в Android
Объяснять, что такое Intent я не буду, позволю себе привести ссылку (Delphi XE5: использование Intent (намерения) в Android) на статью с описанием. Я использовал один из множества вариантов использования Intent’ов (намерений). Единственное, о чём забывают упомянуть в подобных статьях, как автоматически определить MIME-type (Android API: http://developer.android.com/reference/android/webkit/MimeTypeMap.html).

Код для определения MIME-type:
uses
  Androidapi.JNI.JavaTypes, Androidapi.JNI.Webkit;

var
  ExtFile: string;
  mime: JMimeTypeMap;
  ExtToMime: JString;

begin
  //Определяем расширение файла и его mime тип
  ExtFile := AnsiLowerCase(StringReplace(TPath.GetExtension(path), '.', '',[]));
  mime := TJMimeTypeMap.JavaClass.getSingleton();
  ExtToMime := mime.getMimeTypeFromExtension(StringToJString(ExtFile));

Код приложения (Delphi XE5 Update 2):
unit Unit1;

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs,
  FMX.ListBox, FMX.Layouts, FMX.StdCtrls;

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Label1: TLabel;
    ToolBar1: TToolBar;
    SpeedButton1: TSpeedButton;
    SpeedButton2: TSpeedButton;
    procedure FormCreate(Sender: TObject);
    procedure ListBox1ItemClick(const Sender: TCustomListBox;
      const Item: TListBoxItem);
    procedure SpeedButton1Click(Sender: TObject);
    procedure FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
      Shift: TShiftState);
    procedure ListBox1ChangeCheck(Sender: TObject);
    procedure SpeedButton2Click(Sender: TObject);
  private
    procedure AddListItem(list: array of string; itype: string);
    procedure TotalWork(path_tr: string; clear: boolean);
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.fmx}

uses
  System.IOUtils, System.Generics.Collections, Generics.Defaults,
  FMX.Helpers.Android, Androidapi.JNI.JavaTypes,
  Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Webkit;

var
  path: string; // Здесь будем хранить путь
  ItemsCheck: array of string; // Массив с помеченными пунктами

function CompareLowerStr(const Left, Right: string): Integer;
begin
  Result := CompareStr(AnsiLowerCase(Left), AnsiLowerCase(Right));
end;

{Процедура для вставки массивов в ListBox}
procedure TForm1.AddListItem(list: array of string; itype: string);
var
  c: integer;
  LItem: TListBoxItem;
begin

  ListBox1.BeginUpdate;

  for c := 0 to Length(list) - 1 do
  begin
    LItem := TListBoxItem.Create(ListBox1);
    LItem.ItemData.Text := ExtractFileName(list[c]);
    LItem.ItemData.Detail := list[c]; // Помещаем полный путь в Detail
    LItem.TagString := itype;
    ListBox1.AddObject(LItem);
  end;

  ListBox1.EndUpdate;

end;

{Загружаем список файлов в корне устройства}
procedure TForm1.FormCreate(Sender: TObject);
begin
  // Корень устройства
  path := '/';

  TotalWork(path, False);
end;

{Обрабатываем кнопку Hardware Back}
procedure TForm1.FormKeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
  Shift: TShiftState);
begin
  if Key = vkHardwareBack then
  begin
    if path <> '/' then
    begin
      SpeedButton1Click(Self);
      Key := 0;
    end;
  end;
end;

{Пункт пометили галкой}
procedure TForm1.ListBox1ChangeCheck(Sender: TObject);
var
  i: integer;
begin
  SetLength(ItemsCheck, 0);
  for i := 0 to ListBox1.Count-1 do
  begin
    if ListBox1.ListItems[i].IsChecked then
    begin
      SetLength(ItemsCheck, i + 1);
      ItemsCheck[i] := ListBox1.ListItems[i].Text;
    end;
  end;
end;

{Клик по Item'у, вперёд}
procedure TForm1.ListBox1ItemClick(const Sender: TCustomListBox;
  const Item: TListBoxItem);
var
  ExtFile: string;
  mime: JMimeTypeMap;
  ExtToMime: JString;
  Intent: JIntent;
begin
  // Сохраняем выбранный путь
  path := Item.ItemData.Detail;

  if Item.TagString = 'folder' then
  begin
    if TDirectory.Exists(path) then
    begin
      TotalWork(path, True);
    end
    else
    begin
      ListBox1.Items.Delete(Item.Index);
      ShowMessage('Папка не найдена');
    end;
  end
  else if Item.TagString = 'file' then
  begin
    try
      //Определяем расширение файла и его mime тип
      ExtFile := AnsiLowerCase(StringReplace(TPath.GetExtension(path), '.', '',[]));
      mime := TJMimeTypeMap.JavaClass.getSingleton();
      ExtToMime := mime.getMimeTypeFromExtension(StringToJString(ExtFile));

      //Запрашиваем открытие файла
      Intent := TJIntent.Create;
      Intent.setAction(TJIntent.JavaClass.ACTION_VIEW);
      Intent.setDataAndType(StrToJURI('file:' + path), ExtToMime);
      SharedActivity.startActivity(Intent);
    except
      ShowMessage('Невозможно открыть файл!');
    end;
  end;
end;

{Кнопка назад}
procedure TForm1.SpeedButton1Click(Sender: TObject);
begin
  // Определяем предыдущую папку
  path := ExtractFileDir(path);

  if path = '/' then path := '/'
  else path := path;

  TotalWork(path, True);
end;

{Кнопка удалить}
procedure TForm1.SpeedButton2Click(Sender: TObject);
var
  i: integer; // Устанавливаем счётчик
  LItem: TListBoxItem; // Выбранная папка или файл для удаления
  LItemPath: string;
begin

  i := 0; // Обнуляем счётчик

  while i < Length(ItemsCheck) do
  begin

    if ItemsCheck[i]<>'' then
    begin
      // Получаем выбранный пункт
      LItem := ListBox1.ListItems[i];
      LItemPath := LItem.ItemData.Detail;

      if LItem.TagString = 'folder' then
      begin

        if TDirectory.Exists(LItemPath) then
        begin
          TDirectory.Delete(LItemPath, True); // Удаляем папку и подпапки
        end;

        ListBox1.Items.Delete(i); // удаляяем пункт из ListBox'a
        ListBox1ChangeCheck(Self); // обновляем массив выбранных пунктов
        i := 0; // Обнуляем счётчик
      end
      else if LItem.TagString = 'file' then
      begin

        if TFile.Exists(LItemPath) then
        begin
          TFile.Delete(LItemPath); // Удаляем файл
        end;

        ListBox1.Items.Delete(i); // удаляяем пункт из ListBox'a
        ListBox1ChangeCheck(Self); // обновляем массив выбранных пунктов
        i := 0; // Обнуляем счётчик
      end;

    end;
    i := i + 1;
  end;
end;

{Основа для обработки перемещений по файлам}
procedure TForm1.TotalWork(path_tr: string; clear: boolean);
var
  folders, files: TStringDynArray;
begin

  // Выводим текущий путь
  Label1.Text := path_tr;

//*****Папки*****
  // Ищем папки
  folders := TDirectory.GetDirectories(path_tr);

  // Сортируем папки
  TArray.Sort(folders, TComparer.Construct(CompareLowerStr));

  if clear then
  begin
    // Очищаем Листбокс
    ListBox1.Clear;
  end;

  // Заполняем Листбокс списком отсортированных папок
  AddListItem(folders, 'folder');
//***************

//*****Файлы*****
  // Ищем файлы
  files := TDirectory.GetFiles(path_tr);

  // Сортируем файлы
  TArray.Sort(files, TComparer.Construct(CompareLowerStr));

  // Дополняем Листбокс списком отсортированных файлов
  AddListItem(files, 'file');
//***************

end;

end.

Скриншоты:


Видео:

Итог: Приложение отлично работает. Провёл тест, на папке содержащей 400 папок и 570 файлов, общим весом 4,5 Гб. Приложение справилось на отлично.
На скорость чтения папки: По сравнению со стандартным файловым менеджером в самсунге – работают почти одинаково. По сравнению с ES проводником, моё приложение работает заметно медленнее…
В следующей статье добавлю функции создания/переименования файлов и папок, сделаю более дружелюбный вид приложения,  реализую копирование/перемещение файлов и папок. 

Исходный код (Delphi XE5 Update 2): Скачать с Google Drive
Исходный код (Delphi XE6): Скачать с Google Drive

APK файл для установки на ваш девайс: Скачать с Google Drive

На этом всё. Спасибо за внимание. 
Надеюсь, вам понравилась/пригодится данная статья и приложение.