quinta-feira, 22 de outubro de 2015

Consuming a CFapi generated API with Android

Hello folks! Today I am going to show you how to consume an API (dynamically created with CFapi) in an Android application.

I have created the API named "health" with CFapi (http://CFapi.riaforge.org) based on a database view called "hospitals". The view contains three fields: Nome, Latitude and Longitude (sorry, I left field name as "Nome", the word in portuguese for name).

After filling the CFapi main form and clicking the "Generate API" button, I was ready to deploy my instant generated API "Health" to my server on the Internet.

You can check it out in the following address:


By clicking in the above link, you will see a real-time generated REST/JSON flow coming from RAM memory that was previously filled by a database query on view "hospitals".

This JSON shows a list of some of the hospitals available in the city of Rio de Janeiro.

Now, the idea is to show here how to consume this JSON in an Android application.

In out project, the first thing to do is to define a class to receive the objects from JSON. Lets call it HospitalDTO:

package com.example.androidcfapiconsumer;

public class HospitalDTO
{

   String name = "";
   String latitude = "";
   String longitude = "";
  
   public String getName()
   {
      return name;
   }
   public void setName(String name)
   {
      this.name=name;
   }
   public String getLatitude()
   {
      return latitude;
   }
   public void setLatitude(String latitude)
   {
      this.latitude=latitude;
   }
   public String getLongitude()
   {
      return longitude;
   }
   public void setLongitude(String longitude)
   {
      this.longitude=longitude;
   }  
}
 


Then, we have to create a webService consumer. So we are going to add a new class called HospitalWS:


// REST WebService consumer
package com.example.androidcfapiconsumer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.net.Uri;
import android.util.Log;

public class HospitalWS
{

   private static String URL="";
   private static String ENDPOINT="";
   private static int timeout=10000;

   public ArrayList<HospitalDTO> getHospitals(String token)
   {
      URL="http://www.tradeteller.com.br";
      ENDPOINT="/health/api/v1/rest/hospitals.cfm";

      ArrayList<HospitalDTO> hospitals=new ArrayList<HospitalDTO>();
      String result="";

      try
      {
         HttpParams httpParameters=new BasicHttpParams();
         int timeoutConnection=timeout;
         HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
         int timeoutSocket=timeout;
         HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);

         // Define inputStream
         InputStream inputStream=null;

         // Create http client.
         HttpClient httpclient=new DefaultHttpClient(httpParameters);

         // Define input parameters values.
         Uri.Builder b=Uri.parse(URL).buildUpon();
         b.path(ENDPOINT);
         b.appendQueryParameter("token", token);
         b.appendQueryParameter("pretty", "");
         b.appendQueryParameter("filter", "");
         String url=b.build().toString();

         // Create the GET and set URL.
         HttpGet httpget=new HttpGet(url);

         // Define data type.
         httpget.setHeader("Accept", "text/json");

         try
         {
            // Send the URL request, using GET.
            HttpResponse httpResponse=httpclient.execute(httpget);

            // Receive response as "inputStream".
            inputStream=httpResponse.getEntity().getContent();

            String converted=convertInputStreamToString(inputStream);

            // Check if http response code is 200 (OK).
            if (httpResponse.getStatusLine().getStatusCode() == 200)
            {
               result=converted;

               // Parse JSON.
               hospitals=parseJSONString(result);
            }
            else
            {
            }

         }
         catch (Exception g)
         {
            g.printStackTrace();
         }
      }
      catch (Exception h)
      {

      }

      return hospitals;
   }

   private ArrayList<HospitalDTO> parseJSONString(String result)
   {
      ArrayList<HospitalDTO> hospitals=new ArrayList<HospitalDTO>();

      JSONArray ja=null;

      try
      {
         ja=new JSONArray(result);
      }
      catch (JSONException e)
      {
         e.printStackTrace();
      }

      for (int i=0; i < ja.length(); i++)
      {

         JSONObject oneObject=null;

         try
         {
            oneObject=ja.getJSONObject(i);
         }
         catch (JSONException e)
         {
            e.printStackTrace();
         }

         // Pulls items from the array.
         try
         {
            String name=oneObject.getString("NOME");
            String latitude=oneObject.getString("LATITUDE");
            String longitude=oneObject.getString("LONGITUDE");

            HospitalDTO hospital=new HospitalDTO();
            hospital.setName(name);
            hospital.setLatitude(latitude);
            hospital.setLongitude(longitude);
            hospitals.add(hospital);

         }
         catch (JSONException e)
         {
            e.printStackTrace();
         }

      }

      return hospitals;

   }

   private static String convertInputStreamToString(InputStream inputStream) throws IOException
   {
      BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));

      int c;
      StringBuilder response=new StringBuilder();

      while ((c=bufferedReader.read()) != -1)
      {
         response.append((char) c);
      }
      String result=response.toString();
      inputStream.close();

      return result;

   }

}

