diff --git a/AndroidManifest.xml b/AndroidManifest.xml index f6706e7e..3904345d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,37 +1,46 @@ + package="org.fox.ttrss" + android:versionCode="33" + android:versionName="0.3.0" > + - - - + + + - - + + - - + - - - + + + - - + - - + + + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index ea69bd1c..49a27530 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -98,8 +98,12 @@ No feeds to display No articles to display Login failed, but you have stored offline data. Would you like to go offline? + Offline mode is ready Go offline Cancel Synchronizing offline data... Download unread articles and go offline? + Downloading articles (%1$d)... + Starting download... + Downloading feeds... \ No newline at end of file diff --git a/src/org/fox/ttrss/MainActivity.java b/src/org/fox/ttrss/MainActivity.java index 4385c9fe..5fef0277 100644 --- a/src/org/fox/ttrss/MainActivity.java +++ b/src/org/fox/ttrss/MainActivity.java @@ -8,9 +8,13 @@ import java.util.TimerTask; import android.app.AlertDialog; import android.app.Dialog; +import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +import android.content.IntentFilter; +import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.database.Cursor; @@ -19,6 +23,7 @@ import android.database.sqlite.SQLiteStatement; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.Bundle; +import android.os.IBinder; import android.preference.PreferenceManager; import android.provider.BaseColumns; import android.support.v4.app.FragmentActivity; @@ -43,8 +48,6 @@ import com.google.gson.reflect.TypeToken; public class MainActivity extends FragmentActivity implements OnlineServices { private final String TAG = this.getClass().getSimpleName(); - private final int OFFLINE_SYNC_SEQ = 60; - private final int OFFLINE_SYNC_MAX = 500; private SharedPreferences m_prefs; private String m_themeName = ""; @@ -62,11 +65,37 @@ public class MainActivity extends FragmentActivity implements OnlineServices { private boolean m_enableCats = false; private int m_isLicensed = -1; private int m_apiLevel = 0; - private int m_articleOffset = 0; private boolean m_isOffline = false; private SQLiteDatabase m_readableDb; private SQLiteDatabase m_writableDb; + + + private BroadcastReceiver m_broadcastReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context content, Intent intent) { + + AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this). + setMessage(R.string.dialog_offline_success). + setPositiveButton(R.string.dialog_offline_go, new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + Intent refresh = new Intent(MainActivity.this, OfflineActivity.class); + startActivity(refresh); + finish(); + } + }). + setNegativeButton(R.string.dialog_cancel, new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // + } + }); + + AlertDialog dlg = builder.create(); + dlg.show(); + + } + }; public void updateHeadlines() { HeadlinesFragment frag = (HeadlinesFragment)getSupportFragmentManager().findFragmentById(R.id.headlines_fragment); @@ -311,13 +340,13 @@ public class MainActivity extends FragmentActivity implements OnlineServices { return m_unreadOnly; } - private void setUnreadArticlesOnly(boolean unread) { + /* private void setUnreadArticlesOnly(boolean unread) { m_unreadArticlesOnly = unread; HeadlinesFragment frag = (HeadlinesFragment)getSupportFragmentManager().findFragmentById(R.id.headlines_fragment); if (frag != null) frag.refresh(false); - } + } */ @Override public boolean getUnreadArticlesOnly() { @@ -376,6 +405,11 @@ public class MainActivity extends FragmentActivity implements OnlineServices { initDatabase(); + IntentFilter filter = new IntentFilter("org.fox.ttrss.intent.action.DownloadComplete"); + filter.addCategory(Intent.CATEGORY_DEFAULT); + + registerReceiver(m_broadcastReceiver, filter); + m_isOffline = m_prefs.getBoolean("offline_mode_active", false); Log.d(TAG, "m_isOffline=" + m_isOffline); @@ -459,27 +493,6 @@ public class MainActivity extends FragmentActivity implements OnlineServices { return m_writableDb; } - @SuppressWarnings("unchecked") - private void offlineGetArticles() { - Log.d(TAG, "offline: downloading articles... offset=" + m_articleOffset); - - OfflineArticlesRequest req = new OfflineArticlesRequest(this); - - HashMap map = new HashMap() { - { - put("op", "getHeadlines"); - put("sid", m_sessionId); - put("feed_id", "-4"); - put("view_mode", "unread"); - put("show_content", "true"); - put("skip", String.valueOf(m_articleOffset)); - put("limit", String.valueOf(OFFLINE_SYNC_SEQ)); - } - }; - - req.execute(map); - } - @SuppressWarnings("unchecked") private void switchOffline() { @@ -488,75 +501,30 @@ public class MainActivity extends FragmentActivity implements OnlineServices { setPositiveButton(R.string.dialog_offline_go, new Dialog.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - Log.d(TAG, "offline: starting"); - if (m_sessionId != null) { - - findViewById(R.id.loading_container).setVisibility(View.VISIBLE); - findViewById(R.id.main).setVisibility(View.INVISIBLE); - - setLoadingStatus(R.string.offline_downloading, true); - - // Download feeds - - getWritableDb().execSQL("DELETE FROM feeds;"); - - ApiRequest req = new ApiRequest(getApplicationContext()) { + Log.d(TAG, "offline: starting"); + + ServiceConnection m_serviceConnection = new ServiceConnection() { + @Override - protected void onPostExecute(JsonElement content) { - if (content != null) { - - try { - Type listType = new TypeToken>() {}.getType(); - List feeds = new Gson().fromJson(content, listType); - - SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO feeds " + - "("+BaseColumns._ID+", title, feed_url, has_icon, cat_id) " + - "VALUES (?, ?, ?, ?, ?);"); - - for (Feed feed : feeds) { - stmtInsert.bindLong(1, feed.id); - stmtInsert.bindString(2, feed.title); - stmtInsert.bindString(3, feed.feed_url); - stmtInsert.bindLong(4, feed.has_icon ? 1 : 0); - stmtInsert.bindLong(5, feed.cat_id); - - stmtInsert.execute(); - } - - stmtInsert.close(); - - Log.d(TAG, "offline: done downloading feeds"); - - m_articleOffset = 0; - - getWritableDb().execSQL("DELETE FROM articles;"); - - offlineGetArticles(); - } catch (Exception e) { - e.printStackTrace(); - setLoadingStatus(R.string.offline_switch_error, false); - } - - } else { - setLoadingStatus(getErrorMessage(), false); - // TODO error, could not download feeds, properly report API error (toast) - } + public void onServiceDisconnected(ComponentName name) { + Log.d(TAG, "download service disconnected"); + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "download service connected"); + //((OfflineDownloadService.LocalBinder)service).getService().download(); } }; - HashMap map = new HashMap() { - { - put("op", "getFeeds"); - put("sid", m_sessionId); - put("cat_id", "-3"); - put("unread_only", "true"); - } - }; + Intent intent = new Intent(MainActivity.this, OfflineDownloadService.class); + intent.putExtra("sessionId", m_sessionId); - req.execute(map); - } else { - switchOfflineSuccess(); + startService(intent); + + //bindService(intent, m_serviceConnection, Context.BIND_AUTO_CREATE); + } } }). @@ -1082,6 +1050,8 @@ public class MainActivity extends FragmentActivity implements OnlineServices { public void onDestroy() { super.onDestroy(); + unregisterReceiver(m_broadcastReceiver); + m_readableDb.close(); m_writableDb.close(); @@ -1340,7 +1310,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices { if (hasOfflineData()) { - AlertDialog.Builder builder = new AlertDialog.Builder(m_context). + AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this). setMessage(R.string.dialog_offline_prompt). setPositiveButton(R.string.dialog_offline_go, new Dialog.OnClickListener() { public void onClick(DialogInterface dialog, int which) { @@ -1514,7 +1484,7 @@ public class MainActivity extends FragmentActivity implements OnlineServices { } else { - LoginRequest ar = new LoginRequest(this); // do not use getApplicationContext() here because alertdialog chokes on it + LoginRequest ar = new LoginRequest(getApplicationContext()); HashMap map = new HashMap() { { @@ -1770,78 +1740,6 @@ public class MainActivity extends FragmentActivity implements OnlineServices { viewCategory(cat, browse && cat.id >= 0); } - public class OfflineArticlesRequest extends ApiRequest { - public OfflineArticlesRequest(Context context) { - super(context); - } - - @Override - protected void onPostExecute(JsonElement content) { - if (content != null) { - try { - Type listType = new TypeToken>() {}.getType(); - List
articles = new Gson().fromJson(content, listType); - SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO articles " + - "("+BaseColumns._ID+", unread, marked, published, updated, is_updated, title, link, feed_id, tags, content) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"); - - for (Article article : articles) { - - String tagsString = ""; - - for (String t : article.tags) { - tagsString += t + ", "; - } - - tagsString = tagsString.replaceAll(", $", ""); - - stmtInsert.bindLong(1, article.id); - stmtInsert.bindLong(2, article.unread ? 1 : 0); - stmtInsert.bindLong(3, article.marked ? 1 : 0); - stmtInsert.bindLong(4, article.published ? 1 : 0); - stmtInsert.bindLong(5, article.updated); - stmtInsert.bindLong(6, article.is_updated ? 1 : 0); - stmtInsert.bindString(7, article.title); - stmtInsert.bindString(8, article.link); - stmtInsert.bindLong(9, article.feed_id); - stmtInsert.bindString(10, tagsString); // comma-separated tags - stmtInsert.bindString(11, article.content); - - try { - stmtInsert.execute(); - } catch (Exception e) { - e.printStackTrace(); - } - - } - - stmtInsert.close(); - - //m_canGetMoreArticles = articles.size() == 30; - m_articleOffset += articles.size(); - - Log.d(TAG, "offline: received " + articles.size() + " articles"); - - if (articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < OFFLINE_SYNC_MAX) { - offlineGetArticles(); - } else { - switchOfflineSuccess(); - } - - return; - - } catch (Exception e) { - setLoadingStatus(R.string.offline_switch_error, false); - Log.d(TAG, "offline: failed: exception when loading articles"); - e.printStackTrace(); - } - - } else { - Log.d(TAG, "offline: failed: " + getErrorMessage()); - setLoadingStatus(getErrorMessage(), false); - } - } - } } \ No newline at end of file diff --git a/src/org/fox/ttrss/OfflineActivity.java b/src/org/fox/ttrss/OfflineActivity.java index 9ea3252d..4f6befd0 100644 --- a/src/org/fox/ttrss/OfflineActivity.java +++ b/src/org/fox/ttrss/OfflineActivity.java @@ -4,6 +4,7 @@ import org.fox.ttrss.OnlineServices.RelativeArticle; import android.app.AlertDialog; import android.app.Dialog; +import android.app.NotificationManager; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; @@ -63,6 +64,9 @@ public class OfflineActivity extends FragmentActivity implements OfflineServices } super.onCreate(savedInstanceState); + + NotificationManager nmgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); + nmgr.cancel(OfflineDownloadService.NOTIFY_DOWNLOADING); m_themeName = m_prefs.getString("theme", "THEME_DARK"); diff --git a/src/org/fox/ttrss/OfflineDownloadService.java b/src/org/fox/ttrss/OfflineDownloadService.java new file mode 100644 index 00000000..e770bad6 --- /dev/null +++ b/src/org/fox/ttrss/OfflineDownloadService.java @@ -0,0 +1,400 @@ +package org.fox.ttrss; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.os.Bundle; +import android.os.IBinder; +import android.provider.BaseColumns; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +public class OfflineDownloadService extends IntentService { + + private final String TAG = this.getClass().getSimpleName(); + + public static final int NOTIFY_DOWNLOADING = 1; + + private static final int OFFLINE_SYNC_SEQ = 60; + private static final int OFFLINE_SYNC_MAX = 60; //500 + + private SQLiteDatabase m_writableDb; + private SQLiteDatabase m_readableDb; + private int m_articleOffset = 0; + private String m_sessionId; + private NotificationManager m_nmgr; + + private boolean m_downloadInProgress = false; + + public OfflineDownloadService() { + super("OfflineDownloadService"); + } + + public OfflineDownloadService(String name) { + super(name); + } + + @Override + public void onCreate() { + super.onCreate(); + m_nmgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); + initDatabase(); + } + + public boolean getDownloadInProgress() { + return m_downloadInProgress; + } + + private void updateNotification(String msg) { + Notification notification = new Notification(R.drawable.icon, + getString(R.string.app_name), System.currentTimeMillis()); + + PendingIntent contentIntent = PendingIntent.getActivity(this, 0, + new Intent(this, MainActivity.class), 0); + + notification.setLatestEventInfo(this, getString(R.string.go_offline), msg, contentIntent); + + m_nmgr.notify(NOTIFY_DOWNLOADING, notification); + } + + private void updateNotification(int msgResId) { + updateNotification(getString(msgResId)); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + public void downloadComplete() { + m_downloadInProgress = false; + + m_nmgr.cancel(NOTIFY_DOWNLOADING); + + Intent intent = new Intent(); + intent.setAction("org.fox.ttrss.intent.action.DownloadComplete"); + intent.addCategory(Intent.CATEGORY_DEFAULT); + sendBroadcast(intent); + + m_readableDb.close(); + m_writableDb.close(); + } + + private void initDatabase() { + DatabaseHelper dh = new DatabaseHelper(getApplicationContext()); + m_writableDb = dh.getWritableDatabase(); + m_readableDb = dh.getReadableDatabase(); + } + + public synchronized SQLiteDatabase getReadableDb() { + return m_readableDb; + } + + public synchronized SQLiteDatabase getWritableDb() { + return m_writableDb; + } + + @SuppressWarnings("unchecked") + private void downloadArticles() { + Log.d(TAG, "offline: downloading articles... offset=" + m_articleOffset); + + updateNotification(getString(R.string.notify_downloading_articles, m_articleOffset)); + + OfflineArticlesRequest req = new OfflineArticlesRequest(this); + + HashMap map = new HashMap() { + { + put("op", "getHeadlines"); + put("sid", m_sessionId); + put("feed_id", "-4"); + put("view_mode", "unread"); + put("show_content", "true"); + put("skip", String.valueOf(m_articleOffset)); + put("limit", String.valueOf(OFFLINE_SYNC_SEQ)); + } + }; + + req.execute(map); + } + + private void downloadFeeds() { + //findViewById(R.id.loading_container).setVisibility(View.VISIBLE); + //findViewById(R.id.main).setVisibility(View.INVISIBLE); + + //setLoadingStatus(R.string.offline_downloading, true); + + // Download feeds + + updateNotification(R.string.notify_downloading_feeds); + + getWritableDb().execSQL("DELETE FROM feeds;"); + + ApiRequest req = new ApiRequest(getApplicationContext()) { + @Override + protected void onPostExecute(JsonElement content) { + if (content != null) { + + try { + Type listType = new TypeToken>() {}.getType(); + List feeds = new Gson().fromJson(content, listType); + + SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO feeds " + + "("+BaseColumns._ID+", title, feed_url, has_icon, cat_id) " + + "VALUES (?, ?, ?, ?, ?);"); + + for (Feed feed : feeds) { + stmtInsert.bindLong(1, feed.id); + stmtInsert.bindString(2, feed.title); + stmtInsert.bindString(3, feed.feed_url); + stmtInsert.bindLong(4, feed.has_icon ? 1 : 0); + stmtInsert.bindLong(5, feed.cat_id); + + stmtInsert.execute(); + } + + stmtInsert.close(); + + Log.d(TAG, "offline: done downloading feeds"); + + m_articleOffset = 0; + + getWritableDb().execSQL("DELETE FROM articles;"); + downloadArticles(); + } catch (Exception e) { + e.printStackTrace(); + updateNotification(R.string.offline_switch_error); + m_downloadInProgress = false; + //setLoadingStatus(R.string.offline_switch_error, false); + } + + } else { + updateNotification(getErrorMessage()); + m_downloadInProgress = false; + // TODO error, could not download feeds, properly report API error (toast) + } + } + + }; + + HashMap map = new HashMap() { + { + put("op", "getFeeds"); + put("sid", m_sessionId); + put("cat_id", "-3"); + put("unread_only", "true"); + } + }; + + req.execute(map); + } + + @SuppressWarnings("unchecked") + private void switchOffline() { + + AlertDialog.Builder builder = new AlertDialog.Builder(this). + setMessage(R.string.dialog_offline_switch_prompt). + setPositiveButton(R.string.dialog_offline_go, new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + + Log.d(TAG, "offline: starting"); + + if (m_sessionId != null) { + + //findViewById(R.id.loading_container).setVisibility(View.VISIBLE); + //findViewById(R.id.main).setVisibility(View.INVISIBLE); + + //setLoadingStatus(R.string.offline_downloading, true); + + // Download feeds + + getWritableDb().execSQL("DELETE FROM feeds;"); + + ApiRequest req = new ApiRequest(getApplicationContext()) { + @Override + protected void onPostExecute(JsonElement content) { + if (content != null) { + + try { + Type listType = new TypeToken>() {}.getType(); + List feeds = new Gson().fromJson(content, listType); + + SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO feeds " + + "("+BaseColumns._ID+", title, feed_url, has_icon, cat_id) " + + "VALUES (?, ?, ?, ?, ?);"); + + for (Feed feed : feeds) { + stmtInsert.bindLong(1, feed.id); + stmtInsert.bindString(2, feed.title); + stmtInsert.bindString(3, feed.feed_url); + stmtInsert.bindLong(4, feed.has_icon ? 1 : 0); + stmtInsert.bindLong(5, feed.cat_id); + + stmtInsert.execute(); + } + + stmtInsert.close(); + + Log.d(TAG, "offline: done downloading feeds"); + + m_articleOffset = 0; + + getWritableDb().execSQL("DELETE FROM articles;"); + + downloadArticles(); + } catch (Exception e) { + e.printStackTrace(); + //setLoadingStatus(R.string.offline_switch_error, false); + } + + } else { + //setLoadingStatus(getErrorMessage(), false); + // TODO error, could not download feeds, properly report API error (toast) + } + } + }; + + HashMap map = new HashMap() { + { + put("op", "getFeeds"); + put("sid", m_sessionId); + put("cat_id", "-3"); + put("unread_only", "true"); + } + }; + + req.execute(map); + } else { + downloadComplete(); + } + } + }). + setNegativeButton(R.string.dialog_cancel, new Dialog.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // + } + }); + + AlertDialog dlg = builder.create(); + dlg.show(); + + } + + public void download() { + if (!m_downloadInProgress) { + updateNotification(R.string.notify_downloading_init); + m_downloadInProgress = true; + + downloadFeeds(); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + m_nmgr.cancel(NOTIFY_DOWNLOADING); + + //m_readableDb.close(); + //m_writableDb.close(); + } + + public class OfflineArticlesRequest extends ApiRequest { + public OfflineArticlesRequest(Context context) { + super(context); + } + + @Override + protected void onPostExecute(JsonElement content) { + if (content != null) { + try { + Type listType = new TypeToken>() {}.getType(); + List
articles = new Gson().fromJson(content, listType); + + SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO articles " + + "("+BaseColumns._ID+", unread, marked, published, updated, is_updated, title, link, feed_id, tags, content) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"); + + for (Article article : articles) { + + String tagsString = ""; + + for (String t : article.tags) { + tagsString += t + ", "; + } + + tagsString = tagsString.replaceAll(", $", ""); + + stmtInsert.bindLong(1, article.id); + stmtInsert.bindLong(2, article.unread ? 1 : 0); + stmtInsert.bindLong(3, article.marked ? 1 : 0); + stmtInsert.bindLong(4, article.published ? 1 : 0); + stmtInsert.bindLong(5, article.updated); + stmtInsert.bindLong(6, article.is_updated ? 1 : 0); + stmtInsert.bindString(7, article.title); + stmtInsert.bindString(8, article.link); + stmtInsert.bindLong(9, article.feed_id); + stmtInsert.bindString(10, tagsString); // comma-separated tags + stmtInsert.bindString(11, article.content); + + try { + stmtInsert.execute(); + } catch (Exception e) { + e.printStackTrace(); + } + + } + + stmtInsert.close(); + + //m_canGetMoreArticles = articles.size() == 30; + m_articleOffset += articles.size(); + + Log.d(TAG, "offline: received " + articles.size() + " articles"); + + if (articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < OFFLINE_SYNC_MAX) { + downloadArticles(); + } else { + downloadComplete(); + } + + return; + + } catch (Exception e) { + updateNotification(R.string.offline_switch_error); + Log.d(TAG, "offline: failed: exception when loading articles"); + e.printStackTrace(); + m_downloadInProgress = false; + } + + } else { + Log.d(TAG, "offline: failed: " + getErrorMessage()); + m_downloadInProgress = false; + updateNotification(getErrorMessage()); + } + } + } + + @Override + protected void onHandleIntent(Intent intent) { + Bundle extras = intent.getExtras(); + + m_sessionId = extras.getString("sessionId"); + + download(); + } +} diff --git a/src/org/fox/ttrss/OfflineFeedsFragment.java b/src/org/fox/ttrss/OfflineFeedsFragment.java index c0de692b..7c866bfe 100644 --- a/src/org/fox/ttrss/OfflineFeedsFragment.java +++ b/src/org/fox/ttrss/OfflineFeedsFragment.java @@ -253,6 +253,8 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene public void sortFeeds() { try { refresh(); + } catch (NullPointerException e) { + // activity is gone? } catch (IllegalStateException e) { // we're probably closing and DB is gone already }