to top

用 App Engine同步

This lesson teaches you how to

  1. Prepare Your Environment
  2. Create Your Project
  3. Create the Data Layer
  4. Create the Persistence Layer
  5. Query and Update from the Android App
  6. Configure the C2DM Server-Side
  7. Configure the C2DM Client-Side

You should also read

Try it out

This lesson uses the Cloud Tasks sample code, originally shown at the Android + AppEngine: A Developer's Dream Combination talk at Google I/O. You can use the sample application as a source of reusable code for your own application, or simply as a reference for how the Android and cloud pieces of the overall application fit together. You can also build the sample application and see how it runs on your own device or emulator.

Cloud Tasks App

Writing an app that syncs to the cloud can be a challenge. There are many little details to get right, like server-side auth, client-side auth, a shared data model, and an API. One way to make this much easier is to use the Google Plugin for Eclipse, which handles a lot of the plumbing for you when building Android and App Engine applications that talk to each other. This lesson walks you through building such a project.

Following this lesson shows you how to:

  • Build Android and Appengine apps that can communicate with each other
  • Take advantage of Cloud to Device Messaging (C2DM) so your Android app doesn't have to poll for updates

This lesson focuses on local development, and does not cover distribution (i.e, pushing your App Engine app live, or publishing your Android App to market), as those topics are covered extensively elsewhere.

Prepare Your Environment

If you want to follow along with the code example in this lesson, you must do the following to prepare your development environment:

Create Your Projects

After installing the Google Plugin for Eclipse, notice that a new kind of Android project exists when you create a new Eclipse project: The App Engine Connected Android Project (under the Google project category). A wizard guides you through creating this project, during the course of which you are prompted to enter the account credentials for the role account you created.

Note: Remember to enter the credentials for your role account (the one you created to access C2DM services), not an account you'd log into as a user, or as an admin.

Once you're done, you'll see two projects waiting for you in your workspace—An Android application and an App Engine application. Hooray! These two applications are already fully functional— the wizard has created a sample application which lets you authenticate to the App Engine application from your Android device using AccountManager (no need to type in your credentials), and an App Engine app that can send messages to any logged-in device using C2DM. In order to spin up your application and take it for a test drive, do the following:

To spin up the Android application, make sure you have an AVD with a platform version of at least Android 2.2 (API Level 8). Right click on the Android project in Eclipse, and go to Debug As > Local App Engine Connected Android Application. This launches the emulator in such a way that it can test C2DM functionality (which typically works through Google Play). It'll also launch a local instance of App Engine containing your awesome application.

Create the Data Layer

At this point you have a fully functional sample application running. Now it's time to start changing the code to create your own application.

First, create the data model that defines the data shared between the App Engine and Android applications. To start, open up the source folder of your App Engine project, and navigate down to the (yourApp)-AppEngine > src > (yourapp) > server package. Create a new class in there containing some data you want to store server-side. The code ends up looking something like this:

package com.cloudtasks.server;

import javax.persistence.*;

@Entity
public class Task {

    private String emailAddress;
    private String name;
    private String userId;
    private String note;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    public Task() {
    }

    public String getEmailAddress() {
        return this.emailAddress;
    }

    public Long getId() {
        return this.id;
    }
    ...
}

Note the use of annotations: Entity, Id and GeneratedValue are all part of the Java Persistence API. Essentially, the Entity annotation goes above the class declaration, and indicates that this class represents an entity in your data layer. The Id and GeneratedValue annotations, respectively, indicate the field used as a lookup key for this class, and how that id is generated (in this case, GenerationType.IDENTITY indicates that the is generated by the database). You can find more on this topic in the App Engine documentation, on the page Using JPA with App Engine.

Once you've written all the classes that represent entities in your data layer, you need a way for the Android and App Engine applications to communicate about this data. This communication is enabled by creating a Remote Procedure Call (RPC) service. Typically, this involves a lot of monotonous code. Fortunately, there's an easy way! Right click on the server project in your App Engine source folder, and in the context menu, navigate to New > Other and then, in the resulting screen, select Google > RPC Service. A wizard appears, pre-populated with all the Entities you created in the previous step, which it found by seeking out the @Entity annotation in the source files you added. Pretty neat, right? Click Finish, and the wizard creates a Service class with stub methods for the Create, Retrieve, Update and Delete (CRUD) operations of all your entities.

