Подтвердить что ты не робот

Как обрабатывать RESTful обновление удаленного сервера с помощью SyncAdapter

Я наблюдал за разговором о REST Google I/O и читал слайды: http://www.google.com/events/io/2010/sessions/developing-RESTful-android-apps.html

Я все еще немного не понимаю, как красиво обрабатывать, скажем, ошибку обновления, удаленную удаленным сервером. Я реализовал собственный ContentProvider и SyncAdapter. Рассмотрим этот сценарий:

Обновить контактную информацию пользователя через вызов REST:

  • Запросить обновление с помощью ContentResolver.
  • Мой ContentProvider немедленно обновляет локальную базу данных Sqlite приложения и запрашивает синхронизацию (согласно рекомендациям в разговорах ввода-вывода Google).
  • Вызывается My SyncAdapter.onPerformSync() и выполняет вызов REST для обновления удаленных данных.
  • На удаленном сервере отвечает "ERROR: Invalid Phone Number" (например).

Мой вопрос: каким образом SyncAdapter лучше всего сигнализировать моему ContentProvider, что это изменение должно быть исправлено из локальной базы данных приложения, а также для сигнала моей активности, что запрос на обновление был неудачным ( и передать сообщения об ошибках, возвращенные с сервера)?

В моей активности нужно отобразить счетчик прогресса в ожидании результата и узнать, удалось ли выполнить запрос или не удалось.


Для обновления базы данных локального приложения с содержимым с Сервера шаблон SyncAdapter имеет для меня полный смысл, и у меня это нормально работает. Но для обновлений от приложения до сервера я не могу найти хороший способ справиться с описанным выше сценарием.


И еще одна вещь...;)

Скажем, я вызываю ContentResolver.notifyChange(uri, null, true); из моего метода contentProvider update(). true вместе с android:supportsUploading="true" вызовет вызов моего SyncAdapter onPerformSync(). Отлично, но внутри onPerformSync(), как я могу определить, какой URI я должен синхронизировать? Я не хочу просто обновлять всю свою БД каждый раз, когда я получаю запрос синхронизации. Но вы даже не можете передать Bundle в notifyChangeCall() для передачи onPerformSync().

Все примеры, которые я видел в onPerformSync(), были настолько простыми и не использовали пользовательский ContentProvider, какие-либо реальные примеры? И документы - это немного птичье гнездо. Вергилий Добжански, сэр, ты оставил меня за ручью без весла.

4b9b3361

Ответ 1

Короткий ответ, если вы ориентируетесь на уровень API 7, является "Не делать". Возможно, ситуация улучшилась в более поздних API, но так как это было... Я настоятельно рекомендую полностью исключить SyncAdapter; он очень плохо документирован и "автоматическое" управление учетными записями/аутентификацией идет по высокой цене, поскольку API для него также запутан и недостаточно документирован. Эта часть API не была продумана за пределами самых простых случаев использования.

Итак, вот картина, с которой я столкнулся. Внутри моей деятельности у меня был обработчик с простым добавлением из пользовательского суперкласса Handler (можно было проверить m_bStopped bool):

private ResponseHandler mHandler = new ResponseHandler();

class ResponseHandler extends StopableHandler {

    @Override
    public void handleMessage(Message msg) {
        if (isStopped()) {
            return;
        }
        if (msg.what == WebAPIClient.GET_PLANS_RESPONSE) {
            ...
        } 
        ...
    }
}

Эта активность будет вызывать запросы REST, как показано ниже. Обратите внимание: обработчик передается классу WebClient (вспомогательный класс для создания/создания HTTP-запросов и т.д.). WebClient использует этот обработчик, когда он получает ответ HTTP на сообщение обратно на активность и сообщает ему, что данные были получены и, в моем случае, хранится в базе данных SQLite (что я бы рекомендовал). В большинстве операций я бы назвал mHandler.stopHandler(); в onPause() и mHandler.startHandler(); в onResume(), чтобы избежать ответа HTTP-ответа на неактивную активность и т.д. Это оказалось довольно надежным подходом.

final Bundle bundle = new Bundle();
bundle.putBoolean(WebAPIRequestHelper.REQUEST_CREATESIMKITORDER, true);
bundle.putString(WebAPIRequestHelper.REQUEST_PARAM_KIT_TYPE, sCVN);       
final Runnable runnable = new Runnable() { public void run() {
    VendApplication.getWebClient().processRequest(null, bundle, null, null, null,
                    mHandler, NewAccountActivity.this);
    }};
mRequestThread = Utils.performOnBackgroundThread(runnable);

Handler.handleMessage() вызывается в основном потоке. Таким образом, вы можете остановить свои диалоги прогресса здесь и безопасно выполнять другие действия.

Я объявил ContentProvider:

<provider android:name="au.com.myproj.android.app.webapi.WebAPIProvider"
          android:authorities="au.com.myproj.android.app.provider.webapiprovider"
          android:syncable="true" />

