Skip to main content
Build the future with Agentforce at TDX in San Francisco or on Salesforce+ on March 5–6. Register now.

Modificar la aplicación forcedroid nativa

Objetivos de aprendizaje

Después de completar esta unidad, podrá:

  • Modificar la aplicación de plantilla Android nativa para personalizar la pantalla de lista.
  • Controlar una respuesta REST.
  • Eliminar un registro de Salesforce mediante una solicitud REST.

Personalización de la pantalla de lista

Como ha visto anteriormente, la aplicación de plantilla Android nativa muestra una lista de contactos o cuentas de la organización de Salesforce del usuario. En la actualidad, es una lista de solo lectura creada a partir de una consulta SOQL SELECT sencilla que se procesa mediante clases de REST de Mobile SDK. Vamos a ampliar la capacidad de modificación mediante la adición de una acción de eliminación al gesto de pulsación prolongada. Cuando el usuario toca de forma continua un nombre de contacto en la vista de lista, la nueva acción de eliminación intenta eliminar el registro de Salesforce asociado. Si el intento tiene éxito, la aplicación elimina permanentemente la fila de la vista de lista de contactos en la aplicación. Si la solicitud genera un error, la aplicación indica al usuario el motivo del error y restablece la fila de la vista de lista.

Acerca del proceso de escucha de clic prolongado

La implementación del proceso de escucha de clic prolongado es sencilla. Sin embargo, decidir cómo crear la clase de proceso de escucha de clic prolongado, es algo un poco más difícil. Si analiza las distintas opciones de codificación, lo primero que descubre es que no se escuchan los clics en el nivel de vista de lista. En su lugar, se escuchan en el nivel de elemento de lista. Por suerte, la implementación de procesos de escucha no es una tarea compleja gracias a la interfaz OnItemLongClickListener de Android. Esta interfaz define un único proceso de escucha asociado a la vista de lista y que responde a las pulsaciones prolongadas de cualquier elemento de la lista. Cree una instancia de esta clase mediante la implementación de una interfaz pública en su clase de vista.

El siguiente reto es determinar qué clase de vista implementa el proceso de escucha de clic prolongado. En el caso de los objetos ListView, especifique un objeto de datos que proporcione la información mostrada en la lista. La aplicación de plantilla de Mobile SDK crea un objeto ArrayAdapter<Cadena> con este fin y, a continuación, lo asocia a ListView.

listAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, new 
ArrayList<String>());
((ListView) findViewById(R.id.contacts_list)).setAdapter(listAdapter);

Sin embargo, ArrayAdapter<String> es un objeto de datos y no una vista, ¿verdad? Sí, y mucho más. ArrayAdapter crea en la vista de lista de contactos un objeto AdapterView para cada elemento del conjunto de datos de la lista. Dado que estas vistas de adaptador representan los objetos de interés, use la clase AdapterView para implementar OnItemLongClickListener. A continuación, asocie el objeto de proceso de escucha con el objeto ListView, el cual recibe todas las notificaciones de sus elementos secundarios. Esta asociación limita OnItemLongClickListener a la interacción solo con los elementos de la vista de lista de la aplicación de plantilla. Por último, implemente su comportamiento de eliminación en el método de interfaz única.

Un detalle final que solucionar: ¿Dónde se incluye este código de proceso de escucha de clic prolongado? Mobile SDK proporciona los siguientes métodos de devolución de llamada de punto de entrada:

public abstract void onResume(RestClient client);
@Override
protected void onCreate(Bundle savedInstanceState);
@Override 
public void onResume();

Podemos eliminar uno de ellos: onCreate(Bundle savedInstanceState). Este método configura la actividad y controla los flujos de autenticación antes de que se creen instancias de las vistas. Las vistas entran en escena en el método onResume(). Por lo tanto, este método parecer ser la opción más probable. La llamada al método onResume(RestClient client) se realiza mediante la superclase en el inicio de sesión para capturar el objeto RestClient, que vamos a dejar como está. Por lo tanto, ya tenemos los resultados. Agregue el código de proceso de escucha de clic prolongado a onResume().

Implementación de un proceso de escucha de clic prolongado