Finally,copy and paste the code below to MainActivity.java:


package com.example.androidcfapiconsumer;

import java.util.ArrayList;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.support.v4.app.FragmentActivity;
import com.example.projetomapa.R;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;

public class MainActivity extends FragmentActivity
{

   ArrayList<HospitalDTO> hospitals=new ArrayList<HospitalDTO>();
   GoogleMap supportMap;

   @Override
   protected void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      int resultCode=GooglePlayServicesUtil.isGooglePlayServicesAvailable(getApplicationContext());

      if (resultCode == ConnectionResult.SUCCESS)
      {

         FragmentManager fmanager=getSupportFragmentManager();
         Fragment fragment=fmanager.findFragmentById(R.id.map);
         SupportMapFragment supportmapfragment=(SupportMapFragment) fragment;
         supportMap=supportmapfragment.getMap();

         supportMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);

         supportMap.setMyLocationEnabled(true);

         LatLng latlng=new LatLng(-22.910509, -43.203932);

         CameraPosition cameraPosition=new CameraPosition.Builder().target(latlng).zoom(10).build();
         supportMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));

      }
      else
      {
         try
         {
            int RQS_GooglePlayServices=1;

            GooglePlayServicesUtil.getErrorDialog(resultCode, this, RQS_GooglePlayServices);
         }
         catch (Exception erro)
         {
            finish();
         }
      }

      // Start webService consumption.
      showHospitals();

   }

   private void showHospitals()
   {
      new Thread()
      {
         public void run()
         {

            try
            {
               String token="8FBC592A-9899-998C-906EFD25C55AAA97";

               HospitalWS hospitalWS=new HospitalWS();
               hospitals=hospitalWS.getHospitals(token);

               Bundle bundle=new Bundle();
               bundle.putString("message", "ok");

               Message message=new Message();
               message.setData(bundle);
               messageHandler.sendMessage(message);

            }
            catch (Exception e)
            {
               e.printStackTrace();
            }

         }
      }.start();

   }

   public Handler messageHandler=new Handler()
   {
      public void handleMessage(Message message)
      {
         String result=(String) message.getData().getString("message");

         if (result == "ok")
         {
            MarkerOptions markeroptions=new MarkerOptions();

            for (HospitalDTO item : hospitals)
            {

               // Draw markers on the map.
               markeroptions.position(new LatLng(Double.valueOf(item.latitude), Double.valueOf(item.longitude)));
               markeroptions.title(item.name);
               markeroptions.draggable(false);

               supportMap.addMarker(markeroptions);

            }

         }
      }
   };

}

Of course, AndroidManifest.xml must be altered to:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.projetomapa"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />

    <permission
        android:name="com.example.projetomapa.permission.MAPS_RECEIVE"
        android:protectionLevel="signature" />

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <uses-feature
        android:glEsVersion="0x00020000"
        android:required="true" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.androidcfapiconsumer.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <meta-data
            android:name="com.google.android.maps.v2.API_KEY"
            android:value="AIzaSyB_i5ss8XwMWOXTDVj50A8jgdecWhH29X0" />
    </application>

</manifest>

And also this small definition on strings.xml file:


<string name="app_name">Android CFapi consumer</string>



Basically what we are doing here is starting an application with a standard Google Map, then we call an asynchronous task to consume our "Health" JSON API entry-point. When its finished, the hospitals are shown on the map.

The result can be seen in the following screen capture:






The complete Android CFapi consumer example can be downloaded here.



quarta-feira, 21 de outubro de 2015

Integrating CKAN open data with CFapi

Hello everyone. In my last post I introduced CFapi (CFapi.riaforge.org), a dynamic API generator for Coldfusion, Railo and Lucee Server CFML engines, now in version 1.0.5.

Today I am going to show you how easy it is to integrate CKAN open data server with data coming from a CFapi generated API.

In this example, I will use a database table that contains the neighbourhoods of the city of Rio de Janeiro. The word for "Neighbourhood" in portuguese is "Bairro". So that is the name of our database table:

Bairro table:














It has only three fields: BAIRRO (the name of the neighbourhood), LATITUDE and LONGITUDE.

The goal here is to create an API exposing the Bairro table as CSV, and then creating a CKAN new dataset resource to feed it *in real time*.

Of course it is possible to save a CSV file and deploy it to CKAN server, but that approach would demand updates. By using an API entry point, on the other hand, the information will always be up to date, because the CSV is generated by request.

So lets begin by pointing our browser to CFapi main page (CFapi/index.cfm) and fill in the form like this:




 Then press the "Generate API" button. The following screen will show up:





Now we have to follow the CFapi instructions. First, lets click on the "download API" link and deploy our just created City API to our web server root folder, something like "c:\inetpub\wwwroot\City" if you use IIS or "c:\Program files\Apache Software Foundation\Apache2.2\htdocs\City" if you use Apache Server.

After that we will click on the link that calls our City API main page: http://localhost/city/index.cfm - That will show the City API welcome screen and also fill the memory caches:





Now lets switch back to CFapi page and click on the link that runs the CSV file generation:

http://localhost/city/api/v1/csv/neighbourhoods.cfm?token=8AD8E16D-BE6B-AEC8-958DA1D111013363&filter=&download=true&delimiter=,

Notice that the browser tries to download the generated CSV file:




That is not what we want, as CKAN will not understand a download request as data feed. So we have to change the URL parameter "download=true" to "download=false". The delimiter *MUST* be a comma (,) to work with CKAN.



http://localhost/city/api/v1/csv/neighbourhoods.cfm?token=8AD8E16D-BE6B-AEC8-958DA1D111013363&filter=&download=false&delimiter=,


The result is that now we receive the CSV information within the browser screen:




Fine! Now we have got what it takes to integrate data with CKAN Server. Now lets open a CKAN dataset edition page (you must be logged in CKAN) and click on "Edit" button:

CKAN Server dataset page


 Now lets click on "Add New Resource" button:




The "Add New Resource" page will show up. Choose "Link for an API" and fill in the form field "Resource" with our CFapi generated City API link, like below (don´t forget to specify CSV in the format field). Then click on the "Add" button.

http://yourServer/city/api/v1/csv/neighbourhoods.cfm?token=8AD8E16D-BE6B-AEC8-958DA1D111013363&filter=&download=false&delimiter=,


With this we have added a new resouce called "Neighbourhoods" to our CKAN dataset. To visualize the result, lets just click on its name:




CKAN will then show in a table display format, the contents of the Neighbourhoods dataset (whose source data is coming from our City API, created dynamically by CFapi) :



To make things more beautiful, lets click on the "Map" tab and put the value "0-1000" in the range options. The end result is all the Rio de Janeiro´s city neighbourhoods showing on a map:




This example shows the endless possibilities of CFapi instant API generation when used in conjunction with CKAN or other opendata systems. Some time ago it was a real pain to create and integrate  APIs to open data systems, now we can do it in less than ten minutes.

And... That´s it! Let me hear from you about your experience with CFapi. That is important so I keep improving its tools.

Thanks a lot.


sexta-feira, 16 de outubro de 2015

CFapi - Creating REST / JSON / SOAP / WSDL / CSV / KML / GeoJSON APIs dynamically

Although this blog is about Android development, I kindly ask my reader´s permission to talk here about an off-topic subject.

Today I´m going to show you a little bit about the backstage part of mobile application development. By "backstage", I mean the back-end part of the software, i.e.: The API.

Not only I am an Android application developer, I am also a Coldfusion developer. And I usually do my back-end software in Coldfusion language.

Having done dozen of Coldfusion APIs that feeds Open Data CKAN sites, and various Android APPs, I some day had this idea of creating an API Dynamic Generator.

It would work like this: I would take a basic API project and turn it to a template with replaceable terms in the code. Then I would ask the developer some details about his database and which database view/table he/she was going to expose as open data or to feed a mobile APP. Then the dynamic API generator would take these information and create by its own a brand new API, making the data available in REST/JSON, SOAP/WSDL,  CSV, KML and GeoJSON formats to be integrated with any software.

Well... That idea became the project CFapi (Coldfusion Dynamic API Generator). It is available now at http://CFapi.riaforge.org and it is open source. You can download, use, share, modify, at your will. Enjoy!

Let me show you a step by step guide to use CFapi to create a new API:

1) Download CFapi in http://CFapi.riaforge.org
2) Extract CFapi.rar file contents to your Coldfusion developer server.
3) Open your web browser and point it to http://localhost/cfapi/index.cfm
    You will see this screen:


 At first run, CFapi comes prepared to use Coldfusion´s default cfbookclub datasource as the main source of data to create an API. Also, I choose to expose, as an example, two of its tables: authors and books. I decided that the generated API (bookclub) user would be allowed to search for authors using a "lastname" filter and for books using "title". And that´s all that is needed!

4) Press the "Generate API" button. Wait some seconds. If everything went ok,
    the following screen will show up:




This means that CFapi created successfully the Bookclub API with all features, i.e.: Rest, Soap and CSV entry-points.

5) Now you have to download the created API and deploy it to your development server. This is done by clicking in the first item "Click here to download your API Project zip file".  Then you extract that file´s content to your Coldfusion web root folder.

6) Now you have to call the main page of your new created API: http://localhost/bookclub/index.cfm. This will fill the memory caches for the first time. If you skip this step, when you consume the entry- points, you will get "no results found" message. The following screen will show up:




This is the welcome screen of your recently created API, showing the available entry-points to the consumer and the needed arguments. An access token is always needed, as access control. Of course you have to use https on your production server to hide it. there is also a "pretty" parameter for REST/JSON entry-points, which means that if set to true, the browser will show the JSON in a human readable format. Otherwise it will put everything in a single row, for optimisation purposes. The default value for "pretty" is false.

Another important parameter is "filter". This is where you should specify a search value so that the API will only return that value, if found. Notice that filter will use the search fields you have specified in the main form of CFapi application. In our case, "lastname" for authors and "tittle" for books.

In the SOAP entry-point you get a default WSDL file to be processed by your SOAP SOA system. I tested it with SOAPUI software and it worked flawlessly. As you can see in the pictures below (REST and SOAP output):

Rest/JSON output


WSDL output file









SOAPUI consuming WSDL


And also, a picture of standard JAVA program using httpGet() and a JSON extraction library to consume the REST/JSON entry-point. This program is available in the "examples" folder of CFapi project:




7) Last, but not least: The memory caches empty from time to time. It is mandatory thay you create a Scheduled Task in Coldfusion Administrator to call, from time to time (you decide how frequent) to call the "updateCache.cfm" file, located in "CFapi/ScheduledTasks" folder. This file is tailor made for your API. You just have to create the Scheduled Task in CF Admin to call it.


Observations:

* CFapi uses Google GSON library to generate JSON. Also, it uses Mark Mandel´s Javaloader project.

* You have to use https on your production server to hide the access token.

* For standard RESTfull URL compliance, you will have to use Web Server URL rewriting, to take the "*.cfm" file extension out of the URL.


Conclusion

I think that CFapi can give Coldfusion an important role on the API market. It represents a viable and fast (instant) solution in creating mobile back-end APIs and also for Open Data projects, like CKAN or Socrata based web sites.

Hope to hear feedback from developers and users as soon as possible.  Of course there is much room to evolve CFapi, and developers opinion is crucial on that subject.

So, please, leave a comment, send an email, contact me on Twitter or Facebook (luizMilfont).

Hope to have helped people with my CFapi project contribution.

Enjoy!




sábado, 23 de maio de 2015

Carregando um banco de dados SQLite a partir de um arquivo CSV.

Em nosso primeiro artigo sobre banco de dados no Android, vimos como criar uma base de dados e inserimos alguns registros programaticamente. Mas no mundo real, ficaria inviável carregarmos uma grande massa de dados pelo método apresentado no tutorial.

Uma solução muito útil é obter os dados a partir de um arquivo texto, no formato CSV, ou seja, onde os campos são delimitados por vírgulas.

Para exemplificar, vamos, utilizando o "projetoBancoDeDados", criar um arquivo texto na pasta "assets", denominado "funcionarios.csv". O conteúdo do arquivo deve ser como o definido abaixo:

1,JOSÉ SILVA,Auxiliar administrativo,985.63
2,MARIA JOSÉ,Cozinheira,627.35
3,HENRIQUE ARIAS,Motorista,1239.56
4,ASTOLFO GOMES,Zelador,832.57
5,MARINA ZENAIDE,Gerente,5342.00
6,PEDRO TELEZ,Porteiro,854.34
7,AMÉRICO TOLEDO,Balconista,700.88
8,ANTONIO FARIA,Recepcionista,800.00
9,ANA JARDIM,Gerente,6543.36
10,MARCELE PEREIRA,Diretora,9834.23
11,DANIELE GOMEZ,Coordenadora,3454.67
12,AMANDA AGUIAR,Faxineira,643.36
13,ROBERTO DANTAS,Programador,2345.65
14,ANDERSON CLEITON,Analista de sistemas,4534.87
15,CLÉCIO GOMES,Instrutor,3234.88
 


