implement synchronization of offline stuff back to the mothership

This commit is contained in:
Andrew Dolgov 2011-12-06 14:29:57 +03:00
parent 797860e517
commit 19cb77aa35
6 changed files with 247 additions and 27 deletions

View File

@ -86,6 +86,7 @@
<string name="error_other_error">Error: unknown error (see log)</string>
<string name="error_api_disabled">Error: API disabled for this user</string>
<string name="error_api_unknown">Error: unknown API error (see log)</string>
<string name="error_api_incorrect_usage">Error: incorrect API usage</string>
<string name="error_login_failed">Error: username or password incorrect</string>
<string name="error_invalid_api_url">Error: invalid API URL</string>
<string name="combined_mode_summary">Displays full article text inline, instead of a separate panel</string>
@ -96,4 +97,8 @@
<string name="offline_switch_error">Failed to prepare offline mode (see log)</string>
<string name="no_feeds">No feeds to display</string>
<string name="no_headlines">No articles to display</string>
<string name="dialog_offline_prompt">Login failed, but you have stored offline data. Would you like to go offline?</string>
<string name="dialog_offline_go">Go offline</string>
<string name="dialog_cancel">Cancel</string>
<string name="syncing_offline_data">Synchronizing offline data...</string>
</resources>

View File

@ -37,7 +37,7 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
private final String TAG = this.getClass().getSimpleName();
public enum ApiError { NO_ERROR, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND,
HTTP_SERVER_ERROR, HTTP_OTHER_ERROR, SSL_REJECTED, PARSE_ERROR, IO_ERROR, OTHER_ERROR, API_DISABLED, API_UNKNOWN, LOGIN_FAILED, INVALID_URL };
HTTP_SERVER_ERROR, HTTP_OTHER_ERROR, SSL_REJECTED, PARSE_ERROR, IO_ERROR, OTHER_ERROR, API_DISABLED, API_UNKNOWN, LOGIN_FAILED, INVALID_URL, INCORRECT_USAGE };
public static final int API_STATUS_OK = 0;
public static final int API_STATUS_ERR = 1;
@ -94,6 +94,8 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
return R.string.error_login_failed;
case INVALID_URL:
return R.string.error_invalid_api_url;
case INCORRECT_USAGE:
return R.string.error_api_incorrect_usage;
default:
Log.d(TAG, "getErrorMessage: unknown error code=" + m_lastError);
return R.string.error_unknown;
@ -202,6 +204,8 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
m_lastError = ApiError.LOGIN_FAILED;
} else if (error.equals("NOT_LOGGED_IN")) {
m_lastError = ApiError.LOGIN_FAILED;
} else if (error.equals("INCORRECT_USAGE")) {
m_lastError = ApiError.INCORRECT_USAGE;
} else {
Log.d(TAG, "Unknown API error: " + error);
m_lastError = ApiError.API_UNKNOWN;

View File

@ -127,7 +127,7 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
ListView list = (ListView)view.findViewById(R.id.feeds);
m_adapter = new FeedListAdapter(getActivity(), R.layout.feeds_row, (ArrayList<Feed>)m_feeds);
list.setAdapter(m_adapter);
list.setEmptyView(view.findViewById(R.id.no_feeds));
//list.setEmptyView(view.findViewById(R.id.no_feeds));
list.setOnItemClickListener(this);
registerForContextMenu(list);
@ -308,9 +308,9 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
sortFeeds();
//if (m_feeds.size() == 0)
// setLoadingStatus(R.string.no_feeds_to_display, false);
//else
if (m_feeds.size() == 0)
setLoadingStatus(R.string.no_feeds_to_display, false);
else
setLoadingStatus(R.string.blank, false);
if (m_enableFeedIcons && !m_feedIconsChecked) getFeedIcons();

View File

@ -127,7 +127,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
list.setAdapter(m_adapter);
list.setOnItemClickListener(this);
list.setOnScrollListener(this);
list.setEmptyView(view.findViewById(R.id.no_headlines));
//list.setEmptyView(view.findViewById(R.id.no_headlines));
registerForContextMenu(list);
Log.d(TAG, "onCreateView, feed=" + m_feed);
@ -279,9 +279,9 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
m_adapter.notifyDataSetChanged();
//if (m_articles.size() == 0)
// setLoadingStatus(R.string.no_headlines_to_display, false);
//else
if (m_articles.size() == 0)
setLoadingStatus(R.string.no_headlines_to_display, false);
else
setLoadingStatus(R.string.blank, false);
m_refreshInProgress = false;

View File

@ -13,6 +13,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.net.ConnectivityManager;
@ -79,6 +80,36 @@ public class MainActivity extends FragmentActivity implements FeedsFragment.OnFe
return m_apiLevel;
}
public boolean hasPendingOfflineData() {
Cursor c = getReadableDb().query("articles",
new String[] { "COUNT(*)" }, "modified = 1", null, null, null, null);
if (c.moveToFirst()) {
int modified = c.getInt(0);
c.close();
return modified > 0;
}
return false;
}
public void clearPendingOfflineData() {
getWritableDb().execSQL("UPDATE articles SET modified = 0");
}
public boolean hasOfflineData() {
Cursor c = getReadableDb().query("articles",
new String[] { "COUNT(*)" }, null, null, null, null, null);
if (c.moveToFirst()) {
int modified = c.getInt(0);
c.close();
return modified > 0;
}
return false;
}
@SuppressWarnings({ "unchecked", "serial" })
public void saveArticleUnread(final Article article) {
ApiRequest req = new ApiRequest(getApplicationContext());
@ -1031,6 +1062,135 @@ public class MainActivity extends FragmentActivity implements FeedsFragment.OnFe
m_writableDb.close();
}
private void syncOfflineRead() {
Log.d(TAG, "syncing modified offline data... (read)");
final String ids = getOfflineModifiedIds(ModifiedCriteria.READ);
if (ids.length() > 0) {
ApiRequest req = new ApiRequest(getApplicationContext()) {
@Override
protected void onPostExecute(JsonElement result) {
if (result != null) {
syncOfflineMarked();
} else {
setLoadingStatus(getErrorMessage(), false);
}
}
};
@SuppressWarnings("serial")
HashMap<String,String> map = new HashMap<String,String>() {
{
put("sid", m_sessionId);
put("op", "updateArticle");
put("article_ids", ids);
put("mode", "0");
put("field", "2");
}
};
req.execute(map);
} else {
syncOfflineMarked();
}
}
private void syncOfflineMarked() {
Log.d(TAG, "syncing modified offline data... (marked)");
final String ids = getOfflineModifiedIds(ModifiedCriteria.MARKED);
if (ids.length() > 0) {
ApiRequest req = new ApiRequest(getApplicationContext()) {
@Override
protected void onPostExecute(JsonElement result) {
if (result != null) {
syncOfflinePublished();
} else {
setLoadingStatus(getErrorMessage(), false);
}
}
};
@SuppressWarnings("serial")
HashMap<String,String> map = new HashMap<String,String>() {
{
put("sid", m_sessionId);
put("op", "updateArticle");
put("article_ids", ids);
put("mode", "0");
put("field", "0");
}
};
req.execute(map);
} else {
syncOfflinePublished();
}
}
private void syncOfflinePublished() {
Log.d(TAG, "syncing modified offline data... (published)");
final String ids = getOfflineModifiedIds(ModifiedCriteria.MARKED);
if (ids.length() > 0) {
ApiRequest req = new ApiRequest(getApplicationContext()) {
@Override
protected void onPostExecute(JsonElement result) {
if (result != null) {
loginSuccessInitUI();
loginSuccess();
clearPendingOfflineData();
} else {
setLoadingStatus(getErrorMessage(), false);
}
}
};
@SuppressWarnings("serial")
HashMap<String,String> map = new HashMap<String,String>() {
{
put("sid", m_sessionId);
put("op", "updateArticle");
put("article_ids", ids);
put("mode", "0");
put("field", "1");
}
};
req.execute(map);
} else {
loginSuccessInitUI();
loginSuccess();
clearPendingOfflineData();
}
}
private void syncOfflineData() {
setLoadingStatus(R.string.syncing_offline_data, true);
syncOfflineRead();
}
private void loginSuccessInitUI() {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if (m_enableCats) {
FeedCategoriesFragment frag = new FeedCategoriesFragment();
ft.replace(R.id.cats_fragment, frag);
} else {
FeedsFragment frag = new FeedsFragment();
ft.replace(R.id.feeds_fragment, frag);
}
try {
ft.commit();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
private void loginSuccess() {
findViewById(R.id.loading_container).setVisibility(View.INVISIBLE);
@ -1056,6 +1216,42 @@ public class MainActivity extends FragmentActivity implements FeedsFragment.OnFe
m_refreshTimer.schedule(m_refreshTask, 60*1000L, 120*1000L);
}
private enum ModifiedCriteria { READ, MARKED, PUBLISHED };
private String getOfflineModifiedIds(ModifiedCriteria criteria) {
String criteriaStr = "";
switch (criteria) {
case READ:
criteriaStr = "unread = 0";
break;
case MARKED:
criteriaStr = "marked = 1";
break;
case PUBLISHED:
criteriaStr = "published = 1";
break;
}
Cursor c = getReadableDb().query("articles",
null, "modified = 1 AND " + criteriaStr, null, null, null, null);
String tmp = "";
while (c.moveToNext()) {
tmp += c.getInt(0) + ",";
}
tmp = tmp.replaceAll(",$", "");
//Log.d(TAG, "getOfflineModifiedIds " + criteria + " = " + tmp);
c.close();
return tmp;
}
private class LoginRequest extends ApiRequest {
public LoginRequest(Context context) {
super(context);
@ -1081,20 +1277,14 @@ public class MainActivity extends FragmentActivity implements FeedsFragment.OnFe
Log.d(TAG, "Received API level: " + m_apiLevel);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if (m_enableCats) {
FeedCategoriesFragment frag = new FeedCategoriesFragment();
ft.replace(R.id.cats_fragment, frag);
} else {
FeedsFragment frag = new FeedsFragment();
ft.replace(R.id.feeds_fragment, frag);
}
if (hasPendingOfflineData()) {
try {
ft.commit();
} catch (IllegalStateException e) {
e.printStackTrace();
syncOfflineData();
//loginSuccess();
} else {
loginSuccessInitUI();
loginSuccess();
}
}
@ -1112,7 +1302,6 @@ public class MainActivity extends FragmentActivity implements FeedsFragment.OnFe
setLoadingStatus(R.string.loading_message, true);
loginSuccess();
return;
}
@ -1124,6 +1313,26 @@ public class MainActivity extends FragmentActivity implements FeedsFragment.OnFe
m_sessionId = null;
setLoadingStatus(getErrorMessage(), false);
if (hasOfflineData()) {
AlertDialog.Builder builder = new AlertDialog.Builder(m_context).
setMessage(R.string.dialog_offline_prompt).
setPositiveButton(R.string.dialog_offline_go, new Dialog.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
switchOfflineSuccess();
}
}).
setNegativeButton(R.string.dialog_cancel, new Dialog.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
//
}
});
AlertDialog dlg = builder.create();
dlg.show();
}
//m_menu.findItem(R.id.login).setVisible(true);
}
@ -1278,7 +1487,7 @@ public class MainActivity extends FragmentActivity implements FeedsFragment.OnFe
} else {
LoginRequest ar = new LoginRequest(getApplicationContext());
LoginRequest ar = new LoginRequest(this); // do not use getApplicationContext() here because alertdialog chokes on it
HashMap<String,String> map = new HashMap<String,String>() {
{

View File

@ -249,9 +249,11 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene
}
public void sortFeeds() {
// TODO implement
m_adapter.notifyDataSetInvalidated();
try {
refresh();
} catch (IllegalStateException e) {
// we're probably closing and DB is gone already
}
}
@Override