Create the Persistence Layer

The persistence layer is where your application data is stored long-term, so any information you want to keep for your users needs to go here. You have several options for writing your persistence layer, depending on what kind of data you want to store. A few of the options hosted by Google (though you don't have to use these services) include Google Storage for Developers and App Engine's built-in Datastore. The sample code for this lesson uses DataStore code.

Create a class in your com.cloudtasks.server package to handle persistence layer input and output. In order to access the data store, use the PersistenceManager class. You can generate an instance of this class using the PMF class in the com.google.android.c2dm.server.PMF package, and then use that to perform basic CRUD operations on your data store, like this:

/**
* Remove this object from the data store.
*/
public void delete(Long id) {
    PersistenceManager pm = PMF.get().getPersistenceManager();
    try {
        Task item = pm.getObjectById(Task.class, id);
        pm.deletePersistent(item);
    } finally {
        pm.close();
    }
}

You can also use Query objects to retrieve data from your Datastore. Here's an example of a method that searches out an object by its ID.

public Task find(Long id) {
    if (id == null) {
        return null;
    }

    PersistenceManager pm = PMF.get().getPersistenceManager();
    try {
        Query query = pm.newQuery("select from " + Task.class.getName()
        + " where id==" + id.toString() + " && emailAddress=='" + getUserEmail() + "'");
        List<Task> list = (List<Task>) query.execute();
        return list.size() == 0 ? null : list.get(0);
    } catch (RuntimeException e) {
        System.out.println(e);
        throw e;
    } finally {
        pm.close();
    }
}

For a good example of a class that encapsulates the persistence layer for you, check out the DataStore class in the Cloud Tasks app.

Query and Update from the Android App

In order to keep in sync with the App Engine application, your Android application needs to know how to do two things: Pull data from the cloud, and send data up to the cloud. Much of the plumbing for this is generated by the plugin, but you need to wire it up to your Android user interface yourself.

Pop open the source code for the main Activity in your project and look for <YourProjectName> Activity.java, then for the method setHelloWorldScreenContent(). Obviously you're not building a HelloWorld app, so delete this method entirely and replace it with something relevant. However, the boilerplate code has some very important characteristics. For one, the code that communicates with the cloud is wrapped in an AsyncTask and therefore not hitting the network on the UI thread. Also, it gives an easy template for how to access the cloud in your own code, using the RequestFactory class generated that was auto-generated for you by the Eclipse plugin (called MyRequestFactory in the example below), and various Request types.

For instance, if your server-side data model included an object called Task when you generated an RPC layer it automatically created a TaskRequest class for you, as well as a TaskProxy representing the individual task. In code, requesting a list of all these tasks from the server looks like this:

public void fetchTasks (Long id) {
  // Request is wrapped in an AsyncTask to avoid making a network request
  // on the UI thread.
    new AsyncTask<Long, Void, List<TaskProxy>>() {
        @Override
        protected List<TaskProxy> doInBackground(Long... arguments) {
            final List<TaskProxy> list = new ArrayList<TaskProxy>();
            MyRequestFactory factory = Util.getRequestFactory(mContext,
            MyRequestFactory.class);
            TaskRequest taskRequest = factory.taskNinjaRequest();

            if (arguments.length == 0 || arguments[0] == -1) {
                factory.taskRequest().queryTasks().fire(new Receiver<List<TaskProxy>>() {
                    @Override
                    public void onSuccess(List<TaskProxy> arg0) {
                      list.addAll(arg0);
                    }
                });
            } else {
                newTask = true;
                factory.taskRequest().readTask(arguments[0]).fire(new Receiver<TaskProxy>() {
                    @Override
                    public void onSuccess(TaskProxy arg0) {
                      list.add(arg0);
                    }
                });
            }
        return list;
    }

    @Override
    protected void onPostExecute(List<TaskProxy> result) {
        TaskNinjaActivity.this.dump(result);
    }

    }.execute(id);
}
...

public void dump (List<TaskProxy> tasks) {
    for (TaskProxy task : tasks) {
        Log.i("Task output", task.getName() + "\n" + task.getNote());
    }
}

This AsyncTask returns a list of TaskProxy objects, and sends it to the debug dump() method upon completion. Note that if the argument list is empty, or the first argument is a -1, all tasks are retrieved from the server. Otherwise, only the ones with IDs in the supplied list are returned. All the fields you added to the task entity when building out the App Engine application are available via get/set methods in the TaskProxy class.

In order to create new tasks and send them to the cloud, create a request object and use it to create a proxy object. Then populate the proxy object and call its update method. Once again, this should be done in an AsyncTask to avoid doing networking on the UI thread. The end result looks something like this.

new AsyncTask<Void, Void, Void>() {
    @Override
    protected Void doInBackground(Void... arg0) {
        MyRequestFactory factory = (MyRequestFactory)
                Util.getRequestFactory(TasksActivity.this,
                MyRequestFactory.class);
        TaskRequest request = factory.taskRequest();

        // Create your local proxy object, populate it
        TaskProxy task = request.create(TaskProxy.class);
        task.setName(taskName);
        task.setNote(taskDetails);
        task.setDueDate(dueDate);

        // To the cloud!
        request.updateTask(task).fire();
        return null;
    }
}.execute();

Configure the C2DM Server-Side

In order to set up C2DM messages to be sent to your Android device, go back into your App Engine codebase, and open up the service class that was created when you generated your RPC layer. If the name of your project is Foo, this class is called FooService. Add a line to each of the methods for adding, deleting, or updating data so that a C2DM message is sent to the user's device. Here's an example of an update task:

public static Task updateTask(Task task) {
    task.setEmailAddress(DataStore.getUserEmail());
    task = db.update(task);
    DataStore.sendC2DMUpdate(TaskChange.UPDATE + TaskChange.SEPARATOR + task.getId());
    return task;
}

// Helper method.  Given a String, send it to the current user's device via C2DM.
public static void sendC2DMUpdate(String message) {
    UserService userService = UserServiceFactory.getUserService();
    User user = userService.getCurrentUser();
    ServletContext context = RequestFactoryServlet.getThreadLocalRequest().getSession().getServletContext();
    SendMessage.sendMessage(context, user.getEmail(), message);
}

In the following example, a helper class, TaskChange, has been created with a few constants. Creating such a helper class makes managing the communication between App Engine and Android apps much easier. Just create it in the shared folder, define a few constants (flags for what kind of message you're sending and a seperator is typically enough), and you're done. By way of example, the above code works off of a TaskChange class defined as this:

public class TaskChange {
    public static String UPDATE = "Update";
    public static String DELETE = "Delete";
    public static String SEPARATOR = ":";
}

Configure the C2DM Client-Side

In order to define the Android applications behavior when a C2DM is recieved, open up the C2DMReceiver class, and browse to the onMessage() method. Tweak this method to update based on the content of the message.

//In your C2DMReceiver class

public void notifyListener(Intent intent) {
    if (listener != null) {
        Bundle extras = intent.getExtras();
        if (extras != null) {
            String message = (String) extras.get("message");
            String[] messages = message.split(Pattern.quote(TaskChange.SEPARATOR));
            listener.onTaskUpdated(messages[0], Long.parseLong(messages[1]));
        }
    }
}
// Elsewhere in your code, wherever it makes sense to perform local updates
public void onTasksUpdated(String messageType, Long id) {
    if (messageType.equals(TaskChange.DELETE)) {
        // Delete this task from your local data store
        ...
    } else {
        // Call that monstrous Asynctask defined earlier.
        fetchTasks(id);
    }
}

Once you have C2DM set up to trigger local updates, you're all done. Congratulations, you have a cloud-connected Android application!