📣 Attention Salesforce Certified Trailblazers! Link your Trailhead and Webassessor accounts and maintain your credentials by December 14th. Learn more.
close
trailhead

Modify the Forcedroid Native App

Learning Objectives

After completing this unit, you'll be able to:

  • Modify the native Android template app to customize the list screen.
  • Handle a REST response.
  • Delete a Salesforce record through a REST request.

Customizing the List Screen

As you saw earlier, the native Android template app shows a list of contacts or accounts from the user’s Salesforce organization. It’s currently a read-only list built from a simple SOQL SELECT query that is processed through the Mobile SDK REST classes. Let’s add some editing powers by attaching a delete action to the long press gesture. When the user taps and holds a contact name in the list view, your new delete action attempts to delete the associated Salesforce record. If that attempt succeeds, your app permanently removes the row from the Contacts list view in the app. If the request fails, your app tells the user why it failed and reinstates the list view row.

About the Long Click Listener

Long click listener implementation is straightforward. Deciding how to create your long click listener class, however, is a little trickier. If you explore the various coding options, the first thing you discover is that you don’t listen for clicks at the list view level. Instead, you listen at the list item level. Luckily, implementing listeners for list items isn’t an onerous task, thanks to the Android OnItemLongClickListener interface. This interface defines a single listener that is attached to the list view and responds to long presses on any item in the list. You create an instance of this class by implementing a public interface within your view class.

The next challenge is to figure out which view class implements the long click listener. With ListView objects, you provide a data object that provides the information that’s displayed in the list. The Mobile SDK template app creates an ArrayAdapter<String> object for that purpose, and then attaches it to the ListView.

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

But ArrayAdapter<String> is a data object, not a view—right? Yes, and more. The ArrayAdapter on the Contacts list view creates an AdapterView object for each item in the list’s data set. Because these adapter views represent the objects of interest, you use the AdapterView class to implement OnItemLongClickListener. You then associate the listener object with the ListView object, which receives all notifications for its children. This association limits your OnItemLongClickListener to interact only with the items in the template app’s list view. Finally, you implement your delete behavior in the sole interface method.

One final detail to resolve: Where do you place this long click listener code? Mobile SDK gives you the following entry point callback methods:

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

We can eliminate one: onCreate(Bundle savedInstanceState). This method sets up the activity and handles authentication flows before the views are instantiated. The views enter the scene in the onResume() method. This method, then, seems to be the most likely candidate. The onResume(RestClient client) method is called by the superclass at login to capture the authenticated RestClient object—let’s leave that one alone. Therefore, the results are in—add your long click listener code to onResume().

Implementing a Basic Long Click Listener

So, let’s start coding. In Android Studio, open your MainActivity class and look at the onResume() method.
  1. In Android Studio, open your MainActivity.java file.
  2. Find the onResume() method.
    @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();
    }
    We’ll start coding as marked—after the listAdapter has been set for the ListView, but before the super.onResume() call.
  3. Declare and assign a convenience ListView variable that references the Contacts list view. Use the Activity.findViewById() method to look up the list view resource.
    ((ListView) findViewById(R.id.contacts_list)).setAdapter(listAdapter);	
    ListView lv = ((ListView) findViewById(R.id.contacts_list));
    super.onResume();
  4. Using the lv variable, call the AdapterView.setOnItemLongClickListener() method to set up a listener for long click events. For the listener parameter, instantiate an inline stub of the AdapterView.OnItemLongClickListener interface.
    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;
            }
        });
    
    Notice that Android Studio even stubs in the single virtual interface method for you. Helpful!
  5. (Optional) If you get an error about a missing import, add import android.widget.AdapterView to your class imports.
  6. Within the AdapterView.OnItemLongClickListener body, replace the boilerplate return statement with code that presents a confirming toast message.
    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. Build the app and run it.
  8. When you’re logged in to the app, click Fetch Contacts.
  9. Tap an entry in the Contacts list and keep pressing for a couple of seconds. If all is well, the toast message appears.