A ideia é obter cada linha do arquivo CSV acima e extrair os campos um a um (id, nome, cargo e salário). E a partir daí gravar os dados no banco.

Para isto, vamos agora abrir nossa classe "MainActivity.java" e substituir seu conteúdo integralmente por:

package com.example.projetobancodedados;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.content.res.AssetManager;
import android.util.Log;

public class MainActivity extends Activity
{
     
   @Override
   protected void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      // Realiza uma carga inicial do banco de dados, à partir de um arquivo texto CSV.
      fazerCargaInicial(this);

      // Obtem lista de todos os funcionários da base.
      ArrayList<FuncionarioDTO> listaDeFuncionarios = new ArrayList<FuncionarioDTO>();
      FuncionarioDAO funcionarioDAO = new FuncionarioDAO(this);
      funcionarioDAO.open();
      listaDeFuncionarios=funcionarioDAO.getAll();
      funcionarioDAO.close();
     
      // Mostra no LogCat todos os funcionários da base.
      for(FuncionarioDTO f: listaDeFuncionarios)
      {
        
         Log.i("debug", f.getId() + "");
         Log.i("debug", f.getNome());
         Log.i("debug", f.getCargo());
         Log.i("debug", f.getSalario() + "");
        
      }
     
   }

   @Override
   protected void onResume()
   {
      super.onResume();
   }

   @Override
   protected void onPause()
   {
      super.onPause();
   }

  
   public void fazerCargaInicial(Context ctx)
   {
      // Obtém DAO do funcionário.
      FuncionarioDAO db = new FuncionarioDAO(ctx);
      db.open();

      try
      {
                 
         AssetManager assetManager= ctx.getAssets();
         InputStreamReader is=new InputStreamReader(assetManager.open("funcionarios.csv"), "ISO8859-1");
         BufferedReader reader=new BufferedReader(is);

         String linha;
         String id, nome, cargo, salario;

         while ((linha=reader.readLine()) != null)
         {
            String[] dadosDaLinha=linha.split(",");
            FuncionarioDTO funcionarioDTO = new FuncionarioDTO();
           
            funcionarioDTO.setId(Integer.parseInt(dadosDaLinha[0]));
            funcionarioDTO.setNome(dadosDaLinha[1]);
            funcionarioDTO.setCargo(dadosDaLinha[2]);
            funcionarioDTO.setSalario(Double.parseDouble(dadosDaLinha[3]));
           
            db.save(funcionarioDTO);
           
         }
         is.close();
      }
      catch (IOException ex)
      {
         Log.i("debug", "erro" + ex.getMessage() );
      }

      db.close();
     
   }

}


O pulo do gato aqui está no método "fazerCargaInicial()", que lê o arquivo CSV da pasta "Assets" e extrai os dados, gravando-os posteriormente na base. Simples.

Dica boa, hein? Gostou? Comente!

sexta-feira, 22 de maio de 2015

Como fazer o banco de dados residir no SDCARD do Android.

Alô pessoal! Estive um pouco ocupado nos últimos dias e por isso não tive tempo para escrever no blog. No entanto, trago hoje um assunto deveras interessante e útil:

Como fazer o banco de dados de nosso app residir no SDCARD, isto é, na memória externa do dispositivo.

A primeira pergunta é: por quê?

A resposta é simples: O Android possui uma memória interna, inerente ao aparelho, e, opcionalmente, uma memória externa, provida através da adição de um cartão extra, conhecido como SDCARD.

Ocorre que, via de regra, a memória interna é menor do que a externa. Principalmente nos aparelhos mais antigos, onde ela pode ser de alguns poucos megabytes. Quando muito 1 ou 2 gigas.

Certamente o leitor em algum momento já se deparou com a mensagem de erro "Internal memory full" ou "Database Storage full", ou algo similar, na tela de seu Android. Isso acontece porque a grande maioria dos aplicativos usa a localização padrão para criar seus bancos de dados: a pasta "/data/data/nomeDoApp". Afinal, se não informarmos nada na classe SQLiteHelper, ela criará o banco nesse local.

Pois bem, é uma prática inteligente priorizarmos o SDCARD como local de criação do banco de dados de nosso app, tomando o cuidado, é claro, de verificarmos primeiro se ele está presente.