En este caso, vamos a empezar por el código. En Android Studio, abra la clase MainActivity y examine el método onResume().
  1. En Android Studio, abra el archivo MainActivity.java.
  2. Busque el método onResume().
    @Override 
    public void onResume() {
        // Hide everything until we are logged in
        findViewById(R.id.root).setVisibility(View.INVISIBLE);
    
        // Create list adapter
        listAdapter = new ArrayAdapter<String>(this, 
            android.R.layout.simple_list_item_1, new ArrayList<String>());
        ((ListView) findViewById(R.id.contacts_list)).setAdapter(listAdapter);			
        // ADD CODE HERE!
        super.onResume();
    }
    Vamos a empezar con la codificación según el marcado (es decir, después establecer listAdapter para ListView, pero antes de la llamada super.onResume().
  3. Declare y asigne una variable ListView adecuada que haga referencia a la vista de lista de contactos. Use el método Activity.findViewById() para buscar el recurso de la vista de lista.
    ((ListView) findViewById(R.id.contacts_list)).setAdapter(listAdapter);	
    ListView lv = ((ListView) findViewById(R.id.contacts_list));
    super.onResume();
  4. Con la variable lv, llame al método AdapterView.setOnItemLongClickListener() para configurar un proceso de escucha para los eventos de clic prolongado. En el caso del parámetro del proceso de escucha, crea una instancia de código auxiliar en línea de la interfaz AdapterView.OnItemLongClickListener.
    ListView lv = ((ListView) findViewById(R.id.contacts_list));
    lv.setOnItemLongClickListener(
        new AdapterView.OnItemLongClickListener() {
            @Override
    	 public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
                return false;
            }
        });
    
    Observe que Android Studio ordena el código auxiliar en el método de interfaz virtual única por usted. ¡Muy útil!
  5. (Opcional) Si obtiene un error porque falta una importación, agregue import android.widget.AdapterView a sus importaciones de clases.
  6. En el cuerpo AdapterView.OnItemLongClickListener, sustituya la declaración return reutilizable por código que presente un mensaje emergente.
    ListView lv = ((ListView) findViewById(R.id.contacts_list));
    lv.setOnItemLongClickListener (new AdapterView.OnItemLongClickListener() {
        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view,
            int position, long id) {
            Toast.makeText(getApplicationContext(),
                "Long press received", Toast.LENGTH_SHORT).show();
            return true;
            }
    });
  7. Cree la aplicación y ejecútela.
  8. Cuando inicie sesión en la aplicación, haga clic en Fetch Contacts (Obtener contactos).
  9. Toque una entrada de la lista de contactos y mantenga la pulsación durante un par de segundos. Si todo es correcto, se muestra el mensaje emergente.

Adición de solicitudes REST de Mobile SDK

Ya casi estamos listos para agregar elementos de Mobile SDK. En el método onItemLongClick(), cree una solicitud REST para eliminar el registro de Salesforce asociado a la fila tocada. A continuación, envíe la solicitud a Salesforce. Antes de profundizar en el código, revise estos aspectos importantes.

Obtención de una instancia de RestClient

Recuerde que los objetos RestClient nunca se deben crear directamente. Mobile SDK crea uno y lo devuelve a MainActivity mediante el método onResume(RestClient client). Esta instancia de RestClient se autentica con el token de acceso del usuario actual. Para su uso, el método onResume(RestClient client) asigna esta instancia a la variable de clase client.

Creación de una solicitud REST

Para crear la solicitud REST para este ejercicio, llame al método de generador RestRequest para la eliminación de un registro:

public static RestRequest getRequestForDelete(String apiVersion, String objectType, String objectId);

¿Dónde se obtienen los valores de argumento? Consulte esta tabla.

Parámetro Valor
apiVersion Se define en los recursos de la aplicación: getString(R.string.api_version)
objectType “Contacto” (codificado de forma rígida)
objectId ??
El parámetro objectId es un elemento complejo. Requiere un valor de Salesforce que la aplicación forcedroid sin procesar no reconoce. ¿Por qué no lo tiene y qué puede hacer para obtenerlo? Las respuestas son sencillas:
  • No tiene estos Id. porque las solicitudes REST originales (que completan las listas) no los solicitan.
  • Para obtener el Id. cambie la solicitudes REST.