Adding Mobile SDK REST Requests

Now we’re almost ready to add Mobile SDK elements. In the onItemLongClick() method, you create a REST request to delete the Salesforce record associated with the row that was tapped. You then send that request to Salesforce. Before digging into the code, review these important points.

Obtaining a RestClient Instance

Remember that you never create RestClient objects directly. Mobile SDK creates one for you and returns it to MainActivity through the onResume(RestClient client) method. This RestClient instance is authenticated with the current user’s access token. For your use, the onResume(RestClient client) method assigns this instance to the client class variable.

Creating a REST Request

To create the REST request for this exercise, you call the RestRequest factory method for deleting a record:

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

Where do you get the argument values? Check this table.

Parameter Value
apiVersion Defined in your app’s resources: getString(R.string.api_version)
objectType “Contact” (hard-coded)
objectId ??
The objectId parameter is a bit of a stumper. It requires a Salesforce value that your raw forcedroid app doesn’t recognize. Why don’t you have it, and what can you do to get it? The answers are simple:
  • You don’t have the IDs because the original REST requests—the ones that populate the lists—don’t ask for it.
  • You can get the ID by changing the REST requests.

Tweaking the Template App’s SOQL Request

Your MainActivity class issues two REST requests: one for contacts, and another for accounts. Each request contains a SOQL statement. We’re not going to use accounts, so let’s update the Contact records request to return ID values.

  1. In Android Studio, open the MainActivity.java file.
  2. Find the onFetchContactsClick() method.
    public void onFetchContactsClick(View v) throws UnsupportedEncodingException
    {
       sendRequest("SELECT Name FROM Contact");
    }
    
  3. Change the SOQL query to select the Name and Id fields. Be sure to use the camel-case spelling of the ID field name. Field names are case-sensitive.
    public void onFetchContactsClick(View v) throws UnsupportedEncodingException
    {
       sendRequest("SELECT Name, Id FROM Contact");
    }

Now that you’re equipped to receive the ID values in the REST response, where’s the best place to store them? The template app merely copies the Name value for each record into a list view row—it doesn’t cache the ID values. To enable lookups in your long press handler, you must store the IDs in class scope.

Adapting the Template App’s sendRequest() Method

Scroll down to the sendRequest(String soql) method, which is where the fetch response arrives. This method provides a clear demonstration of how the Mobile SDK REST mechanism works. Let’s take a quick look. First, the method calls a RestRequest factory method that defines a REST request for a given SOQL query:
RestRequest restRequest = RestRequest.getRequestForQuery(
    ApiVersionStrings.getVersionNumber(this), soql);

Then, it sends the new RestRequest object to Salesforce in the client.sendAsync() call.

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(SalesforceSDKManager.getInstance().
                        getSalesforceR().stringGenericError(), exception.toString()),
                    Toast.LENGTH_LONG).show();
            }
        });
    }
});

In addition to the RestRequest object, the sendAsync() call requires an implementation of the Mobile SDK AsyncRequestCallback interface. The onSuccess() method of this interface receives the REST response asynchronously through a callback. Your default AsyncRequestCallback implementation handles only the SOQL queries defined in your forcedroid app.

This onSuccess() method already does what we need. It contains code that extracts the records returned by the REST response and assigns them to the local records variable. Let’s move this variable to class scope by redeclaring it outside of the method body.
  1. In Android Studio, open the MainActivity class.
  2. Near the top of the class definition, declare JSONArray records as a private variable with the existing class variable declarations:
    public class MainActivity extends SalesforceActivity {
        private RestClient client;
        private ArrayAdapter<String> listAdapter;
        private JSONArray records;
       ….
  3. In the onSuccess() method, remove the type declaration for the records variable:
    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);
                }
            }
        });
    }

Finishing the onItemLongClick() Method