Isto é feito da seguinte forma:

import android.os.Environment;
// Verifica se existe SDCARD montado no sistema.
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
{
   // Realiza alguma operação em caso afirmativo.
}

Agora que já sabemos como verificar se há SDCARD instalado no sistema, vamos modificar nosso projetoBancoDeDados para que os construtores das classes que criam nosso banco façam esta checagem e, no caso da existência do SDCARD, gravar o banco de dados nele.

Primeiro vamos alterar o construtor da classe "FuncionarioDAO", para que fique assim:

  public FuncionarioDAO(Context context)
   {
      String path="";
      String FILE_DIR="projetoBancoDeDados";

      // Se houver SDCARD montado, então cria a base no SDCARD. Caso contrário,
      // na memória interna.
      if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED))
      {
         path=Environment.getExternalStorageDirectory().toString() + File.separator + FILE_DIR + File.separator;
      }
     
      dbHelper=new MySQLiteHelper(context, path);
   }

Como estamos agora passando mais um parâmetro para o construtor da classe "mySQLiteHelper", ou seja, além do contexto, estamos também passando o caminho onde o banco deve ser criado (path) então temos que mudar o construtor desta última classe também:

   public MySQLiteHelper(Context context, String path)
   {
      super(context, path + DATABASE_NAME, null, DATABASE_VERSION);
   } 

A mudança é bastante simples. Só estamos acrescentando o caminho, informando assim, onde o banco deve ser criado.

Precisaremos também, informar no arquivo "AndroidManifest.xml" que utilizaremos a permissão de escrita no armazenamento externo:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />



Executando novamente o código será possível notar que o banco agora está residindo na pasta "/mnt/sdcard/projetoBancoDeDados". E economizando um bocado de memória interna!

That´s it. Stay tuned!

segunda-feira, 18 de maio de 2015

Trabalhando com banco de dados SQLite no Android

Uma das coisas que me deixaram de queixo caído quando comecei a aprender a desenvolver para Android foi saber que todo celular com esse sistema operacional vem de fábrica com um servidor de banco de dados relacional completo embutido, gratuito e pronto para o uso. Quem trabalha com informática há muitos anos e já teve experiência com grandes servidores de banco de dados Oracle ou SQL Server compartilhará da mesma opinião.

O Android vem com um banco de dados conhecido como "SQLite". Ele está embutido em todo dispositivo Android. É um banco de dados "open source" e suporta as ferramentas relacionais tradicionais, queries em SQL e transações. Requer pouquíssima memória para executar (cerca de 250KB). 

Para que o desenvolvedor utilize SQLite não é necessário nenhum procedimento de instalação ou mesmo privilégios de administrador de banco de dados. O banco apenas está lá, aguardando para ser utilizado.

Só é preciso definir algumas instruções SQL para criar e atualizar a base de dados. À partir daí a plataforma Android administra o banco automaticamente.

Acessar o banco de dados SQLite envolve a utilização do sistema de arquivos, o que pode trazer algum overhead. Portanto, acessos à base de dados em aplicativos reais devem ser feitos de forma assíncrona.

Quando uma aplicação cria um banco de dados, por padrão ele será armazenado no seguinte diretório (da memória interna do aparelho):

data/data/[nomeDoApp]/databases/[nomeDoArquivo.db]

A criação do banco de dados em aplicativos Android é feita utilizando-se a classe "SQLiteOpenHelper".

Vamos então, começar o trabalho:

Primeiro vamos criar um projeto chamado "projetoBancoDeDados". Então, na pasta "src" vamos criar uma classe chamada "MySQLiteHelper", que será responsável por criar a base (caso ela ainda não exista) ou atualizá-la (caso tenhamos modificado o valor da versão do banco de dados), com o seguinte conteúdo:

package com.example.projetobancodedados;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class MySQLiteHelper extends SQLiteOpenHelper
{

   // Define o nome da base de dados e a versão.
   private static final String DATABASE_NAME="banco.db";
   private static final int DATABASE_VERSION=1;

   // Script de criação do banco de dados.
   private static final String DATABASE_CREATE = " create table if not exists funcionario (" +
                                                                        " id integer primary key autoincrement," +
                                                                        " nome varchar(100), " +
                                                                        " cargo varchar(50), " +
                                                                        " salario real " +
                                                                        " );";


   public MySQLiteHelper(Context context)
   {
      super(context, DATABASE_NAME, null, DATABASE_VERSION);
   }

   @Override
   public void onCreate(SQLiteDatabase database)
   {
      // Cria o banco de dados, executando o SQL.
      database.execSQL(DATABASE_CREATE);
   }

   @Override
   public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
   {
      // Elimina e recria a base, caso o valor da versão tenha sido alterado.
      db.execSQL("DROP TABLE IF EXISTS funcionario ");
      onCreate(db);
   }

}