Ajuste de la solicitud SOQL de la aplicación de plantilla

La clase MainActivity genera dos solicitudes REST: una para contactos y otra para cuentas. Cada solicitud contiene una declaración SOQL. Dado que no vamos a usar cuentas, vamos a actualizar la solicitud de registros de contacto para devolver valores de Id.

  1. En Android Studio, abra el archivo MainActivity.java.
  2. Busque el método onFetchContactsClick().
    public void onFetchContactsClick(View v) throws UnsupportedEncodingException
    {
       sendRequest("SELECT Name FROM Contact");
    }
    
  3. Cambie la consulta SOQL para seleccionar los campos Name e Id. Asegúrese de usar la ortografía con distinción entre mayúsculas y minúsculas del nombre de campo de Id. Los nombres de campo distinguen entre mayúsculas y minúsculas.
    public void onFetchContactsClick(View v) throws UnsupportedEncodingException
    {
       sendRequest("SELECT Name, Id FROM Contact");
    }

Ahora que está preparado para recibir los valores de Id. en la respuesta REST, ¿cuál es la mejor ubicación para almacenarlos? La aplicación de plantilla solo copia el valor de nombre para cada registro en una fila de la vista de lista, pero no almacena en caché los valores de Id. Para activar las búsquedas en el controlador de pulsación prolongada, debe almacenar los Id. en el ámbito de clase.

Adaptación del método sendRequest() de la aplicación de plantilla

Desplácese al método sendRequest(String soql), que es a donde llega la respuesta obtenida. Este método es una demostración clara de cómo funciona el mecanismo REST de Mobile SDK. Veamos rápidamente cómo funciona. Primero, el método llama a un método de generador RestRequest que define una solicitud REST para una consulta SOQL determinada:
RestRequest restRequest = RestRequest.getRequestForQuery(
    ApiVersionStrings.getVersionNumber(this), soql);

A continuación, envía el nuevo objeto RestRequest a Salesforce en la llamada client.sendAsync().

client.sendAsync(restRequest, new AsyncRequestCallback() {
    @Override
    public void onSuccess(RestRequest request, final RestResponse result) {
        result.consumeQuietly(); // consume before going back to main thread
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    listAdapter.clear();
                    JSONArray records = result.asJSONObject().getJSONArray("records");
                    for (int i = 0; i < records.length(); i++) {
                        listAdapter.add(records.getJSONObject(i).getString("Name"));
                    }
                } catch (Exception e) {
                    onError(e);
                }
            }
        });
    }
 
    @Override
    public void onError(final Exception exception) {
        runOnUiThread(new Runnable() {
        @Override
            public void run() {
                Toast.makeText(MainActivity.this,
                    MainActivity.this.getString(
                        R.string.sf__generic_error, 
                        exception.toString()),
                    Toast.LENGTH_LONG).show();
            }
        });
    }
});

Además del objeto RestRequest, la llamada sendAsync() requiere una implementación de la interfaz AsyncRequestCallback de Mobile SDK. El método onSuccess() de esta interfaz recibe la respuesta REST de forma asíncrona mediante una devolución de llamada. La implementación AsyncRequestCallback predeterminada controla solo las consultas SOQL definidas en la aplicación forcedroid.

Este método onSuccess() ya hace lo que necesitamos. Contiene el código que extrae los registros devueltos por la respuesta REST y los asigna a la variable records local. Vamos a mover esta variable al ámbito de clase. Para ello, se vuelve a declarar fuera del cuerpo del método.
  1. Junto a la parte superior de la definición de clase MainActivity, declare JSONArray records como una variable privada con las siguientes declaraciones de variable de clase existentes:
    public class MainActivity extends SalesforceActivity {
        private RestClient client;
        private ArrayAdapter<String> listAdapter;
        private JSONArray records;
       ….
  2. En el método onSuccess(), elimine la declaración de tipo para la variable records:
    public void onSuccess(RestRequest request, final RestResponse result) {
        result.consumeQuietly(); // consume before going back to main thread
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                try {
                    listAdapter.clear();
                    records = result.asJSONObject().getJSONArray("records");
                    for (int i = 0; i < records.length(); i++) {
                        listAdapter.add(records.getJSONObject(i).getString("Name"));
                    }
                } catch (Exception e) {
                    onError(e);
                }
            }
        });
    }

