Jak poradzić sobie z ponowną aktualizacją zdalnego serwera za pomocą SyncAdapter

Obejrzałem rozmowę Google I/O REST i przeczytałem slajdy: http://www.google.com/events/io/2010/sessions/developing-RESTful-android-apps.html

Nadal jestem trochę niejasny, jak ładnie poradzić sobie, powiedzmy, z błędem aktualizacji wyrzuconym przez zdalny serwer. Zaimplementowałem własny ContentProvider i SyncAdapter. Rozważmy ten scenariusz:

Zaktualizuj dane kontaktowe użytkownika za pomocą połączenia REST:

  1. poproś o aktualizację używając ContentResolver.
  2. mój ContentProvidernatychmiast aktualizuje lokalną bazę danych SQLite aplikacji i prosi o synchronizację(zgodnie z zaleceniami w Google I/O talk).
  3. Mój SyncAdapter.wywołana jest metoda onPerformSync () i wykonuje wywołanie REST, aby zaktualizować dane zdalne.
  4. serwer zdalny odpowiada "ERROR: Invalid Phone Number" (na przykład).

Moje pytanie brzmi, jaki jest najlepszy sposób, aby SyncAdapter zasygnalizował do mojego ContentProvider, że ta zmiana musi być poparta z lokalnej bazy danych aplikacji, a także zasygnalizować mojej aktywności, że żądanie aktualizacji nie powiodło się (i przekazać komunikaty o błędach zwrócone z serwera)?

Moja aktywność musi wyświetlać wskaźnik postępu w oczekiwaniu na wynik i wiedzieć, czy żądanie się powiodło, czy nie.


Do aktualizacji lokalnej bazy danych aplikacji z zawartością z serwera, wzór SyncAdapter ma dla mnie pełny sens, i mam to działa dobrze. Ale dla aktualizacji z aplikacja do serwera, nie mogę znaleźć ładnego sposobu, aby poradzić sobie z powyższym scenariuszem.


I jeszcze jedno... ;) Powiedz, że zadzwonię do Contentresolvera.notifyChange( uri, null, true); z poziomu mojej metody Update () ContentProvider. {[0] } wraz z android:supportsUploading="true" spowoduje wywołanie Onperformsync () mojej SyncAdapter. Świetnie, ale wewnątrz onperformsync(), jak powiedzieć, który URI powinienem zsynchronizować? Nie chcę po prostu odświeżać całego DB za każdym razem, gdy otrzymuję żądanie synchronizacji. Ale ty nie można nawet przekazać pakietu do notifyChangeCall (), który ma zostać przekazany do onperformsync ().

Wszystkie przykłady, które widziałem z onperformsync() były tak proste, a nie za pomocą niestandardowego ContentProvider, jakieś prawdziwe przykłady świata tam? A doktorzy to trochę ptasie gniazdo. Virgil Dobjanschi, Sir, zostawił mnie Pan w rzece bez wiosła.

Author: JJD, 2011-11-04

2 answers

Krótka odpowiedź, jeśli kierujesz ~ API poziom 7, to "nie". Sytuacja mogła się poprawić w późniejszych API, ale tak było... Zdecydowanie zalecam całkowite unikanie SyncAdapter; jest to udokumentowane bardzo słabo, a "automatyczne" zarządzanie kontem/uwierzytelnianiem ma wysoką cenę, ponieważ API jest również zawiłe i niedostatecznie udokumentowane. Ta część API nie została przemyślana poza najbardziej banalnymi przypadkami użycia.

Oto wzór, z którym skończyłem. Wewnątrz moich działań miałem Handlera z prostym dodatkiem z niestandardowej superklasy Handlera (mogłem sprawdzić dla 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) {
            ...
        } 
        ...
    }
}

Aktywność wywoła prośby o resztę, jak pokazano poniżej. Zauważ, że handler jest przekazywany do klasy WebClient (klasy pomocniczej do budowania/wykonywania żądań HTTP itd.). WebClient używa tej funkcji obsługi, gdy otrzymuje odpowiedź HTTP na wiadomość z powrotem do aktywności i informuje ją, że dane zostały odebrane i, w moim przypadku, przechowywane w Bazy danych SQLite (co polecam). W większości działań wywołałbym mHandler.stopHandler(); in onPause() i mHandler.startHandler(); in onResume(), aby uniknąć zasygnalizowania odpowiedzi HTTP z powrotem do nieaktywnej aktywności itp. Okazało się to dość solidnym podejściem.

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() jest wywoływany w głównym wątku. Możesz więc zatrzymać okna dialogowe postępu i bezpiecznie wykonywać inne czynności.

Zadeklarowałem treść:

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

I zaimplementował go do tworzenia i zarządzania dostępem do SQLite db:

public class WebAPIProvider extends ContentProvider

Więc możesz uzyskać Kursory nad bazą danych w swoich działaniach w następujący sposób:

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

Uznałem, że klasa org.apache.commons.lang3.text.StrSubstitutor jest niezmiernie pomocna w konstruowaniu niezgrabnych żądań XML wymaganych przez REST API, z którymi musiałem się zintegrować np. w WebAPIRequestHelper miałem metody pomocnicze takie jak:

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);
}

Którą dopisałbym do odpowiedniego adresu URL punktu końcowego.

Oto kilka szczegółów na temat tego, jak Klasa WebClient wykonuje żądania HTTP. Jest to metoda processRequest() o nazwie wcześniej w biegu. Zwróć uwagę na parametr handler, który służy do przesyłania wyników z powrotem do ResponseHandler, który opisałem powyżej. syncResult jest parametrem in out używanym przez SyncAdapter do wykładniczego cofania itp. Używam go w executeRequest(), zwiększając liczbę błędów itp. Ponownie, bardzo słabo udokumentowane i PITA do pracy. parseXML() wykorzystuje znakomity prosty 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))
    {
      ...
    }
    ...

}

Powinieneś umieścić kilka timeoutów na żądaniach HTTP, aby aplikacja nie czekała wiecznie jeśli dane komórkowe wypadną lub przełącza się z Wifi na 3G. spowoduje to wyrzucenie wyjątku, jeśli wystąpi limit czasu.

    // 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);          

Więc ogólnie rzecz biorąc, sprawy SyncAdapter i konta były całkowitym bólem i kosztowały mnie dużo czasu za brak zysku. ContentProvider był dość przydatny, głównie do obsługi kursora i transakcji. Baza danych SQLite była naprawdę dobra. A zajęcia z obsługi są świetne. Użyłbym teraz klasy AsyncTask zamiast tworzenia własnych wątków, jak to zrobiłem powyżej aby wywołać żądania HTTP.

Mam nadzieję, że to bełkotliwe Wyjaśnienie pomoże komuś trochę.

 3
Author: Jarrod Smith,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-08-22 02:45:59

A co ze wzorem projektowym obserwatora? Czy Twoja aktywność może być obserwatorem SyncAdapter lub bazy danych? W ten sposób, gdy aktualizacja nie powiedzie się, adapter powiadomi swoich obserwatorów, a następnie będzie mógł działać na zmienionych danych. Istnieje kilka obserwowalnych klas w SDK, sprawdź, która z nich działa najlepiej w twojej sytuacji. http://developer.android.com/search.html#q=Observer&t=0

 1
Author: smith324,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-04 01:54:58