O próximo passo é criar uma classe "FuncionarioDTO", que definirá a estrutura que utilizaremos para armazenar um funcionário no banco de dados e também recuperá-lo:


package com.example.projetobancodedados;

public class FuncionarioDTO
{
   private int id;
   private String nome;
   private String cargo;
   private Double salario;

   public int getId()
   {
      return id;
   }
   public void setId(int id)
   {
      this.id=id;
   }
   public String getNome()
   {
      return nome;
   }
   public void setNome(String nome)
   {
      this.nome=nome;
   }
   public String getCargo()
   {
      return cargo;
   }
   public void setCargo(String cargo)
   {
      this.cargo=cargo;
   }
   public Double getSalario()
   {
      return salario;
   }
   public void setSalario(Double salario)
   {
      this.salario=salario;
   }   
}


Agora vamos criar a classe mais importante, que será o nosso ponto de acesso ao banco de dados (DAO). Vamos nomear essa classe "FuncionarioDAO". Definiremos aqui os métodos "save", "delete" e "getAll", que serão responsáveis em gravar e apagar um funcionário, e obter todos os registros de funcionário:



package com.example.projetobancodedados;


import java.util.ArrayList;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;

public class FuncionarioDAO
{

   private SQLiteDatabase database;
   private MySQLiteHelper dbHelper;

   public FuncionarioDAO(Context context)
   {
      dbHelper=new MySQLiteHelper(context);
   }


   // Abre o banco de dados.
   public void open() throws SQLException
   {
      database=dbHelper.getWritableDatabase();
   }

   
   // Fecha o banco de dados.
   public void close()
   {
      dbHelper.close();
   }

   
   // Insere o funcionário, caso ainda não exista. Ou atualiza.
   public boolean save(FuncionarioDTO funcionario)
   {
      ContentValues values=new ContentValues();
      values.put("id", funcionario.getId());
      values.put("nome", funcionario.getNome());
      values.put("cargo", funcionario.getCargo());
      values.put("salario", funcionario.getSalario());

      Cursor cursor=database.rawQuery("SELECT 1 from funcionario where id = " + funcionario.getId(), null);

      if (cursor.getCount() == 0)
      {
         database.insert("funcionario", null, values);
      }
      else
      {
         database.update("funcionario", values, " id = ? ", new String[] {  String.valueOf(funcionario.getId()) });
      }

      return true;

   }

   
   // Apaga um registro de funcionário.
   public void delete(FuncionarioDTO funcionario)
   {
      int id=funcionario.getId();
      database.delete("funcionario", " id = " + id, null);
   }

   
   // Retorna uma lista com todos os funcionários.
   public ArrayList<FuncionarioDTO> getAll()
   {
      ArrayList<FuncionarioDTO> funcionarios=new ArrayList<FuncionarioDTO>();

      Cursor cursor=database.rawQuery("SELECT * from funcionario;", null);

      cursor.moveToFirst();
      while (!cursor.isAfterLast())
      {
         FuncionarioDTO funcionario=cursorToFuncionario(cursor);
         funcionarios.add(funcionario);
         cursor.moveToNext();
      }

      // Fecha o cursor.
      cursor.close();

      // Retorna lista de funcionários.
      return funcionarios;

   }

   // Converte de cursor para FuncionarioDTO.
   private FuncionarioDTO cursorToFuncionario(Cursor cursor)
   {
      FuncionarioDTO funcionario=new FuncionarioDTO();
      funcionario.setId(cursor.getInt(0));
      funcionario.setNome(cursor.getString(1));
      funcionario.setCargo(cursor.getString(2));
      funcionario.setSalario(cursor.getDouble(3));

      return funcionario;
   }

}


Por último, vamos alterar a classe "MainActivity" para que fique como o código descrito abaixo. O que estamos fazendo aqui é incluir três funcionários (JOSÉ, MARIA E HENRIQUE) e, logo em seguida, apagar o registro de um deles (MARIA), para exemplificar as operações. Você poderá modificar o código para experimentar livremente a inclusão de outros funcionários e a alteração dos dados dos já existentes.