Now you’re prepared to finish the onItemLongClick() method. The basic algorithm is as follows:

  1. Get a “request for delete” object by calling an appropriate Mobile SDK RestRequest factory method. All RestRequest methods throw exceptions, so be sure to enclose the call in a try...catch block.
  2. Send the “request for delete” object to Salesforce using the generated RestClient object.
  3. Handle the REST result in callback methods.

Obtain a RestRequest Object

  1. Scroll back to the onItemLongClick() method in the onResume() method.
  2. After the Toast.makeText() call, declare a local RestRequest object named restRequest. Initialize it to null.
    RestRequest restRequest = null;
  3. Add an empty try...catch construct.
    RestRequest restRequest = null;
    try {
    
    } catch (Exception e) {
     
    }
  4. In the try block, call a factory method that obtains a REST request object for a delete operation. HINT: Use the static RestRequest.getRequestForDelete() method.
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
                   // arguments?
                );
    } catch (Exception e) {
     
    }
  5. For the first parameter, let’s hard-code an actual Salesforce API version number. The current version at time of publication is v39.0.
    RestRequest restRequest = null;
    try {
         restRequest = RestRequest.getRequestForDelete(
            “v39.0”, //...);
    } catch (Exception e) {
    
    }
  6. For the objectType parameter, specify “Contact”.
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            “v39.0”, "Contact", //...);
    } catch (Exception e) {
    
    }
  7. Pass RestRequest.getRequestForDelete() the ID of the entry in the records array that matches the current list view position. Here’s how to retrieve the ID:
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            “v39.0”, "Contact",
            records.getJSONObject(position).getString("Id"));
        // Send the request
        // ...
    } catch (Exception e) {
    
    }
  8. In the catch block, call printStackTrace() on the Exception argument.
    RestRequest restRequest = null;
    try {
        restRequest = RestRequest.getRequestForDelete(
            “v39.0”, "Contact",
            records.getJSONObject(position).getString("Id"));
        // Send the request
        // ...
    } catch (Exception e) {
        e.printStackTrace();
    }

Once you’ve obtained the “request for delete” object, send it to Salesforce and then handle the result in callback methods.

Add the RestClient.sendAsync() Method

You’re almost done! The final piece of the puzzle is to send the request using the RestClient.sendAsync() method. This method requires you to implement the AsyncRequestCallback interface. As you know, Mobile SDK sends REST responses to your AsyncRequestCallback methods.

  1. In onItemLongClick(), after the getRequestForDelete() call, copy and paste the RestClient.sendAsync() code from the sendRequest() method.
  2. Remove the internal code of the try branch of the onSuccess() method. Don’t remove the catch branch, since it merely calls out to an error handler.
  3. Keep the onError() override implementation—it’s generic enough to work with any Salesforce response.

Here’s the stubbed-out call to 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(SalesforceSDKManager.getInstance().getSalesforceR().stringGenericError(), exception.toString()),
                        Toast.LENGTH_LONG).show();
            }
        });
    }
});

Implement the onSuccess() Callback Method

In the onSuccess() method of AsyncRequestCallback(), you do the following:
  1. Make sure the delete operation succeeded by testing the HTTP status. This check is necessary because the underlying network component reports only transport layer failures—not REST request failures.
  2. If the operation succeeded, remove the item at the given position from the list view.
  3. Post a success message.
