martes, agosto 29, 2017

Cómo pasar parámetros a un JobService

En mi post anterior expliqué como crear un JobSerivce, este job service recibe parámetros; Para pasar parámetros al JobService usamos la clase PersistableBundle de la siguiente manera.

private void requestAlertsToDelete(int[] alerts) {

   Alerts alertsRequest = Alerts.getAlertsRequest(
         SessionManager.getAuth(getActivity()),
         new int[]{},
         alerts);

   PersistableBundle bundle = new PersistableBundle();

   bundle.putString(Constants.INSTANCE.getREQUEST_PARAMS(), alertsRequest.toJSON());

   JobScheduler jobScheduler = (JobScheduler) getActivity()
                         .getSystemService(Context.JOB_SCHEDULER_SERVICE);

   JobInfo jobInfo = new JobInfo.Builder(0, new ComponentName(getActivity(), AlertJobService.class))
         .setExtras(bundle)
         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
         .build();

   if (jobScheduler != null) {
      jobScheduler.schedule(jobInfo);
   }


Esta clase acepta tipos básicos, enteros, cadenas de caracteres y algunos otros, pero no Parcelables, por lo tanto lo que hice fue crear en mi clase Alerts un método llamado toJSON() que devuelve la instancia de esa clase a JSON.

Para esto uso la librería Gson, por si acaso les dejo el código también.

public String toJSON() {
   return gs.toJson(this, Alerts.class);
}

la variable gs está definida de forma global de la siguiente manera.
public  final Gson gs = new GsonBuilder().serializeNulls()
      .disableHtmlEscaping()
      .create();

Saludos

Reemplazando Intent Services por Job Services


Android Oreo sería el mejor de todas las versiones de Android hasta ahora. La verdad la verdad, no lo comparto. Las notificaciones de aplicaciones en segundo plano son molestas e inútiles, los mensajes en apps de mensajes como WhatsApp llegan con retraso, y así por el estilo sin embargo, este post no es para hablar del sistema operativo como tal. Con los cambios hechos los Googlers te sugieren usar JobScheduler para ejecutar tareas en segundo plano,  lo resalto por eso, para ejecutar así en background, si la app esta en foreground seguimos usando intent services o los mecanismos de siempre.

JobScheduler  fue introducido en Lollipop (API 21) pero ahora es cuando me he dado la tarea de aprender sobre JS al ver los ANR en Android Oreo de mi app. La primera estrellada, JobService trabaja en el Main Thread (UI Thread) no leí esa parte y pensaba que funcionaba como un IntentService luego de meter código de networking en onStartJob me apareció el stack trace de networking en el UI thread, por lo que hay que sacar código de ejecución larga y pesada a su propio thread.

La solución (según los ejemplos de Googlers) es usar un AsyncTask , algo así como esto

@Overridepublic boolean onStartJob(final JobParameters params) {
    mDownloadArtworkTask = new DownloadArtworkTask(this) {
        @Override        protected void onPostExecute(Boolean success) {
            super.onPostExecute(success);
            jobFinished(params, !success);
        }
    };
    mDownloadArtworkTask.execute();
    return true;
}

pero lamentablemente esto tiene una inspección de código  bien fea que dice.

Static Field Leaks:  A static field will leak contexts.  Non-static inner classes have an implicit reference to their outer class. If that outer class is for example a Fragment or Activity, then this reference means that the long-running handler/loader/task will hold a reference to the activity which prevents it from getting garbage collected.  Similarly, direct field references to activities and fragments from these longer running instances can cause leaks.  ViewModel classes should never point to Views or non-application Contexts.

En español , podría ocurrir memory leaks al tener clases internas no estáticas, ya que guardan una referencia implícita del contexto de la clase externa. Si ocurren o no, no lo sé. Supongo que no, pero particularmente no me gusta tener Warnings en mi código así que decidí buscar otra forma de hacerlo.

Leyendo un poco mas, te hablan también de usar Handlers y Threads estos hace tiempo que no los usaba y había olvidado como se implementaban;  Luego de terminar mi implementación pues funciono muy bien, y no habían warnings molestos por ningún lado. Así que aquí es a donde vamos.

JobService tiene tres métodos principales:  onStartJob, onStopJob y el onCreate de siempre.

public class AlertJobService extends JobService {

   private Handler mJobServiceHandler;

   private final int TASK_COMPLETE = 0;
@Override
   public void onCreate() {
      super.onCreate();
   }


   @Override   
   public boolean onStartJob(JobParameters jobParameters) {
      return true;
   }


   @Override   public boolean onStopJob(JobParameters jobParameters) {
      return false;
   }
}

En onCreate puedes iniciar lo que quieras entre éstas y una bien importante es el handler, el cual se encargar de manejar los mensajes que reciba dese otros Threads.

Definimos una variable global de tipo Handler -yo la llame mJobServiceHandler - y la iniciamos en onCreate. Estoy asignando a este handle el main looper o en otras palabras el UI Thread.

@Overridepublic void onCreate() {

   super.onCreate();

   mJobServiceHandler = new Handler(Looper.getMainLooper()) {

      @Override
      public void handleMessage(Message msg) {
         switch (msg.what) {

            default:
               super.handleMessage(msg);
         }
      }
   };
}

Creamos el Thread para networking quedando algo así como esto.

/** * Network thread with background priority */

class NetworkThread extends Thread {

   JobParameters jobParameters;

   String url;

   String requestParameters;

   /*** Network handler constructor    
     ** @param jobParameters Job parameters of this Job service    
     * @param url           Service url    
     * @param requestParams Request parameters    
     */  
   NetworkThread(JobParameters jobParameters, String url, String requestParams) {
      this.url = url;
      this.jobParameters = jobParameters;
      this.requestParameters = requestParams;
   }

   @Override
   public void run() {

      Response response = null;

      android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

      OkHttpClient client = new OkHttpClient();

      RequestBody requestBody = RequestBody.create(JSON, requestParameters);

      Request request = new Request.Builder()
            .url(url)
            .post(requestBody)
            .build();
      try {
         response = client.newCall(request).execute();
      } catch (IOException e) {
         e.printStackTrace();
      }

      Message message = new Message();

      message.what = TASK_COMPLETE;

      Bundle bundle = new Bundle();

      bundle.putParcelable(Constants.JOB_PARAMETERS, jobParameters);

      if (response != null) {
         bundle.putString(Constants.RESPONSE, response.body().string());
      }

      message.setData(bundle);

      mJobServiceHandler.sendMessage(message);

   }
}

Llamamos el thread en onStartJob. En mi caso, estoy llamando a un servicio que me retorna una lista de alertas. Si te das cuenta, estoy creando el url y los parámetros que necesito en onStartJob para poder usar los métodos como getString(), getResources() etc. Estos tienen un contexto definido y puedo hacerlo, luego se los paso al thread desde el constructor.


@Override

public boolean onStartJob(JobParameters jobParameters) {

   if (BuildConfig.DEBUG) {
      Log.d(TAG, "onStartJob: Alert Job Service");
   }

   String url = getString(R.string.base_server_url);

   String requestParams = jobParameters.getExtras().getString(Constants.REQUEST_PARAMS, "");

   if (requestParams.isEmpty()) {
      throw new IllegalArgumentException(getResources().getString(R.string.throw_add_parameters_to_request));

   } else {

      NetworkThread networkThread = new NetworkThread(jobParameters, url, requestParams);

      new Thread(networkThread).start();
   }

   return true;
}


onStartJob retorna true para que el framework lo mantenga vivo, es decir, retornamos true si llamamos a hacer algo en otro thread y tenemos que esperar la respuesta como en el caso de mi NetworkThread.

Una vez empezado el thread, este llama al servicio remoto hace lo que tiene que hacer y regresa, al final llama a mJobServiceHandler.sendMessage(message); Ahí defino mi mensaje el cual va a ser recibido por handleMessage().  A continuación el código completo de handleMessage

@Override

public void onCreate() {

   super.onCreate();

   mJobServiceHandler = new Handler(Looper.getMainLooper()) {

      @Override
public void handleMessage(Message msg) { switch (msg.what) { case TASK_COMPLETE: if (msg.getData().containsKey(Constants.INSTANCE.getRESPONSE())) { Intent broadcast = new Intent(); Type alertListType = new TypeToken<ArrayList<Alert>>() { }.getType(); AlertsResponse response = gs.fromJson(msg.getData().getString(Constants.INSTANCE.getRESPONSE()), alertListType); broadcast.putExtra(Constants.INSTANCE.getRESPONSE(), response); LocalBroadcastManager.getInstance(AlertJobService.this).sendBroadcast(broadcast); } JobParameters parameters = msg.getData().getParcelable("jobParameters"); jobFinished(parameters, true); break; default: super.handleMessage(msg); } } }; }


Handle message tiene un switch case en msg.what esto lo definí dentro del Network thread para indicar que ya se terminó el trabajo pendiente, una vez recibido envío mi broadcast o local broadcast con el resultado, y listo.

martes, agosto 22, 2017

Límites de ejecución en segundo plano de Android Oreo