Também colocamos a funcionalidade de listar todos os registros existentes. Sendo que, neste exemplo, a saída é realizada no LogCat (devendo existir uma tag "debug" para visualização no Eclipse).

Observe com especial atenção os métodos onResume() e onPause(). É muito importante fechar o banco e reabri-lo nos momentos corretos.


package com.example.projetobancodedados;

import java.util.ArrayList;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;

public class MainActivity extends Activity
{

   // Define o ponto de acesso ao banco (DAO) e o objeto da classe descritiva de funcionário.
   private FuncionarioDAO funcionarioDAO;
   private FuncionarioDTO funcionario;
   
   @Override
   protected void onCreate(Bundle savedInstanceState)
   {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);

      // Abre o banco de dados de funcionário.
      funcionarioDAO=new FuncionarioDAO(this);
      funcionarioDAO.open();

      // Instancia em memória um novo funcionário e preenche seus dados.
      funcionario = new FuncionarioDTO();
      funcionario.setId(1);
      funcionario.setNome("JOSÉ SILVA");
      funcionario.setCargo("Auxiliar administrativo");
      funcionario.setSalario(985.63);
      // Grava no banco (insere ou atualiza) o funcionário.
      funcionarioDAO.save(funcionario);

      
      // Instancia outro funcionário.
      funcionario = new FuncionarioDTO();
      funcionario.setId(2);
      funcionario.setNome("MARIA JOSÉ");
      funcionario.setCargo("Cozinheira");
      funcionario.setSalario(627.35);    
      // Grava no banco (insere ou atualiza) o funcionário.
      funcionarioDAO.save(funcionario);


      // Instancia outro funcionário.
      funcionario = new FuncionarioDTO();
      funcionario.setId(3);
      funcionario.setNome("HENRIQUE ARIAS");
      funcionario.setCargo("Motorista");
      funcionario.setSalario(1239.56);
      // Grava no banco (insere ou atualiza) o funcionário.
      funcionarioDAO.save(funcionario);
      
      
      // Apaga um funcionário.
      funcionario.setId(2);
      funcionarioDAO.delete(funcionario);
      

      // Obtem lista de todos os funcionários da base.
      ArrayList<FuncionarioDTO> listaDeFuncionarios = new ArrayList<FuncionarioDTO>();
      listaDeFuncionarios=funcionarioDAO.getAll();
      
      // Mostra no LogCat todos os funcionários da base.
      for(FuncionarioDTO f: listaDeFuncionarios)
      {
         
         Log.i("debug", f.getId() + "");
         Log.i("debug", f.getNome());
         Log.i("debug", f.getCargo());
         Log.i("debug", f.getSalario() + "");
         
      }
      
   }

   @Override
   protected void onResume()
   {
      // Reabre o banco.
      funcionarioDAO.open();
      super.onResume();
   }

   @Override
   protected void onPause()
   {
      // Fecha o banco.
      funcionarioDAO.close();
      super.onPause();
   }

}


Depois que executarmos o código pela primeira vez, poderemos visualizar as tabelas do banco de dados e seu conteúdo através do próprio Eclipse (disponível apenas para quem baixou nosso ambiente de desenvolvimento), utilizando o "Questoid SQLite browser", que é um plugin que já incluímos no nosso ambiente.

Para isto, devemos utilizar a aba "DDMS" no Eclipse, conforme a imagem:




E então devemos clicar no nome do dispositivo e navegar até o arquivo de banco de dados criado pelo nosso aplicativo na pasta:

"data/data/com.example.projetobancodedados/banco.db"

Clicando no ícone azul (com desenho de um banco de dados) no canto superior direito da tela, teremos acesso ao "Questoid SQLite browser" na parte de baixo da tela. Ele nos permite ver tanto a estrutura do banco de dados quanto os valores, conforme ilustram as imagens abaixo:






Para quem preferir baixar o projeto completo, ei-lo aqui para download.

Com isto, demonstramos com sucesso, de forma introdutória, como utilizar banco de dados local no Android. O artigo, obviamente, não esgota o assunto, mas fornece ao leitor os subsídios necessários para ter uma ideia a respeito do uso do banco neste ambiente.

Isso abre novas perspectivas, permitindo a criação de apps ainda mais úteis. Também significa novas possibilidades de otimização (utilizando o banco local como cache) e, inclusive, o desenvolvimento de aplicativos offline (atualizando seus dados somente na presença de sinal de Internet).

Espero que tenha sido proveitoso. Um forte abraço e até a próxima!