Finalización del método onItemLongClick()

Ya está preparado para finalizar el método onItemLongClick(). El algoritmo básico es el siguiente:

  1. Obtenga un objeto de “solicitud de eliminación” mediante la llamada a un método de generador RestRequest de Mobile SDK apropiado. Dado que todos los métodos RestRequest devuelven excepciones, asegúrese de incluir la llamada en un bloque try...catch.
  2. Envíe el objeto de “solicitud de eliminación” a Salesforce mediante el objeto RestClient generado.
  3. Controle el resultado de REST en métodos de devolución de llamada.

Obtener un objeto RestRequest

  1. Vuelva a desplazarse al método onItemLongClick() en el método onResume().
  2. Después de la llamada Toast.makeText(), declare un objeto local RestRequest llamado restRequest. Inicialícelo como null.
    RestRequest restRequest = null;
  3. Agregue una construcción try...catch vacía.
    RestRequest restRequest = null;
    try {
    
    } catch (Exception e) {
     
    }
  4. En el bloque try, llame a un método de generador que obtenga un objeto de solicitud REST para una operación de eliminación. SUGERENCIA: Use el método estático RestRequest.getRequestForDelete().
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
                   // arguments?
                );
    } catch (Exception e) {
     
    }
  5. Para el primer parámetro, recupere la versión de la API de Salesforce especificada en los recursos del Mobile SDK de su proyecto.
    RestRequest restRequest = null;
    try {
         restRequest = RestRequest.getRequestForDelete(
            getString(R.string.api_version), //...);
    } catch (Exception e) {
    
    }
  6. Para el parámetro objectType, especifique “Contact”.
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            getString(R.string.api_version), "Contact", //...);
    } catch (Exception e) {
    
    }
  7. Pase a RestRequest.getRequestForDelete() el Id. de la entrada de la matriz de registros que coincide con la posición de vista de lista actual. Así es como puede recuperar el Id.:
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            getString(R.string.api_version), "Contact",
            records.getJSONObject(position).getString("Id"));
        // Send the request
        // ...
    } catch (Exception e) {
    
    }
  8. En el bloque catch, llame a printStackTrace() en el argumento Exception.
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            getString(R.string.api_version), "Contact",
            records.getJSONObject(position).getString("Id"));
        // Send the request
        // ...
    } catch (Exception e) {
        e.printStackTrace();
    }

Una vez que obtenga el objeto de “solicitud de eliminación”, envíelo a Salesforce y controle el resultado en métodos de devolución de llamada.

Agregar el método RestClient.sendAsync()

¡Ya casi ha terminado! La última pieza del puzle es el envío de la solicitud mediante el método RestClient.sendAsync(). Este método requiere que implemente la interfaz AsyncRequestCallback. Como sabe, Mobile SDK envía las respuestas REST a los métodos AsyncRequestCallback.

  1. En onItemLongClick(), después de la llamada getRequestForDelete(), copie y peque el código RestClient.sendAsync() del método sendRequest().
  2. Elimine el código interno de la rama try del método onSuccess(). No elimine la rama catch, ya que solo llama a un controlador de errores.
  3. Mantenga la implementación de sustitución onError(), ya que es lo bastante genérica para funcionar con cualquier respuesta de Salesforce.

Esta es la llamada de código auxiliar a RestClient.sendAsync().

restRequest = RestRequest.getRequestForDelete(
        getString(R.string.api_version), "Contact",
        records.getJSONObject(position).getString("Id"));
client.sendAsync(restRequest, new AsyncRequestCallback() {
    @Override
    public void onSuccess(RestRequest request, final RestResponse result) {
        result.consumeQuietly();
        runOnUiThread(new Runnable() { 
            @Override
            public void run() {
                // Network component doesn’t report app layer status.
                // Use Mobile SDK RestResponse.isSuccess() method to check
                // whether the REST request itself succeeded.
                if (result.isSuccess()) {
                    try {

                    } catch (Exception e) {
                        onError(e);
                    }
                }
            }
        });
    }

    @Override
    public void onError(final Exception exception) {
        runOnUiThread(new Runnable() {
        @Override
            public void run() {
                Toast.makeText(MainActivity.this,
                        MainActivity.this.getString(R.string.sf__generic_error, exception.toString()),
                        Toast.LENGTH_LONG).show();
            }
        });
    }
});

