Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android in Practice

Android in Practice

Android in Practice

jmortegac

April 26, 2015
Tweet

More Decks by jmortegac

Other Decks in Programming

Transcript

  1. Android in Practice
    José Manuel Ortega
    Techfest uc3m February 2014

    View full-size slide

  2. Index
     DEVELOPMENT ENVIRONMENT / SDK / TOOLS / ADB
     UI DESIGN /ANDROID ASSET STUDIO
     GOOGLE PLAY SERVICES / GOOGLE MAPS V2 / MAP FRAGMENTS
     ACTION BAR / SEARCH VIEW / NOTIFICATIONS / ACCESSIBILITY /
    CONTEXTUAL MENU
     WEBVIEW / NETWORK CONNECTIONS / ASYNCTASK
     SINGLETON / ADAPTER PATTERN / VIEW HOLDER PATTERN
     FRAGMENTS / MASTER-DETAIL / DIALOG FRAGMENTS
     NAVIGATION DRAWER / ACTION BAR DRAWER TOOGLE /TABS
     ACTION SHARE / CONTACTS / LOADERS
     HTTP REQUEST /VOLLEY / INSTAGRAM API
     GEOLOCATION / SHARED PREFERENCES / SQLITE DATABASE
     LIBRARIES

    View full-size slide

  3. Development environment
     JAVA JDK+ANDROID SDK
     http://www.oracle.com/technetwork/es/java/javasebusiness/downlo
    ads/index.html
     http://developer.android.com/sdk/index.html
     https://developer.android.com/intl/es/reference/android/app/packag
    e-summary.html
    Android SDK Starter Package

    View full-size slide

  4. Development process

    View full-size slide

  5. Tools
     Tools directory
    http://developer.android.com/tools/help/index.html
     Support libraries and compatibility
    In recent updates of Eclipse and the Android SDK
    android-support-V4.jar is added by default in the libs folder when
    new project is created
    http://developer.android.com/tools/support-library/index.html
     Adb commands
     Process for communicating the device(real or emulated) with
    hardware development
     adb devices
     adb install aplicación.apk –r[update]
     adb uninstall aplicación.apk –k[dont clean application/cache data]

    View full-size slide

  6. Spain Computing University

    View full-size slide

  7. UI Design
     http://developer.android.com/design/index.html
    Ways to specify the size of an item:
     dp/dip: Density independent pixel.
     sp/sip: Scale independent pixel. Used in font sizes.
     pt: Point.
     px: Pixel. Not use
    Ancho y alto
     match_parent:takes all the space available.
     wrap_content: uses the space needed
     fill_parent:equivalent to match_parent

    View full-size slide

  8. Android Asset Studio
     Generate icons and graphic elements for each resolution
     http://android-ui-utils.googlecode.com/hg/asset-studio/dist/index.html

    View full-size slide

  9. Android manifest.xml
     Define the components and application permissions





    android:allowBackup="true"
    android:icon="@drawable/icon"
    android:label="@string/app_name"
    android:theme="@style/Theme.Styled"
    android:configChanges="locale|keyboard|keyboardHidden|orientation"
    android:name="com.proyecto.spaincomputing.singleton.MySingleton"
    android:hardwareAccelerated="true" >
    android:name="com.proyecto.spaincomputing.SplashScreenActivity«
    android:label="@string/app_name"
    android:theme="@style/TransparentTheme" >





    android:configChanges="locale|keyboard|keyboardHidden|orientation"
    android:hardwareAccelerated="true"/>

    http://developer.android.com/reference/android/Manifest.permission.html

    View full-size slide

  10. Resources /Screen support
     External elements you want to include and reference in the application.
     Declaratively include in /res ,accesing by @/
     Are programmatically accessible via the R class
    (compiled with Android Asset Packaging Tool)
     Android automatically selects the resource that adapts to the environment
     Each resource type in a folder / res.
     drawable: Images, Icons.
     layout: Layout to organize views.
     values:
     string.xml: Text strings
     colors.xml
     dimens.xml: font sizes
     anim: Animations
     raw: Other resources like audio or video
     menu: Menus and dialogs
     xml: Other xml (preferences, app widget, …)
    android:xlargeScreens="true" android:largeScreens="true"
    android:normalScreens="true" android:smallScreens="true" />

    View full-size slide

  11. Google play services

    View full-size slide

  12. Google play services
     Check if the service is available
    import com.google.android.gms.common.GooglePlayServicesUtil;
    int resultCode =
    GooglePlayServicesUtil.isGooglePlayServicesAvailable
    (getApplicationContext());
    Can return the constants:
    ConnectionResult.SUCCESS:
    ConnectionResult.DEVELOPER_ERROR:
    ConnectionResult.INTERNAL_ERROR:
    ConnectionResult.INVALID_ACCOUNT:
    ConnectionResult.NETWORK_ERROR:
    ConnectionResult.RESOLUTION_REQUIRED:
    ConnectionResult.SERVICE_DISABLED:
    ConnectionResult.SERVICE_INVALID:
    ConnectionResult.SERVICE_MISSING:
    ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED:
    ConnectionResult.SIGN_IN_REQUIRED:

    View full-size slide

  13. OPENGL 2.0 SUPPORT
     Google Maps Android API v2 only supports devices with
    OpenGL ES 2.0 and above.
     Check if the system supports OpenGL ES 2.0
    final ActivityManager activityManager =
    (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
    final ConfigurationInfo configurationInfo =
    activityManager.getDeviceConfigurationInfo();
    final boolean supportsEs2 = configurationInfo.reqGlEsVersion >=
    0x20000;

    View full-size slide

  14. GoogleMaps v2
     The maps are integrated into Google services play
     Obtain key
     https://developers.google.com/maps/documentation/android/start#the_goo
    gle_maps_api_key
     Get signed certificate using the keytool command
     Google APIS Console
     https://code.google.com/apis/console/
     Google CLOUD Console
     https://cloud.google.com/console
    keytool -list -v -keystore "%USERPROFILE%/.android/debug.keystore"

    View full-size slide

  15. GoogleMaps v2
     Windowpreferencesandroidbuild

    View full-size slide

  16. GoogleMaps v2

    View full-size slide

  17. Permissions /Features Google Mapsv2
    android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>







    android:glEsVersion="0x00020000"
    android:required="true"/>
    android:name="com.proyecto.spaincomputing.permission.MAPS_RECEIVE"
    android:protectionLevel="signature"/>
    android:name="com.proyecto.spaincomputing.permission.MAPS_RECEIVE"/>
    android:value="YOUR_API_KEY"/>
    android:name="com.google.android.gms.version"
    android:value="@integer/google_play_services_version" />

    View full-size slide

  18. Map fragments
     layout/fragment_mapa.xml
    android:id="@+id/map"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    class="com.google.android.gms.maps.SupportMapFragment"/>
     MapaActivity. FragmentActivity from android.support.v4
    import android.support.v4.app.FragmentActivity;
    public class MapaActivity extends FragmentActivity{
    private GoogleMap mapa=null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.fragment_mapa);
    mapa = ((SupportMapFragment)
    getSupportFragmentManager().findFragmentById(R.id.map))
    .getMap();
    }}

    View full-size slide

  19. Markers/InfoWindow
    public class MapaActivity extends FragmentActivity implements
    OnMapLongClickListener, InfoWindowAdapter{
    @Override
    public void onMapLongClick(LatLng location) {
    MarkerOptions options = new MarkerOptions()
    .position(location) //location mandatory
    .title(title)
    .snippet(snippet);
    Marker marker = mapa.addMarker(options);
    }
    @Override
    public View getInfoContents(Marker marker) {
    View window = ((LayoutInflater)
    getApplicationContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE).
    inflate(R.layout.info_window, null);
    txt_title.setText(marker.getTitle());
    txt_snippet.setText(marker.getSnippet());
    return window;
    }
    }

    View full-size slide

  20. Markers/InfoWindow

    View full-size slide

  21. OPENGL / DISTANCE POLYGON

    View full-size slide

  22. Action Bar
     ActionBArSherlock.
     It is an extension of functionality implemented in the support library
     http://actionbarsherlock.com/
     ActionBarCompat
    Create a new project with API 18 and add the project in the folder
    sdk\extras\android\support\v7\appcompat.

    View full-size slide

  23. Action Bar search view

    View full-size slide

  24. Action Bar search view
     Implement interface OnQueryTextListener
     Override methods onQueryTextSubmit, onQueryTextChange
    @Override
    public boolean onQueryTextSubmit(String query) {
    UniversityListFragment fragmentList =
    (UniversityListFragment)getSupportFragmentManager().findFragmentBy
    Tag("list_fragment");
    fragmentList.searchData(query);
    return true;
    }
    @Override
    public boolean onQueryTextChange(String query) {
    return true;
    }

    View full-size slide

  25. Action Bar search view
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    mSearchView = (SearchView) searchItem.getActionView();
    mSearchView.setQueryHint("Search...");
    mSearchView.setOnQueryTextListener(this);
    return true;
    }
    android:showAsAction="always"
    android:title="@string/search"
    android:icon="@android:drawable/ic_menu_search"
    android:actionViewClass=
    "android.support.v7.widget.SearchView" />
    private SearchView mSearchView;

    View full-size slide

  26. ACTION BAR
     Icon to go back in Action Bar
    ActionBar ab = getSupportActionBar();
    ab.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE
    |ActionBar.DISPLAY_SHOW_HOME|ActionBar.DISPLAY_HOME_AS_UP);
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
    case android.R.id.home:
    this.finish();
    break;
    }
    return super.onOptionsItemSelected(item);
    }
     Action Bar in fragments
     setHasOptionsMenu(true)
     onCreateOptionsMenu: to inflate the menu provided by the fragment.
     onOptionsItemSelected: to respond to the user touch in an item provided by
    the fragment menu.

    View full-size slide

  27. ACTION BAR PROGRESS
     Progress bar in Action Bar
    private Activity activity = this;
    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    this.getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
    ActionBar ab = getSupportActionBar();
    ………
    }
    activity.setProgressBarIndeterminateVisibility(true);
    viewer.setWebViewClient(new WebViewClient() {
    public void onPageFinished(WebView view, String url) {
    activity.setProgressBarIndeterminateVisibility(false);
    }
     Show the progress bar to start loading and hide when finished.

    View full-size slide

  28. Notifications
     To generate notifications in the status bar of the system we use a class included in
    library compatibility android-support-v4.jar.
     The class is NotificationCompat.Builder and we have to create a new object
    passing by parameter application context and assign all the properties you want
    through their set () methods.
    import android.support.v4.app.NotificationCompat;
    import android.support.v4.app.TaskStackBuilder;
    NotificationCompat.Builder myNotification = new
    NotificationCompat.Builder(context)
    .setContentTitle(titulo)
    .setContentText(texto)
    .setTicker(aviso)
    .setWhen(System.currentTimeMillis())
    .setContentIntent(contentIntent)
    .setDefaults(Notification.DEFAULT_SOUND)
    .setAutoCancel(true)
    .setSmallIcon(R.drawable.favorito)
    .setLargeIcon(largeIcon);

    View full-size slide

  29. Notifications
     Create and launch notification via object NotificationManager
    TaskStackBuilder
    stackBuilder=TaskStackBuilder.create(context);
    stackBuilder.addParentStack(PrincipalActivity.class);
    stackBuilder.addNextIntent(notificationIntent);
    PendingIntent
    resultPendingIntent=stackBuilder.getPendingIntent(0,
    PendingIntent.FLAG_UPDATE_CURRENT);
     Associating action to notification
    NotificationManager notificationManager;
    notificationManager =(NotificationManager)
    context.getSystemService(Context.NOTIFICATION_SERVICE);
    /*Create notification with builder*/
    Notification notification=myNotification.build();
    notificationManager.notify((int)
    System.currentTimeMillis(), notification);

    View full-size slide

  30. Accessibility
    http://developer.android.com/training/accessibility/index.html
    android:id="@+id/pause_button"
    android:src="@drawable/pause"
    android:contentDescription="@string/pause"/>
     Through layout
     Through code
    String contentDescription = "Select " + strValues[position];
    label.setContentDescription(contentDescription);

    View full-size slide

  31. Contextual menu

    View full-size slide

  32. Contextual menu
     Add menu items by code
    @Override
    public void onCreateContextMenu(ContextMenu menu, View
    v,ContextMenuInfo menuInfo) {
    if(listado.size()>0){
    menu.setHeaderTitle(R.string.accion);
    if(!favoritos){
    menu.add(0, Constants.MENU_FAVORITO, 3,
    getResources().getText(R.string.anyadir_favorito));
    }else{
    menu.add(0, Constants.MENU_FAVORITO, 3,
    getResources().getText(R.string.eliminar_favorito));
    }
    }}
    lstListado.setAdapter(new UniversidadAdapter(this));
    registerForContextMenu(lstListado); //view register
     Floating list at long press on a View row

    View full-size slide

  33. Contextual menu
     Add options menu with xml (recommended option)


    android:showAsAction="ifRoom|withText"
    android:icon="@drawable/location" />
    android:title="@string/ruta_google"
    android:showAsAction="ifRoom|withText"
    android:icon="@drawable/google_map" />

    @Override
    public void onCreateContextMenu(ContextMenu menu, View
    v,ContextMenuInfo menuInfo) {
    super.onCreateContextMenu(menu, v, menuInfo);
    MenuInflater inflater=getActivity().getMenuInflater();
    if(listado.size()>0){
    inflater.inflate(R.menu.menu_mostrar, menu);
    }}

    View full-size slide

  34. Contextual menu
    //Manage touch in contextual menu
    @Override
    public boolean onContextItemSelected(android.view.MenuItem item)
    {
    AdapterContextMenuInfo info = (AdapterContextMenuInfo)
    item.getMenuInfo(); //selected row
    UniversidadBean ub = (UniversidadBean) getUniversidad((int)
    info.id);
    switch (item.getItemId()) {
    case Constants.MENU_ENLACE:
    if(ub.getEnlace()!=null){
    listener.onUniversidadLink(ub.getEnlace());
    }
    return true;
    default:
    return super.onContextItemSelected(item);
    }
    }

    View full-size slide

  35. WebView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/visorWebView">

    import android.webkit.WebView;
    import android.webkit.WebViewClient;
    WebView viewer;
    viewer = (WebView) findViewById(R.id.visorWebView);
    viewer.setWebViewClient(new myWebClient());
    viewer.getSettings().setJavaScriptEnabled(true);
    viewer.getSettings().setBuiltInZoomControls(true);
    viewer.loadUrl(content);

    View full-size slide

  36. WebView
    public class myWebClient extends WebViewClient{
    @Override
    public void onPageStarted(WebView view, String url,
    Bitmap favicon) {
    super.onPageStarted(view, url, favicon);
    }
    @Override
    public boolean shouldOverrideUrlLoading(WebView view,
    String url) {
    view.loadUrl(url);
    return true;
    }
    @Override
    public void onPageFinished(WebView view, String url) {
    super.onPageFinished(view, url);
    }
    }

    View full-size slide

  37. Network connection
    import android.net.ConnectivityManager;
    import android.net.NetworkInfo;
    ConnectivityManager connectivityManager;
    connectivityManager = (ConnectivityManager)
    context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo networkInfo =
    connectivityManager.getActiveNetworkInfo();
    Boolean connected = networkInfo != null &&
    networkInfo.isAvailable() && networkInfo.isConnected();
    android:name="android.permission.ACCESS_NETWORK_STATE"/>

    View full-size slide

  38. Network connection
     Enable Wifi / Check connection type
    NetworkInfo wifiInfo=connectivityManager.getNetworkInfo
    (ConnectivityManager.TYPE_WIFI);
    NetworkInfo
    mobileInfo=connectivityManager.getNetworkInfo
    (ConnectivityManager.TYPE_MOBILE);
    if(wifiInfo.isConnected()){
    Toast.makeText(context, "Wifi is connected",
    Toast.LENGTH_LONG).show();
    }
    if(mobileInfo.isConnected()){
    Toast.makeText(context, "3G/4G is connected",
    Toast.LENGTH_LONG).show();
    }
    WifiManager wifiManager=(WifiManager)
    context.getSystemService(Context.WIFI_SERVICE);
    wifiManager.setWifiEnabled(true);
    android:name="android.permission.CHANGE_WIFI_STATE"/>

    View full-size slide

  39. Asynctask
     This class will allow us to perform background tasks without using neither
    directly nor Handlers Threads, trying these elements in a fully transparent way
    to the programmer.
     When we define a AsyncTask class must define the type of three elements, the
    input parameters, its progress and outcome.
     Override
    onPreExecute(),doInBackground(),onPostExecute(),onProgressUpdate()
    class RequestTask extends AsyncTask{
    @Override
    protected void onPreExecute() {
    }
    @Override
    protected void onProgressUpdate(Integer... values) {
    }
    @Override
    protected String doInBackground(String... uri) {
    }
    @Override
    protected void onPostExecute(String result) {
    super.onPostExecute(result);
    }
    new
    RequestTask().
    execute(url);

    View full-size slide

  40. Asynctask
     onPreExecute, run before executing our task and we can use it to initialize
    tasks as displaying a progress bar.
     doInBackground(Params...), here we schedule our background task. Params
    in the case discussed earlier will be an array of Strings that are passed when
    calling the execute method. Being a process that can take an indefinite time to
    complete, we can give feedback to the user thanks to percent complete in
    publishProgress(Progress. ..) method.
     publishProgress receives an integer parameter and make the execution of
    onProgressUpdate method to indicate the user the task percentage.
     onPostExecute(Result), runs to finish the process in the background.
     The problem of doInBackground method is blocking the UI, so while this
    method is running can not get anything in the ui, but if we have no
    choice ,we can use this code for debug.
    runOnUiThread(new Runnable() {
    public void run() {
    Toast.makeText(getApplicationContext(), "Example for
    Toast",Toast.LENGTH_SHORT).show();}
    });

    View full-size slide

  41. Singleton
     Only one instance
     Application class
    android:allowBackup="true"
    android:icon="@drawable/icon"
    android:label="@string/app_name"
    android:theme="@style/Theme.Styled"
    android:name=
    "com.proyecto.spaincomputing.singleton.MySingleton">
    public class MySingleton extends Application
    {
    private static MySingleton instance;
    public static Context context;
    @Override public void onCreate() {
    super.onCreate();
    context = getApplicationContext();
    }
    }

    View full-size slide

  42. Navigation Pattern
    import android.support.v4.app.NavUtils;
    NavUtils.navigateUpTo(this,
    new Intent(this, ListadoActivity.class));
    http://developer.android.com/design/patterns/navigation.html
    Intent intent = NavUtils.getParentActivityIntent(this);
    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|
    Intent.FLAG_ACTIVITY_SINGLE_TOP);
    NavUtils.navigateUpTo(this, intent);
    NavUtils.navigateUpFromSameTask(this);
    android:parentActivityName=".ListadoActivity"

    android:value=".ListadoActivity" />

    View full-size slide

  43. Adapter Pattern
     Link between the source data and the view
     It works with ListView or GridView
     There are many types of adapter
     You can perform a custom adaptar
     The most used ArrayAdapter / CursorAdapter
     The Adapter interacts with a collection of data objects for display in
    View
    ArrayList
    listado=newArrayList();
    private ListView lstListado;
    lstListado=(ListView)getView().findViewById
    (R.id.LstListado);
    lstListado.setAdapter
    (new UniversidadAdapter(this,listado));

    View full-size slide

  44. View Holder/View Container Pattern
     ViewContainer
    static class ViewContainer{
    public ImageView imagen;
    public TextView nombre;
    public TextView descripcion;
    }
     Improve the performance of listview
     The viewholder is used to avoid calling findViewById whenever prompted a
    new row to the adapter. Thus, instead of calling findViewById each time you
    use the references to the fields you have stored in the viewholder.
     This pattern will help us to limit the number of calls to findViewById method.
    The idea would be to call it once, and then save the view daughter that refers
    to the instance of ViewHolder to be associated with the object by the method
    convertView View.setTag ()
     Its recommend using a static class to store the items of each row in the view,
    functioning as a kind of cache for our view.

    View full-size slide

  45. View Holder/View Container Pattern
    @Override
    public View getView(int position, View convertView,ViewGroup parent) {
    ViewContainer viewContainer;
    //si es la primera vez que se imprime la fila
    if(convertView==null){
    LayoutInflater inflater = context.getLayoutInflater();
    convertView = inflater.inflate(R.layout.row, null,true);
    //crea una vista para el objeto contenedor
    viewContainer=new ViewContainer()
    //obtiene una referencia a todas las vistas de la fila
    viewContainer.nombre=(TextView)convertView.findViewById(R.id.textView_superior);
    viewContainer.descripcion=(TextView)convertView.findViewById(R.id.textView_inferior);
    viewContainer.imagen=(ImageView)convertView.findViewById(R.id.imageView_imagen);
    //asigna el contenedor de la vista a rowView
    convertView.setTag(viewContainer);
    }else{
    viewContainer=(ViewContainer) convertView.getTag(); //recicling }
    //personaliza el contenido de cada fila basándone en su posición
    viewContainer.nombre.setText(listado.get(position).getNombre());
    viewContainer.descripcion.setText(listado.get(position).getDescripcion());
    viewContainer.imagen.setImageResource(listado.get(position).getIdImagen());
    return(convertView);
    }

    View full-size slide

  46. View Holder/View Container Pattern
     Debug

    View full-size slide

  47. Fragments
     A fragment represents a certain behavior or a portion of a user interface
    activity.
     Multiple fragments can be combined.
     A fragment must always be part of an activity.
     They emerged to provide greater flexibility to build the user interface
     Override methods
    @Override
    //called when finish onCreate method in activity
    public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    }
    @Override
    public void onCreate(Bundle savedInstanceState) {//inicializar componentes
    super.onCreate(savedInstanceState);
    }
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle
    savedInstanceState) {
    }
    http://developer.android.com/guide/components/fragments.html

    View full-size slide

  48. Fragments
     Add a fragment to view
     by layout xml

    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    class="com.proyecto.spaincomputing.fragment.UniversidadesFragment"
    android:id="@+id/FrgListado"
    android:layout_width="375dp"
    android:layout_height="match_parent"/>
    class="com.proyecto.spaincomputing.fragment.FragmentDetalle"
    android:id="@+id/FrgDetalle"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    View full-size slide

  49. Fragments
     Add a fragment to view
     By code
    //Fragments array
    Fragment[] fragments = new Fragment[]{new PortadaFragment(),
    new UniversityListFragment(),new UniversidadesImagesFragment()};
    FragmentManager manager = getSupportFragmentManager();
    manager.beginTransaction()
    .add(R.id.contentFrame, fragments[0])
    .add(R.id.contentFrame, fragments[1])
    .add(R.id.contentFrame, fragments[2])
    .commit();
    //show/hide
    manager.beginTransaction().show(fragments[0]).commit();
    manager.beginTransaction().hide(fragments[1]).commit();
    manager.beginTransaction().hide(fragments[2]).commit();

    View full-size slide

  50. Fragments/ Master-detail
    PORTRAIT
    LANDSCAPE

    View full-size slide

  51. Fragments/ Master-detail
    PORTRAIT
    LANDSCAPE

    View full-size slide

  52. Fragments/ Master-detail
     2 layout
     layout\fragment_universidades_list.xml

    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    android:id="@+id/listView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" >


    View full-size slide

  53. Fragments/ Master-detail
     2 layout
     layout-land\fragment_universidades_list.xml
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:baselineAligned="false"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="horizontal"
    tools:context=".TabsActivity" >
    android:id="@+id/listView"
    android:layout_width="0px"
    android:layout_height="wrap_content"
    android:layout_weight="1.5">

    android:id="@+id/fragmentUniversidadInfo"
    android:name="com.proyecto.spaincomputing.fragment.UniversidadInfoFragment"
    android:layout_width="0px"
    android:layout_height="wrap_content"
    android:layout_weight="4"
    tools:layout="@layout/fragment_universidad_info" />

    View full-size slide

  54. Fragments/ Master-detail
     Check orientation to display the detail page
    UniversidadBean ub=listado.get(position);
    if (getResources().getConfiguration().orientation ==
    Configuration.ORIENTATION_LANDSCAPE) {
    FragmentManager manager =
    getActivity().getSupportFragmentManager();
    UniversidadInfoFragment fragment = (UniversidadInfoFragment)
    manager.findFragmentById(R.id.fragmentUniversidadInfo);
    fragment.loadWebViewContent(ub.getEnlace());
    getActivity().invalidateOptionsMenu();
    } else {
    Intent intent = new Intent(getActivity().getApplicationContext(),
    UniversityDetailActivity.class);
    intent.putExtra(UniversityDetailActivity.URL, ub.getEnlace());
    url=ub.getEnlace();
    intent.putExtra(UniversityDetailActivity.UNIVERSIDAD,
    ub.getNombre());
    startActivity(intent);
    }

    View full-size slide

  55. Dialog Fragment
     The DialogFragment class provides all the controls you need to create a dialogue and
    change its appearance. It was introduced in Android 3.0 (API 11) and is available in the
    support library, so that it can be used without problems with older versions.
     Use the class you DialogFragment ensures proper management of events that may
    occur, such as clicking the Back button or rotate the screen. Furthermore, using this class
    we can reuse the interface dialogues as a larger component interface.
     To create a dialogue create a class that inherits from the DialogFragment class and
    create an AlertDialog object in the onCreateDialog() method
    import android.support.v4.app.DialogFragment;
    public class LinkDialogFragment extends DialogFragment {
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
    AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
    builder.setTitle(getText(R.string.title).toString());
    .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener()
    {
    public void onClick(DialogInterface dialog, int id) {
    listener.onDialogNegativeClick(LinkDialogFragment.this);
    }
    });
    return builder.create();
    }}

    View full-size slide

  56. Dialog Fragment
     To display the dialog we just created we have to create an instance of the
    class in our business and invoke the show () method so that the dialog
    appears.
     With getSupportFragmentManager() call method get access to
    FragmentManager that is responsible for managing the fragments of the
    application.
    //LinkDialogFragment object
    LinkDialogFragment linkDialogFragment=new LinkDialogFragment();
    //fragment parameters
    Bundle args = new Bundle();
    args.putString("ENLACE", enlace);
    linkDialogFragment.setArguments(args);
    linkDialogFragment.show(getSupportFragmentManager(), "");

    View full-size slide

  57. Navigation Drawer

    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/drawerLayout">

    android:id="@+id/contentFrame"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

    android:layout_width="240dp"
    android:layout_height="match_parent"
    android:layout_gravity="start"
    android:choiceMode="singleChoice"
    android:divider="@android:color/transparent"
    android:dividerHeight="0dp"
    android:background="#111"/>

     http://developer.android.com/training/implementing-navigation/nav-
    drawer.html

    View full-size slide

  58. Navigation Drawer
    private ListView drawerList;
    private String[] drawerOptions;
    private DrawerLayout drawerLayout;
    drawerLayout = (DrawerLayout) findViewById(R.id.drawerLayout);
    drawerList = (ListView) findViewById(R.id.left_drawer);
    drawerOptions = getResources().getStringArray(R.array.drawer_options);
    // Set the adapter for the list view
    drawerList.setAdapter(new ArrayAdapter(getApplicationContext(),
    R.layout.drawer_list_item,drawerOptions));
    // Set the list's click listener
    drawerList.setOnItemClickListener(new DrawerItemClickListener());
    private class DrawerItemClickListener implements ListView.OnItemClickListener {
    @Override
    public void onItemClick(AdapterView> parent, View view, int position,long id)
    { setContent(position);
    }
    }

    View full-size slide

  59. Navigation Drawer/ ActionBarDrawerToogle
    private ActionBarDrawerToggle drawerToggle;
    drawerToggle = new ActionBarDrawerToggle(
    this,
    drawerLayout,
    R.drawable.ic_drawer,
    R.string.drawer_open, //accesibility text string
    R.string.drawer_close
    ) {
    public void onDrawerClosed(View view) {
    ActivityCompat.invalidateOptionsMenu(PrincipalActivity.this);
    }
    public void onDrawerOpened(View drawerView) {
    ActivityCompat.invalidateOptionsMenu(PrincipalActivity.this);
    }
    }
    //listener
    drawerLayout.setDrawerListener(drawerToggle);
    drawerToggle.setDrawerIndicatorEnabled(true);
     Open/close sidebar by Icon ActionBar
     Through ActionBarDrawerToogle object in support library

    View full-size slide

  60. Navigation Drawer/ ActionBarDrawerToogle
    getSupportActionBar().setDisplayHomeAsUpEnabled(true);
    getSupportActionBar().setHomeButtonEnabled(true);
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    drawerToggle.onConfigurationChanged(newConfig);
    }
    @Override
    protected void onPostCreate(Bundle savedInstanceState) {
    super.onPostCreate(savedInstanceState);
    drawerToggle.syncState(); //sincronize drawer toogle state
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    if (item.getItemId() == android.R.id.home) {
    if (drawerLayout.isDrawerOpen(drawerList)) {
    drawerLayout.closeDrawer(drawerList);
    } else {
    drawerLayout.openDrawer(drawerList);
    }
    return true;
    }
    }
     We allow the use of the icon in Action Bar

    View full-size slide

  61. Tabs
     Implement interface TabListener
     Override methods in TabListener
    @Override
    public void onTabUnselected(Tab arg0, FragmentTransaction arg1) {
    }
    @Override
    public void onTabReselected(Tab tab, FragmentTransaction ft) {
    }
    @Override
    public void onTabSelected(Tab tab, FragmentTransaction ft) {
    setContent(tab.getPosition());
    }
    import com.actionbarsherlock.app.ActionBar.Tab;
    import com.actionbarsherlock.app.ActionBar.TabListener;
    public class TabsActivity extends
    SherlockFragmentActivity implements TabListener {
    }

    View full-size slide

  62. Tabs
     Reference obtain ActionBar
    final ActionBar actionBar = getSupportActionBar();
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_T
    ABS);
     Add tabs
    actionBar.addTab(
    actionBar.newTab()
    .setText(getResources().getString(R.string.listado))
    .setTabListener(this));
    actionBar.addTab(
    actionBar.newTab()
    .setText(getResources().getString(R.string.imagenes))
    .setTabListener(this));

    View full-size slide

  63. Action Share / Share Action Provider

    View full-size slide

  64. Action Share
     Launch an intent of type "ACTION_SEND" to be received by the applications you have installed
    public void mostrarShare(UniversidadBean ubBean){
    List targetedShareIntents = new ArrayList();
    Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND);
    shareIntent.setType("text/html");
    List resInfo =
    getActivity().getPackageManager().queryIntentActivities(shareIntent, 0);
    if (!resInfo.isEmpty()){
    for (ResolveInfo resolveInfo : resInfo) {
    String packageName = resolveInfo.activityInfo.packageName;
    Intent targetedShareIntent = new
    Intent(android.content.Intent.ACTION_SEND);
    targetedShareIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    targetedShareIntent.setType("text/html");
    targetedShareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
    "Spain COmputing UNiversity");
    targetedShareIntent.setPackage(packageName);
    targetedShareIntents.add(targetedShareIntent);
    }
    Intent chooserIntent =
    Intent.createChooser(targetedShareIntents.remove(targetedShareIntents.size()-1),
    getText(R.string.compartir));
    chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS,
    targetedShareIntents.toArray(new Parcelable[]{}));
    startActivity(chooserIntent);
    }
    }

    View full-size slide

  65. Share Action Provider
    android:title="@string/compartir"
    spaincomputing:showAsAction="ifRoom"
    spaincomputing:actionProviderClass=
    "android.support.v7.widget.ShareActionProvider"/>
    private Intent createShareIntent() {
    Intent shareIntent = new Intent(Intent.ACTION_SEND);
    shareIntent.setType("text/html");
    shareIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,
    "Spain COmputing UNiversity");
    UniversidadBean ub=new UniversidadBean(id, idImagen, nombre,
    descripcion, enlace, tipo, grado, latitud, longitud);
    shareIntent.putExtra(android.content.Intent.EXTRA_TEXT,
    Html.fromHtml(ub.getNombre()+"\n"+ub.getDescripcion()+"\n"+ub.getGrado()+"
    \n\n"+ub.getEnlace()+"\n\n"+ub.getLatitud()+"\n"+ub.getLongitud()));
    return shareIntent;
    }
    private ShareActionProvider mShareActionProvider;
    MenuItem shareItem = menu.findItem(R.id.action_share);
    mShareActionProvider =
    (ShareActionProvider)MenuItemCompat.getActionProvider(shareItem);
    mShareActionProvider.setShareIntent(createShareIntent());

    View full-size slide

  66. Contacts
    import android.provider.ContactsContract; //CONTACTS PROVIDER
    Cursor mCursor;
    // acceder al content provider de contactos
    mCursor = getContentResolver().query(Data.CONTENT_URI,
    new String[] { Data._ID, Data.DISPLAY_NAME, Email.DATA1,
    Phone.TYPE },
    Data.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "' AND "
    + Email.DATA1 + " IS NOT NULL", null,Data.DISPLAY_NAME + " ASC");
    ListAdapter adapter = new SimpleCursorAdapter(this, // context
    android.R.layout.simple_list_item_2, // Layout para las filas
    mCursor, // cursor
    new String[] { Data.DISPLAY_NAME, Phone.NUMBER }, //COLUMNS
    new int[] { android.R.id.text1, android.R.id.text2 }, //views
    );
    setListAdapter(adapter);

    android.os.Build.VERSION.SDK_INT<11 //before honeycomb

    View full-size slide

  67. Contacts
    CursorLoader cursorLoader=new CursorLoader(this,
    Data.CONTENT_URI,
    new String[] { Data._ID, Data.DISPLAY_NAME,
    Email.DATA1,Phone.TYPE },
    Data.MIMETYPE + "='" + Email.CONTENT_ITEM_TYPE + "' AND "+
    Email.DATA1 + " IS NOT NULL", null,Data.DISPLAY_NAME + " ASC");
    mCursor=cursorLoader.loadInBackground();
    ListAdapter adapter = new SimpleCursorAdapter(this, // context
    android.R.layout.simple_list_item_2, // Layout para las filas
    mCursor, // cursor
    new String[] { Data.DISPLAY_NAME, Phone.NUMBER }, //COLUMNS
    new int[] { android.R.id.text1, android.R.id.text2 }, //views
    CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); //flag
    );
    setListAdapter(adapter);
    android.os.Build.VERSION.SDK_INT>=11 //after honeycomb
    Using CursorLoader allows the query cursor executing on a separate thread

    View full-size slide

  68. Loaders in contacts
    public class ContactsListFragment implements
    LoaderManager.LoaderCallbacks {
    }
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    getLoaderManager().initLoader(ContactsQuery.QUERY_ID, null, this);}}
     Inicializar loader
    @Override
    public Loader onCreateLoader(int id, Bundle args) {
    return new
    CursorLoader(getActivity(),contentUri,ContactsQuery.PROJECTION,
    ContactsQuery.SELECTION,null,ContactsQuery.SORT_ORDER);}
    }
    @Override
    public void onLoadFinished(Loader loader, Cursor data) {
    // This swaps the new cursor into the adapter.
    if (loader.getId() == ContactsQuery.QUERY_ID) {
    mAdapter.swapCursor(data);
    }
    }
    @Override
    public void onLoaderReset(Loader loader) {}

    View full-size slide

  69. HTTP Request
     org.apache.http
    // Create HTTP Client
    HttpParams httpParameters = new BasicHttpParams();
    // 3s max for connection
    HttpConnectionParams.setConnectionTimeout
    (httpParameters,3000);
    // 4s max to get data
    HttpConnectionParams.setSoTimeout(httpParameters, 4000);
    HttpClient httpclient = new DefaultHttpClient(httpParameters);
    HttpResponse response;
    String responseString = null;
    try {response = httpclient.execute(new HttpGet(uri[0]));//Execute uri
    StatusLine statusLine = response.getStatusLine();
    if(statusLine.getStatusCode() == HttpStatus.SC_OK){
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    response.getEntity().writeTo(out);
    out.close();
    responseString = out.toString();
    } else{
    //Closes the connection.
    response.getEntity().getContent().close();
    throw new IOException(statusLine.getReasonPhrase());
    }} catch (ClientProtocolException e) {//TODO Handle problems..
    } catch (IOException e) {//TODO Handle problems..}

    View full-size slide

  70. API INSTAGRAM
     .Developer register
     New client register
    http://instagram.com/developer/clients/manage/
    http://instagram.com/developer/

    View full-size slide

  71. API INSTAGRAM

    View full-size slide

  72. API INSTAGRAM
     Endpoint tag media recent
    http://instagram.com/developer/endpoints/tags/#get_tags_media_recent

    View full-size slide

  73. VOLLEY
    https://android.googlesource.com/platform/frameworks/volley
    https://github.com/mcxiaoke/android-volley
     Unzip and import from Eclipse as a new project with code available. Export
    the project as Java Volley / jar checking only the "src" folder.
     Volley is a library that facilitates and speeds up the creation of applications
    that make use of networking in Android handling concurrency and network
    requests.
     The advantage is that volley is responsible for managing the request threads
    transparently to the developer.
     libs\volley.jar
     Objects
     RequestQueue
     Request: Contains all the necessary details of API calls to Web. For example,
    the method to use (GET or POST), application data, listeners, error listeners.

    View full-size slide

  74. VOLLEY REQUEST
    import com.android.volley.*;
    public static RequestQueue requestQueue;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    requestQueue = Volley.newRequestQueue(this);
    }
     JSON Request with volley.Request object
    JsonObjectRequest jsObjRequest = new
    JsonObjectRequest(Request.Method.GET, url, null,
    successListener,null);
    requestQueue.add(jsObjRequest);
     Instagram Activity

    View full-size slide

  75. VOLLEY RESPONSE
    //Callback that is executed once the request has completed
    Response.Listener successListener =
    new Response.Listener() {
    @Override
    public void onResponse(JSONObject response) {
    }
    }
     JSON Response by volley.Response object
     More volley examples
    https://github.com/PareshMayani/Android-Volley-Example

    View full-size slide

  76. Geolocation
     LocationManager / android.location package
     ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION
    //Obtenemos el Location Manager del sistema
    LocationManager locationManager = (LocationManager)this.getSystemService
    (Context.LOCATION_SERVICE);
    // Definimos el Listener que gestionará los eventos
    LocationListener locationListener = new LocationListener() {
    public void onLocationChanged(Location location) {
    if (location != null) {
    Toast.makeText(getBaseContext(),
    "Location changed : Lat: " + location.getLatitude() +
    " Lng: " + location.getLongitude(),Toast.LENGTH_SHORT).show();
    }
    }
    // Registramos el Listener en el Location Manager para recibir actualizaciones
    locationManager.
    requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
    0, 0, locationListener);

    View full-size slide

  77. Geolocation
     Implement interface ConnectionCallbacks
    import com.google.android.gms.common.GooglePlayServicesClient.
    ConnectionCallbacks;
     Override onConnected / User location
    @Override
    public void onConnected(Bundle arg0) {
    updateLocation(locationClient.getLastLocation());
    locationClient.requestLocationUpdates(locationRequest, this);
    }
    import com.google.android.gms.location.LocationClient;
    import com.google.android.gms.location.LocationRequest;
    locationClient = new LocationClient(this, this, this);
    locationRequest = LocationRequest.create();

    View full-size slide

  78. Reverse Geolocation
    Geocoder geoCoder = new Geocoder(context, Locale.getDefault());
    List addresses = null;
    try {
    addresses = geoCoder.getFromLocation(gp.latitude , gp.longitude , 1);
    if (addresses!=null && addresses.size() > 0) {
    Address address = addresses.get(0);
    if(address.getMaxAddressLineIndex() > 0 ){
    addressText =address.getAddressLine(0);
    }
    if(address.getLocality()!=null){
    addressText = addressText+" / "+address.getLocality();
    }
    if(address.getCountryName()!=null){
    addressText = addressText+" / "+address.getCountryName();
    }
    }
    } catch (IOException e) {
    }
     Obtain place address from latidude and longitude

    View full-size slide

  79. Restore state in activity
    @Override
    public void onSaveInstanceState(Bundle savedInstanceState) {
    // Always call the superclass so it can save the view hierarchy state
    super.onSaveInstanceState(savedInstanceState);
    }
     Save the Activity state before being destroyed
     Restore the Activity state after being created
    public void onRestoreInstanceState(Bundle savedInstanceState) {
    // Always call the superclass so it can restore the view hierarchy
    super.onRestoreInstanceState(savedInstanceState);
    }

    View full-size slide

  80. SEND EMAIL
     ACTION_SEND INTENT
    Intent intent = new Intent(android.content.Intent.ACTION_SEND);
    intent.putExtra(android.content.Intent.EXTRA_EMAIL, new
    String[]{destino});
    //colocamos la imagen adjunta en el stream
    intent.putExtra(Intent.EXTRA_STREAM,
    Uri.parse("android.resource://" + getPackageName() + "/"
    +R.drawable.image));
    startActivity(Intent.createChooser(intent, "Send email"));
     libs
    https://code.google.com/p/javamail-android/downloads/list
     Java mail API / Gmail SMTP /GMAILSender
     Gmail authentication
     Configure SMTP SERVER

    View full-size slide

  81. Shared Preferences
     Key/value dictionary
     res/xml/preferencias.xml

    xmlns:android="http://schemas.android.com/apk/res/android">
    android:title="@string/pref_notificaciones">
    android:key="pref_notificaciones_favoritos"
    android:title="@string/pref_notificaciones_favoritos"
    android:summary="@string/pref_notificaciones_favoritos"
    android:defaultValue="true" />


    PreferenceScreen is the entire screen that will show us where the other elements are
    displayed, and PreferenceCategory will allow us to create categories or groupings within our
    screen.
    The common element in different items is the key property. This will be the key that is
    stored in SharedPreferences and later retrieve the stored values by this key.

    View full-size slide

  82. Shared Preferences

    View full-size slide

  83. Shared Preferences
     Preferences Activity
    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
    onCreatePreferenceActivity();
    } else {
    onCreatePreferenceFragment();
    }
    }
    private void onCreatePreferenceActivity() {
    PreferenceManager prefManager=getPreferenceManager();
    prefManager.setSharedPreferencesName("appPreferences");
    addPreferencesFromResource(R.xml.preferencias);
    }
    private void onCreatePreferenceFragment() {
    getFragmentManager().beginTransaction()
    .replace(android.R.id.content, new MyPreferenceFragment ())
    .commit();
    }

    View full-size slide

  84. Shared Preferences
     PreferenciasActivity
    public static class MyPreferenceFragment extends PreferenceFragment
    {
    @Override
    public void onCreate(final Bundle savedInstanceState)
    {
    super.onCreate(savedInstanceState);
    PreferenceManager prefManager=getPreferenceManager();
    prefManager.setSharedPreferencesName("appPreferences");
    addPreferencesFromResource(R.xml.preferencias);
    }
    }
     Get Preferences values
    SharedPreferences sharedPreferences =
    getApplicationContext().getSharedPreferences("appPreferences",
    MODE_PRIVATE);
    //Launch favorites notification depending preferences value
    if(sharedPreferences.getBoolean("pref_notificaciones_favoritos",true)){
    com.proyecto.spaincomputing.utils.StatusBarNotify.getInstance(
    getApplicationContext()).statusBarNotify("eliminado_favorito",
    nombre,descripcion);
    }

    View full-size slide

  85. SQLITE DATABASE

    View full-size slide

  86. SQLITE DATABASE
     import android.database.sqlite.*;
     Create class that extends SQLiteOpenHelper and override onCreate()
    method to create database
    public class DBHelper extends SQLiteOpenHelper {
    //constructor
    public DBAdapter (Context context){
    dbHelper = new DBHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    private final static String DATABASE_CREATE = "CREATE TABLE ..“;
    @Override
    public void onCreate(SQLiteDatabase db) {
    db.execSQL(DATABASE_CREATE);
    }
    }
     Emulator /root
     /data/data//databases

    View full-size slide

  87. SQLITE DATABASE
     INSERT,UPDATE,DELETE
    SQLiteDatabase db = dbHelper.getWritableDatabase();
     QUERY
    SQLiteDatabase db = dbHelper.getReadableDatabase();
    Cursor cursor= myDatabase.query(DATABASE_TABLE, null,
    where,null, null, null, order);
    // Create a new row of values to insert.
    ContentValues newValues = new ContentValues();
    // Assign values for each row.
    newValues.put(COLUMN_NAME, newValue);
    [ ... Repeat for each column ... ]
    // Insert
    myDatabase.insert(DATABASE_TABLE, null, newValues);
    // Update
    myDatabase.update(DATABASE_TABLE, updatedValues, where,
    null);
    // Delete
    myDatabase.delete(DATABASE_TABLE, null, null);

    View full-size slide

  88. Developer libraries in google play

    View full-size slide

  89. Libraries
     Indicator in ViewPager
     http://viewpagerindicator.com/
     https://github.com/chrisbanes/ActionBar-PullToRefresh
     Pager Sliding Tabstrip
     https://github.com/astuetz/PagerSlidingTabStrip
     Show routes in map.
     https://github.com/tyczj/MapNavigator

    View full-size slide

  90. Sliding Menu
     Library to implement a sliding flyout with similar behavior to
    navigation drawer
     https://github.com/jfeinstein10/SlidingMenu
    MessageBar
     Library to improve toast messages
     http://simonvt.github.io/MessageBar/

    View full-size slide

  91. Fading Action Bar
     Used in Google play music
     https://github.com/ManuelPeinado/FadingActionBar
    +
     http://www.androidviews.net/

    View full-size slide

  92. About me
    https://github.com/jmortega/apps
    https://github.com/jmortega/android
    https://www.linkedin.com/in/jmortega1
    [email protected]
    https://play.google.com/store/apps/developer?id=Jos%C3%A9+Manuel+Ortega+Candel

    View full-size slide