При необходимости загрузить файла на сервер из приложения встает вопрос об индикации процесса загрузки. Если дизайн позволяет то можно отобразить диалог прогресс бара (при этом заблокировать интерфейс пользователя). Но на мой взгляд более интересным вариантом индикации являются уведомления в статус баре системы, которые нельзя смахнуть.
При реализации такого подхода может возникнуть несколько проблем:
Как создать уведомление для каждого загружаемого файла (если например существует возможность единовременно загружать несколько файлов)
Как программно отменить уведомление после завершения процесса загрузки на сервер
Для решения этих проблем необходимо знать как система индексирует уведомления, создаваемые приложением. Каждое приложение может создать сколь угодно много уведомлений, но чтобы иметь возможность обновить определенное уведомление, либо отменить его — необходимо присвоить каждому из уведомлений свой уникальный ID. Ниже код реализации создания уведомления при старте загрузки файла на сервер и его отмены, по окончании загрузки:
//Кеш, хранящий список id активных на данный момент уведомлений
private List<int> _notificationCache = new List<int>();
private void UploadFile()
{
//получим рандомный ID для уведомления, при этом он не должен пересекаться с уже существующими (для этого используем кеш)
Random r = new Random(System.Environment.TickCount);
int notificationId = 0;
while (notificationId == 0 || _notificationCache.Contains(notificationId))
{
notificationId = r.Next(1, 1000);
}
_notificationCache.Add(notificationId);
//
Notification.Builder builder = new Notification.Builder(Activity);
builder.SetSmallIcon(Resource.Drawable.file_32x32)
//устанавливаем заголовок для уведомления
.SetContentTitle(filename)
//в дескрипшне будет храниться наша строка "Загружаю файл на сервер..."
.SetContentText(Activity.GetString(Resource.String.attachement_uploading))
.SetAutoCancel(false)
//запрещаем отмену уведомления пользователем
.SetOngoing(true)
//разрешаем уведомлению появиться в статус баре, мигать светодиодом и т.п.
.SetDefaults(NotificationDefaults.All);
NotificationManager manager = (NotificationManager)Activity.GetSystemService(Activity.NotificationService);
manager.Notify(notificationId, builder.Build());
//////////////////////////////////////////
// ... код загрузки файла на сервер ... //
//////////////////////////////////////////
//отменяем уведомление и удаляем его ID из кеша
manager.Cancel(notificationId);
_notificationCache.Remove(notificationId);
}
В прошлой статье я рассказывал о том, как скачивать файлы из сети и сохранять их на диск, а так же использовать MediaScanner для того, чтобы сделать файлы доступными другим приложениям (например галерее, если это фотография или видео).
Сегодня же хотелось бы рассказать о более интересном и предпочтительном способе скачивания файлов — использовании системного сервиса DownloadManager.
DownloadManager это такой сервис платформы Android, который предоставляет весь спектр функциональности, связанной с загрузкой файлов. Он сам начнет скачивание, корректно отреагирует на обрыв связи или смену типа сети, запустит скачивание по новой (если был обрыв связи), создаст и отобразит уведомление о загрузке файла, уведомит MediaScanner о том, что файл загружен и многое другое.
Для корректного использования DownloadManager нам понадобится:
Создать свой класс — наследник BroadcastReceiver, в котором мы будем реагировать на интенты, отсылаемые DownloadManager’ом;
Подготовить URI с ресурсом, который нужно загрузить;
Создать реквест с нашим URI и другими настройками DownloadManager;
Запустить скачивание.
Итак перво наперво — напишем класс-наследник BroadcastReceiver и реализуем логику, которая покажет Toast, когда наш файл скачается:
/// <summary>
/// Ресивер, который принимает информацию о состоянии загружаемых файлов приложением (задача только одна - отобразить сообщение об успешной загрузке)
/// </summary>
public class DownloadManagerReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
if (intent.Action == DownloadManager.ActionDownloadComplete)
{
//это не совсем правильно с точки зрения производительности, лучше передать ссылку на менеджер извне
DownloadManager downloadManager = (DownloadManager)context.GetSystemService(Activity.DownloadService);
long downloadId = intent.GetLongExtra(DownloadManager.ExtraDownloadId, -1);
if (downloadId == -1)
{
return;
}
DownloadManager.Query query = new DownloadManager.Query();
query.SetFilterById(new long[]{ downloadId });
ICursor cursor = downloadManager.InvokeQuery(query);
if (cursor != null && cursor.MoveToFirst())
{
if (DownloadStatus.Successful == (DownloadStatus)cursor.GetInt(cursor.GetColumnIndex(DownloadManager.ColumnStatus)))
{
string filename = cursor.GetString(cursor.GetColumnIndex(DownloadManager.ColumnTitle));
Toast.makeText(context, string.Format(context.GetString(Resource.String.chat_attachement_succesfully_downloaded), filename), Toast.LENGTH_SHORT);
}
}
}
}
}
Далее необходимо создать и зарегистрировать наш ресивер:
protected override async void OnCreate(Bundle bundle)
{
//код инициализации активности
//...
/////////
//создаем ресивер для корректной обработки успешного скачивания файла
_downloadManagerReceiver = new DownloadManagerReceiver();
RegisterReceiver(_downloadManagerReceiver, new IntentFilter(DownloadManager.ActionDownloadComplete));
}
Последним этапом — необходимо получить ссылку на экземпляр сервиса DownloadService, настроить менеджер и запустить скачивание:
private void DownloadFile(string filename){
DownloadManager downloadManager = (DownloadManager)GetSystemService(Activity.DownloadService);
//реквест содержит URI файла и дополнительные настройки DownloadManager'а
DownloadManager.Request request = new DownloadManager.Request(Android.Net.Uri.Parse("http://наш_url_для_скачивания"));
//нотификейшн будет виден всегда, пока юзер не отменит его вручную или не кликнет
request.SetNotificationVisibility(DownloadVisibility.VisibleNotifyCompleted);
//задаем публичную директорию с загрузками и вторым параметром имя файла + расширение
request.SetDestinationInExternalPublicDir(Android.OS.Environment.DirectoryDownloads, filename);
//разрешаем сканить новый файл МедиаСканнеру
request.AllowScanningByMediaScanner();
//запускаем скачивание
downloadManager.Enqueue(request);
}
Вот и все, после старта скачивания менеджер создать нотификейшн со статусом скачивания, а после позволит открыть файл в приложении по-умолчанию.
Практически любому приложению, которое позволяет обмениваться контентом необходимо скачивать данные из сети и сохранять их на диск устройства. Существует два распространенных способа это сделать:
Скачать и сохранить файл вручную;
Использовать класс DownloadManager и доверить работу ему.
При этом скачанные файлы не будут видны приложениям на устройстве до тех пор, пока разработчик самостоятельно не сделает их доступными (либо до следующей перезагрузке устройства).
В этой статье мы рассмотрим ручной способ сохранения на диск применительно к Xamarin.Android. Более интересный вариант с использованием DownloadManager будет рассмотрен в другой статье.
Для начала необходимо получить массив байт файла из сети (например по его URL). Для этого можно использовать например простейший WebClient, написать примерно такой код:
string url = “url_к_нашему_файлу”;
byte[] dataBytes = new System.Net.WebClient().DownloadData(url);
Далее необходимо сохранить полученный массив байт как файл на диске. Для этого сначала необходимо получить путь до одной из публичных директорий, они могут быть следующими:
Environment.DirectoryMusic;
Environment.DirectoryPodcasts;
Environment.DirectoryRingtones;
Environment.DirectoryAlarms;
Environment.DirectoryNotifications;
Environment.DirectoryPictures;
Environment.DirectoryMovies;
Environment.DirectoryDownloads;
Environment.DirectoryDcim.
Назначение директорий вполне ясно из их названия, будем использовать Environment.DirectoryDownloads. Напишем следующий код (пояснение в комментариях):
//получаем путь к директории с загрузками
string dirPath = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath;
//если директории нет (такое может быть, да О_о, создадим ее)
if (!Directory.Exists(dirPath))
{
Directory.CreateDirectory(dirPath);
}
//получаем путь до конечного файла в нашей директории, если он уже есть - удалим его
string filePath = Path.Combine(dirPath, file.Name);
if (File.Exists(filePath))
{
File.Delete(filePath);
}
//записываем массив байт на диск
File.WriteAllBytes(filePath, fileBytes);
Почти готово, осталось только объявить системе о том, что на диске теперь есть новый файл, доступный всем вокруг. Для этого используется подсистема Android, которая называется MediaScanner. Существует несколько способов работы с МедиаСканнером, все они обладают разным уровнем гибкости и сложности. Мы воспользуемся самым простым — отправим широковещательный интент системе, МедиаСканнер его поймает и сделает добавит файл в свою базу.
//объявим МедиаСканеру о том, что появился новый файл
Intent mediaScannerIntent = new Intent(Intent.ActionMediaScannerScanFile);
mediaScannerIntent.SetData(Android.Net.Uri.FromFile(new Java.IO.File(filePath)));
SendBroadcast(mediaScannerIntent);