Implementar el método de devolución de llamada onSuccess()

En el método onSuccess() de AsyncRequestCallback(), haga lo siguiente:
  1. Asegúrese de que la operación de eliminación es correcta mediante la prueba del estado HTTP. Esta comprobación es necesaria, ya que el componente de red subyacente solo indica errores de la capa de transporte y no errores de solicitud.
  2. Si la operación es correcta, elimine el elemento de la posición correspondiente de la vista de lista.
  3. Publique un mensaje de resultado correcto.
Use la variable de clase listAdapter para eliminar la fila. Puede llamar a ArrayAdapter.remove(T object) con el valor de posición pasado al método onItemLongClick() para obtener el objeto. Por ejemplo:
listAdapter.remove(listAdapter.getItem(position));
Si agrega este código, se genera un problema de ámbito. Dado que está trabajando en un contexto de implementación de interfaz, no puede usar la variable position local del contexto onItemLongClick(). En su lugar, puede agregar una variable de clase y asignarle la variable de posición.
  1. En la parte superior de la clase, declare e inicialice una variable de clase privada llamada pos del tipo int.
    public class MainActivity extends SalesforceActivity {
    
        private RestClient client;
        private ArrayAdapter<String> listAdapter;
        private JSONArray records;
        private int pos = -1;
  2. En la primera línea del método onItemLongClick(), capture el valor position:
    public boolean onItemLongClick(AdapterView<?> parent, View view,
    	int position, long id) {
    	pos = position;
    	...
  3. En el método onSuccess() de su implementación de AsyncRequestCallback, desplácese hacia abajo algunas líneas hasta el bloque auxiliar if.
    if (result.isSuccess()) {
        try {
    
        } catch (Exception e) {
            onError(e);
        }
    }
  4. Si result.isSuccess() es verdadero, elimine la fila mediante la llamada al método listAdapter.remove(). Use pos en lugar de position para eliminar la fila:
    if (result.isSuccess()) {
        listAdapter.remove(listAdapter.getItem(pos));
    
    }
  5. Después de eliminar un elemento de lista, llame a sendRequest(request) para actualizar la lista reordenada y mantenerla en sincronía con su matriz records local:
    if (result.isSuccess()) {
        listAdapter.remove(listAdapter.getItem(pos));
        sendRequest(”SELECT Name, Id FROM Contact”);
    
    }
  6. Por último, publique un cuadro de alerta para mostrar un mensaje de resultado correcto. En caso contrario, indique el mensaje de error correspondiente.
    if (result.isSuccess()) {
        listAdapter.remove(listAdapter.getItem(pos));
        sendRequest(”SELECT Name, Id FROM Contact”);
        AlertDialog.Builder b = new AlertDialog.Builder(findViewById(R.id.contacts_list).getContext());
        b.setMessage("Record successfully deleted!");
        b.setCancelable(true);
        AlertDialog a = b.create();
        a.show();
    } else {
       Toast.makeText(MainActivity.this,
             MainActivity.this.getString(R.string.sf__generic_error, result.toString()),
             Toast.LENGTH_LONG).show();
    }

Este es el método onItemLongClick() completado.

@Override
public boolean onItemLongClick(AdapterView<?> parent, 
        View view, int position, long id) {
    pos = position;

    Toast.makeText(getApplicationContext(),
        "Long press detected", Toast.LENGTH_SHORT).show();
    RestRequest restRequest = null;
    try {
       RestRequest request = RestRequest.getRequestForDelete(
           getString(R.string.api_version), "Contact", 
               records.getJSONObject(position).getString("Id"));
       client.sendAsync(request, new AsyncRequestCallback() {
            @Override
            public void onSuccess(RestRequest request, final RestResponse result) {
                result.consumeQuietly();
                runOnUiThread(new Runnable() { 
                    @Override
                    public void run() {
                        try {
                            // Network component doesn’t report app layer status.
                            // Use Mobile SDK RestResponse.isSuccess() method to check
                            // whether the REST request itself succeeded. 
                            if (result.isSuccess()) {                                        
                                listAdapter.remove(listAdapter.getItem(pos));
                                sendRequest(”SELECT Name, Id FROM Contact”);
                                AlertDialog.Builder b = 
                                    new AlertDialog.Builder(findViewById
                                        (R.id.contacts_list).getContext());
                                b.setMessage("Record successfully deleted!");
                                b.setCancelable(true);
                                AlertDialog a = b.create();
                                a.show();
                            } else {
                                Toast.makeText(MainActivity.this,
                                    MainActivity.this.getString(
                                        R.string.sf__generic_error, 
                                        result.toString()),
                                    Toast.LENGTH_LONG).show(); 
                            }   
                        } catch (Exception e) {
                            onError(e);
                        }
                    }});
                }

                @Override
                public void onError(final Exception exception) {
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {		          
                            Toast.makeText(MainActivity.this,
                                MainActivity.this.getString(
                                    R.string.sf__generic_error, 
                                    exception.toString()),
                                Toast.LENGTH_LONG).show();
                        }
                    });
                }
            });
        } catch (Exception e) {
            e.printStackTrace();
        }
    return true;
}