И реализовал его для создания и управления доступом к SQLite db:

public class WebAPIProvider extends ContentProvider

Итак, вы можете получить курсоры по базе данных в своих действиях, например:

mCursor = this.getContentResolver().query (
          WebAPIProvider.PRODUCTS_URI, null, 
          Utils.getProductsWhereClause(this), null, 
          Utils.getProductsOrderClause(this));
startManagingCursor(mCursor);

Я нашел класс org.apache.commons.lang3.text.StrSubstitutor чрезвычайно полезным при построении неуклюжих XML-запросов, требуемых API REST, которые мне пришлось интегрировать, например, in WebAPIRequestHelper У меня были вспомогательные методы вроде:

public static String makeAuthenticateQueryString(Bundle params)
{
    Map<String, String> valuesMap = new HashMap<String, String>();
    checkRequiredParam("makeAuthenticateQueryString()", params, REQUEST_PARAM_ACCOUNTNUMBER);
    checkRequiredParam("makeAuthenticateQueryString()", params, REQUEST_PARAM_ACCOUNTPASSWORD);

    valuesMap.put(REQUEST_PARAM_APIUSERNAME, API_USERNAME);
    valuesMap.put(REQUEST_PARAM_ACCOUNTNUMBER, params.getString(REQUEST_PARAM_ACCOUNTNUMBER));
    valuesMap.put(REQUEST_PARAM_ACCOUNTPASSWORD, params.getString(REQUEST_PARAM_ACCOUNTPASSWORD));

    String xmlTemplate = VendApplication.getContext().getString(R.string.XMLREQUEST_AUTHENTICATE_ACCOUNT);
    StrSubstitutor sub = new StrSubstitutor(valuesMap);
    return sub.replace(xmlTemplate);
}

Я бы добавил к соответствующему URL-адресу конечной точки.

Вот еще несколько подробностей о том, как класс WebClient выполняет HTTP-запросы. Это метод processRequest(), называемый ранее в Runnable. Обратите внимание на параметр handler, который используется для сообщения результатов обратно в ResponseHandler, описанный выше. syncResult находится в выпадающем параметре, используемом SyncAdapter для выполнения экспоненциального отсрочки и т.д. Я использую его в executeRequest(), увеличивая его количество ошибок и т.д. Опять же, очень плохо документировано и PITA для работы. parseXML() использует превосходный Simple XML lib.

public synchronized void processRequest(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult, Handler handler, Context context)
{
    // Helper to construct the query string from the query params passed in the extras Bundle.
    HttpUriRequest request = createHTTPRequest(extras);
    // Helper to perform the HTTP request using org.apache.http.impl.client.DefaultHttpClient.
    InputStream instream = executeRequest(request, syncResult);

    /*
     * Process the result.
     */
    if(extras.containsKey(WebAPIRequestHelper.REQUEST_GETBALANCE))
    {
        GetServiceBalanceResponse xmlDoc = parseXML(GetServiceBalanceResponse.class, instream, syncResult);
        Assert.assertNotNull(handler);
        Message m = handler.obtainMessage(WebAPIClient.GET_BALANCE_RESPONSE, xmlDoc);
        m.sendToTarget();
    }
    else if(extras.containsKey(WebAPIRequestHelper.REQUEST_GETACCOUNTINFO))
    {
      ...
    }
    ...

}

Вы должны поместить несколько тайм-аутов в HTTP-запросы, чтобы приложение не ожидало навсегда, если мобильные данные выпадают, или он переключается с Wi-Fi на 3G. Это вызовет исключение, если произойдет таймаут.

    // Set the timeout in milliseconds until a connection is established.
    int timeoutConnection = 30000;
    HttpConnectionParams.setConnectionTimeout(httpParameters, timeoutConnection);
    // Set the default socket timeout (SO_TIMEOUT) in milliseconds which is the timeout for waiting for data.
    int timeoutSocket = 30000;
    HttpConnectionParams.setSoTimeout(httpParameters, timeoutSocket);
    HttpClient client = new DefaultHttpClient(httpParameters);          

В целом, материал SyncAdapter и Accounts был полной болью и стоил мне много времени, чтобы не получить прибыль. ContentProvider был довольно полезен, главным образом для поддержки курсора и транзакций. База данных SQLite была действительно хороша. И класс Handler классный. Теперь я бы использовал класс AsyncTask вместо создания ваших собственных потоков, как я сделал выше, чтобы вызвать запросы HTTP.

Я надеюсь, что это бессвязное объяснение немного поможет кому-то.

Ответ 2

Как насчет схемы проектирования Observer? Может ли ваша деятельность быть наблюдателем SyncAdapter или базы данных? Таким образом, при сбое обновления адаптер будет уведомлять своих наблюдателей и затем может действовать на измененные данные. В SDK есть куча классов Observable, которые лучше всего подходят для вашей ситуации. http://developer.android.com/search.html#q=Observer&t=0