You use the listAdapter class variable to remove the row. You can call ArrayAdapter.remove(T object), using the position value passed to the onItemLongClick() method, to obtain the object. For example:
listAdapter.remove(listAdapter.getItem(position));
If you add this code, you encounter a scope issue. Because you’re working in an interface implementation context, you can’t use the local position variable from the onItemLongClick() context. Instead, you can add a class variable and assign the position variable to it.
  1. At the top of the class, declare and initialize a private class variable named pos of type int.
    public class MainActivity extends SalesforceActivity {
    
        private RestClient client;
        private ArrayAdapter<String> listAdapter;
        private JSONArray records;
        private int pos = -1;
  2. On the first line of the onItemLongClick() method, capture the position value:
    public boolean onItemLongClick(AdapterView<?> parent, View view,
    	int position, long id) {
    	pos = position;
    	...
  3. If isSuccess() is true, remove the row by calling the listAdapter.remove() method. Use pos instead of position to remove the row:
    if (result.isSuccess()) {
        listAdapter.remove(listAdapter.getItem(pos));
    
    }
  4. Finally, post an alert box showing a success message. Otherwise, report the error message.
    if (result.isSuccess()) {
        listAdapter.remove(listAdapter.getItem(pos));
        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(SalesforceSDKManager.getInstance().getSalesforceR().stringGenericError(), result.toString()),
             Toast.LENGTH_LONG).show();
    }

The completed onItemLongClick() method looks like this:

public boolean onItemLongClick(AdapterView<?> parent, 
        View view, int position, long id) {
    pos = position;
    // TODO Auto-generated method stub
    Toast.makeText(getApplicationContext(),
        "Long press detected", Toast.LENGTH_SHORT).show();
        try {
	     RestRequest request = RestRequest.getRequestForDelete(
	         getString(R.string.api_version), "Opportunity", 
                    records.getJSONObject(position).getString("Id"));
            client.sendAsync(request, new AsyncRequestCallback() {
                @Override
                public void onSuccess(RestRequest request, 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));
                                    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(
                                            SalesforceSDKManager.getInstance().getSalesforceR().stringGenericError(), 
                                            result.toString()),
                                        Toast.LENGTH_LONG).show(); 
                                }   
                            } catch (Exception e) {
                                    onError(e);
                            }
                        }
                    });
                }
                @Override
		      public void onError(Exception exception) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {		          
                                Toast.makeText(MainActivity.this,
                                    MainActivity.this.getString(
                                        SalesforceSDKManager.getInstance().
                                            getSalesforceR().stringGenericError(), 
                                        exception.toString()),
                                    Toast.LENGTH_LONG).show();
                            }
                        });
		      }
                } 
	     });
        } catch (JSONException e) {
	     e.printStackTrace();
	 }
	 return true;				
    }
});

Final Cleanup and Run Time!

One final bit of cleanup is to remove the Fetch Accounts button. Since the list view is shared between Fetch Contacts and Fetch Accounts, your long press handler applies equally to both. However, this handler is useless for accounts because the ID you use to delete the record only applies to a contact. Alternatively—as “extra credit”—you can apply what you’ve learned and adapt the long press handler to delete both contacts and accounts. For this tutorial, though, let’s remove the accounts-related code.

Delete the following items from the indicated files:

File Action
MainActivity.java Delete onFetchAccountsClick(View v) method.
res/layout/Main.xml Do one of the following:
  • In Graphical Layout, delete the “Fetch Accounts” button.
  • In XML view, delete the <Button> node whose ID is "@+id/fetch_accounts".
res/values/strings.xml Do one of the following:
  • In Resources tab: Select “fetch_accounts_button (String)” and click Remove.
  • In XML view: Delete the <string> node whose name is “fetch_accounts_button”.

At last, your app is finished and ready to run!

  1. In Android Studio, click Run | Run ‘app’.
  2. Select a Mobile SDK-compatible emulator or connected device.
  3. When the app is running, log in to your Salesforce organization, then click Fetch Contacts to see the list. Tap and hold any item in the list until you see a toast message confirming the long press.

Notice that you get an error response when you try to delete any default contact in the Developer Edition database. These errors occur because each pre-packaged contact that comes in a Developer Edition org is the parent of other records. To prepare for testing, log in to your Developer Edition org and create one or more test contacts that don’t own other records. If the deletion is successful, a message appears to indicate that the record was deleted.

retargeting