понедельник, 14 мая 2012 г.

BroadcastReceiver: общение процессов в Android-приложении

Давайте снова вернёмся к вопросу организации работы фоновых процессов в Android-приложении. В этой статье я описал, как выполнять загрузку файлов с удалённого сервера в фоне. Задача замечательно решается с помощью AsyncTask, но только в случае небольших файлов и недолгого ожидания. Что произойдёт в случае если файл будет загружаться двадцать минут? Телефон уснёт, вы воспользуетесь другим приложением и т.п. - в любом случае загрузка будет прервана. Решение тут очевидно: заменить AsyncTask сервисом, который может работать и без основонго приложения. В случае же использования WakefulIntentService мы практически гарантировано докачаем наш файл несмотря ни на что. И только одну проблему остаётся решить для получения окончательной победы разума над материей: обновлять прелоадер нам нужно в Activity, которая недоступна из сервиса. Выход: использовать внутренний механизм платформы для обмена сообщениями между отдельными компонентами нашего приложения.
Итак, давайте переработаем наше приложения для загрузки файлов так, чтобы оно могло докачать файл при любых обстоятельствах. Научимся использовать BroadcastReceiver.

Создаём сервис и шлём сообщение

Код взаимодействия с сервером размещаем в классе, расширяющем WakefulIntentService:

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
 
import android.content.Intent;
 
import com.commonsware.cwac.wakeful.WakefulIntentService;
 
public class LoaderService extends WakefulIntentService {
 
 public static final String FURL = "FURL";
 
 public LoaderService() {
  super("LoaderService");
 }
 
 @Override
 protected void doWakefulWork(Intent intent) {
  String furl = intent.getStringExtra(FURL);
  FileOutputStream fos = null;
  InputStream inputStream = null;
  try {
   updateUI(0, 100);
   URL url = new URL(furl);
   HttpURLConnection urlConnection = (HttpURLConnection) url
     .openConnection();
   urlConnection.setRequestMethod("GET");
   urlConnection.setDoOutput(true);
   urlConnection.connect();
 
   fos = new FileOutputStream(File.createTempFile("Tmp",
     "download"));
   inputStream = urlConnection.getInputStream();
 
   int totalSize = urlConnection.getContentLength();
   int downloadedSize = 0;
 
   byte[] buffer = new byte[1024];
   int bufferLength = 0;
 
   while ((bufferLength = inputStream.read(buffer)) > 0) {
    fos.write(buffer, 0, bufferLength);
    downloadedSize += bufferLength;
    updateUI(downloadedSize, totalSize);
   }
  } catch (Exception e) {
   e.printStackTrace();
  } finally {
   try {
    if (fos != null)
     fos.close();
    if (inputStream != null)
     inputStream.close();
   } catch (Exception e) {
   }
  }
 }
 
 private void updateUI(int downloadedSize, int totalSize) {
  Intent intt = new Intent(BackFLoaderActivity.ACTION_LOAD);
  intt.putExtra(BackFLoaderActivity.CURRVAL, downloadedSize);
  intt.putExtra(BackFLoaderActivity.MAXVAL, totalSize);
  sendBroadcast(intt);
 }
}

Тут мы в методе doWakefulWork выполняем чтение файла по сети буфером по киллобайту, и на каждой итерации чтения вызываем метод updateUI. В этом методе мы создаём Intent указывая ему наш action, укладываем в него общий размер и суммарный скачанный размер файла. Intent отдаём в метод sendBroadcast, посылая тем самым сообщение.

Слушаем и реагируем на сообщение

Вот код главного Activity:

import com.commonsware.cwac.wakeful.WakefulIntentService;
 
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
 
public class BackFLoaderActivity extends Activity {
 
 public static final String CURRVAL = "CURRVAL", MAXVAL = "MAXVAL";
 public static final String ACTION_LOAD = "ACTION_LOAD";
 private ProgressDialog progressDialog;
 private BroadcastReceiver rec;
 
 @Override
 public void onResume() {
  // creating and register receiver
  rec = new BroadcastReceiver() {
   @Override
   public void onReceive(Context context, Intent intent) {
             int curr = intent.getIntExtra(CURRVAL, 50);
             int max = intent.getIntExtra(MAXVAL, 100);
             if (!progressDialog.isShowing()) {
              progressDialog.setMessage("Downloading ...");
              progressDialog.setCancelable(false);
              progressDialog.setMax(100);
              progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
              progressDialog.show();
             }
             progressDialog.setProgress((int) ((curr / (float) max) * 100));
             if (curr==max) {
                     progressDialog.dismiss();
             }
   }
  };
  registerReceiver(rec, new IntentFilter(new IntentFilter(ACTION_LOAD)));
  super.onResume();
 }
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
 
  progressDialog = new ProgressDialog(this);
 
  Button load = new Button(this);
  load.setText("Load file");
  load.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    progressDialog.setMessage("Downloading ...");
    progressDialog.setCancelable(false);
    progressDialog.setMax(100);
    progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
    progressDialog.show();
    Intent intent = new Intent(BackFLoaderActivity.this, LoaderService.class);
                intent.putExtra(LoaderService.FURL, "http://anjedi.com/api_lib/2.2_level8.jar");
                WakefulIntentService.sendWakefulWork(BackFLoaderActivity.this, intent);
   }
  });
  setContentView(load, new LayoutParams(LayoutParams.WRAP_CONTENT,
    LayoutParams.WRAP_CONTENT));
 }
 
 @Override
 protected void onPause() {
  unregisterReceiver(rec);
  super.onPause();
 }
}

Отсюда всё начинается, тут всё и заканчивается. По-порядку:
  • В onCreate рисуем кнопку, определяя что нужно делать по клику. А по клику нужно запустить прогресс-бар, создать Intent, уложить в него url для загрузки файла и запустить с этим intent-ом серис. 
  • В onResume создаём экземпляр BroadcastReceiver и регистрируем его с указанием того же Action, с которым посылаем сообщение из сервиса. Зарегистрировать получателя сообщений можно и в манифесте, но в нашем случае так нагляднее. 
  • При создании BroadcastReceiver-а описываем, что он должен делать при получении сообщения в методе onRecrive. А должен он прочитать данные о общем размере и размере скачанной части файла и отобразить соответствующую долю на progress-баре. 
  • И не забудем удалить регистрацию BroadcastReceiver-а в методе onPause, чтобы не получить ошибку при уходе приложения в фон. 
Итого: приложение из двух маленьких классов упорно качает файл, и настойчиво отображает прогресс несмотря на переключение на другие приложения и обратно, засыпание и пробуждение устройства, поворот экрана и другие "отвлекающие факторы".

2 комментария:

  1. WakefulIntentService - это откуда ?

    ОтветитьУдалить
  2. где взять этот класс
    import com.commonsware.cwac.wakeful.WakefulIntentService; ?

    ОтветитьУдалить