Limpieza final y tiempo de ejecución

Parte de la limpieza final consiste en eliminar el botón Fetch Accounts (Obtener cuentas). Dado que la vista de lista se comparte entre Fetch Contacts (Obtener contactos) y Fetch Accounts (Obtener cuentas), el controlador de pulsación prolongada se aplica a ambos por igual. No obstante, este controlador no es válido para las cuentas, ya que el Id. usado para eliminar el registro solo se aplica a un contacto. De forma alternativa y como “ventaja adicional”, puede aplicar lo que ha aprendido y adaptar el controlador de pulsación prolongada para eliminar contactos y cuentas. Sin embargo, en este tutorial, vamos a eliminar el código relacionado con las cuentas.

Elimine los siguientes elementos de los archivos indicados:

Archivo Acción
MainActivity.java Elimine el método onFetchAccountsClick(View v).
res/layout/Main.xml Realice una de las siguientes acciones:
  • En Graphical Layout (Formato gráfico), elimine el botón “Fetch Accounts” (Obtener cuentas).
  • En la vista XML, elimine el nodo <Button> con el Id. "@+id/fetch_accounts".
res/values/strings.xml Realice una de las siguientes acciones:
  • En la ficha Resources (Recursos): seleccione “fetch_accounts_button (String)” y haga clic en Remove (Eliminar).
  • En la vista XML: elimine el nodo <string> con el nombre “fetch_accounts_button”.

Al fin, su aplicación está terminada y lista para su ejecución.

  1. En Android Studio, haga clic en Run | Run ‘app’ (Ejecutar | Ejecutar ‘aplicación’).
  2. Seleccione un emulador o dispositivo conectado compatible con Mobile SDK.
  3. Cuando la aplicación se esté ejecutando, inicie sesión en su organización de Salesforce y, a continuación, haga clic en Fetch Contacts (Obtener contactos) para ver la lista. Toque cualquier elemento de la lista y mantenga la pulsación hasta que se muestre un mensaje emergente para confirmar la pulsación prolongada.

Observe que obtiene una respuesta de error al intentar eliminar cualquier contacto predeterminado de la base de datos de Developer Edition. Estos errores se producen porque cada contacto que viene empaquetado previamente de una organización de Developer Edition es el elemento principal de otros registros. Para prepararse para las pruebas, inicie sesión en su organización de Developer Edition y cree uno o varios contactos de prueba que no tengan otros registros en propiedad. Si la eliminación es correcta, se muestra un mensaje para indicar que el registro se ha eliminado.

Comparta sus comentarios de Trailhead en la Ayuda de Salesforce.

Nos encantaría saber más sobre su experiencia con Trailhead. Ahora puede acceder al nuevo formulario de comentarios en cualquier momento en el sitio de Ayuda de Salesforce.

Más información Continuar a Compartir comentarios