merge separate-activities

This commit is contained in:
Andrew Dolgov 2012-09-19 23:49:03 +04:00
commit d361b0fec4
58 changed files with 4785 additions and 5370 deletions

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.fox.ttrss" package="org.fox.ttrss"
android:versionCode="98" android:versionCode="100"
android:versionName="0.7.6" > android:versionName="0.8.0" >
<uses-sdk <uses-sdk
android:minSdkVersion="8" android:minSdkVersion="8"
@ -13,21 +13,14 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application <application
android:name=".GlobalState"
android:allowBackup="true" android:allowBackup="true"
android:backupAgent=".util.PrefsBackupAgent" android:backupAgent=".util.PrefsBackupAgent"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:icon="@drawable/icon" android:icon="@drawable/icon"
android:label="@string/app_name" > android:label="@string/app_name" >
<activity <activity
android:name=".LoginActivity" android:name=".OnlineActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name=".offline.OfflineActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name=".MainActivity"
android:label="@string/app_name" > android:label="@string/app_name" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -40,6 +33,41 @@
android:label="@string/preferences" > android:label="@string/preferences" >
</activity> </activity>
<activity
android:name=".FeedsActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name=".HeadlinesActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name=".CommonActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name=".ArticleActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name=".offline.OfflineActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name=".offline.OfflineFeedsActivity"
android:label="@string/app_name" >
</activity>
<activity
android:name=".offline.OfflineHeadlinesActivity"
android:label="@string/app_name" >
</activity>
<service <service
android:name=".offline.OfflineDownloadService" android:name=".offline.OfflineDownloadService"
android:enabled="true" /> android:enabled="true" />

Binary file not shown.

View File

@ -7,11 +7,6 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical" > android:orientation="vertical" >
<LinearLayout
android:id="@+id/linearLayout7"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout <LinearLayout
android:id="@+id/linearLayout1" android:id="@+id/linearLayout1"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -100,7 +95,6 @@
android:src="@drawable/ic_rss_bw" /> android:src="@drawable/ic_rss_bw" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout>
<TextView <TextView
android:id="@+id/content" android:id="@+id/content"

View File

@ -7,11 +7,6 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical" > android:orientation="vertical" >
<LinearLayout
android:id="@+id/linearLayout7"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout <LinearLayout
android:id="@+id/linearLayout1" android:id="@+id/linearLayout1"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -100,7 +95,6 @@
android:src="@drawable/ic_rss_bw" /> android:src="@drawable/ic_rss_bw" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout>
<TextView <TextView
android:id="@+id/content" android:id="@+id/content"

View File

@ -7,11 +7,6 @@
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="vertical" > android:orientation="vertical" >
<LinearLayout
android:id="@+id/linearLayout7"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout <LinearLayout
android:id="@+id/linearLayout1" android:id="@+id/linearLayout1"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -100,7 +95,6 @@
android:src="@drawable/ic_rss_bw" /> android:src="@drawable/ic_rss_bw" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout>
<TextView <TextView
android:id="@+id/content" android:id="@+id/content"

View File

@ -0,0 +1,48 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<LinearLayout
android:id="@+id/loading_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?loadingBackground"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible" >
<TextView
android:id="@+id/loading_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/loading_message"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
<LinearLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/headlines_fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.3"
android:background="?headlinesBackgroundSolid" >
</FrameLayout>
<FrameLayout
android:id="@+id/article_fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.7"
android:background="?articleBackground" >
</FrameLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,61 +0,0 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<LinearLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<FrameLayout
android:id="@+id/feeds_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="0.6"
android:background="?feedlistBackground" >
</FrameLayout>
<LinearLayout
android:id="@+id/vertical_fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="0.4"
android:orientation="vertical" >
<FrameLayout
android:id="@+id/headlines_fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0.30"
android:background="?headlinesBackgroundSolid" >
</FrameLayout>
<FrameLayout
android:id="@+id/article_fragment"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="?articleBackground"
android:layout_weight="0.70" >
</FrameLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/loading_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="?loadingBackground"
android:orientation="vertical" >
<TextView
android:id="@+id/loading_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/loading_message"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
</FrameLayout>

View File

@ -1,39 +1,8 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main" android:id="@+id/main"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"
> android:orientation="vertical" >
<LinearLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal" >
<FrameLayout
android:id="@+id/feeds_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.3"
android:background="?feedlistBackground" >
</FrameLayout>
<FrameLayout
android:id="@+id/headlines_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.4"
android:background="?headlinesBackground" >
</FrameLayout>
<FrameLayout
android:id="@+id/article_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.6"
android:background="?articleBackground" >
</FrameLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/loading_container" android:id="@+id/loading_container"
@ -52,4 +21,28 @@
android:textAppearance="?android:attr/textAppearanceLarge" /> android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout> </LinearLayout>
</FrameLayout> <LinearLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal" >
<FrameLayout
android:id="@+id/feeds_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.4"
android:background="?feedlistBackground" >
</FrameLayout>
<FrameLayout
android:id="@+id/headlines_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.6"
android:background="?headlinesBackground" >
</FrameLayout>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,49 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/headlines"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<LinearLayout
android:id="@+id/loading_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?loadingBackground"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible" >
<TextView
android:id="@+id/loading_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/loading_message"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
<LinearLayout
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal" >
<FrameLayout
android:id="@+id/headlines_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.4"
android:background="?headlinesBackground" >
</FrameLayout>
<FrameLayout
android:id="@+id/article_fragment"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.6"
android:background="?articleBackground" >
</FrameLayout>
</LinearLayout>
</LinearLayout>

View File

@ -1,18 +1,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/article_fragment" android:id="@+id/article_fragment"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:padding="5sp"
android:layout_height="fill_parent" android:layout_height="fill_parent"
android:orientation="horizontal" > android:orientation="vertical" >
<LinearLayout
xmlns:ads="http://schemas.android.com/apk/lib/com.google.ads"
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/article_holder"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:padding="5sp" >
<LinearLayout <LinearLayout
android:id="@+id/article_header" android:id="@+id/article_header"
@ -24,6 +15,7 @@
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
android:textColor="?linkColor"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
@ -91,6 +83,13 @@
android:layout_weight="0" android:layout_weight="0"
android:text="@string/attachment_view" /> android:text="@string/attachment_view" />
<Button
android:id="@+id/attachment_share"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="0"
android:text="@string/attachment_share" />
<Button <Button
android:id="@+id/attachment_copy" android:id="@+id/attachment_copy"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -98,6 +97,5 @@
android:layout_weight="0" android:layout_weight="0"
android:text="@string/attachment_copy" /> android:text="@string/attachment_copy" />
</LinearLayout> </LinearLayout>
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -19,7 +19,7 @@
</LinearLayout> </LinearLayout>
<FrameLayout <FrameLayout
android:id="@+id/fragment_container" android:id="@+id/feeds_fragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" > android:layout_height="match_parent" >
</FrameLayout> </FrameLayout>

27
res/layout/headlines.xml Normal file
View File

@ -0,0 +1,27 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/headlines"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >
<LinearLayout
android:id="@+id/loading_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical" >
<TextView
android:id="@+id/loading_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/loading_message" />
</LinearLayout>
<FrameLayout
android:id="@+id/headlines_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</FrameLayout>
</LinearLayout>

View File

@ -4,18 +4,10 @@
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?headlineNormalBackground" android:background="?headlineNormalBackground"
android:gravity="center_vertical" android:gravity="center"
android:padding="5dp"
android:orientation="horizontal" > android:orientation="horizontal" >
<LinearLayout
android:id="@+id/loadmore_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_weight="1"
android:orientation="horizontal"
android:padding="10dp" >
<ProgressBar <ProgressBar
android:id="@+id/loadmore_progress" android:id="@+id/loadmore_progress"
style="?android:attr/progressBarStyleSmall" style="?android:attr/progressBarStyleSmall"
@ -29,6 +21,5 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="?headlineTextColor" android:textColor="?headlineTextColor"
android:text="@string/loading_message" /> android:text="@string/loading_message" />
</LinearLayout>
</LinearLayout> </LinearLayout>

14
res/layout/login.xml Normal file
View File

@ -0,0 +1,14 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/loading_container"
android:layout_width="fill_parent"
android:gravity="center"
android:layout_height="fill_parent" >
<TextView
android:id="@+id/loading_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/loading_message" />
</LinearLayout>

View File

@ -1,5 +1,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/browse_headlines"
android:title="@string/category_browse_headlines"/>
<item <item
android:id="@+id/browse_articles" android:id="@+id/browse_articles"
android:title="@string/category_browse_articles"/> android:title="@string/category_browse_articles"/>

View File

@ -1,5 +1,13 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/browse_headlines"
android:title="@string/category_browse_headlines"/>
<item
android:id="@+id/browse_articles"
android:title="@string/category_browse_articles"/>
<item <item
android:id="@+id/catchup_feed" android:id="@+id/catchup_feed"
android:title="@string/catchup"/> android:title="@string/catchup"/>

View File

@ -1,7 +1,6 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" > <menu xmlns:android="http://schemas.android.com/apk/res/android" >
<group android:id="@+id/menu_group_logged_in" > <group android:id="@+id/menu_group_logged_in" >
<group android:id="@+id/menu_group_feeds" > <group android:id="@+id/menu_group_feeds" >
<!-- <!--
@ -17,136 +16,125 @@
android:icon="@android:drawable/ic_menu_agenda" android:icon="@android:drawable/ic_menu_agenda"
android:showAsAction="" android:showAsAction=""
android:title="@string/menu_all_feeds"/> android:title="@string/menu_all_feeds"/>
<item <item
android:id="@+id/update_feeds" android:id="@+id/update_feeds"
android:icon="@android:drawable/ic_menu_rotate" android:icon="@android:drawable/ic_menu_rotate"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:title="@string/update_feeds"/> android:title="@string/update_feeds"/>
<item <item
android:id="@+id/go_offline" android:id="@+id/go_offline"
android:icon="@drawable/ic_menu_cloud" android:icon="@drawable/ic_menu_cloud"
android:showAsAction="" android:showAsAction=""
android:title="@string/go_offline"/> android:title="@string/go_offline"/>
<item <item
android:id="@+id/logout" android:id="@+id/logout"
android:icon="@drawable/ic_menu_exit" android:icon="@drawable/ic_menu_exit"
android:showAsAction="" android:showAsAction=""
android:title="@string/logout"/> android:title="@string/logout"/>
</group> </group>
<group android:id="@+id/menu_group_headlines" > <group android:id="@+id/menu_group_headlines" >
<item <item
android:id="@+id/go_offline" android:id="@+id/update_headlines"
android:icon="@drawable/ic_menu_cloud" android:icon="@android:drawable/ic_menu_rotate"
android:title="@string/go_offline"/> android:showAsAction="ifRoom"
android:title="@string/update_headlines"/>
<item <item
android:id="@+id/search" android:id="@+id/search"
android:actionViewClass="android.widget.SearchView" android:actionViewClass="android.widget.SearchView"
android:icon="@android:drawable/ic_menu_search" android:icon="@android:drawable/ic_menu_search"
android:showAsAction="ifRoom|collapseActionView" android:showAsAction="ifRoom|collapseActionView"
android:title="@string/search"/> android:title="@string/search"/>
<item <item
android:id="@+id/headlines_mark_as_read" android:id="@+id/headlines_mark_as_read"
android:icon="@drawable/ic_menu_tick" android:icon="@drawable/ic_menu_tick"
android:showAsAction="" android:showAsAction=""
android:title="@string/headlines_mark_as_read"/> android:title="@string/headlines_mark_as_read"/>
<item <item
android:id="@+id/headlines_select" android:id="@+id/headlines_select"
android:icon="@drawable/ic_menu_database" android:icon="@drawable/ic_menu_database"
android:showAsAction="ifRoom" android:showAsAction=""
android:title="@string/headlines_select"/> android:title="@string/headlines_select"/>
<item <!--
<item
android:id="@+id/close_feed" android:id="@+id/close_feed"
android:icon="@android:drawable/ic_menu_close_clear_cancel" android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:showAsAction="" android:showAsAction=""
android:title="@string/close_feed"/> android:title="@string/close_feed"/>
-->
</group> </group>
<group android:id="@+id/menu_group_headlines_selection" > <group android:id="@+id/menu_group_headlines_selection" >
<item <item
android:id="@+id/selection_toggle_unread" android:id="@+id/selection_toggle_unread"
android:icon="@drawable/ic_menu_tick" android:icon="@drawable/ic_menu_tick"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:title="@string/selection_toggle_unread"/> android:title="@string/selection_toggle_unread"/>
<item <item
android:id="@+id/selection_toggle_marked" android:id="@+id/selection_toggle_marked"
android:icon="@drawable/ic_menu_marked" android:icon="@drawable/ic_menu_marked"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:title="@string/selection_toggle_marked"/> android:title="@string/selection_toggle_marked"/>
<item <item
android:id="@+id/selection_toggle_published" android:id="@+id/selection_toggle_published"
android:icon="@drawable/ic_menu_rss" android:icon="@drawable/ic_menu_rss"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:title="@string/selection_toggle_published"/> android:title="@string/selection_toggle_published"/>
<item <item
android:id="@+id/selection_select_none" android:id="@+id/selection_select_none"
android:icon="@android:drawable/ic_menu_close_clear_cancel" android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:showAsAction="" android:showAsAction=""
android:title="@string/selection_select_none"/> android:title="@string/selection_select_none"/>
</group> </group>
<group android:id="@+id/menu_group_article" > <group android:id="@+id/menu_group_article" >
<item <item
android:id="@+id/toggle_marked" android:id="@+id/toggle_marked"
android:icon="@drawable/ic_menu_marked" android:icon="@drawable/ic_menu_marked"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:title="@string/article_toggle_marked"/> android:title="@string/article_toggle_marked"/>
<item <item
android:id="@+id/toggle_published" android:id="@+id/toggle_published"
android:icon="@drawable/ic_menu_rss" android:icon="@drawable/ic_menu_rss"
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:title="@string/article_toggle_published"/> android:title="@string/article_toggle_published"/>
<item <item
android:id="@+id/share_article" android:id="@+id/share_article"
android:actionProviderClass="android.widget.ShareActionProvider"
android:icon="@android:drawable/ic_menu_share" android:icon="@android:drawable/ic_menu_share"
android:showAsAction="" android:showAsAction=""
android:actionProviderClass="android.widget.ShareActionProvider"
android:title="@string/share_article"/> android:title="@string/share_article"/>
<item <item
android:id="@+id/set_unread" android:id="@+id/set_unread"
android:icon="@android:drawable/ic_menu_recent_history" android:icon="@android:drawable/ic_menu_recent_history"
android:showAsAction="" android:showAsAction=""
android:title="@string/article_set_unread"/> android:title="@string/article_set_unread"/>
<item <item
android:id="@+id/catchup_above" android:id="@+id/catchup_above"
android:icon="@drawable/ic_menu_tick" android:icon="@drawable/ic_menu_tick"
android:title="@string/article_mark_read_above"/> android:title="@string/article_mark_read_above"/>
<item <item
android:id="@+id/set_labels" android:id="@+id/set_labels"
android:icon="@drawable/ic_menu_marked" android:icon="@drawable/ic_menu_marked"
android:title="@string/article_set_labels"/> android:title="@string/article_set_labels"/>
<item <item
android:id="@+id/article_set_note" android:id="@+id/article_set_note"
android:showAsAction="" android:showAsAction=""
android:title="@string/article_set_note"/> android:title="@string/article_set_note"/>
<item <!--
<item
android:id="@+id/close_article" android:id="@+id/close_article"
android:icon="@android:drawable/ic_menu_close_clear_cancel" android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:showAsAction="" android:showAsAction=""
android:title="@string/close_article"/> android:title="@string/close_article"/>
-->
</group> </group>
<item
android:id="@+id/donate"
android:showAsAction=""
android:title="@string/donate"/>
</group> </group>
<item <item
@ -156,7 +144,6 @@
android:title="@string/preferences"/> android:title="@string/preferences"/>
<group android:id="@+id/menu_group_logged_out" > <group android:id="@+id/menu_group_logged_out" >
<item <item
android:id="@+id/login" android:id="@+id/login"
android:icon="@android:drawable/ic_menu_rotate" android:icon="@android:drawable/ic_menu_rotate"

View File

@ -23,11 +23,11 @@
android:title="@string/menu_all_feeds"/> android:title="@string/menu_all_feeds"/>
</group> </group>
<group android:id="@+id/menu_group_headlines" > <group android:id="@+id/menu_group_headlines" >
<item <!-- <item
android:id="@+id/go_online" android:id="@+id/go_online"
android:icon="@drawable/ic_menu_cloud" android:icon="@drawable/ic_menu_cloud"
android:title="@string/go_online" android:title="@string/go_online"
android:visible="false"/> android:visible="false"/> -->
<item <item
android:id="@+id/search" android:id="@+id/search"
android:actionViewClass="android.widget.SearchView" android:actionViewClass="android.widget.SearchView"
@ -41,13 +41,13 @@
<item <item
android:id="@+id/headlines_select" android:id="@+id/headlines_select"
android:icon="@drawable/ic_menu_database" android:icon="@drawable/ic_menu_database"
android:showAsAction="ifRoom" android:showAsAction=""
android:title="@string/headlines_select"/> android:title="@string/headlines_select"/>
<item <!-- <item
android:id="@+id/close_feed" android:id="@+id/close_feed"
android:icon="@android:drawable/ic_menu_close_clear_cancel" android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:showAsAction="" android:showAsAction=""
android:title="@string/close_feed"/> android:title="@string/close_feed"/> -->
</group> </group>
<group android:id="@+id/menu_group_headlines_selection" > <group android:id="@+id/menu_group_headlines_selection" >
<item <item
@ -97,11 +97,11 @@
android:id="@+id/catchup_above" android:id="@+id/catchup_above"
android:icon="@drawable/ic_menu_tick" android:icon="@drawable/ic_menu_tick"
android:title="@string/article_mark_read_above"/> android:title="@string/article_mark_read_above"/>
<item <!-- <item
android:id="@+id/close_article" android:id="@+id/close_article"
android:icon="@android:drawable/ic_menu_close_clear_cancel" android:icon="@android:drawable/ic_menu_close_clear_cancel"
android:showAsAction="" android:showAsAction=""
android:title="@string/close_article"/> android:title="@string/close_article"/> -->
</group> </group>
<item <item

View File

@ -28,7 +28,7 @@
<item name="feedlistBackground">@drawable/ics_divider_vertical</item> <item name="feedlistBackground">@drawable/ics_divider_vertical</item>
<item name="unreadCounterColor">#303030</item> <item name="unreadCounterColor">#303030</item>
<item name="headlinesBackground">@drawable/headlines_dark</item> <item name="headlinesBackground">@drawable/headlines_dark</item>
<item name="headlinesBackgroundSolid">@android:color/transparent</item> <item name="headlinesBackgroundSolid">@drawable/headlines_dark</item>
<item name="articleBackground">@android:color/black</item> <item name="articleBackground">@android:color/black</item>
<item name="headlineSelectedBackground">@drawable/headline_row_selected_dark</item> <item name="headlineSelectedBackground">@drawable/headline_row_selected_dark</item>
<item name="headlineSelectedBackgroundSolid">@color/ics_cyan</item> <item name="headlineSelectedBackgroundSolid">@color/ics_cyan</item>

View File

@ -17,5 +17,13 @@
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>
</string-array> </string-array>
<string-array name="pref_view_mode_names">
<item>@string/category_browse_headlines</item>
<item>@string/category_browse_articles</item>
</string-array>
<string-array name="pref_view_mode_values">
<item>HEADLINES</item>
<item>ARTICLES</item>
</string-array>
</resources> </resources>

View File

@ -24,7 +24,7 @@
<string name="loading_message">Loading, please wait…</string> <string name="loading_message">Loading, please wait…</string>
<string name="menu_unread_feeds">Show unread feeds</string> <string name="menu_unread_feeds">Show unread feeds</string>
<string name="menu_all_feeds">Show all feeds</string> <string name="menu_all_feeds">Show all feeds</string>
<string name="update_feeds">Refresh feeds</string> <string name="update_feeds">Refresh</string>
<string name="share_article">Share article</string> <string name="share_article">Share article</string>
<string name="catchup">Mark read</string> <string name="catchup">Mark read</string>
<string name="sort_feeds_by_unread">Sort feeds by unread count</string> <string name="sort_feeds_by_unread">Sort feeds by unread count</string>
@ -128,6 +128,11 @@
<string name="notify_article_published">Article published</string> <string name="notify_article_published">Article published</string>
<string name="notify_article_unpublished">Article unpublished</string> <string name="notify_article_unpublished">Article unpublished</string>
<string name="notify_article_note_set">Article note saved</string> <string name="notify_article_note_set">Article note saved</string>
<string name="force_small_tablet_ui">Force compact interface</string> <string name="update_headlines">Refresh</string>
<string name="prefs_for_tablets">Tablets</string> <string name="attachment_share">Share</string>
<string name="error_network_unavailable">Error: network unavailable</string>
<string name="category_browse_headlines">Browse headlines</string>
<string name="pref_default_view_mode">Default feed view</string>
<string name="pref_default_view_mode_long">Which feed view to open by default on smartphones</string>
<string name="donate_thanks">Donation found, thank you for support!</string>
</resources> </resources>

View File

@ -2,18 +2,21 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<PreferenceCategory android:title="@string/connection" > <PreferenceCategory android:title="@string/connection" >
<EditTextPreference <EditTextPreference
android:key="login" android:key="login"
android:singleLine="true" android:singleLine="true"
android:summary="@string/login_summary" android:summary="@string/login_summary"
android:title="@string/login" > android:title="@string/login" >
</EditTextPreference> </EditTextPreference>
<EditTextPreference <EditTextPreference
android:key="password" android:key="password"
android:password="true" android:password="true"
android:singleLine="true" android:singleLine="true"
android:title="@string/password" > android:title="@string/password" >
</EditTextPreference> </EditTextPreference>
<EditTextPreference <EditTextPreference
android:hint="@string/default_url" android:hint="@string/default_url"
android:inputType="textUri" android:inputType="textUri"
@ -28,13 +31,16 @@
android:key="ssl_trust_any" android:key="ssl_trust_any"
android:title="@string/ssl_trust_any" /> android:title="@string/ssl_trust_any" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/http_authentication" > <PreferenceCategory android:title="@string/http_authentication" >
<EditTextPreference <EditTextPreference
android:key="http_login" android:key="http_login"
android:singleLine="true" android:singleLine="true"
android:summary="@string/http_login_summary" android:summary="@string/http_login_summary"
android:title="@string/login" > android:title="@string/login" >
</EditTextPreference> </EditTextPreference>
<EditTextPreference <EditTextPreference
android:key="http_password" android:key="http_password"
android:password="true" android:password="true"
@ -42,9 +48,9 @@
android:title="@string/password" > android:title="@string/password" >
</EditTextPreference> </EditTextPreference>
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:key="category_look_and_feel" <PreferenceCategory android:key="category_look_and_feel" android:title="@string/look_and_feel" >
android:title="@string/look_and_feel" >
<ListPreference <ListPreference
android:defaultValue="THEME_DARK" android:defaultValue="THEME_DARK"
android:entries="@array/pref_theme_names" android:entries="@array/pref_theme_names"
@ -52,6 +58,7 @@
android:key="theme" android:key="theme"
android:summary="@string/pref_theme_long" android:summary="@string/pref_theme_long"
android:title="@string/pref_theme" /> android:title="@string/pref_theme" />
<ListPreference <ListPreference
android:defaultValue="0" android:defaultValue="0"
android:entries="@array/pref_font_size_names" android:entries="@array/pref_font_size_names"
@ -63,50 +70,56 @@
android:defaultValue="false" android:defaultValue="false"
android:key="sort_feeds_by_unread" android:key="sort_feeds_by_unread"
android:title="@string/sort_feeds_by_unread" /> android:title="@string/sort_feeds_by_unread" />
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="download_feed_icons" android:key="download_feed_icons"
android:title="@string/download_feed_icons" /> android:title="@string/download_feed_icons" />
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="enable_cats" android:key="enable_cats"
android:title="@string/enable_cats" /> android:title="@string/enable_cats" />
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:dependency="enable_cats"
android:key="browse_cats_like_feeds" android:key="browse_cats_like_feeds"
android:dependency="enable_cats"
android:summary="@string/browse_cats_like_feeds_summary" android:summary="@string/browse_cats_like_feeds_summary"
android:title="@string/browse_cats_like_feeds" /> android:title="@string/browse_cats_like_feeds" />
<CheckBoxPreference
<!-- <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="combined_mode" android:key="combined_mode"
android:summary="@string/combined_mode_summary" android:summary="@string/combined_mode_summary"
android:title="@string/combined_mode" /> android:title="@string/combined_mode" /> -->
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="true" android:defaultValue="true"
android:key="justify_article_text" android:key="justify_article_text"
android:title="@string/justify_article_text" /> android:title="@string/justify_article_text" />
<ListPreference
android:defaultValue="HEADLINES"
android:entries="@array/pref_view_mode_names"
android:entryValues="@array/pref_view_mode_values"
android:key="default_view_mode"
android:summary="@string/pref_default_view_mode_long"
android:title="@string/pref_default_view_mode" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:key="category_tablets"
android:title="@string/prefs_for_tablets" >
<CheckBoxPreference
android:defaultValue="false"
android:key="tablet_article_swipe"
android:title="@string/tablet_article_swipe" />
<CheckBoxPreference
android:defaultValue="false"
android:key="force_small_tablet_ui"
android:title="@string/force_small_tablet_ui" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/offline_mode" > <PreferenceCategory android:title="@string/offline_mode" >
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="offline_image_cache_enabled" android:key="offline_image_cache_enabled"
android:summary="@string/offline_image_cache_enabled_summary" android:summary="@string/offline_image_cache_enabled_summary"
android:title="@string/offline_image_cache_enabled" /> android:title="@string/offline_image_cache_enabled" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/debugging" > <PreferenceCategory android:title="@string/debugging" >
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="false" android:defaultValue="false"
android:key="transport_debugging" android:key="transport_debugging"

View File

@ -4,29 +4,31 @@ import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.MalformedURLException; import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.nio.CharBuffer;
import java.security.cert.CertificateException;
import java.util.HashMap; import java.util.HashMap;
import org.apache.http.HttpHost; import javax.net.ssl.HttpsURLConnection;
import org.apache.http.HttpResponse; import javax.net.ssl.SSLContext;
import org.apache.http.auth.AuthScope; import javax.net.ssl.TrustManager;
import org.apache.http.auth.UsernamePasswordCredentials; import javax.net.ssl.X509TrustManager;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpPost; import org.apache.http.util.CharArrayBuffer;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.scheme.Scheme; import java.security.cert.X509Certificate;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.fox.ttrss.util.EasySSLSocketFactory;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.net.http.AndroidHttpClient; import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import com.google.gson.Gson; import com.google.gson.Gson;
@ -38,7 +40,8 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
private final String TAG = this.getClass().getSimpleName(); private final String TAG = this.getClass().getSimpleName();
public enum ApiError { NO_ERROR, HTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, 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, INCORRECT_USAGE }; HTTP_SERVER_ERROR, HTTP_OTHER_ERROR, SSL_REJECTED, PARSE_ERROR, IO_ERROR, OTHER_ERROR, API_DISABLED,
API_UNKNOWN, LOGIN_FAILED, INVALID_URL, INCORRECT_USAGE, NETWORK_UNAVAILABLE };
public static final int API_STATUS_OK = 0; public static final int API_STATUS_OK = 0;
public static final int API_STATUS_ERR = 1; public static final int API_STATUS_ERR = 1;
@ -46,8 +49,10 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
private String m_api; private String m_api;
private boolean m_trustAny = false; private boolean m_trustAny = false;
private boolean m_transportDebugging = false; private boolean m_transportDebugging = false;
protected int m_httpStatusCode = 0; protected int m_responseCode = 0;
protected String m_responseMessage;
protected int m_apiStatusCode = 0; protected int m_apiStatusCode = 0;
protected boolean m_canUseProgress = false;
protected Context m_context; protected Context m_context;
private SharedPreferences m_prefs; private SharedPreferences m_prefs;
@ -97,6 +102,8 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
return R.string.error_invalid_api_url; return R.string.error_invalid_api_url;
case INCORRECT_USAGE: case INCORRECT_USAGE:
return R.string.error_api_incorrect_usage; return R.string.error_api_incorrect_usage;
case NETWORK_UNAVAILABLE:
return R.string.error_network_unavailable;
default: default:
Log.d(TAG, "getErrorMessage: unknown error code=" + m_lastError); Log.d(TAG, "getErrorMessage: unknown error code=" + m_lastError);
return R.string.error_unknown; return R.string.error_unknown;
@ -106,37 +113,42 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
@Override @Override
protected JsonElement doInBackground(HashMap<String, String>... params) { protected JsonElement doInBackground(HashMap<String, String>... params) {
if (!isNetworkAvailable()) {
m_lastError = ApiError.NETWORK_UNAVAILABLE;
return null;
}
Gson gson = new Gson(); Gson gson = new Gson();
String requestStr = gson.toJson(new HashMap<String,String>(params[0])); String requestStr = gson.toJson(new HashMap<String,String>(params[0]));
byte[] postData = null;
try {
postData = requestStr.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
m_lastError = ApiError.OTHER_ERROR;
e.printStackTrace();
return null;
}
disableConnectionReuseIfNecessary();
if (m_transportDebugging) Log.d(TAG, ">>> (" + requestStr + ") " + m_api); if (m_transportDebugging) Log.d(TAG, ">>> (" + requestStr + ") " + m_api);
AndroidHttpClient client = AndroidHttpClient.newInstance("Tiny Tiny RSS"); if (m_trustAny) trustAllHosts();
if (m_trustAny) { URL url;
client.getConnectionManager().getSchemeRegistry().register(new Scheme("https", new EasySSLSocketFactory(), 443));
try {
url = new URL(m_api + "/api/");
} catch (Exception e) {
m_lastError = ApiError.INVALID_URL;
e.printStackTrace();
return null;
} }
try { try {
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
HttpPost httpPost;
try {
httpPost = new HttpPost(m_api + "/api/");
} catch (IllegalArgumentException e) {
m_lastError = ApiError.INVALID_URL;
e.printStackTrace();
client.close();
return null;
} catch (Exception e) {
m_lastError = ApiError.OTHER_ERROR;
e.printStackTrace();
client.close();
return null;
}
HttpContext context = null;
String httpLogin = m_prefs.getString("http_login", "").trim(); String httpLogin = m_prefs.getString("http_login", "").trim();
String httpPassword = m_prefs.getString("http_password", "").trim(); String httpPassword = m_prefs.getString("http_password", "").trim();
@ -144,56 +156,51 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
if (httpLogin.length() > 0) { if (httpLogin.length() > 0) {
if (m_transportDebugging) Log.d(TAG, "Using HTTP Basic authentication."); if (m_transportDebugging) Log.d(TAG, "Using HTTP Basic authentication.");
URL targetUrl; conn.setRequestProperty("Authorization", "Basic " +
try { Base64.encode((httpLogin + ":" + httpPassword).getBytes("UTF-8"), Base64.NO_WRAP));
targetUrl = new URL(m_api);
} catch (MalformedURLException e) {
m_lastError = ApiError.INVALID_URL;
e.printStackTrace();
client.close();
return null;
}
HttpHost targetHost = new HttpHost(targetUrl.getHost(), targetUrl.getPort(), targetUrl.getProtocol());
CredentialsProvider cp = new BasicCredentialsProvider();
context = new BasicHttpContext();
cp.setCredentials(
new AuthScope(targetHost.getHostName(), targetHost.getPort()),
new UsernamePasswordCredentials(httpLogin, httpPassword));
context.setAttribute(ClientContext.CREDS_PROVIDER, cp);
} }
httpPost.setEntity(new StringEntity(requestStr, "utf-8")); conn.setDoInput(true);
HttpResponse execute = client.execute(httpPost, context); conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Length", Integer.toString(postData.length));
m_httpStatusCode = execute.getStatusLine().getStatusCode(); OutputStream out = conn.getOutputStream();
out.write(postData);
out.close();
switch (m_httpStatusCode) { m_responseCode = conn.getResponseCode();
case 200: m_responseMessage = conn.getResponseMessage();
InputStream content = execute.getEntity().getContent();
BufferedReader buffer = new BufferedReader( switch (m_responseCode) {
new InputStreamReader(content), 8192); case HttpURLConnection.HTTP_OK:
StringBuffer response = new StringBuffer();
InputStreamReader in = new InputStreamReader(conn.getInputStream(), "UTF-8");
char[] buf = new char[256];
int read = 0;
int total = 0;
String s = ""; int contentLength = conn.getHeaderFieldInt("Api-Content-Length", -1);
String response = "";
while ((s = buffer.readLine()) != null) { m_canUseProgress = (contentLength != -1);
response += s;
while ((read = in.read(buf)) >= 0) {
response.append(buf, 0, read);
total += read;
publishProgress(Integer.valueOf(total), Integer.valueOf(contentLength));
} }
if (m_transportDebugging) Log.d(TAG, "<<< " + response); if (m_transportDebugging) Log.d(TAG, "<<< " + response);
JsonParser parser = new JsonParser(); JsonParser parser = new JsonParser();
JsonElement result = parser.parse(response); JsonElement result = parser.parse(response.toString());
JsonObject resultObj = result.getAsJsonObject(); JsonObject resultObj = result.getAsJsonObject();
m_apiStatusCode = resultObj.get("status").getAsInt(); m_apiStatusCode = resultObj.get("status").getAsInt();
client.close(); conn.disconnect();
switch (m_apiStatusCode) { switch (m_apiStatusCode) {
case API_STATUS_OK: case API_STATUS_OK:
@ -217,16 +224,16 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
} }
return null; return null;
case 401: case HttpURLConnection.HTTP_UNAUTHORIZED:
m_lastError = ApiError.HTTP_UNAUTHORIZED; m_lastError = ApiError.HTTP_UNAUTHORIZED;
break; break;
case 403: case HttpURLConnection.HTTP_FORBIDDEN:
m_lastError = ApiError.HTTP_FORBIDDEN; m_lastError = ApiError.HTTP_FORBIDDEN;
break; break;
case 404: case HttpURLConnection.HTTP_NOT_FOUND:
m_lastError = ApiError.HTTP_NOT_FOUND; m_lastError = ApiError.HTTP_NOT_FOUND;
break; break;
case 500: case HttpURLConnection.HTTP_INTERNAL_ERROR:
m_lastError = ApiError.HTTP_SERVER_ERROR; m_lastError = ApiError.HTTP_SERVER_ERROR;
break; break;
default: default:
@ -234,8 +241,8 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
break; break;
} }
client.close(); conn.disconnect();
return null; return null;
} catch (javax.net.ssl.SSLPeerUnverifiedException e) { } catch (javax.net.ssl.SSLPeerUnverifiedException e) {
m_lastError = ApiError.SSL_REJECTED; m_lastError = ApiError.SSL_REJECTED;
e.printStackTrace(); e.printStackTrace();
@ -250,7 +257,64 @@ public class ApiRequest extends AsyncTask<HashMap<String,String>, Integer, JsonE
e.printStackTrace(); e.printStackTrace();
} }
client.close();
return null; return null;
} }
private static void trustAllHosts() {
X509TrustManager easyTrustManager = new X509TrustManager() {
public void checkClientTrusted(
X509Certificate[] chain,
String authType) throws CertificateException {
// Oh, I am easy!
}
public void checkServerTrusted(
X509Certificate[] chain,
String authType) throws CertificateException {
// Oh, I am easy!
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] {easyTrustManager};
// Install the all-trusting trust manager
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
}
@SuppressWarnings("deprecation")
private static void disableConnectionReuseIfNecessary() {
// HTTP connection reuse which was buggy pre-froyo
if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
System.setProperty("http.keepAlive", "false");
}
}
protected boolean isNetworkAvailable() {
ConnectivityManager cm = (ConnectivityManager)
m_context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
// if no network is available networkInfo will be null
// otherwise check if we are connected
if (networkInfo != null && networkInfo.isConnected()) {
return true;
}
return false;
}
} }

View File

@ -17,34 +17,38 @@ import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Paint;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.text.Html; import android.text.Html;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebSettings.LayoutAlgorithm; import android.webkit.WebSettings.LayoutAlgorithm;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button; import android.widget.Button;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
public class ArticleFragment extends Fragment { public class ArticleFragment extends Fragment {
@SuppressWarnings("unused")
private final String TAG = this.getClass().getSimpleName(); private final String TAG = this.getClass().getSimpleName();
private SharedPreferences m_prefs; private SharedPreferences m_prefs;
private Article m_article; private Article m_article;
private OnlineServices m_onlineServices; private OnlineActivity m_activity;
//private Article m_nextArticle; //private Article m_nextArticle;
//private Article m_prevArticle; //private Article m_prevArticle;
@ -60,6 +64,28 @@ public class ArticleFragment extends Fragment {
private View.OnTouchListener m_gestureListener; private View.OnTouchListener m_gestureListener;
@Override
public boolean onContextItemSelected(MenuItem item) {
/* AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
.getMenuInfo(); */
switch (item.getItemId()) {
case R.id.article_link_share:
if (true) {
((OnlineActivity) getActivity()).shareArticle(m_article);
}
return true;
case R.id.article_link_copy:
if (true) {
((OnlineActivity) getActivity()).copyToClipboard(m_article.link);
}
return true;
default:
Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
return super.onContextItemSelected(item);
}
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) { ContextMenuInfo menuInfo) {
@ -75,6 +101,8 @@ public class ArticleFragment extends Fragment {
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
m_activity.setProgressBarVisibility(true);
if (savedInstanceState != null) { if (savedInstanceState != null) {
m_article = savedInstanceState.getParcelable("article"); m_article = savedInstanceState.getParcelable("article");
} }
@ -94,14 +122,37 @@ public class ArticleFragment extends Fragment {
else else
titleStr = m_article.title; titleStr = m_article.title;
title.setMovementMethod(LinkMovementMethod.getInstance()); title.setText(titleStr);
title.setText(Html.fromHtml("<a href=\""+m_article.link.trim().replace("\"", "\\\"")+"\">" + titleStr + "</a>")); title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
title.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse(m_article.link.trim()));
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
m_activity.toast(R.string.error_other_error);
}
}
});
registerForContextMenu(title); registerForContextMenu(title);
} }
WebView web = (WebView)view.findViewById(R.id.content); WebView web = (WebView)view.findViewById(R.id.content);
if (web != null) { if (web != null) {
web.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int progress) {
m_activity.setProgress(Math.round(((float)progress / 100f) * 10000));
if (progress == 100) {
m_activity.setProgressBarVisibility(false);
}
}
});
String content; String content;
String cssOverride = ""; String cssOverride = "";
@ -186,9 +237,10 @@ public class ArticleFragment extends Fragment {
try { try {
URL url = new URL(a.content_url.trim()); URL url = new URL(a.content_url.trim());
String strUrl = url.toString().trim();
if (a.content_type.indexOf("image") != -1) { if (a.content_type.indexOf("image") != -1 && !articleContent.contains(strUrl)) {
content += "<br/><img src=\"" + url.toString().trim().replace("\"", "\\\"") + "\">"; content += "<p><img src=\"" + strUrl.replace("\"", "\\\"") + "\"></p>";
} }
spinnerArray.add(a); spinnerArray.add(a);
@ -226,11 +278,29 @@ public class ArticleFragment extends Fragment {
Attachment attachment = (Attachment) spinner.getSelectedItem(); Attachment attachment = (Attachment) spinner.getSelectedItem();
if (attachment != null) { if (attachment != null) {
m_onlineServices.copyToClipboard(attachment.content_url); m_activity.copyToClipboard(attachment.content_url);
} }
} }
}); });
Button attachmentsShare = (Button) view.findViewById(R.id.attachment_share);
if (!m_activity.isPortrait()) {
attachmentsShare.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Attachment attachment = (Attachment) spinner.getSelectedItem();
if (attachment != null) {
m_activity.shareText(attachment.content_url);
}
}
});
} else {
attachmentsShare.setVisibility(View.GONE);
}
} else { } else {
view.findViewById(R.id.attachments_holder).setVisibility(View.GONE); view.findViewById(R.id.attachments_holder).setVisibility(View.GONE);
} }
@ -243,7 +313,7 @@ public class ArticleFragment extends Fragment {
e.printStackTrace(); e.printStackTrace();
} }
if (m_onlineServices.isSmallScreen()) if (m_activity.isSmallScreen())
web.setOnTouchListener(m_gestureListener); web.setOnTouchListener(m_gestureListener);
} }
@ -296,7 +366,7 @@ public class ArticleFragment extends Fragment {
super.onAttach(activity); super.onAttach(activity);
m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
m_onlineServices = (OnlineServices)activity; m_activity = (OnlineActivity)activity;
//m_article = m_onlineServices.getSelectedArticle(); //m_article = m_onlineServices.getSelectedArticle();
} }

View File

@ -1,6 +1,11 @@
package org.fox.ttrss; package org.fox.ttrss;
import java.util.HashMap;
import org.fox.ttrss.types.Article; import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList;
import org.fox.ttrss.types.Feed;
import org.fox.ttrss.util.HeadlinesRequest;
import android.app.Activity; import android.app.Activity;
import android.os.Bundle; import android.os.Bundle;
@ -8,17 +13,23 @@ import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.gson.JsonElement;
public class ArticlePager extends Fragment { public class ArticlePager extends Fragment {
private final String TAG = "ArticlePager"; private final String TAG = "ArticlePager";
private PagerAdapter m_adapter; private PagerAdapter m_adapter;
private OnlineServices m_onlineServices; private HeadlinesEventListener m_listener;
private HeadlinesFragment m_hf;
private Article m_article; private Article m_article;
private ArticleList m_articles = GlobalState.getInstance().m_loadedArticles;
private OnlineActivity m_activity;
private String m_searchQuery = "";
private Feed m_feed;
private class PagerAdapter extends FragmentStatePagerAdapter { private class PagerAdapter extends FragmentStatePagerAdapter {
@ -28,7 +39,7 @@ public class ArticlePager extends Fragment {
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
Article article = m_hf.getArticleAtPosition(position); Article article = m_articles.get(position);
if (article != null) { if (article != null) {
ArticleFragment af = new ArticleFragment(article); ArticleFragment af = new ArticleFragment(article);
@ -39,7 +50,7 @@ public class ArticlePager extends Fragment {
@Override @Override
public int getCount() { public int getCount() {
return m_hf.getAllArticles().size(); return m_articles.size();
} }
} }
@ -48,21 +59,33 @@ public class ArticlePager extends Fragment {
super(); super();
} }
public ArticlePager(Article article) { public ArticlePager(Article article, Feed feed) {
super(); super();
m_article = article; m_article = article;
m_feed = feed;
}
public void setSearchQuery(String searchQuery) {
m_searchQuery = searchQuery;
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.article_pager, container, false); View view = inflater.inflate(R.layout.article_pager, container, false);
if (savedInstanceState != null) {
m_article = savedInstanceState.getParcelable("article");
m_feed = savedInstanceState.getParcelable("feed");
}
m_adapter = new PagerAdapter(getActivity().getSupportFragmentManager()); m_adapter = new PagerAdapter(getActivity().getSupportFragmentManager());
ViewPager pager = (ViewPager) view.findViewById(R.id.article_pager); ViewPager pager = (ViewPager) view.findViewById(R.id.article_pager);
int position = m_hf.getArticlePosition(m_article); int position = m_articles.indexOf(m_article);
m_listener.onArticleSelected(m_article, false);
pager.setAdapter(m_adapter); pager.setAdapter(m_adapter);
pager.setCurrentItem(position); pager.setCurrentItem(position);
@ -78,20 +101,23 @@ public class ArticlePager extends Fragment {
@Override @Override
public void onPageSelected(int position) { public void onPageSelected(int position) {
Article article = m_hf.getArticleAtPosition(position); Article article = m_articles.get(position);
if (article != null) { if (article != null) {
if (article.unread) { m_article = article;
/* if (article.unread) {
article.unread = false; article.unread = false;
m_onlineServices.saveArticleUnread(article); m_activity.saveArticleUnread(article);
} } */
m_onlineServices.setSelectedArticle(article);
m_listener.onArticleSelected(article, false);
//Log.d(TAG, "Page #" + position + "/" + m_adapter.getCount()); //Log.d(TAG, "Page #" + position + "/" + m_adapter.getCount());
if (position == m_adapter.getCount() - 5) { if (m_activity.isSmallScreen() && position == m_adapter.getCount() - 5) {
m_hf.refresh(true); Log.d(TAG, "loading more articles...");
m_adapter.notifyDataSetChanged(); refresh(true);
} }
} }
} }
@ -100,12 +126,134 @@ public class ArticlePager extends Fragment {
return view; return view;
} }
@SuppressWarnings({ "unchecked", "serial" })
private void refresh(boolean append) {
m_activity.setLoadingStatus(R.string.blank, true);
m_activity.setProgressBarVisibility(true);
if (!m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
append = false;
}
HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext(), m_activity) {
@Override
protected void onProgressUpdate(Integer... progress) {
m_activity.setProgress(progress[0] / progress[1] * 10000);
}
@Override
protected void onPostExecute(JsonElement result) {
m_activity.setProgressBarVisibility(false);
super.onPostExecute(result);
if (result != null) {
m_adapter.notifyDataSetChanged();
if (m_article.id == 0 || m_articles.indexOf(m_article) == -1) {
if (m_articles.size() > 0) {
m_article = m_articles.get(0);
m_listener.onArticleSelected(m_article, false);
}
}
} else {
if (m_lastError == ApiError.LOGIN_FAILED) {
m_activity.login();
} else {
m_activity.toast(getErrorMessage());
//setLoadingStatus(getErrorMessage(), false);
}
}
}
};
final Feed feed = m_feed;
final String sessionId = m_activity.getSessionId();
final boolean showUnread = m_activity.getUnreadArticlesOnly();
int skip = 0;
if (append) {
for (Article a : m_articles) {
if (a.unread) ++skip;
}
if (skip == 0) skip = m_articles.size();
}
final int fskip = skip;
req.setOffset(skip);
HashMap<String,String> map = new HashMap<String,String>() {
{
put("op", "getHeadlines");
put("sid", sessionId);
put("feed_id", String.valueOf(feed.id));
put("show_content", "true");
put("include_attachments", "true");
put("limit", String.valueOf(HeadlinesFragment.HEADLINES_REQUEST_SIZE));
put("offset", String.valueOf(0));
put("view_mode", showUnread ? "adaptive" : "all_articles");
put("skip", String.valueOf(fskip));
put("include_nested", "true");
if (feed.is_cat) put("is_cat", "true");
if (m_searchQuery != null && m_searchQuery.length() != 0) {
put("search", m_searchQuery);
put("search_mode", "");
put("match_on", "both");
}
}
};
req.execute(map);
}
@Override
public void onSaveInstanceState(Bundle out) {
super.onSaveInstanceState(out);
out.putParcelable("article", m_article);
out.putParcelable("feed", m_feed);
}
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
m_hf = (HeadlinesFragment) getActivity().getSupportFragmentManager().findFragmentByTag(MainActivity.FRAG_HEADLINES); m_listener = (HeadlinesEventListener)activity;
m_onlineServices = (OnlineServices)activity; m_activity = (OnlineActivity)activity;
} }
@Override
public void onResume() {
super.onResume();
if (m_articles.size() == 0 || !m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
refresh(false);
GlobalState.getInstance().m_activeFeed = m_feed;
}
m_activity.initMenu();
}
public Article getSelectedArticle() {
return m_article;
}
public void setActiveArticle(Article article) {
if (m_article != article) {
m_article = article;
int position = m_articles.indexOf(m_article);
ViewPager pager = (ViewPager) getView().findViewById(R.id.article_pager);
pager.setCurrentItem(position);
}
}
} }

View File

@ -2,15 +2,15 @@ package org.fox.ttrss;
import org.fox.ttrss.util.DatabaseHelper; import org.fox.ttrss.util.DatabaseHelper;
import android.content.SharedPreferences; import android.annotation.SuppressLint;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentActivity;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.FloatMath; import android.util.FloatMath;
import android.util.Log; import android.util.Log;
import android.view.Display; import android.view.Display;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
public class CommonActivity extends FragmentActivity { public class CommonActivity extends FragmentActivity {
@ -21,20 +21,43 @@ public class CommonActivity extends FragmentActivity {
public final static String FRAG_FEEDS = "feeds"; public final static String FRAG_FEEDS = "feeds";
public final static String FRAG_CATS = "cats"; public final static String FRAG_CATS = "cats";
private SharedPreferences m_prefs;
private SQLiteDatabase m_readableDb; private SQLiteDatabase m_readableDb;
private SQLiteDatabase m_writableDb; private SQLiteDatabase m_writableDb;
private boolean m_smallScreenMode = true; private boolean m_smallScreenMode = true;
private boolean m_compatMode = false; private boolean m_compatMode = false;
private boolean m_smallTablet = false;
protected void setSmallScreen(boolean smallScreen) { protected void setSmallScreen(boolean smallScreen) {
Log.d(TAG, "m_smallScreenMode=" + smallScreen); Log.d(TAG, "m_smallScreenMode=" + smallScreen);
m_smallScreenMode = smallScreen; m_smallScreenMode = smallScreen;
} }
public boolean getUnreadArticlesOnly() {
return GlobalState.getInstance().m_unreadArticlesOnly;
}
public boolean getUnreadOnly() {
return GlobalState.getInstance().m_unreadOnly;
}
public void setUnreadOnly(boolean unread) {
GlobalState.getInstance().m_unreadOnly = unread;
}
public void setUnreadArticlesOnly(boolean unread) {
GlobalState.getInstance().m_unreadArticlesOnly = unread;
}
public void setLoadingStatus(int status, boolean showProgress) {
TextView tv = (TextView) findViewById(R.id.loading_message);
if (tv != null) {
tv.setText(status);
}
setProgressBarIndeterminateVisibility(showProgress);
}
public void toast(int msgId) { public void toast(int msgId) {
Toast toast = Toast.makeText(CommonActivity.this, msgId, Toast.LENGTH_SHORT); Toast toast = Toast.makeText(CommonActivity.this, msgId, Toast.LENGTH_SHORT);
toast.show(); toast.show();
@ -45,23 +68,6 @@ public class CommonActivity extends FragmentActivity {
toast.show(); toast.show();
} }
protected void detectSmallTablet() {
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
float inHeight = displayMetrics.heightPixels / displayMetrics.ydpi;
float inWidth = displayMetrics.widthPixels / displayMetrics.xdpi;
float inDiag = FloatMath.sqrt(inHeight * inHeight + inWidth * inWidth);
if (inDiag < 9 || m_prefs.getBoolean("force_small_tablet_ui", false)) {
m_smallTablet = true;
}
Log.d(TAG, "m_smallTabletMode=" + m_smallTablet + " " + inDiag);
}
private void initDatabase() { private void initDatabase() {
DatabaseHelper dh = new DatabaseHelper(getApplicationContext()); DatabaseHelper dh = new DatabaseHelper(getApplicationContext());
@ -83,7 +89,6 @@ public class CommonActivity extends FragmentActivity {
m_readableDb.close(); m_readableDb.close();
m_writableDb.close(); m_writableDb.close();
} }
@Override @Override
@ -92,13 +97,8 @@ public class CommonActivity extends FragmentActivity {
m_compatMode = android.os.Build.VERSION.SDK_INT <= 10; m_compatMode = android.os.Build.VERSION.SDK_INT <= 10;
m_prefs = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext());
Log.d(TAG, "m_compatMode=" + m_compatMode); Log.d(TAG, "m_compatMode=" + m_compatMode);
detectSmallTablet();
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
} }
@ -106,14 +106,11 @@ public class CommonActivity extends FragmentActivity {
return m_smallScreenMode; return m_smallScreenMode;
} }
public boolean isSmallTablet() {
return m_smallTablet;
}
public boolean isCompatMode() { public boolean isCompatMode() {
return m_compatMode; return m_compatMode;
} }
@SuppressWarnings("deprecation")
public boolean isPortrait() { public boolean isPortrait() {
Display display = getWindowManager().getDefaultDisplay(); Display display = getWindowManager().getDefaultDisplay();
@ -123,9 +120,10 @@ public class CommonActivity extends FragmentActivity {
return width < height; return width < height;
} }
@SuppressLint("NewApi")
@SuppressWarnings("deprecation")
public void copyToClipboard(String str) { public void copyToClipboard(String str) {
if (android.os.Build.VERSION.SDK_INT < 11) { if (android.os.Build.VERSION.SDK_INT < 11) {
@SuppressWarnings("deprecation")
android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
clipboard.setText(str); clipboard.setText(str);
} else { } else {

View File

@ -7,6 +7,7 @@ import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import org.fox.ttrss.types.Feed;
import org.fox.ttrss.types.FeedCategory; import org.fox.ttrss.types.FeedCategory;
import org.fox.ttrss.types.FeedCategoryList; import org.fox.ttrss.types.FeedCategoryList;
@ -17,11 +18,14 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
@ -36,13 +40,12 @@ import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
public class FeedCategoriesFragment extends Fragment implements OnItemClickListener, OnSharedPreferenceChangeListener { public class FeedCategoriesFragment extends Fragment implements OnItemClickListener, OnSharedPreferenceChangeListener {
@SuppressWarnings("unused")
private final String TAG = this.getClass().getSimpleName(); private final String TAG = this.getClass().getSimpleName();
private SharedPreferences m_prefs; private SharedPreferences m_prefs;
private FeedCategoryListAdapter m_adapter; private FeedCategoryListAdapter m_adapter;
private FeedCategoryList m_cats = new FeedCategoryList(); private FeedCategoryList m_cats = new FeedCategoryList();
private FeedCategory m_selectedCat; private FeedCategory m_selectedCat;
private OnlineServices m_onlineServices; private FeedsActivity m_activity;
class CatUnreadComparator implements Comparator<FeedCategory> { class CatUnreadComparator implements Comparator<FeedCategory> {
@Override @Override
@ -82,11 +85,59 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
} }
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
.getMenuInfo();
switch (item.getItemId()) {
case R.id.browse_articles:
if (true) {
FeedCategory cat = getCategoryAtPosition(info.position);
if (cat != null) {
m_activity.openFeedArticles(new Feed(cat.id, cat.title, true));
//setSelectedCategory(cat);
}
}
return true;
case R.id.browse_headlines:
if (true) {
FeedCategory cat = getCategoryAtPosition(info.position);
if (cat != null) {
m_activity.onCatSelected(cat, true);
//setSelectedCategory(cat);
}
}
return true;
case R.id.browse_feeds:
if (true) {
FeedCategory cat = getCategoryAtPosition(info.position);
if (cat != null) {
m_activity.onCatSelected(cat, false);
//cf.setSelectedCategory(cat);
}
}
return true;
case R.id.catchup_category:
if (true) {
FeedCategory cat = getCategoryAtPosition(info.position);
if (cat != null) {
m_activity.catchupFeed(new Feed(cat.id, cat.title, true));
}
}
return true;
default:
Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
return super.onContextItemSelected(item);
}
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) { ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.category_menu, menu); m_activity.getMenuInflater().inflate(R.menu.category_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
FeedCategory cat = m_adapter.getItem(info.position); FeedCategory cat = m_adapter.getItem(info.position);
@ -117,11 +168,6 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
list.setOnItemClickListener(this); list.setOnItemClickListener(this);
registerForContextMenu(list); registerForContextMenu(list);
if (m_cats == null || m_cats.size() == 0)
refresh(false);
else
getActivity().setProgressBarIndeterminateVisibility(false);
return view; return view;
} }
@ -129,13 +175,22 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
m_onlineServices = (OnlineServices)activity; m_activity = (FeedsActivity)activity;
m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
m_prefs.registerOnSharedPreferenceChangeListener(this); m_prefs.registerOnSharedPreferenceChangeListener(this);
} }
@Override
public void onResume() {
super.onResume();
refresh(false);
m_activity.initMenu();
}
@Override @Override
public void onSaveInstanceState (Bundle out) { public void onSaveInstanceState (Bundle out) {
super.onSaveInstanceState(out); super.onSaveInstanceState(out);
@ -153,31 +208,26 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
} }
} }
if (getActivity() != null) m_activity.setProgressBarIndeterminateVisibility(showProgress);
getActivity().setProgressBarIndeterminateVisibility(showProgress);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void refresh(boolean background) { public void refresh(boolean background) {
CatsRequest req = new CatsRequest(getActivity().getApplicationContext()); CatsRequest req = new CatsRequest(getActivity().getApplicationContext());
final String sessionId = m_onlineServices.getSessionId(); final String sessionId = m_activity.getSessionId();
final boolean unreadOnly = m_onlineServices.getUnreadOnly(); final boolean unreadOnly = m_activity.getUnreadOnly();
if (sessionId != null) { if (sessionId != null) {
setLoadingStatus(R.string.blank, true);
getActivity().runOnUiThread(new Runnable() { m_activity.setProgressBarVisibility(true);
@Override
public void run() {
setLoadingStatus(R.string.blank, true);
}
});
@SuppressWarnings("serial") @SuppressWarnings("serial")
HashMap<String,String> map = new HashMap<String,String>() { HashMap<String,String> map = new HashMap<String,String>() {
{ {
put("op", "getCategories"); put("op", "getCategories");
put("sid", sessionId); put("sid", sessionId);
put("enable_nested", "true");
if (unreadOnly) { if (unreadOnly) {
put("unread_only", String.valueOf(unreadOnly)); put("unread_only", String.valueOf(unreadOnly));
} }
@ -185,7 +235,6 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
}; };
req.execute(map); req.execute(map);
} }
} }
@ -195,7 +244,15 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
super(context); super(context);
} }
@Override
protected void onProgressUpdate(Integer... progress) {
m_activity.setProgress(Math.round((((float)progress[0] / (float)progress[1]) * 10000)));
}
@Override
protected void onPostExecute(JsonElement result) { protected void onPostExecute(JsonElement result) {
m_activity.setProgressBarVisibility(false);
if (result != null) { if (result != null) {
try { try {
JsonArray content = result.getAsJsonArray(); JsonArray content = result.getAsJsonArray();
@ -205,7 +262,7 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
m_cats.clear(); m_cats.clear();
int apiLevel = m_onlineServices.getApiLevel(); int apiLevel = m_activity.getApiLevel();
// virtual cats implemented in getCategories since api level 1 // virtual cats implemented in getCategories since api level 1
if (apiLevel == 0) { if (apiLevel == 0) {
@ -233,7 +290,7 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
} }
if (m_lastError == ApiError.LOGIN_FAILED) { if (m_lastError == ApiError.LOGIN_FAILED) {
m_onlineServices.login(); m_activity.login();
} else { } else {
setLoadingStatus(getErrorMessage(), false); setLoadingStatus(getErrorMessage(), false);
} }
@ -247,7 +304,7 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
if (m_prefs.getBoolean("sort_feeds_by_unread", false)) { if (m_prefs.getBoolean("sort_feeds_by_unread", false)) {
cmp = new CatUnreadComparator(); cmp = new CatUnreadComparator();
} else { } else {
if (m_onlineServices.getApiLevel() >= 3) { if (m_activity.getApiLevel() >= 3) {
cmp = new CatOrderComparator(); cmp = new CatOrderComparator();
} else { } else {
cmp = new CatTitleComparator(); cmp = new CatTitleComparator();
@ -280,7 +337,7 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
public int getItemViewType(int position) { public int getItemViewType(int position) {
FeedCategory cat = items.get(position); FeedCategory cat = items.get(position);
if (!m_onlineServices.isSmallScreen() && m_selectedCat != null && cat.id == m_selectedCat.id) { if (!m_activity.isSmallScreen() && m_selectedCat != null && cat.id == m_selectedCat.id) {
return VIEW_SELECTED; return VIEW_SELECTED;
} else { } else {
return VIEW_NORMAL; return VIEW_NORMAL;
@ -344,9 +401,17 @@ public class FeedCategoriesFragment extends Fragment implements OnItemClickListe
if (list != null) { if (list != null) {
FeedCategory cat = (FeedCategory)list.getItemAtPosition(position); FeedCategory cat = (FeedCategory)list.getItemAtPosition(position);
m_onlineServices.onCatSelected(cat);
if (!m_onlineServices.isSmallScreen()) if ("ARTICLES".equals(m_prefs.getString("default_view_mode", "HEADLINES")) &&
m_prefs.getBoolean("browse_cats_like_feeds", false)) {
m_activity.openFeedArticles(new Feed(cat.id, cat.title, true));
} else {
m_activity.onCatSelected(cat);
}
if (!m_activity.isSmallScreen())
m_selectedCat = cat; m_selectedCat = cat;
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();

View File

@ -0,0 +1,300 @@
package org.fox.ttrss;
import java.util.Date;
import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList;
import org.fox.ttrss.types.Feed;
import org.fox.ttrss.types.FeedCategory;
import org.fox.ttrss.util.AppRater;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
public class FeedsActivity extends OnlineActivity implements HeadlinesEventListener {
private final String TAG = this.getClass().getSimpleName();
protected SharedPreferences m_prefs;
protected long m_lastRefresh = 0;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
m_prefs = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext());
if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) {
setTheme(R.style.DarkTheme);
} else {
setTheme(R.style.LightTheme);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.feeds);
setSmallScreen(findViewById(R.id.headlines_fragment) == null);
Intent intent = getIntent();
if (savedInstanceState == null) {
if (intent.getParcelableExtra("feed") != null || intent.getParcelableExtra("category") != null ||
intent.getParcelableExtra("article") != null) {
if (!isCompatMode()) {
getActionBar().setDisplayHomeAsUpEnabled(true);
}
Feed feed = (Feed) intent.getParcelableExtra("feed");
FeedCategory cat = (FeedCategory) intent.getParcelableExtra("category");
Article article = (Article) intent.getParcelableExtra("article");
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if (article != null) {
Article original = GlobalState.getInstance().m_loadedArticles.findById(article.id);
ArticlePager ap = new ArticlePager(original != null ? original : article, feed);
ft.replace(R.id.feeds_fragment, ap, FRAG_ARTICLE);
ap.setSearchQuery(intent.getStringExtra("searchQuery"));
setTitle(feed.title);
} else {
if (feed != null) {
HeadlinesFragment hf = new HeadlinesFragment(feed);
ft.replace(R.id.feeds_fragment, hf, FRAG_HEADLINES);
setTitle(feed.title);
}
if (cat != null) {
FeedsFragment ff = new FeedsFragment(cat);
ft.replace(R.id.feeds_fragment, ff, FRAG_FEEDS);
setTitle(cat.title);
}
}
ft.commit();
} else {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if (m_prefs.getBoolean("enable_cats", false)) {
ft.replace(R.id.feeds_fragment, new FeedCategoriesFragment(), FRAG_CATS);
} else {
ft.replace(R.id.feeds_fragment, new FeedsFragment(), FRAG_FEEDS);
}
ft.commit();
AppRater.appLaunched(this);
}
}
}
@Override
protected void initMenu() {
super.initMenu();
if (m_menu != null && getSessionId() != null) {
Fragment ff = getSupportFragmentManager().findFragmentByTag(FRAG_FEEDS);
Fragment cf = getSupportFragmentManager().findFragmentByTag(FRAG_CATS);
ArticlePager af = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
HeadlinesFragment hf = (HeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
m_menu.setGroupVisible(R.id.menu_group_feeds, (ff != null && ff.isAdded()) || (cf != null && cf.isAdded()));
m_menu.setGroupVisible(R.id.menu_group_article, af != null && af.isAdded());
m_menu.setGroupVisible(R.id.menu_group_headlines, hf != null && hf.isAdded() && hf.getSelectedArticles().size() == 0);
m_menu.setGroupVisible(R.id.menu_group_headlines_selection, hf != null && hf.isAdded() && hf.getSelectedArticles().size() != 0);
if (isSmallScreen()) {
m_menu.findItem(R.id.update_headlines).setVisible(hf != null && hf.isAdded());
} else {
m_menu.findItem(R.id.update_headlines).setVisible(false);
}
MenuItem item = m_menu.findItem(R.id.show_feeds);
if (getUnreadOnly()) {
item.setTitle(R.string.menu_all_feeds);
} else {
item.setTitle(R.string.menu_unread_feeds);
}
}
}
public void onFeedSelected(Feed feed) {
GlobalState.getInstance().m_loadedArticles.clear();
if (isSmallScreen()) {
Intent intent = new Intent(FeedsActivity.this, FeedsActivity.class);
intent.putExtra("feed", feed);
startActivityForResult(intent, 0);
} else {
FragmentTransaction ft = getSupportFragmentManager()
.beginTransaction();
HeadlinesFragment hf = new HeadlinesFragment(feed);
ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES);
ft.commit();
Date date = new Date();
if (date.getTime() - m_lastRefresh > 10000) {
m_lastRefresh = date.getTime();
refresh(false);
}
}
}
public void onCatSelected(FeedCategory cat, boolean openAsFeed) {
if (!openAsFeed) {
if (isSmallScreen()) {
Intent intent = new Intent(FeedsActivity.this, FeedsActivity.class);
intent.putExtra("category", cat);
startActivityForResult(intent, 0);
} else {
FragmentTransaction ft = getSupportFragmentManager()
.beginTransaction();
FeedsFragment ff = new FeedsFragment(cat);
ft.replace(R.id.feeds_fragment, ff, FRAG_FEEDS);
ft.addToBackStack(null);
ft.commit();
}
} else {
Feed feed = new Feed(cat.id, cat.title, true);
onFeedSelected(feed);
}
}
public void onCatSelected(FeedCategory cat) {
onCatSelected(cat, m_prefs.getBoolean("browse_cats_like_feeds", false));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.show_feeds:
setUnreadOnly(!getUnreadOnly());
initMenu();
refresh();
return true;
case R.id.update_feeds:
refresh();
return true;
default:
Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId());
return super.onOptionsItemSelected(item);
}
}
@Override
protected void loginSuccess() {
setLoadingStatus(R.string.blank, false);
findViewById(R.id.loading_container).setVisibility(View.GONE);
initMenu();
}
@Override
public void onSaveInstanceState(Bundle out) {
super.onSaveInstanceState(out);
}
@Override
public void onResume() {
super.onResume();
initMenu();
}
@Override
public void onArticleListSelectionChange(ArticleList m_selectedArticles) {
initMenu();
}
public void openFeedArticles(Feed feed) {
if (isSmallScreen()) {
Intent intent = new Intent(FeedsActivity.this, FeedsActivity.class);
GlobalState.getInstance().m_activeFeed = feed;
GlobalState.getInstance().m_loadedArticles.clear();
intent.putExtra("feed", feed);
intent.putExtra("article", new Article());
startActivityForResult(intent, 0);
} else {
}
}
public void onArticleSelected(Article article, boolean open) {
if (article.unread) {
article.unread = false;
saveArticleUnread(article);
}
if (open) {
HeadlinesFragment hf = (HeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
if (isSmallScreen()) {
Intent intent = new Intent(FeedsActivity.this, FeedsActivity.class);
intent.putExtra("feed", hf.getFeed());
intent.putExtra("article", article);
intent.putExtra("searchQuery", hf.getSearchQuery());
startActivityForResult(intent, 0);
} else {
Intent intent = new Intent(FeedsActivity.this, HeadlinesActivity.class);
intent.putExtra("feed", hf.getFeed());
intent.putExtra("article", article);
intent.putExtra("searchQuery", hf.getSearchQuery());
startActivityForResult(intent, 0);
}
} else {
initMenu();
}
}
@Override
public void onArticleSelected(Article article) {
onArticleSelected(article, true);
}
public void catchupFeed(final Feed feed) {
super.catchupFeed(feed);
refresh();
}
@Override
public void onHeadlinesLoaded(boolean appended) {
// TODO Auto-generated method stub
}
}

View File

@ -5,29 +5,25 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.net.MalformedURLException; import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import org.apache.http.HttpHost; import javax.net.ssl.HttpsURLConnection;
import org.apache.http.HttpResponse; import javax.net.ssl.SSLContext;
import org.apache.http.auth.AuthScope; import javax.net.ssl.TrustManager;
import org.apache.http.auth.UsernamePasswordCredentials; import javax.net.ssl.X509TrustManager;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpGet; import org.fox.ttrss.types.Article;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.fox.ttrss.types.Feed; import org.fox.ttrss.types.Feed;
import org.fox.ttrss.types.FeedCategory; import org.fox.ttrss.types.FeedCategory;
import org.fox.ttrss.types.FeedList; import org.fox.ttrss.types.FeedList;
import org.fox.ttrss.util.EasySSLSocketFactory;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
@ -37,14 +33,17 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.net.http.AndroidHttpClient; import android.net.http.AndroidHttpClient;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.util.Base64;
import android.util.Log; import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
@ -65,7 +64,7 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
private SharedPreferences m_prefs; private SharedPreferences m_prefs;
private FeedListAdapter m_adapter; private FeedListAdapter m_adapter;
private FeedList m_feeds = new FeedList(); private FeedList m_feeds = new FeedList();
private OnlineServices m_onlineServices; private FeedsActivity m_activity;
private Feed m_selectedFeed; private Feed m_selectedFeed;
private FeedCategory m_activeCategory; private FeedCategory m_activeCategory;
private static final String ICON_PATH = "/data/org.fox.ttrss/icons/"; private static final String ICON_PATH = "/data/org.fox.ttrss/icons/";
@ -98,7 +97,13 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
@Override @Override
public int compare(Feed a, Feed b) { public int compare(Feed a, Feed b) {
if (a.id >= 0 && b.id >= 0) if (a.is_cat && b.is_cat)
return a.title.compareTo(b.title);
else if (a.is_cat && !b.is_cat)
return -1;
else if (!a.is_cat && b.is_cat)
return 1;
else if (a.id >= 0 && b.id >= 0)
return a.title.compareTo(b.title); return a.title.compareTo(b.title);
else else
return a.id - b.id; return a.id - b.id;
@ -111,7 +116,13 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
@Override @Override
public int compare(Feed a, Feed b) { public int compare(Feed a, Feed b) {
if (a.id >= 0 && b.id >= 0) if (a.id >= 0 && b.id >= 0)
if (a.order_id != 0 && b.order_id != 0) if (a.is_cat && b.is_cat)
return a.title.compareTo(b.title);
else if (a.is_cat && !b.is_cat)
return -1;
else if (!a.is_cat && b.is_cat)
return 1;
else if (a.order_id != 0 && b.order_id != 0)
return a.order_id - b.order_id; return a.order_id - b.order_id;
else else
return a.title.compareTo(b.title); return a.title.compareTo(b.title);
@ -121,6 +132,42 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
} }
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
.getMenuInfo();
switch (item.getItemId()) {
case R.id.browse_articles:
if (true) {
Feed feed = getFeedAtPosition(info.position);
if (feed != null) {
m_activity.openFeedArticles(feed);
}
}
return true;
case R.id.browse_headlines:
if (true) {
Feed feed = getFeedAtPosition(info.position);
if (feed != null) {
m_activity.onFeedSelected(feed);
}
}
return true;
case R.id.catchup_feed:
if (true) {
Feed feed = getFeedAtPosition(info.position);
if (feed != null) {
m_activity.catchupFeed(feed);
}
}
return true;
default:
Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
return super.onContextItemSelected(item);
}
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) { ContextMenuInfo menuInfo) {
@ -133,6 +180,11 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
if (feed != null) if (feed != null)
menu.setHeaderTitle(feed.title); menu.setHeaderTitle(feed.title);
if (!m_activity.isSmallScreen()) {
menu.findItem(R.id.browse_headlines).setVisible(false);
menu.findItem(R.id.browse_feeds).setVisible(false);
}
super.onCreateContextMenu(menu, v, menuInfo); super.onCreateContextMenu(menu, v, menuInfo);
} }
@ -159,11 +211,6 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
m_enableFeedIcons = m_prefs.getBoolean("download_feed_icons", false); m_enableFeedIcons = m_prefs.getBoolean("download_feed_icons", false);
if (m_feeds == null || m_feeds.size() == 0)
refresh(false);
else
getActivity().setProgressBarIndeterminateVisibility(false);
return view; return view;
} }
@ -179,9 +226,16 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
m_prefs.registerOnSharedPreferenceChangeListener(this); m_prefs.registerOnSharedPreferenceChangeListener(this);
m_onlineServices = (OnlineServices)activity; m_activity = (FeedsActivity)activity;
}
//m_selectedFeed = m_onlineServices.getActiveFeed(); @Override
public void onResume() {
super.onResume();
refresh(false);
m_activity.initMenu();
} }
@Override @Override
@ -200,9 +254,22 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
if (list != null) { if (list != null) {
Feed feed = (Feed)list.getItemAtPosition(position); Feed feed = (Feed)list.getItemAtPosition(position);
m_onlineServices.onFeedSelected(feed);
if (!m_onlineServices.isSmallScreen()) if (feed.is_cat) {
FeedCategory cat = new FeedCategory();
cat.id = feed.id;
cat.title = feed.title;
m_activity.onCatSelected(cat);
} else {
if ("ARTICLES".equals(m_prefs.getString("default_view_mode", "HEADLINES"))) {
m_activity.openFeedArticles(feed);
} else {
m_activity.onFeedSelected(feed);
}
}
if (!m_activity.isSmallScreen())
m_selectedFeed = feed; m_selectedFeed = feed;
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
@ -213,26 +280,23 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
public void refresh(boolean background) { public void refresh(boolean background) {
//FeedCategory cat = m_onlineServices.getActiveCategory(); //FeedCategory cat = m_onlineServices.getActiveCategory();
m_activity.setProgressBarVisibility(true);
final int catId = (m_activeCategory != null) ? m_activeCategory.id : -4; final int catId = (m_activeCategory != null) ? m_activeCategory.id : -4;
final String sessionId = m_onlineServices.getSessionId(); final String sessionId = m_activity.getSessionId();
final boolean unreadOnly = m_onlineServices.getUnreadOnly(); final boolean unreadOnly = m_activity.getUnreadOnly();
FeedsRequest req = new FeedsRequest(getActivity().getApplicationContext(), catId); FeedsRequest req = new FeedsRequest(getActivity().getApplicationContext(), catId);
if (sessionId != null) { if (sessionId != null) {
setLoadingStatus(R.string.blank, true);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setLoadingStatus(R.string.blank, true);
}
});
HashMap<String,String> map = new HashMap<String,String>() { HashMap<String,String> map = new HashMap<String,String>() {
{ {
put("op", "getFeeds"); put("op", "getFeeds");
put("sid", sessionId); put("sid", sessionId);
put("include_nested", "true");
put("cat_id", String.valueOf(catId)); put("cat_id", String.valueOf(catId));
if (unreadOnly) { if (unreadOnly) {
put("unread_only", String.valueOf(unreadOnly)); put("unread_only", String.valueOf(unreadOnly));
@ -292,7 +356,7 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
} }
}; };
final String sessionId = m_onlineServices.getSessionId(); final String sessionId = m_activity.getSessionId();
HashMap<String,String> map = new HashMap<String,String>() { HashMap<String,String> map = new HashMap<String,String>() {
{ {
@ -312,7 +376,15 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
m_catId = catId; m_catId = catId;
} }
@Override
protected void onProgressUpdate(Integer... progress) {
m_activity.setProgress(Math.round((((float)progress[0] / (float)progress[1]) * 10000)));
}
@Override
protected void onPostExecute(JsonElement result) { protected void onPostExecute(JsonElement result) {
m_activity.setProgressBarVisibility(false);
if (result != null) { if (result != null) {
try { try {
JsonArray content = result.getAsJsonArray(); JsonArray content = result.getAsJsonArray();
@ -347,7 +419,7 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
} }
if (m_lastError == ApiError.LOGIN_FAILED) { if (m_lastError == ApiError.LOGIN_FAILED) {
m_onlineServices.login(); m_activity.login();
} else { } else {
setLoadingStatus(getErrorMessage(), false); setLoadingStatus(getErrorMessage(), false);
} }
@ -375,7 +447,7 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
public int getItemViewType(int position) { public int getItemViewType(int position) {
Feed feed = items.get(position); Feed feed = items.get(position);
if (!m_onlineServices.isSmallScreen() && m_selectedFeed != null && feed.id == m_selectedFeed.id) { if (!m_activity.isSmallScreen() && m_selectedFeed != null && feed.id == m_selectedFeed.id) {
return VIEW_SELECTED; return VIEW_SELECTED;
} else { } else {
return VIEW_NORMAL; return VIEW_NORMAL;
@ -449,7 +521,7 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
if (m_prefs.getBoolean("sort_feeds_by_unread", false)) { if (m_prefs.getBoolean("sort_feeds_by_unread", false)) {
cmp = new FeedUnreadComparator(); cmp = new FeedUnreadComparator();
} else { } else {
if (m_onlineServices.getApiLevel() >= 3) { if (m_activity.getApiLevel() >= 3) {
cmp = new FeedOrderComparator(); cmp = new FeedOrderComparator();
} else { } else {
cmp = new FeedTitleComparator(); cmp = new FeedTitleComparator();
@ -478,7 +550,7 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
if (iconPath.exists()) { if (iconPath.exists()) {
for (Feed feed : params[0]) { for (Feed feed : params[0]) {
if (feed.id > 0 && feed.has_icon) { if (feed.id > 0 && feed.has_icon && !feed.is_cat) {
File outputFile = new File(iconPath.getAbsolutePath() + "/" + feed.id + ".ico"); File outputFile = new File(iconPath.getAbsolutePath() + "/" + feed.id + ".ico");
String fetchUrl = m_baseUrl + "/" + feed.id + ".ico"; String fetchUrl = m_baseUrl + "/" + feed.id + ".ico";
@ -496,46 +568,73 @@ public class FeedsFragment extends Fragment implements OnItemClickListener, OnSh
return null; return null;
} }
private void trustAllHosts() {
X509TrustManager easyTrustManager = new X509TrustManager() {
public void checkClientTrusted(
X509Certificate[] chain,
String authType) throws CertificateException {
// Oh, I am easy!
}
public void checkServerTrusted(
X509Certificate[] chain,
String authType) throws CertificateException {
// Oh, I am easy!
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
// Create a trust manager that does not validate certificate chains
TrustManager[] trustAllCerts = new TrustManager[] {easyTrustManager};
// Install the all-trusting trust manager
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
} catch (Exception e) {
e.printStackTrace();
}
}
@SuppressWarnings("deprecation")
private void disableConnectionReuseIfNecessary() {
// HTTP connection reuse which was buggy pre-froyo
if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
System.setProperty("http.keepAlive", "false");
}
}
protected void downloadFile(String fetchUrl, String outputFile) { protected void downloadFile(String fetchUrl, String outputFile) {
AndroidHttpClient client = AndroidHttpClient.newInstance("Tiny Tiny RSS"); AndroidHttpClient client = AndroidHttpClient.newInstance("Tiny Tiny RSS");
disableConnectionReuseIfNecessary();
if (m_prefs.getBoolean("ssl_trust_any", false)) { if (m_prefs.getBoolean("ssl_trust_any", false)) {
client.getConnectionManager().getSchemeRegistry().register(new Scheme("https", new EasySSLSocketFactory(), 443)); trustAllHosts();
} }
HttpGet httpGet = new HttpGet(fetchUrl);
HttpContext context = null;
String httpLogin = m_prefs.getString("http_login", "");
String httpPassword = m_prefs.getString("http_password", "");
if (httpLogin.length() > 0) {
URL targetUrl;
try {
targetUrl = new URL(fetchUrl);
} catch (MalformedURLException e) {
e.printStackTrace();
client.close();
return;
}
HttpHost targetHost = new HttpHost(targetUrl.getHost(), targetUrl.getPort(), targetUrl.getProtocol());
CredentialsProvider cp = new BasicCredentialsProvider();
context = new BasicHttpContext();
cp.setCredentials(
new AuthScope(targetHost.getHostName(), targetHost.getPort()),
new UsernamePasswordCredentials(httpLogin, httpPassword));
context.setAttribute(ClientContext.CREDS_PROVIDER, cp);
}
try { try {
HttpResponse execute = client.execute(httpGet, context); URL url = new URL(fetchUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
InputStream content = execute.getEntity().getContent(); String httpLogin = m_prefs.getString("http_login", "");
String httpPassword = m_prefs.getString("http_password", "");
if (httpLogin.length() > 0) {
conn.setRequestProperty("Authorization", "Basic " +
Base64.encode((httpLogin + ":" + httpPassword).getBytes("UTF-8"), Base64.NO_WRAP));
}
InputStream content = conn.getInputStream();
BufferedInputStream is = new BufferedInputStream(content, 1024); BufferedInputStream is = new BufferedInputStream(content, 1024);
FileOutputStream fos = new FileOutputStream(outputFile); FileOutputStream fos = new FileOutputStream(outputFile);

View File

@ -0,0 +1,31 @@
package org.fox.ttrss;
import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList;
import org.fox.ttrss.types.Feed;
import android.app.Application;
public class GlobalState extends Application {
private static GlobalState m_singleton;
public ArticleList m_loadedArticles = new ArticleList();
public Feed m_activeFeed;
public Article m_activeArticle;
public int m_selectedArticleId;
public boolean m_unreadOnly = true;
public boolean m_unreadArticlesOnly = true;
public String m_sessionId;
public int m_apiLevel;
public boolean m_canUseProgress;
public static GlobalState getInstance(){
return m_singleton;
}
@Override
public final void onCreate() {
super.onCreate();
m_singleton = this;
}
}

View File

@ -0,0 +1,187 @@
package org.fox.ttrss;
import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList;
import org.fox.ttrss.types.Feed;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
public class HeadlinesActivity extends OnlineActivity implements HeadlinesEventListener {
private final String TAG = this.getClass().getSimpleName();
protected SharedPreferences m_prefs;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
m_prefs = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext());
if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) {
setTheme(R.style.DarkTheme);
} else {
setTheme(R.style.LightTheme);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.headlines);
if (!isCompatMode()) {
getActionBar().setDisplayHomeAsUpEnabled(true);
}
setSmallScreen(findViewById(R.id.headlines_fragment) == null);
if (savedInstanceState == null) {
Intent i = getIntent();
if (i.getExtras() != null) {
Feed feed = i.getParcelableExtra("feed");
Article article = i.getParcelableExtra("article");
String searchQuery = i.getStringExtra("searchQuery");
HeadlinesFragment hf = new HeadlinesFragment(feed, article);
ArticlePager af = new ArticlePager(hf.getArticleById(article.id), feed);
hf.setSearchQuery(searchQuery);
af.setSearchQuery(searchQuery);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES);
ft.replace(R.id.article_fragment, af, FRAG_ARTICLE);
ft.commit();
setTitle(feed.title);
}
}
}
@Override
protected void refresh() {
super.refresh();
}
@Override
protected void loginSuccess() {
Log.d(TAG, "loginSuccess");
setLoadingStatus(R.string.blank, false);
findViewById(R.id.loading_container).setVisibility(View.GONE);
initMenu();
}
@Override
public void onSaveInstanceState(Bundle out) {
super.onSaveInstanceState(out);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
default:
Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId());
return super.onOptionsItemSelected(item);
}
}
@Override
public void onResume() {
super.onResume();
}
@Override
protected void initMenu() {
super.initMenu();
if (m_menu != null && getSessionId() != null) {
m_menu.setGroupVisible(R.id.menu_group_feeds, false);
HeadlinesFragment hf = (HeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
m_menu.setGroupVisible(R.id.menu_group_headlines, hf != null && hf.getSelectedArticles().size() == 0);
m_menu.setGroupVisible(R.id.menu_group_headlines_selection, hf != null && hf.getSelectedArticles().size() != 0);
Fragment af = getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
m_menu.setGroupVisible(R.id.menu_group_article, af != null);
m_menu.findItem(R.id.search).setVisible(false);
}
}
@Override
public void onArticleListSelectionChange(ArticleList m_selectedArticles) {
initMenu();
}
@Override
public void onArticleSelected(Article article) {
onArticleSelected(article, true);
}
@Override
public void onArticleSelected(Article article, boolean open) {
if (article.unread) {
article.unread = false;
saveArticleUnread(article);
}
if (open) {
FragmentTransaction ft = getSupportFragmentManager()
.beginTransaction();
ArticlePager af = (ArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
af.setActiveArticle(article);
ft.commit();
} else {
HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
hf.setActiveArticle(article);
}
GlobalState.getInstance().m_activeArticle = article;
initMenu();
}
@Override
public void onHeadlinesLoaded(boolean appended) {
HeadlinesFragment hf = (HeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
if (hf != null) {
Article article = hf.getActiveArticle();
if (article == null) {
article = hf.getAllArticles().get(0);
hf.setActiveArticle(article);
ArticlePager af = new ArticlePager(article, hf.getFeed());
FragmentTransaction ft = getSupportFragmentManager()
.beginTransaction();
ft.replace(R.id.article_fragment, af, FRAG_ARTICLE);
ft.commit();
}
}
}
}

View File

@ -0,0 +1,11 @@
package org.fox.ttrss;
import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList;
public interface HeadlinesEventListener {
void onArticleListSelectionChange(ArticleList m_selectedArticles);
void onArticleSelected(Article article);
void onArticleSelected(Article article, boolean open);
void onHeadlinesLoaded(boolean appended);
}

View File

@ -1,6 +1,5 @@
package org.fox.ttrss; package org.fox.ttrss;
import java.lang.reflect.Type;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.text.DateFormat; import java.text.DateFormat;
@ -8,13 +7,13 @@ import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.TimeZone; import java.util.TimeZone;
import org.fox.ttrss.types.Article; import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList; import org.fox.ttrss.types.ArticleList;
import org.fox.ttrss.types.Attachment; import org.fox.ttrss.types.Attachment;
import org.fox.ttrss.types.Feed; import org.fox.ttrss.types.Feed;
import org.fox.ttrss.util.HeadlinesRequest;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import android.app.Activity; import android.app.Activity;
@ -34,6 +33,7 @@ import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -51,10 +51,7 @@ import android.widget.ListView;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; import android.widget.TextView;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
public class HeadlinesFragment extends Fragment implements OnItemClickListener, OnScrollListener { public class HeadlinesFragment extends Fragment implements OnItemClickListener, OnScrollListener {
public static enum ArticlesSelection { ALL, NONE, UNREAD }; public static enum ArticlesSelection { ALL, NONE, UNREAD };
@ -66,28 +63,27 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
private Feed m_feed; private Feed m_feed;
private Article m_activeArticle; private Article m_activeArticle;
private boolean m_refreshInProgress = false;
private boolean m_canLoadMore = false;
private boolean m_combinedMode = true; private boolean m_combinedMode = true;
private String m_searchQuery = ""; private String m_searchQuery = "";
private boolean m_refreshInProgress = false;
private SharedPreferences m_prefs; private SharedPreferences m_prefs;
private ArticleListAdapter m_adapter; private ArticleListAdapter m_adapter;
private ArticleList m_articles = new ArticleList(); private ArticleList m_articles = GlobalState.getInstance().m_loadedArticles;
private ArticleList m_selectedArticles = new ArticleList(); private ArticleList m_selectedArticles = new ArticleList();
private HeadlinesEventListener m_listener;
private OnlineServices m_onlineServices; private OnlineActivity m_activity;
private ImageGetter m_dummyGetter = new ImageGetter() { private ImageGetter m_dummyGetter = new ImageGetter() {
@SuppressWarnings("deprecation")
@Override @Override
public Drawable getDrawable(String source) { public Drawable getDrawable(String source) {
return new BitmapDrawable(); return new BitmapDrawable();
} }
}; };
public ArticleList getSelectedArticles() { public ArticleList getSelectedArticles() {
return m_selectedArticles; return m_selectedArticles;
} }
@ -96,15 +92,151 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
m_feed = feed; m_feed = feed;
} }
public HeadlinesFragment(Feed feed, Article activeArticle) {
m_feed = feed;
m_activeArticle = getArticleById(activeArticle.id);
}
public HeadlinesFragment() { public HeadlinesFragment() {
// //
} }
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
.getMenuInfo();
switch (item.getItemId()) {
case R.id.set_labels:
if (true) {
Article article = getArticleAtPosition(info.position);
if (article != null) {
m_activity.editArticleLabels(article);
}
}
return true;
case R.id.article_set_note:
if (true) {
Article article = getArticleAtPosition(info.position);
if (article != null) {
m_activity.editArticleNote(article);
}
}
return true;
case R.id.article_link_copy:
if (true) {
Article article = getArticleAtPosition(info.position);
if (article != null) {
m_activity.copyToClipboard(article.link);
}
}
return true;
case R.id.selection_toggle_marked:
if (true) {
ArticleList selected = getSelectedArticles();
if (selected.size() > 0) {
for (Article a : selected)
a.marked = !a.marked;
m_activity.toggleArticlesMarked(selected);
//updateHeadlines();
} else {
Article article = getArticleAtPosition(info.position);
if (article != null) {
article.marked = !article.marked;
m_activity.saveArticleMarked(article);
//updateHeadlines();
}
}
m_adapter.notifyDataSetChanged();
}
return true;
case R.id.selection_toggle_published:
if (true) {
ArticleList selected = getSelectedArticles();
if (selected.size() > 0) {
for (Article a : selected)
a.published = !a.published;
m_activity.toggleArticlesPublished(selected);
//updateHeadlines();
} else {
Article article = getArticleAtPosition(info.position);
if (article != null) {
article.published = !article.published;
m_activity.saveArticlePublished(article);
//updateHeadlines();
}
}
m_adapter.notifyDataSetChanged();
}
return true;
case R.id.selection_toggle_unread:
if (true) {
ArticleList selected = getSelectedArticles();
if (selected.size() > 0) {
for (Article a : selected)
a.unread = !a.unread;
m_activity.toggleArticlesUnread(selected);
//updateHeadlines();
} else {
Article article = getArticleAtPosition(info.position);
if (article != null) {
article.unread = !article.unread;
m_activity.saveArticleUnread(article);
//updateHeadlines();
}
}
m_adapter.notifyDataSetChanged();
}
return true;
case R.id.share_article:
if (true) {
Article article = getArticleAtPosition(info.position);
if (article != null)
m_activity.shareArticle(article);
}
return true;
case R.id.catchup_above:
if (true) {
Article article = getArticleAtPosition(info.position);
if (article != null) {
ArticleList articles = getAllArticles();
ArticleList tmp = new ArticleList();
for (Article a : articles) {
a.unread = false;
tmp.add(a);
if (article.id == a.id)
break;
}
if (tmp.size() > 0) {
m_activity.toggleArticlesUnread(tmp);
//updateHeadlines();
}
}
m_adapter.notifyDataSetChanged();
}
return true;
default:
Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
return super.onContextItemSelected(item);
}
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) { ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.headlines_menu, menu); getActivity().getMenuInflater().inflate(R.menu.headlines_context_menu, menu);
if (m_selectedArticles.size() > 0) { if (m_selectedArticles.size() > 0) {
menu.setHeaderTitle(R.string.headline_context_multiple); menu.setHeaderTitle(R.string.headline_context_multiple);
@ -116,8 +248,8 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
menu.setGroupVisible(R.id.menu_group_single_article, true); menu.setGroupVisible(R.id.menu_group_single_article, true);
} }
menu.findItem(R.id.set_labels).setEnabled(m_onlineServices.getApiLevel() >= 1); menu.findItem(R.id.set_labels).setEnabled(m_activity.getApiLevel() >= 1);
menu.findItem(R.id.article_set_note).setEnabled(m_onlineServices.getApiLevel() >= 1); menu.findItem(R.id.article_set_note).setEnabled(m_activity.getApiLevel() >= 1);
super.onCreateContextMenu(menu, v, menuInfo); super.onCreateContextMenu(menu, v, menuInfo);
@ -128,10 +260,9 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
if (savedInstanceState != null) { if (savedInstanceState != null) {
m_feed = savedInstanceState.getParcelable("feed"); m_feed = savedInstanceState.getParcelable("feed");
m_articles = savedInstanceState.getParcelable("articles"); //m_articles = savedInstanceState.getParcelable("articles");
m_activeArticle = savedInstanceState.getParcelable("activeArticle"); m_activeArticle = savedInstanceState.getParcelable("activeArticle");
m_selectedArticles = savedInstanceState.getParcelable("selectedArticles"); m_selectedArticles = savedInstanceState.getParcelable("selectedArticles");
m_canLoadMore = savedInstanceState.getBoolean("canLoadMore");
m_combinedMode = savedInstanceState.getBoolean("combinedMode"); m_combinedMode = savedInstanceState.getBoolean("combinedMode");
m_searchQuery = (String) savedInstanceState.getCharSequence("searchQuery"); m_searchQuery = (String) savedInstanceState.getCharSequence("searchQuery");
} }
@ -146,26 +277,45 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
//list.setEmptyView(view.findViewById(R.id.no_headlines)); //list.setEmptyView(view.findViewById(R.id.no_headlines));
registerForContextMenu(list); registerForContextMenu(list);
if (m_onlineServices.isSmallScreen() || m_onlineServices.isPortrait()) if (m_activity.isSmallScreen() || m_activity.isPortrait())
view.findViewById(R.id.headlines_fragment).setPadding(0, 0, 0, 0); view.findViewById(R.id.headlines_fragment).setPadding(0, 0, 0, 0);
Log.d(TAG, "onCreateView, feed=" + m_feed); Log.d(TAG, "onCreateView, feed=" + m_feed);
if (m_feed != null && (m_articles == null || m_articles.size() == 0))
refresh(false);
else
getActivity().setProgressBarIndeterminateVisibility(false);
return view; return view;
} }
@Override
public void onResume() {
super.onResume();
if (GlobalState.getInstance().m_activeArticle != null) {
m_activeArticle = GlobalState.getInstance().m_activeArticle;
GlobalState.getInstance().m_activeArticle = null;
}
if (m_activeArticle != null) {
setActiveArticle(m_activeArticle);
}
if (m_articles.size() == 0 || !m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
refresh(false);
GlobalState.getInstance().m_activeFeed = m_feed;
} else {
notifyUpdated();
}
m_activity.initMenu();
}
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
m_onlineServices = (OnlineServices) activity; m_activity = (OnlineActivity) activity;
m_listener = (HeadlinesEventListener) activity;
m_combinedMode = m_prefs.getBoolean("combined_mode", false); m_combinedMode = false; /* m_prefs.getBoolean("combined_mode", false); */
} }
@Override @Override
@ -179,12 +329,16 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
if (article.id >= 0) { if (article.id >= 0) {
if (m_combinedMode) { if (m_combinedMode) {
article.unread = false; article.unread = false;
m_onlineServices.saveArticleUnread(article); m_activity.saveArticleUnread(article);
} else { } else {
m_onlineServices.onArticleSelected(article); m_listener.onArticleSelected(article);
}
// only set active article when it makes sense (in HeadlinesActivity)
if (getActivity().findViewById(R.id.article_fragment) != null) {
m_activeArticle = article;
} }
m_activeArticle = article;
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
} }
} }
@ -192,52 +346,91 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
@SuppressWarnings({ "unchecked", "serial" }) @SuppressWarnings({ "unchecked", "serial" })
public void refresh(boolean append) { public void refresh(boolean append) {
m_refreshInProgress = true; if (m_activity != null) {
m_refreshInProgress = true;
HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext()); m_activity.setProgressBarVisibility(true);
final String sessionId = m_onlineServices.getSessionId(); if (!m_feed.equals(GlobalState.getInstance().m_activeFeed)) {
final boolean showUnread = m_onlineServices.getUnreadArticlesOnly(); append = false;
final boolean isCat = m_feed.is_cat;
int skip = 0;
if (append) {
for (Article a : m_articles) {
if (a.unread) ++skip;
} }
if (skip == 0) skip = m_articles.size(); final boolean fappend = append;
} else { final String sessionId = m_activity.getSessionId();
setLoadingStatus(R.string.blank, true); final boolean showUnread = m_activity.getUnreadArticlesOnly();
} final boolean isCat = m_feed.is_cat;
final int fskip = skip; HeadlinesRequest req = new HeadlinesRequest(getActivity().getApplicationContext(), m_activity) {
@Override
req.setOffset(skip); protected void onProgressUpdate(Integer... progress) {
m_activity.setProgress(Math.round((((float)progress[0] / (float)progress[1]) * 10000)));
HashMap<String,String> map = new HashMap<String,String>() {
{
put("op", "getHeadlines");
put("sid", sessionId);
put("feed_id", String.valueOf(m_feed.id));
put("show_content", "true");
put("include_attachments", "true");
put("limit", String.valueOf(HEADLINES_REQUEST_SIZE));
put("offset", String.valueOf(0));
put("view_mode", showUnread ? "adaptive" : "all_articles");
put("skip", String.valueOf(fskip));
if (isCat) put("is_cat", "true");
if (m_searchQuery.length() != 0) {
put("search", m_searchQuery);
put("search_mode", "");
put("match_on", "both");
} }
}
};
req.execute(map); @Override
protected void onPostExecute(JsonElement result) {
m_activity.setProgressBarVisibility(false);
super.onPostExecute(result);
if (result != null) {
m_refreshInProgress = false;
if (m_articles.indexOf(m_activeArticle) == -1)
m_activeArticle = null;
m_adapter.notifyDataSetChanged();
m_listener.onHeadlinesLoaded(fappend);
} else {
if (m_lastError == ApiError.LOGIN_FAILED) {
m_activity.login();
} else {
setLoadingStatus(getErrorMessage(), false);
}
}
}
};
int skip = 0;
if (append) {
for (Article a : m_articles) {
if (a.unread) ++skip;
}
if (skip == 0) skip = m_articles.size();
} else {
setLoadingStatus(R.string.blank, true);
}
final int fskip = skip;
req.setOffset(skip);
HashMap<String,String> map = new HashMap<String,String>() {
{
put("op", "getHeadlines");
put("sid", sessionId);
put("feed_id", String.valueOf(m_feed.id));
put("show_content", "true");
put("include_attachments", "true");
put("limit", String.valueOf(HEADLINES_REQUEST_SIZE));
put("offset", String.valueOf(0));
put("view_mode", showUnread ? "adaptive" : "all_articles");
put("skip", String.valueOf(fskip));
put("include_nested", "true");
if (isCat) put("is_cat", "true");
if (m_searchQuery != null && m_searchQuery.length() != 0) {
put("search", m_searchQuery);
put("search_mode", "");
put("match_on", "both");
}
}
};
req.execute(map);
}
} }
@Override @Override
@ -245,10 +438,9 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
super.onSaveInstanceState(out); super.onSaveInstanceState(out);
out.putParcelable("feed", m_feed); out.putParcelable("feed", m_feed);
out.putParcelable("articles", m_articles); //out.putParcelable("articles", m_articles);
out.putParcelable("activeArticle", m_activeArticle); out.putParcelable("activeArticle", m_activeArticle);
out.putParcelable("selectedArticles", m_selectedArticles); out.putParcelable("selectedArticles", m_selectedArticles);
out.putBoolean("canLoadMore", m_canLoadMore);
out.putBoolean("combinedMode", m_combinedMode); out.putBoolean("combinedMode", m_combinedMode);
out.putCharSequence("searchQuery", m_searchQuery); out.putCharSequence("searchQuery", m_searchQuery);
} }
@ -266,7 +458,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
getActivity().setProgressBarIndeterminateVisibility(showProgress); getActivity().setProgressBarIndeterminateVisibility(showProgress);
} }
private class HeadlinesRequest extends ApiRequest { /* private class HeadlinesRequest extends ApiRequest {
int m_offset = 0; int m_offset = 0;
public HeadlinesRequest(Context context) { public HeadlinesRequest(Context context) {
@ -319,7 +511,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
if (m_lastError == ApiError.LOGIN_FAILED) { if (m_lastError == ApiError.LOGIN_FAILED) {
m_onlineServices.login(); m_activity.login();
} else { } else {
setLoadingStatus(getErrorMessage(), false); setLoadingStatus(getErrorMessage(), false);
} }
@ -329,7 +521,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
public void setOffset(int skip) { public void setOffset(int skip) {
m_offset = skip; m_offset = skip;
} }
} } */
private class ArticleListAdapter extends ArrayAdapter<Article> { private class ArticleListAdapter extends ArrayAdapter<Article> {
private ArrayList<Article> items; private ArrayList<Article> items;
@ -409,7 +601,12 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
if (ft != null) { if (ft != null) {
if (article.feed_title != null && (m_feed.is_cat || m_feed.id < 0)) { if (article.feed_title != null && (m_feed.is_cat || m_feed.id < 0)) {
ft.setText(article.feed_title);
if (article.feed_title.length() > 20)
ft.setText(article.feed_title.substring(0, 20) + "...");
else
ft.setText(article.feed_title);
} else { } else {
ft.setVisibility(View.GONE); ft.setVisibility(View.GONE);
} }
@ -428,7 +625,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
article.marked = !article.marked; article.marked = !article.marked;
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
m_onlineServices.saveArticleMarked(article); m_activity.saveArticleMarked(article);
} }
}); });
} }
@ -445,7 +642,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
article.published = !article.published; article.published = !article.published;
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
m_onlineServices.saveArticlePublished(article); m_activity.saveArticlePublished(article);
} }
}); });
} }
@ -533,7 +730,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
Attachment attachment = (Attachment) spinner.getSelectedItem(); Attachment attachment = (Attachment) spinner.getSelectedItem();
if (attachment != null) { if (attachment != null) {
m_onlineServices.copyToClipboard(attachment.content_url); m_activity.copyToClipboard(attachment.content_url);
} }
} }
}); });
@ -590,7 +787,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
m_selectedArticles.remove(article); m_selectedArticles.remove(article);
} }
m_onlineServices.onArticleListSelectionChange(m_selectedArticles); m_listener.onArticleListSelectionChange(m_selectedArticles);
Log.d(TAG, "num selected: " + m_selectedArticles.size()); Log.d(TAG, "num selected: " + m_selectedArticles.size());
} }
@ -615,27 +812,25 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
/* public void notifyUpdated() { public void notifyUpdated() {
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
}
Article article = m_onlineServices.getSelectedArticle();
setActiveArticle(article);
} */
public ArticleList getAllArticles() { public ArticleList getAllArticles() {
return m_articles; return m_articles;
} }
public void setActiveArticle(Article article) { public void setActiveArticle(Article article) {
m_activeArticle = article; if (article != m_activeArticle) {
m_adapter.notifyDataSetChanged(); m_activeArticle = article;
m_adapter.notifyDataSetChanged();
ListView list = (ListView)getView().findViewById(R.id.headlines); ListView list = (ListView)getView().findViewById(R.id.headlines);
if (list != null && article != null) { if (list != null && article != null) {
int position = m_adapter.getPosition(article); int position = m_adapter.getPosition(article);
list.setSelection(position); list.setSelection(position);
}
} }
} }
@ -681,7 +876,7 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
@Override @Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (!m_refreshInProgress && m_canLoadMore && firstVisibleItem + visibleItemCount == m_articles.size()) { if (!m_refreshInProgress && m_articles.findById(-1) != null && firstVisibleItem + visibleItemCount == m_articles.size()) {
refresh(true); refresh(true);
} }
} }
@ -703,6 +898,10 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
} }
public String getSearchQuery() {
return m_searchQuery;
}
public void setSearchQuery(String query) { public void setSearchQuery(String query) {
if (!m_searchQuery.equals(query)) { if (!m_searchQuery.equals(query)) {
m_searchQuery = query; m_searchQuery = query;
@ -710,5 +909,9 @@ public class HeadlinesFragment extends Fragment implements OnItemClickListener,
} }
} }
public Feed getFeed() {
return m_feed;
}
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
package org.fox.ttrss;
import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList;
import org.fox.ttrss.types.Feed;
import org.fox.ttrss.types.FeedCategory;
public interface OnlineServices {
public enum RelativeArticle { BEFORE, AFTER };
public void saveArticleUnread(final Article article);
public void saveArticleMarked(final Article article);
public void saveArticlePublished(final Article article);
public void setSelectedArticle(Article article);
public boolean getUnreadArticlesOnly();
public void onCatSelected(FeedCategory cat);
public void onFeedSelected(Feed feed);
public void onArticleSelected(Article article);
public void onArticleListSelectionChange(ArticleList selection);
//public void initMainMenu();
public void login();
public String getSessionId();
public boolean isSmallScreen();
public boolean getUnreadOnly();
public int getApiLevel();
public boolean isPortrait();
public void copyToClipboard(String str);
}

View File

@ -1,16 +1,14 @@
package org.fox.ttrss; package org.fox.ttrss;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference; import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity; import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
public class PreferencesActivity extends PreferenceActivity { public class PreferencesActivity extends PreferenceActivity {
@SuppressWarnings("deprecation")
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@ -22,12 +20,12 @@ public class PreferencesActivity extends PreferenceActivity {
findPreference("justify_article_text").setEnabled(!prefs.getBoolean("combined_mode", false)); findPreference("justify_article_text").setEnabled(!prefs.getBoolean("combined_mode", false));
findPreference("combined_mode").setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { /* findPreference("combined_mode").setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override @Override
public boolean onPreferenceChange(Preference preference, Object newValue) { public boolean onPreferenceChange(Preference preference, Object newValue) {
findPreference("justify_article_text").setEnabled(!newValue.toString().equals("true")); findPreference("justify_article_text").setEnabled(!newValue.toString().equals("true"));
return true; return true;
} }
}); }); */
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -10,34 +10,42 @@ import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
import org.jsoup.select.Elements; import org.jsoup.select.Elements;
import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.BaseColumns; import android.provider.BaseColumns;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.text.Html; import android.text.Html;
import android.text.method.LinkMovementMethod; import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnClickListener;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings; import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebSettings.LayoutAlgorithm; import android.webkit.WebSettings.LayoutAlgorithm;
import android.webkit.WebView;
import android.widget.TextView; import android.widget.TextView;
public class OfflineArticleFragment extends Fragment { public class OfflineArticleFragment extends Fragment {
@SuppressWarnings("unused")
private final String TAG = this.getClass().getSimpleName(); private final String TAG = this.getClass().getSimpleName();
private SharedPreferences m_prefs; private SharedPreferences m_prefs;
private int m_articleId; private int m_articleId;
private boolean m_isCat = false; // FIXME use
private Cursor m_cursor; private Cursor m_cursor;
private OfflineServices m_offlineServices; private OfflineActivity m_activity;
public OfflineArticleFragment() { public OfflineArticleFragment() {
super(); super();
@ -48,6 +56,32 @@ public class OfflineArticleFragment extends Fragment {
m_articleId = articleId; m_articleId = articleId;
} }
@Override
public boolean onContextItemSelected(MenuItem item) {
/* AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
.getMenuInfo(); */
switch (item.getItemId()) {
case R.id.article_link_share:
m_activity.shareArticle(m_articleId);
return true;
case R.id.article_link_copy:
if (true) {
Cursor article = m_activity.getArticleById(m_articleId);
if (article != null) {
m_activity.copyToClipboard(article.getString(article.getColumnIndex("link")));
article.close();
}
}
return true;
default:
Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
return super.onContextItemSelected(item);
}
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) { ContextMenuInfo menuInfo) {
@ -59,6 +93,7 @@ public class OfflineArticleFragment extends Fragment {
} }
@SuppressLint("NewApi")
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -68,7 +103,7 @@ public class OfflineArticleFragment extends Fragment {
View view = inflater.inflate(R.layout.article_fragment, container, false); View view = inflater.inflate(R.layout.article_fragment, container, false);
m_cursor = m_offlineServices.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")", m_cursor = m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
new String[] { "articles.*", "feeds.title AS feed_title" }, "articles." + BaseColumns._ID + "=?", new String[] { "articles.*", "feeds.title AS feed_title" }, "articles." + BaseColumns._ID + "=?",
new String[] { String.valueOf(m_articleId) }, null, null, null); new String[] { String.valueOf(m_articleId) }, null, null, null);
@ -87,8 +122,24 @@ public class OfflineArticleFragment extends Fragment {
else else
titleStr = m_cursor.getString(m_cursor.getColumnIndex("title")); titleStr = m_cursor.getString(m_cursor.getColumnIndex("title"));
title.setMovementMethod(LinkMovementMethod.getInstance()); final String link = m_cursor.getString(m_cursor.getColumnIndex("link"));
title.setText(Html.fromHtml("<a href=\""+m_cursor.getString(m_cursor.getColumnIndex("link")).trim().replace("\"", "\\\"")+"\">" + titleStr + "</a>"));
title.setText(titleStr);
title.setPaintFlags(title.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
title.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse(link.trim()));
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
m_activity.toast(R.string.error_other_error);
}
}
});
registerForContextMenu(title); registerForContextMenu(title);
} }
@ -96,6 +147,16 @@ public class OfflineArticleFragment extends Fragment {
if (web != null) { if (web != null) {
web.setWebChromeClient(new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int progress) {
m_activity.setProgress(Math.round(((float)progress / 100f) * 10000));
if (progress == 100) {
m_activity.setProgressBarVisibility(false);
}
}
});
String content; String content;
String cssOverride = ""; String cssOverride = "";
@ -199,7 +260,7 @@ public class OfflineArticleFragment extends Fragment {
if (tagv != null) { if (tagv != null) {
int feedTitleIndex = m_cursor.getColumnIndex("feed_title"); int feedTitleIndex = m_cursor.getColumnIndex("feed_title");
if (feedTitleIndex != -1 && m_offlineServices.activeFeedIsCat()) { if (feedTitleIndex != -1 && m_isCat) {
tagv.setText(m_cursor.getString(feedTitleIndex)); tagv.setText(m_cursor.getString(feedTitleIndex));
} else { } else {
String tagsStr = m_cursor.getString(m_cursor.getColumnIndex("tags")); String tagsStr = m_cursor.getString(m_cursor.getColumnIndex("tags"));
@ -231,7 +292,7 @@ public class OfflineArticleFragment extends Fragment {
m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
m_offlineServices = (OfflineServices)activity; m_activity = (OfflineActivity) activity;
} }

View File

@ -3,36 +3,77 @@ package org.fox.ttrss.offline;
import org.fox.ttrss.R; import org.fox.ttrss.R;
import android.app.Activity; import android.app.Activity;
import android.database.sqlite.SQLiteStatement; import android.database.Cursor;
import android.os.Bundle; import android.os.Bundle;
import android.provider.BaseColumns; import android.provider.BaseColumns;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
public class OfflineArticlePager extends Fragment { public class OfflineArticlePager extends Fragment {
private final String TAG = this.getClass().getSimpleName();
private PagerAdapter m_adapter; private PagerAdapter m_adapter;
private OfflineServices m_offlineServices; private OfflineActivity m_activity;
private OfflineHeadlinesFragment m_hf; private OfflineHeadlinesEventListener m_listener;
private boolean m_isCat;
private int m_feedId;
private int m_articleId; private int m_articleId;
private String m_searchQuery = "";
private Cursor m_cursor;
public int getFeedId() {
return m_feedId;
}
public boolean getFeedIsCat() {
return m_isCat;
}
public Cursor createCursor() {
String feedClause = null;
if (m_isCat) {
feedClause = "feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?)";
} else {
feedClause = "feed_id = ?";
}
if (m_searchQuery == null || m_searchQuery.equals("")) {
return m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
new String[] { "articles."+BaseColumns._ID, "feeds.title AS feed_title" }, feedClause,
new String[] { String.valueOf(m_feedId) }, null, null, "updated DESC");
} else {
return m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
new String[] { "articles."+BaseColumns._ID },
feedClause + " AND (articles.title LIKE '%' || ? || '%' OR content LIKE '%' || ? || '%')",
new String[] { String.valueOf(m_feedId), m_searchQuery, m_searchQuery }, null, null, "updated DESC");
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
}
private class PagerAdapter extends FragmentStatePagerAdapter { private class PagerAdapter extends FragmentStatePagerAdapter {
public PagerAdapter(FragmentManager fm) { public PagerAdapter(FragmentManager fm) {
super(fm); super(fm);
} }
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
int articleId = m_hf.getArticleIdAtPosition(position); Log.d(TAG, "getItem: " + position);
if (articleId != 0) { if (m_cursor.moveToPosition(position)) {
return new OfflineArticleFragment(articleId); return new OfflineArticleFragment(m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID)));
} }
return null; return null;
@ -40,30 +81,48 @@ public class OfflineArticlePager extends Fragment {
@Override @Override
public int getCount() { public int getCount() {
return m_hf.getArticleCount(); return m_cursor.getCount();
} }
} }
public OfflineArticlePager() { public OfflineArticlePager() {
super(); super();
} }
public OfflineArticlePager(int articleId) { public OfflineArticlePager(int articleId, int feedId, boolean isCat) {
super(); super();
m_feedId = feedId;
m_isCat = isCat;
m_articleId = articleId; m_articleId = articleId;
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.article_pager, container, false); View view = inflater.inflate(R.layout.article_pager, container, false);
if (savedInstanceState != null) {
m_articleId = savedInstanceState.getInt("articleId", 0);
m_feedId = savedInstanceState.getInt("feedId", 0);
m_isCat = savedInstanceState.getBoolean("isCat", false);
}
m_adapter = new PagerAdapter(getActivity().getSupportFragmentManager()); m_adapter = new PagerAdapter(getActivity().getSupportFragmentManager());
ViewPager pager = (ViewPager) view.findViewById(R.id.article_pager); m_cursor.moveToFirst();
int position = m_hf.getArticleIdPosition(m_articleId); int position = 0;
while (!m_cursor.isLast()) {
if (m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID)) == m_articleId) {
position = m_cursor.getPosition();
break;
}
m_cursor.moveToNext();
}
ViewPager pager = (ViewPager) view.findViewById(R.id.article_pager);
pager.setAdapter(m_adapter); pager.setAdapter(m_adapter);
pager.setCurrentItem(position); pager.setCurrentItem(position);
@ -79,18 +138,13 @@ public class OfflineArticlePager extends Fragment {
@Override @Override
public void onPageSelected(int position) { public void onPageSelected(int position) {
int articleId = m_hf.getArticleIdAtPosition(position); if (m_cursor.moveToPosition(position)) {
int articleId = m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID));
if (articleId != 0) { m_listener.onArticleSelected(articleId, false);
m_offlineServices.setSelectedArticleId(articleId);
SQLiteStatement stmt = m_offlineServices.getWritableDb().compileStatement( m_articleId = articleId;
"UPDATE articles SET unread = 0 " + "WHERE " + BaseColumns._ID
+ " = ?");
stmt.bindLong(1, articleId);
stmt.execute();
stmt.close();
} }
} }
}); });
@ -102,8 +156,58 @@ public class OfflineArticlePager extends Fragment {
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
m_hf = (OfflineHeadlinesFragment) getActivity().getSupportFragmentManager().findFragmentByTag(OfflineActivity.FRAG_HEADLINES); m_activity = (OfflineActivity)activity;
m_offlineServices = (OfflineServices)activity; m_listener = (OfflineHeadlinesEventListener)activity;
m_cursor = createCursor();
} }
public void refresh() {
if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
m_cursor = createCursor();
if (m_cursor != null) {
m_adapter.notifyDataSetChanged();
}
}
public int getSelectedArticleId() {
return m_articleId;
}
@Override
public void onSaveInstanceState(Bundle out) {
super.onSaveInstanceState(out);
out.putInt("articleId", m_articleId);
out.putInt("feedId", m_feedId);
out.putBoolean("isCat", m_isCat);
}
public void setSearchQuery(String searchQuery) {
m_searchQuery = searchQuery;
}
public void setArticleId(int articleId) {
m_articleId = articleId;
m_cursor.moveToFirst();
int position = 0;
while (!m_cursor.isLast()) {
if (m_cursor.getInt(m_cursor.getColumnIndex(BaseColumns._ID)) == m_articleId) {
position = m_cursor.getPosition();
break;
}
m_cursor.moveToNext();
}
ViewPager pager = (ViewPager) getView().findViewById(R.id.article_pager);
pager.setCurrentItem(position);
}
} }

View File

@ -5,7 +5,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import org.fox.ttrss.ApiRequest; import org.fox.ttrss.ApiRequest;
import org.fox.ttrss.MainActivity; import org.fox.ttrss.OnlineActivity;
import org.fox.ttrss.R; import org.fox.ttrss.R;
import org.fox.ttrss.types.Article; import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.Feed; import org.fox.ttrss.types.Feed;
@ -46,8 +46,8 @@ public class OfflineDownloadService extends Service {
public static final String INTENT_ACTION_SUCCESS = "org.fox.ttrss.intent.action.DownloadComplete"; public static final String INTENT_ACTION_SUCCESS = "org.fox.ttrss.intent.action.DownloadComplete";
public static final String INTENT_ACTION_CANCEL = "org.fox.ttrss.intent.action.Cancel"; public static final String INTENT_ACTION_CANCEL = "org.fox.ttrss.intent.action.Cancel";
private static final int OFFLINE_SYNC_SEQ = 40; private static final int OFFLINE_SYNC_SEQ = 50;
private static final int OFFLINE_SYNC_MAX = 500; private static final int OFFLINE_SYNC_MAX = OFFLINE_SYNC_SEQ * 10;
private SQLiteDatabase m_writableDb; private SQLiteDatabase m_writableDb;
private SQLiteDatabase m_readableDb; private SQLiteDatabase m_readableDb;
@ -87,11 +87,12 @@ public class OfflineDownloadService extends Service {
initDatabase(); initDatabase();
} }
@SuppressWarnings("deprecation")
private void updateNotification(String msg) { private void updateNotification(String msg) {
Notification notification = new Notification(R.drawable.icon, Notification notification = new Notification(R.drawable.icon,
getString(R.string.notify_downloading_title), System.currentTimeMillis()); getString(R.string.notify_downloading_title), System.currentTimeMillis());
Intent intent = new Intent(this, MainActivity.class); Intent intent = new Intent(this, OnlineActivity.class);
intent.setAction(INTENT_ACTION_CANCEL); intent.setAction(INTENT_ACTION_CANCEL);
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
@ -158,9 +159,9 @@ public class OfflineDownloadService extends Service {
m_readableDb = dh.getReadableDatabase(); m_readableDb = dh.getReadableDatabase();
} }
private synchronized SQLiteDatabase getReadableDb() { /* private synchronized SQLiteDatabase getReadableDb() {
return m_readableDb; return m_readableDb;
} } */
private synchronized SQLiteDatabase getWritableDb() { private synchronized SQLiteDatabase getWritableDb() {
return m_writableDb; return m_writableDb;
@ -198,7 +199,9 @@ public class OfflineDownloadService extends Service {
ApiRequest req = new ApiRequest(getApplicationContext()) { ApiRequest req = new ApiRequest(getApplicationContext()) {
@Override @Override
protected void onPostExecute(JsonElement content) { protected JsonElement doInBackground(HashMap<String, String>... params) {
JsonElement content = super.doInBackground(params);
if (content != null) { if (content != null) {
try { try {
@ -226,18 +229,24 @@ public class OfflineDownloadService extends Service {
m_articleOffset = 0; m_articleOffset = 0;
getWritableDb().execSQL("DELETE FROM articles;"); getWritableDb().execSQL("DELETE FROM articles;");
if (m_canProceed) {
downloadArticles();
} else {
downloadFailed();
}
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
updateNotification(R.string.offline_switch_error); updateNotification(R.string.offline_switch_error);
downloadFailed(); downloadFailed();
} }
}
return content;
}
@Override
protected void onPostExecute(JsonElement content) {
if (content != null) {
if (m_canProceed) {
downloadArticles();
} else {
downloadFailed();
}
} else { } else {
updateNotification(getErrorMessage()); updateNotification(getErrorMessage());
downloadFailed(); downloadFailed();
@ -266,10 +275,10 @@ public class OfflineDownloadService extends Service {
getWritableDb().execSQL("DELETE FROM categories;"); getWritableDb().execSQL("DELETE FROM categories;");
ApiRequest req = new ApiRequest(getApplicationContext()) { ApiRequest req = new ApiRequest(getApplicationContext()) {
@Override protected JsonElement doInBackground(HashMap<String, String>... params) {
protected void onPostExecute(JsonElement content) { JsonElement content = super.doInBackground(params);
if (content != null) {
if (content != null) {
try { try {
Type listType = new TypeToken<List<FeedCategory>>() {}.getType(); Type listType = new TypeToken<List<FeedCategory>>() {}.getType();
List<FeedCategory> cats = new Gson().fromJson(content, listType); List<FeedCategory> cats = new Gson().fromJson(content, listType);
@ -289,17 +298,23 @@ public class OfflineDownloadService extends Service {
Log.d(TAG, "offline: done downloading categories"); Log.d(TAG, "offline: done downloading categories");
if (m_canProceed) {
downloadFeeds();
} else {
downloadFailed();
}
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
updateNotification(R.string.offline_switch_error); updateNotification(R.string.offline_switch_error);
downloadFailed(); downloadFailed();
} }
}
return content;
}
@Override
protected void onPostExecute(JsonElement content) {
if (content != null) {
if (m_canProceed) {
downloadFeeds();
} else {
downloadFailed();
}
} else { } else {
updateNotification(getErrorMessage()); updateNotification(getErrorMessage());
downloadFailed(); downloadFailed();
@ -335,22 +350,27 @@ public class OfflineDownloadService extends Service {
} }
public class OfflineArticlesRequest extends ApiRequest { public class OfflineArticlesRequest extends ApiRequest {
List<Article> m_articles;
public OfflineArticlesRequest(Context context) { public OfflineArticlesRequest(Context context) {
super(context); super(context);
} }
@Override @Override
protected void onPostExecute(JsonElement content) { protected JsonElement doInBackground(HashMap<String, String>... params) {
JsonElement content = super.doInBackground(params);
if (content != null) { if (content != null) {
try { try {
Type listType = new TypeToken<List<Article>>() {}.getType(); Type listType = new TypeToken<List<Article>>() {}.getType();
List<Article> articles = new Gson().fromJson(content, listType); m_articles = new Gson().fromJson(content, listType);
SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO articles " + SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO articles " +
"("+BaseColumns._ID+", unread, marked, published, updated, is_updated, title, link, feed_id, tags, content) " + "("+BaseColumns._ID+", unread, marked, published, updated, is_updated, title, link, feed_id, tags, content) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"); "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
for (Article article : articles) { for (Article article : m_articles) {
String tagsString = ""; String tagsString = "";
@ -402,25 +422,12 @@ public class OfflineDownloadService extends Service {
} }
m_articleOffset += m_articles.size();
Log.d(TAG, "offline: received " + m_articles.size() + " articles; canProc=" + m_canProceed);
stmtInsert.close(); stmtInsert.close();
//m_canGetMoreArticles = articles.size() == 30;
m_articleOffset += articles.size();
Log.d(TAG, "offline: received " + articles.size() + " articles; canProc=" + m_canProceed);
if (m_canProceed) {
if (articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < m_syncMax) {
downloadArticles();
} else {
downloadComplete();
}
} else {
downloadFailed();
}
return;
} catch (Exception e) { } catch (Exception e) {
updateNotification(R.string.offline_switch_error); updateNotification(R.string.offline_switch_error);
Log.d(TAG, "offline: failed: exception when loading articles"); Log.d(TAG, "offline: failed: exception when loading articles");
@ -428,6 +435,25 @@ public class OfflineDownloadService extends Service {
downloadFailed(); downloadFailed();
} }
}
return content;
}
@Override
protected void onPostExecute(JsonElement content) {
if (content != null) {
if (m_canProceed && m_articles != null) {
if (m_articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < m_syncMax) {
downloadArticles();
} else {
downloadComplete();
}
} else {
downloadFailed();
}
} else { } else {
Log.d(TAG, "offline: failed: " + getErrorMessage()); Log.d(TAG, "offline: failed: " + getErrorMessage());
updateNotification(getErrorMessage()); updateNotification(getErrorMessage());

View File

@ -16,6 +16,7 @@ import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
@ -31,7 +32,7 @@ public class OfflineFeedCategoriesFragment extends Fragment implements OnItemCli
private FeedCategoryListAdapter m_adapter; private FeedCategoryListAdapter m_adapter;
private int m_selectedCatId; private int m_selectedCatId;
private Cursor m_cursor; private Cursor m_cursor;
private OfflineServices m_offlineServices; private OfflineFeedsActivity m_activity;
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, public void onCreateContextMenu(ContextMenu menu, View v,
@ -50,11 +51,11 @@ public class OfflineFeedCategoriesFragment extends Fragment implements OnItemCli
} }
public Cursor createCursor() { public Cursor createCursor() {
String unreadOnly = BaseColumns._ID + "> 0 AND " + (m_offlineServices.getUnreadOnly() ? "unread > 0" : "1"); String unreadOnly = BaseColumns._ID + "> 0 AND " + (m_activity.getUnreadOnly() ? "unread > 0" : "1");
String order = m_prefs.getBoolean("sort_feeds_by_unread", false) ? "unread DESC, title" : "title"; String order = m_prefs.getBoolean("sort_feeds_by_unread", false) ? "unread DESC, title" : "title";
return m_offlineServices.getReadableDb().query("cats_unread", return m_activity.getReadableDb().query("cats_unread",
null, unreadOnly, null, null, null, order); null, unreadOnly, null, null, null, order);
} }
@ -69,6 +70,48 @@ public class OfflineFeedCategoriesFragment extends Fragment implements OnItemCli
} }
} }
@Override
public void onResume() {
super.onResume();
refresh();
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
.getMenuInfo();
switch (item.getItemId()) {
case R.id.browse_articles:
if (true) {
int catId = getCatIdAtPosition(info.position);
if (catId != -10000) {
m_activity.onCatSelected(catId, true);
}
}
return true;
case R.id.browse_feeds:
if (true) {
int catId = getCatIdAtPosition(info.position);
if (catId != -10000) {
m_activity.onCatSelected(catId, false);
}
}
return true;
case R.id.catchup_category:
if (true) {
int catId = getCatIdAtPosition(info.position);
if (catId != -10000) {
m_activity.catchupFeed(catId, true);
}
}
return true;
default:
Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
return super.onContextItemSelected(item);
}
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -106,7 +149,7 @@ public class OfflineFeedCategoriesFragment extends Fragment implements OnItemCli
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
m_offlineServices = (OfflineServices)activity; m_activity = (OfflineFeedsActivity)activity;
m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
m_prefs.registerOnSharedPreferenceChangeListener(this); m_prefs.registerOnSharedPreferenceChangeListener(this);
@ -131,9 +174,9 @@ public class OfflineFeedCategoriesFragment extends Fragment implements OnItemCli
int feedId = (int) cursor.getLong(0); int feedId = (int) cursor.getLong(0);
Log.d(TAG, "clicked on feed " + feedId); Log.d(TAG, "clicked on feed " + feedId);
m_offlineServices.onCatSelected(feedId); m_activity.onCatSelected(feedId);
if (!m_offlineServices.isSmallScreen()) if (!m_activity.isSmallScreen())
m_selectedCatId = feedId; m_selectedCatId = feedId;
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
@ -175,7 +218,7 @@ public class OfflineFeedCategoriesFragment extends Fragment implements OnItemCli
public int getItemViewType(int position) { public int getItemViewType(int position) {
Cursor cursor = (Cursor) this.getItem(position); Cursor cursor = (Cursor) this.getItem(position);
if (!m_offlineServices.isSmallScreen() && cursor.getLong(0) == m_selectedCatId) { if (!m_activity.isSmallScreen() && cursor.getLong(0) == m_selectedCatId) {
return VIEW_SELECTED; return VIEW_SELECTED;
} else { } else {
return VIEW_NORMAL; return VIEW_NORMAL;
@ -251,7 +294,7 @@ public class OfflineFeedCategoriesFragment extends Fragment implements OnItemCli
return catId; return catId;
} }
return 0; return -10000;
} }
public void setSelectedFeedId(int feedId) { public void setSelectedFeedId(int feedId) {

View File

@ -0,0 +1,253 @@
package org.fox.ttrss.offline;
import org.fox.ttrss.R;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.database.sqlite.SQLiteStatement;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
public class OfflineFeedsActivity extends OfflineActivity implements OfflineHeadlinesEventListener {
private final String TAG = this.getClass().getSimpleName();
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
m_prefs = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext());
if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) {
setTheme(R.style.DarkTheme);
} else {
setTheme(R.style.LightTheme);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.feeds);
setSmallScreen(findViewById(R.id.headlines_fragment) == null);
if (savedInstanceState != null) {
} else {
Intent intent = getIntent();
if (intent.getIntExtra("feed", -10000) != -10000 || intent.getIntExtra("category", -10000) != -10000 ||
intent.getIntExtra("article", -10000) != -10000) {
if (!isCompatMode()) {
getActionBar().setDisplayHomeAsUpEnabled(true);
}
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
int feedId = intent.getIntExtra("feed", -10000);
int catId = intent.getIntExtra("category", -10000);
int articleId = intent.getIntExtra("article", -10000);
boolean isCat = intent.getBooleanExtra("isCat", false);
if (articleId != -10000) {
ft.replace(R.id.feeds_fragment, new OfflineArticlePager(articleId, feedId, isCat), FRAG_ARTICLE);
} else {
if (feedId != -10000) {
ft.replace(R.id.feeds_fragment, new OfflineHeadlinesFragment(feedId, isCat), FRAG_HEADLINES);
}
if (catId != -10000) {
ft.replace(R.id.feeds_fragment, new OfflineFeedsFragment(catId), FRAG_FEEDS);
}
}
ft.commit();
} else {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if (m_prefs.getBoolean("enable_cats", false)) {
ft.replace(R.id.feeds_fragment, new OfflineFeedCategoriesFragment(), FRAG_CATS);
} else {
ft.replace(R.id.feeds_fragment, new OfflineFeedsFragment(), FRAG_FEEDS);
}
ft.commit();
}
}
setLoadingStatus(R.string.blank, false);
findViewById(R.id.loading_container).setVisibility(View.GONE);
initMenu();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.show_feeds:
setUnreadOnly(!getUnreadOnly());
initMenu();
refresh();
return true;
default:
Log.d(TAG, "onOptionsItemSelected, unhandled id=" + item.getItemId());
return super.onOptionsItemSelected(item);
}
}
@Override
public void onSaveInstanceState(Bundle out) {
super.onSaveInstanceState(out);
}
public void initMenu() {
super.initMenu();
if (m_menu != null) {
Fragment ff = getSupportFragmentManager().findFragmentByTag(FRAG_FEEDS);
Fragment cf = getSupportFragmentManager().findFragmentByTag(FRAG_CATS);
OfflineArticlePager af = (OfflineArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
m_menu.setGroupVisible(R.id.menu_group_feeds, (ff != null && ff.isAdded()) || (cf != null && cf.isAdded()));
m_menu.setGroupVisible(R.id.menu_group_article, af != null && af.isAdded());
m_menu.setGroupVisible(R.id.menu_group_headlines, hf != null && hf.isAdded() && getSelectedArticleCount() == 0);
m_menu.setGroupVisible(R.id.menu_group_headlines_selection, hf != null && hf.isAdded() && getSelectedArticleCount() != 0);
MenuItem item = m_menu.findItem(R.id.show_feeds);
if (getUnreadOnly()) {
item.setTitle(R.string.menu_all_feeds);
} else {
item.setTitle(R.string.menu_unread_feeds);
}
}
}
public void onCatSelected(int catId) {
onCatSelected(catId, m_prefs.getBoolean("browse_cats_like_feeds", false));
}
public void onCatSelected(int catId, boolean openAsFeed) {
if (openAsFeed) {
onFeedSelected(catId, true, true);
} else {
if (isSmallScreen()) {
Intent intent = new Intent(OfflineFeedsActivity.this, OfflineFeedsActivity.class);
intent.putExtra("category", catId);
startActivityForResult(intent, 0);
} else {
FragmentTransaction ft = getSupportFragmentManager()
.beginTransaction();
OfflineFeedsFragment ff = new OfflineFeedsFragment(catId);
ft.replace(R.id.feeds_fragment, ff, FRAG_FEEDS);
ft.addToBackStack(null);
ft.commit();
}
}
}
public void onFeedSelected(int feedId) {
onFeedSelected(feedId, false, true);
}
public void onFeedSelected(int feedId, boolean isCat, boolean open) {
if (open) {
if (isSmallScreen()) {
Intent intent = new Intent(OfflineFeedsActivity.this, OfflineFeedsActivity.class);
intent.putExtra("feed", feedId);
intent.putExtra("isCat", isCat);
startActivityForResult(intent, 0);
} else {
FragmentTransaction ft = getSupportFragmentManager()
.beginTransaction();
OfflineHeadlinesFragment hf = new OfflineHeadlinesFragment(feedId, isCat);
ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES);
ft.commit();
}
}
}
public void catchupFeed(int feedId, boolean isCat) {
if (isCat) {
SQLiteStatement stmt = getWritableDb().compileStatement(
"UPDATE articles SET unread = 0 WHERE feed_id IN (SELECT "+
BaseColumns._ID+" FROM feeds WHERE cat_id = ?)");
stmt.bindLong(1, feedId);
stmt.execute();
stmt.close();
} else {
SQLiteStatement stmt = getWritableDb().compileStatement(
"UPDATE articles SET unread = 0 WHERE feed_id = ?");
stmt.bindLong(1, feedId);
stmt.execute();
stmt.close();
}
refresh();
}
@Override
public void onArticleSelected(int articleId, boolean open) {
SQLiteStatement stmt = getWritableDb().compileStatement(
"UPDATE articles SET unread = 0 " + "WHERE " + BaseColumns._ID
+ " = ?");
stmt.bindLong(1, articleId);
stmt.execute();
stmt.close();
if (open) {
if (isSmallScreen()) {
OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
Intent intent = new Intent(OfflineFeedsActivity.this, OfflineFeedsActivity.class);
intent.putExtra("feed", hf.getFeedId());
intent.putExtra("isCat", hf.getFeedIsCat());
intent.putExtra("article", articleId);
startActivityForResult(intent, 0);
} else {
OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
Intent intent = new Intent(OfflineFeedsActivity.this, OfflineHeadlinesActivity.class);
intent.putExtra("feed", hf.getFeedId());
intent.putExtra("isCat", hf.getFeedIsCat());
intent.putExtra("article", articleId);
startActivityForResult(intent, 0);
}
} else {
refresh();
}
initMenu();
}
@Override
public void onArticleSelected(int articleId) {
onArticleSelected(articleId, true);
}
}

View File

@ -21,6 +21,7 @@ import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.AdapterView; import android.widget.AdapterView;
@ -39,7 +40,7 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene
private int m_catId = -1; private int m_catId = -1;
private boolean m_enableFeedIcons; private boolean m_enableFeedIcons;
private Cursor m_cursor; private Cursor m_cursor;
private OfflineServices m_offlineServices; private OfflineFeedsActivity m_activity;
public OfflineFeedsFragment() { public OfflineFeedsFragment() {
// //
@ -49,6 +50,29 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene
m_catId = catId; m_catId = catId;
} }
@Override
public void onResume() {
super.onResume();
refresh();
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
.getMenuInfo();
switch (item.getItemId()) {
case R.id.catchup_feed:
int feedId = getFeedIdAtPosition(info.position);
if (feedId != -10000) {
m_activity.catchupFeed(feedId, false);
}
return true;
default:
Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
return super.onContextItemSelected(item);
}
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) { ContextMenuInfo menuInfo) {
@ -66,14 +90,14 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene
} }
public Cursor createCursor() { public Cursor createCursor() {
String unreadOnly = m_offlineServices.getUnreadOnly() ? "unread > 0" : "1"; String unreadOnly = m_activity.getUnreadOnly() ? "unread > 0" : "1";
String order = m_prefs.getBoolean("sort_feeds_by_unread", false) ? "unread DESC, title" : "title"; String order = m_prefs.getBoolean("sort_feeds_by_unread", false) ? "unread DESC, title" : "title";
if (m_catId != -1) { if (m_catId != -1) {
return m_offlineServices.getReadableDb().query("feeds_unread", return m_activity.getReadableDb().query("feeds_unread",
null, unreadOnly + " AND cat_id = ?", new String[] { String.valueOf(m_catId) }, null, null, order); null, unreadOnly + " AND cat_id = ?", new String[] { String.valueOf(m_catId) }, null, null, order);
} else { } else {
return m_offlineServices.getReadableDb().query("feeds_unread", return m_activity.getReadableDb().query("feeds_unread",
null, unreadOnly, null, null, null, order); null, unreadOnly, null, null, null, order);
} }
} }
@ -129,7 +153,7 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
m_offlineServices = (OfflineServices)activity; m_activity = (OfflineFeedsActivity)activity;
m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
m_prefs.registerOnSharedPreferenceChangeListener(this); m_prefs.registerOnSharedPreferenceChangeListener(this);
@ -155,9 +179,9 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene
int feedId = (int) cursor.getLong(0); int feedId = (int) cursor.getLong(0);
Log.d(TAG, "clicked on feed " + feedId); Log.d(TAG, "clicked on feed " + feedId);
m_offlineServices.onFeedSelected(feedId); m_activity.onFeedSelected(feedId);
if (!m_offlineServices.isSmallScreen()) if (!m_activity.isSmallScreen())
m_selectedFeedId = feedId; m_selectedFeedId = feedId;
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
@ -199,7 +223,7 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene
public int getItemViewType(int position) { public int getItemViewType(int position) {
Cursor cursor = (Cursor) this.getItem(position); Cursor cursor = (Cursor) this.getItem(position);
if (!m_offlineServices.isSmallScreen() && cursor.getLong(0) == m_selectedFeedId) { if (!m_activity.isSmallScreen() && cursor.getLong(0) == m_selectedFeedId) {
return VIEW_SELECTED; return VIEW_SELECTED;
} else { } else {
return VIEW_NORMAL; return VIEW_NORMAL;
@ -295,7 +319,7 @@ public class OfflineFeedsFragment extends Fragment implements OnItemClickListene
return feedId; return feedId;
} }
return 0; return -10000;
} }
public void setSelectedFeedId(int feedId) { public void setSelectedFeedId(int feedId) {

View File

@ -0,0 +1,142 @@
package org.fox.ttrss.offline;
import org.fox.ttrss.GlobalState;
import org.fox.ttrss.R;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteStatement;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
public class OfflineHeadlinesActivity extends OfflineActivity implements OfflineHeadlinesEventListener {
@SuppressWarnings("unused")
private final String TAG = this.getClass().getSimpleName();
protected SharedPreferences m_prefs;
@SuppressLint("NewApi")
@Override
public void onCreate(Bundle savedInstanceState) {
m_prefs = PreferenceManager
.getDefaultSharedPreferences(getApplicationContext());
if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) {
setTheme(R.style.DarkTheme);
} else {
setTheme(R.style.LightTheme);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.headlines);
if (!isCompatMode()) {
getActionBar().setDisplayHomeAsUpEnabled(true);
}
setSmallScreen(findViewById(R.id.headlines_fragment) == null);
if (savedInstanceState == null) {
Intent i = getIntent();
if (i.getExtras() != null) {
int feedId = i.getIntExtra("feed", 0);
boolean isCat = i.getBooleanExtra("isCat", false);
int articleId = i.getIntExtra("article", 0);
String searchQuery = i.getStringExtra("searchQuery");
OfflineHeadlinesFragment hf = new OfflineHeadlinesFragment(feedId, isCat);
OfflineArticlePager af = new OfflineArticlePager(articleId, feedId, isCat);
hf.setActiveArticleId(articleId);
hf.setSearchQuery(searchQuery);
af.setSearchQuery(searchQuery);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.headlines_fragment, hf, FRAG_HEADLINES);
ft.replace(R.id.article_fragment, af, FRAG_ARTICLE);
ft.commit();
Cursor c;
if (isCat) {
c = getCatById(feedId);
} else {
c = getFeedById(feedId);
}
if (c != null) {
setTitle(c.getString(c.getColumnIndex("title")));
c.close();
}
}
}
setLoadingStatus(R.string.blank, false);
findViewById(R.id.loading_container).setVisibility(View.GONE);
initMenu();
}
@Override
public void onArticleSelected(int articleId, boolean open) {
SQLiteStatement stmt = getWritableDb().compileStatement(
"UPDATE articles SET unread = 0 " + "WHERE " + BaseColumns._ID
+ " = ?");
stmt.bindLong(1, articleId);
stmt.execute();
stmt.close();
if (open) {
OfflineArticlePager af = (OfflineArticlePager) getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
af.setArticleId(articleId);
} else {
OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
hf.setActiveArticleId(articleId);
}
GlobalState.getInstance().m_selectedArticleId = articleId;
initMenu();
refresh();
}
@Override
protected void initMenu() {
super.initMenu();
if (m_menu != null) {
m_menu.setGroupVisible(R.id.menu_group_feeds, false);
OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment)getSupportFragmentManager().findFragmentByTag(FRAG_HEADLINES);
m_menu.setGroupVisible(R.id.menu_group_headlines, hf != null && hf.getSelectedArticleCount() == 0);
m_menu.setGroupVisible(R.id.menu_group_headlines_selection, hf != null && hf.getSelectedArticleCount() != 0);
Fragment af = getSupportFragmentManager().findFragmentByTag(FRAG_ARTICLE);
m_menu.setGroupVisible(R.id.menu_group_article, af != null);
m_menu.findItem(R.id.search).setVisible(false);
}
}
@Override
public void onArticleSelected(int articleId) {
onArticleSelected(articleId, true);
}
}

View File

@ -0,0 +1,7 @@
package org.fox.ttrss.offline;
public interface OfflineHeadlinesEventListener {
void onArticleSelected(int articleId, boolean open);
void onArticleSelected(int articleId);
}

View File

@ -5,6 +5,7 @@ import java.text.SimpleDateFormat;
import java.util.Date; import java.util.Date;
import java.util.TimeZone; import java.util.TimeZone;
import org.fox.ttrss.GlobalState;
import org.fox.ttrss.R; import org.fox.ttrss.R;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
@ -27,6 +28,7 @@ import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -55,10 +57,12 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
private Cursor m_cursor; private Cursor m_cursor;
private ArticleListAdapter m_adapter; private ArticleListAdapter m_adapter;
private OfflineServices m_offlineServices; private OfflineHeadlinesEventListener m_listener;
private OfflineActivity m_activity;
private ImageGetter m_dummyGetter = new ImageGetter() { private ImageGetter m_dummyGetter = new ImageGetter() {
@SuppressWarnings("deprecation")
@Override @Override
public Drawable getDrawable(String source) { public Drawable getDrawable(String source) {
return new BitmapDrawable(); return new BitmapDrawable();
@ -83,7 +87,7 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
} }
public int getSelectedArticleCount() { public int getSelectedArticleCount() {
Cursor c = m_offlineServices.getReadableDb().query("articles", Cursor c = m_activity.getReadableDb().query("articles",
new String[] { "COUNT(*)" }, "selected = 1", null, null, null, null); new String[] { "COUNT(*)" }, "selected = 1", null, null, null, null);
c.moveToFirst(); c.moveToFirst();
int selected = c.getInt(0); int selected = c.getInt(0);
@ -92,11 +96,123 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
return selected; return selected;
} }
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
.getMenuInfo();
switch (item.getItemId()) {
case R.id.article_link_copy:
if (true) {
int articleId = getArticleIdAtPosition(info.position);
Cursor article = m_activity.getArticleById(articleId);
if (article != null) {
m_activity.copyToClipboard(article.getString(article.getColumnIndex("link")));
article.close();
}
}
return true;
case R.id.selection_toggle_marked:
if (getSelectedArticleCount() > 0) {
SQLiteStatement stmt = m_activity.getWritableDb()
.compileStatement(
"UPDATE articles SET marked = NOT marked WHERE selected = 1");
stmt.execute();
stmt.close();
} else {
int articleId = getArticleIdAtPosition(info.position);
SQLiteStatement stmt = m_activity.getWritableDb().compileStatement(
"UPDATE articles SET marked = NOT marked WHERE "
+ BaseColumns._ID + " = ?");
stmt.bindLong(1, articleId);
stmt.execute();
stmt.close();
}
refresh();
return true;
case R.id.selection_toggle_published:
if (getSelectedArticleCount() > 0) {
SQLiteStatement stmt = m_activity.getWritableDb()
.compileStatement(
"UPDATE articles SET published = NOT published WHERE selected = 1");
stmt.execute();
stmt.close();
} else {
int articleId = getArticleIdAtPosition(info.position);
SQLiteStatement stmt = m_activity.getWritableDb().compileStatement(
"UPDATE articles SET published = NOT published WHERE "
+ BaseColumns._ID + " = ?");
stmt.bindLong(1, articleId);
stmt.execute();
stmt.close();
}
refresh();
return true;
case R.id.selection_toggle_unread:
if (getSelectedArticleCount() > 0) {
SQLiteStatement stmt = m_activity.getWritableDb()
.compileStatement(
"UPDATE articles SET unread = NOT unread WHERE selected = 1");
stmt.execute();
stmt.close();
} else {
int articleId = getArticleIdAtPosition(info.position);
SQLiteStatement stmt = m_activity.getWritableDb().compileStatement(
"UPDATE articles SET unread = NOT unread WHERE "
+ BaseColumns._ID + " = ?");
stmt.bindLong(1, articleId);
stmt.execute();
stmt.close();
}
refresh();
return true;
case R.id.share_article:
if (true) {
int articleId = getArticleIdAtPosition(info.position);
m_activity.shareArticle(articleId);
}
return true;
case R.id.catchup_above:
if (true) {
int articleId = getArticleIdAtPosition(info.position);
SQLiteStatement stmt = null;
if (m_feedIsCat) {
stmt = m_activity.getWritableDb().compileStatement(
"UPDATE articles SET unread = 0 WHERE " +
"updated >= (SELECT updated FROM articles WHERE " + BaseColumns._ID + " = ?) " +
"AND feed_id IN (SELECT "+BaseColumns._ID+" FROM feeds WHERE cat_id = ?)");
} else {
stmt = m_activity.getWritableDb().compileStatement(
"UPDATE articles SET unread = 0 WHERE " +
"updated >= (SELECT updated FROM articles WHERE " + BaseColumns._ID + " = ?) " +
"AND feed_id = ?");
}
stmt.bindLong(1, articleId);
stmt.bindLong(2, m_feedId);
stmt.execute();
stmt.close();
}
refresh();
return true;
default:
Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
return super.onContextItemSelected(item);
}
}
@Override @Override
public void onCreateContextMenu(ContextMenu menu, View v, public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) { ContextMenuInfo menuInfo) {
getActivity().getMenuInflater().inflate(R.menu.headlines_menu, menu); getActivity().getMenuInflater().inflate(R.menu.headlines_context_menu, menu);
if (getSelectedArticleCount() > 0) { if (getSelectedArticleCount() > 0) {
menu.setHeaderTitle(R.string.headline_context_multiple); menu.setHeaderTitle(R.string.headline_context_multiple);
@ -107,12 +223,31 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
menu.setHeaderTitle(c.getString(c.getColumnIndex("title"))); menu.setHeaderTitle(c.getString(c.getColumnIndex("title")));
//c.close(); //c.close();
menu.setGroupVisible(R.id.menu_group_single_article, true); menu.setGroupVisible(R.id.menu_group_single_article, true);
menu.findItem(R.id.set_labels).setVisible(false);
menu.findItem(R.id.article_set_note).setVisible(false);
} }
super.onCreateContextMenu(menu, v, menuInfo); super.onCreateContextMenu(menu, v, menuInfo);
} }
@Override
public void onResume() {
super.onResume();
if (GlobalState.getInstance().m_selectedArticleId != 0) {
m_activeArticleId = GlobalState.getInstance().m_selectedArticleId;
GlobalState.getInstance().m_selectedArticleId = 0;
}
if (m_activeArticleId != 0) {
setActiveArticleId(m_activeArticleId);
}
refresh();
}
public void refresh() { public void refresh() {
if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close(); if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close();
@ -120,7 +255,6 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
if (m_cursor != null) { if (m_cursor != null) {
m_adapter.changeCursor(m_cursor); m_adapter.changeCursor(m_cursor);
setActiveArticleId(m_offlineServices.getSelectedArticleId());
m_adapter.notifyDataSetChanged(); m_adapter.notifyDataSetChanged();
} }
} }
@ -135,6 +269,8 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
m_combinedMode = savedInstanceState.getBoolean("combinedMode"); m_combinedMode = savedInstanceState.getBoolean("combinedMode");
m_searchQuery = (String) savedInstanceState.getCharSequence("searchQuery"); m_searchQuery = (String) savedInstanceState.getCharSequence("searchQuery");
m_feedIsCat = savedInstanceState.getBoolean("feedIsCat"); m_feedIsCat = savedInstanceState.getBoolean("feedIsCat");
} else {
m_activity.getWritableDb().execSQL("UPDATE articles SET selected = 0 ");
} }
View view = inflater.inflate(R.layout.headlines_fragment, container, false); View view = inflater.inflate(R.layout.headlines_fragment, container, false);
@ -150,7 +286,7 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
list.setEmptyView(view.findViewById(R.id.no_headlines)); list.setEmptyView(view.findViewById(R.id.no_headlines));
registerForContextMenu(list); registerForContextMenu(list);
if (m_offlineServices.isSmallScreen() || m_offlineServices.isPortrait()) if (m_activity.isSmallScreen() || m_activity.isPortrait())
view.findViewById(R.id.headlines_fragment).setPadding(0, 0, 0, 0); view.findViewById(R.id.headlines_fragment).setPadding(0, 0, 0, 0);
getActivity().setProgressBarIndeterminateVisibility(false); getActivity().setProgressBarIndeterminateVisibility(false);
@ -167,12 +303,12 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
feedClause = "feed_id = ?"; feedClause = "feed_id = ?";
} }
if (m_searchQuery.equals("")) { if (m_searchQuery == null || m_searchQuery.equals("")) {
return m_offlineServices.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")", return m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
new String[] { "articles.*", "feeds.title AS feed_title" }, feedClause, new String[] { "articles.*", "feeds.title AS feed_title" }, feedClause,
new String[] { String.valueOf(m_feedId) }, null, null, "updated DESC"); new String[] { String.valueOf(m_feedId) }, null, null, "updated DESC");
} else { } else {
return m_offlineServices.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")", return m_activity.getReadableDb().query("articles LEFT JOIN feeds ON (feed_id = feeds."+BaseColumns._ID+")",
new String[] { "articles.*", "feeds.title AS feed_title" }, new String[] { "articles.*", "feeds.title AS feed_title" },
feedClause + " AND (articles.title LIKE '%' || ? || '%' OR content LIKE '%' || ? || '%')", feedClause + " AND (articles.title LIKE '%' || ? || '%' OR content LIKE '%' || ? || '%')",
new String[] { String.valueOf(m_feedId), m_searchQuery, m_searchQuery }, null, null, "updated DESC"); new String[] { String.valueOf(m_feedId), m_searchQuery, m_searchQuery }, null, null, "updated DESC");
@ -182,10 +318,11 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
@Override @Override
public void onAttach(Activity activity) { public void onAttach(Activity activity) {
super.onAttach(activity); super.onAttach(activity);
m_offlineServices = (OfflineServices)activity; m_listener = (OfflineHeadlinesEventListener) activity;
m_activity = (OfflineActivity) activity;
m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext());
m_combinedMode = m_prefs.getBoolean("combined_mode", false); m_combinedMode = false; /* m_prefs.getBoolean("combined_mode", false); */
} }
@Override @Override
@ -197,17 +334,22 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
if (list != null) { if (list != null) {
Cursor cursor = (Cursor)list.getItemAtPosition(position); Cursor cursor = (Cursor)list.getItemAtPosition(position);
m_activeArticleId = cursor.getInt(0); int articleId = cursor.getInt(0);
SQLiteStatement stmtUpdate = m_offlineServices.getWritableDb().compileStatement("UPDATE articles SET unread = 0 " + if (getActivity().findViewById(R.id.article_fragment) != null) {
"WHERE " + BaseColumns._ID + " = ?"); m_activeArticleId = articleId;
}
stmtUpdate.bindLong(1, m_activeArticleId);
stmtUpdate.execute();
stmtUpdate.close();
if (!m_combinedMode) { if (!m_combinedMode) {
m_offlineServices.openArticle(m_activeArticleId, 0); m_listener.onArticleSelected(articleId);
} else {
SQLiteStatement stmt = m_activity.getWritableDb().compileStatement(
"UPDATE articles SET unread = 0 " + "WHERE " + BaseColumns._ID
+ " = ?");
stmt.bindLong(1, articleId);
stmt.execute();
stmt.close();
} }
refresh(); refresh();
@ -321,7 +463,10 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
if (ft != null && feedTitleIndex != -1 && m_feedIsCat) { if (ft != null && feedTitleIndex != -1 && m_feedIsCat) {
String feedTitle = article.getString(feedTitleIndex); String feedTitle = article.getString(feedTitleIndex);
if (feedTitle != null) { if (feedTitle.length() > 20)
feedTitle = feedTitle.substring(0, 20) + "...";
if (feedTitle.length() > 0) {
ft.setText(feedTitle); ft.setText(feedTitle);
} else { } else {
ft.setVisibility(View.GONE); ft.setVisibility(View.GONE);
@ -339,7 +484,7 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
@Override @Override
public void onClick(View v) { public void onClick(View v) {
SQLiteStatement stmtUpdate = m_offlineServices.getWritableDb().compileStatement("UPDATE articles SET marked = NOT marked " + SQLiteStatement stmtUpdate = m_activity.getWritableDb().compileStatement("UPDATE articles SET marked = NOT marked " +
"WHERE " + BaseColumns._ID + " = ?"); "WHERE " + BaseColumns._ID + " = ?");
stmtUpdate.bindLong(1, articleId); stmtUpdate.bindLong(1, articleId);
@ -360,7 +505,7 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
@Override @Override
public void onClick(View v) { public void onClick(View v) {
SQLiteStatement stmtUpdate = m_offlineServices.getWritableDb().compileStatement("UPDATE articles SET published = NOT published " + SQLiteStatement stmtUpdate = m_activity.getWritableDb().compileStatement("UPDATE articles SET published = NOT published " +
"WHERE " + BaseColumns._ID + " = ?"); "WHERE " + BaseColumns._ID + " = ?");
stmtUpdate.bindLong(1, articleId); stmtUpdate.bindLong(1, articleId);
@ -438,7 +583,7 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
public void onClick(View view) { public void onClick(View view) {
CheckBox cb = (CheckBox)view; CheckBox cb = (CheckBox)view;
SQLiteStatement stmtUpdate = m_offlineServices.getWritableDb().compileStatement("UPDATE articles SET selected = ? " + SQLiteStatement stmtUpdate = m_activity.getWritableDb().compileStatement("UPDATE articles SET selected = ? " +
"WHERE " + BaseColumns._ID + " = ?"); "WHERE " + BaseColumns._ID + " = ?");
stmtUpdate.bindLong(1, cb.isChecked() ? 1 : 0); stmtUpdate.bindLong(1, cb.isChecked() ? 1 : 0);
@ -448,7 +593,7 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
refresh(); refresh();
m_offlineServices.initMainMenu(); m_activity.initMenu();
} }
}); });
@ -476,12 +621,18 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
public void setActiveArticleId(int articleId) { public void setActiveArticleId(int articleId) {
m_activeArticleId = articleId; m_activeArticleId = articleId;
// m_adapter.notifyDataSetChanged(); try {
m_adapter.notifyDataSetChanged();
ListView list = (ListView)getView().findViewById(R.id.headlines); ListView list = (ListView)getView().findViewById(R.id.headlines);
if (list != null) { Log.d(TAG, articleId + " position " + getArticleIdPosition(articleId));
list.setSelection(getArticleIdPosition(articleId));
if (list != null) {
list.setSelection(getArticleIdPosition(articleId));
}
} catch (NullPointerException e) {
// invoked before view is created, nvm
} }
} }
@ -520,8 +671,19 @@ public class OfflineHeadlinesFragment extends Fragment implements OnItemClickLis
public void setSearchQuery(String query) { public void setSearchQuery(String query) {
if (!m_searchQuery.equals(query)) { if (!m_searchQuery.equals(query)) {
m_searchQuery = query; m_searchQuery = query;
refresh();
} }
} }
public int getFeedId() {
return m_feedId;
}
public boolean getFeedIsCat() {
return m_feedIsCat;
}
public String getSearchQuery() {
return m_searchQuery;
}
} }

View File

@ -1,22 +0,0 @@
package org.fox.ttrss.offline;
import org.fox.ttrss.OnlineServices;
import android.database.sqlite.SQLiteDatabase;
public interface OfflineServices {
public int getActiveFeedId();
public SQLiteDatabase getReadableDb();
public SQLiteDatabase getWritableDb();
public int getRelativeArticleId(int baseId, int feedId, OnlineServices.RelativeArticle mode);
public void onFeedSelected(int feedId);
public void onCatSelected(int catId);
public void openArticle(int articleId, int compatAnimation);
public boolean getUnreadOnly();
public int getSelectedArticleId();
public void initMainMenu();
public boolean isSmallScreen();
public void setSelectedArticleId(int articleId);
public boolean activeFeedIsCat();
public boolean isPortrait();
}

View File

@ -3,7 +3,7 @@ package org.fox.ttrss.offline;
import java.util.HashMap; import java.util.HashMap;
import org.fox.ttrss.ApiRequest; import org.fox.ttrss.ApiRequest;
import org.fox.ttrss.MainActivity; import org.fox.ttrss.OnlineActivity;
import org.fox.ttrss.R; import org.fox.ttrss.R;
import org.fox.ttrss.util.DatabaseHelper; import org.fox.ttrss.util.DatabaseHelper;
@ -48,12 +48,13 @@ public class OfflineUploadService extends IntentService {
m_nmgr.cancel(NOTIFY_UPLOADING); m_nmgr.cancel(NOTIFY_UPLOADING);
} }
@SuppressWarnings("deprecation")
private void updateNotification(String msg) { private void updateNotification(String msg) {
Notification notification = new Notification(R.drawable.icon, Notification notification = new Notification(R.drawable.icon,
getString(R.string.notify_uploading_title), System.currentTimeMillis()); getString(R.string.notify_uploading_title), System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, MainActivity.class), 0); new Intent(this, OnlineActivity.class), 0);
notification.flags |= Notification.FLAG_ONGOING_EVENT; notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE; notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE;

View File

@ -21,6 +21,14 @@ public class ArticleList extends ArrayList<Article> implements Parcelable {
} }
} }
public Article findById(int id) {
for (Article a : this) {
if (a.id == id)
return a;
}
return null;
}
public void readFromParcel(Parcel in) { public void readFromParcel(Parcel in) {
int length = in.readInt(); int length = in.readInt();

View File

@ -28,6 +28,16 @@ public class Feed implements Comparable<Feed>, Parcelable {
} }
public boolean equals(Feed feed) {
if (feed == this)
return true;
if (feed == null)
return false;
return feed.id == this.id && (this.title == null || this.title.equals(feed.title)) && this.is_cat == feed.is_cat;
}
@Override @Override
public int compareTo(Feed feed) { public int compareTo(Feed feed) {
if (feed.unread != this.unread) if (feed.unread != this.unread)

View File

@ -1,582 +0,0 @@
// Portions copyright 2002, Google, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.fox.ttrss.util;
// This code was converted from code at http://iharder.sourceforge.net/base64/
// Lots of extraneous features were removed.
/* The original code said:
* <p>
* I am placing this code in the Public Domain. Do with it as you will.
* This software comes with no guarantees or warranties but with
* plenty of well-wishing instead!
* Please visit
* <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
* periodically to check for updates or to contribute improvements.
* </p>
*
* @author Robert Harder
* @author rharder@usa.net
* @version 1.3
*/
/**
* Base64 converter class. This code is not a complete MIME encoder; it simply
* converts binary data to base64 data and back.
*
* <p>
* Note {@link CharBase64} is a GWT-compatible implementation of this class.
*/
public class Base64 {
/** Specify encoding (value is {@code true}). */
public final static boolean ENCODE = true;
/** Specify decoding (value is {@code false}). */
public final static boolean DECODE = false;
/** The equals sign (=) as a byte. */
private final static byte EQUALS_SIGN = (byte) '=';
/** The new line character (\n) as a byte. */
private final static byte NEW_LINE = (byte) '\n';
/**
* The 64 valid Base64 values.
*/
private final static byte[] ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G',
(byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q',
(byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a',
(byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k',
(byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u',
(byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4',
(byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' };
/**
* The 64 valid web safe Base64 values.
*/
private final static byte[] WEBSAFE_ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G',
(byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q',
(byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a',
(byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k',
(byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u',
(byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4',
(byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_' };
/**
* Translates a Base64 value to either its 6-bit reconstruction value or a
* negative number indicating some other meaning.
**/
private final static byte[] DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal
// 0
// -
// 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 -
// 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
62, // Plus sign at decimal 43
-9, -9, -9, // Decimal 44 - 46
63, // Slash at decimal 47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through
// 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O'
// through 'Z'
-9, -9, -9, -9, -9, -9, // Decimal 91 - 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a'
// through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n'
// through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
/*
* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
*/
};
/** The web safe decodabet */
private final static byte[] WEBSAFE_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal
// 0
// -
// 8
-5, -5, // Whitespace: Tab and Linefeed
-9, -9, // Decimal 11 - 12
-5, // Whitespace: Carriage Return
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 -
// 26
-9, -9, -9, -9, -9, // Decimal 27 - 31
-5, // Whitespace: Space
-9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
62, // Dash '-' sign at decimal 45
-9, -9, // Decimal 46-47
52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
-9, -9, -9, // Decimal 58 - 60
-1, // Equals sign at decimal 61
-9, -9, -9, // Decimal 62 - 64
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through
// 'N'
14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O'
// through 'Z'
-9, -9, -9, -9, // Decimal 91-94
63, // Underscore '_' at decimal 95
-9, // Decimal 96
26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a'
// through 'm'
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n'
// through 'z'
-9, -9, -9, -9, -9 // Decimal 123 - 127
/*
* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
* -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
*/
};
// Indicates white space in encoding
private final static byte WHITE_SPACE_ENC = -5;
// Indicates equals sign in encoding
private final static byte EQUALS_SIGN_ENC = -1;
/** Defeats instantiation. */
private Base64() {
}
/* ******** E N C O D I N G M E T H O D S ******** */
/**
* Encodes up to three bytes of the array <var>source</var> and writes the
* resulting four Base64 bytes to <var>destination</var>. The source and
* destination arrays can be manipulated anywhere along their length by
* specifying <var>srcOffset</var> and <var>destOffset</var>. This method
* does not check to make sure your arrays are large enough to accommodate
* <var>srcOffset</var> + 3 for the <var>source</var> array or
* <var>destOffset</var> + 4 for the <var>destination</var> array. The
* actual number of significant bytes in your array is given by
* <var>numSigBytes</var>.
*
* @param source
* the array to convert
* @param srcOffset
* the index where conversion begins
* @param numSigBytes
* the number of significant bytes in your array
* @param destination
* the array to hold the conversion
* @param destOffset
* the index where output will be put
* @param alphabet
* is the encoding alphabet
* @return the <var>destination</var> array
* @since 1.3
*/
private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
// 1 2 3
// 01234567890123456789012345678901 Bit position
// --------000000001111111122222222 Array position from threeBytes
// --------| || || || | Six bit groups to index alphabet
// >>18 >>12 >> 6 >> 0 Right shift necessary
// 0x3f 0x3f 0x3f Additional AND
// Create buffer with zero-padding if there are only one or two
// significant bytes passed in the array.
// We have to shift left 24 in order to flush out the 1's that appear
// when Java treats a value as negative that is cast from a byte to an
// int.
int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
| (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
| (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
switch (numSigBytes) {
case 3:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
return destination;
case 2:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
case 1:
destination[destOffset] = alphabet[(inBuff >>> 18)];
destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
destination[destOffset + 2] = EQUALS_SIGN;
destination[destOffset + 3] = EQUALS_SIGN;
return destination;
default:
return destination;
} // end switch
} // end encode3to4
/**
* Encodes a byte array into Base64 notation. Equivalent to calling {@code
* encodeBytes(source, 0, source.length)}
*
* @param source
* The data to convert
* @since 1.4
*/
public static String encode(byte[] source) {
return encode(source, 0, source.length, ALPHABET, true);
}
/**
* Encodes a byte array into web safe Base64 notation.
*
* @param source
* The data to convert
* @param doPadding
* is {@code true} to pad result with '=' chars if it does not
* fall on 3 byte boundaries
*/
public static String encodeWebSafe(byte[] source, boolean doPadding) {
return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
}
/**
* Encodes a byte array into Base64 notation.
*
* @param source
* the data to convert
* @param off
* offset in array where conversion should begin
* @param len
* length of data to convert
* @param alphabet
* the encoding alphabet
* @param doPadding
* is {@code true} to pad result with '=' chars if it does not
* fall on 3 byte boundaries
* @since 1.4
*/
public static String encode(byte[] source, int off, int len, byte[] alphabet, boolean doPadding) {
byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
int outLen = outBuff.length;
// If doPadding is false, set length to truncate '='
// padding characters
while (doPadding == false && outLen > 0) {
if (outBuff[outLen - 1] != '=') {
break;
}
outLen -= 1;
}
return new String(outBuff, 0, outLen);
}
/**
* Encodes a byte array into Base64 notation.
*
* @param source
* the data to convert
* @param off
* offset in array where conversion should begin
* @param len
* length of data to convert
* @param alphabet
* is the encoding alphabet
* @param maxLineLength
* maximum length of one line.
* @return the BASE64-encoded byte array
*/
public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, int maxLineLength) {
int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
int len43 = lenDiv3 * 4;
byte[] outBuff = new byte[len43 // Main 4:3
+ (len43 / maxLineLength)]; // New lines
int d = 0;
int e = 0;
int len2 = len - 2;
int lineLength = 0;
for (; d < len2; d += 3, e += 4) {
// The following block of code is the same as
// encode3to4( source, d + off, 3, outBuff, e, alphabet );
// but inlined for faster encoding (~20% improvement)
int inBuff = ((source[d + off] << 24) >>> 8) | ((source[d + 1 + off] << 24) >>> 16) | ((source[d + 2 + off] << 24) >>> 24);
outBuff[e] = alphabet[(inBuff >>> 18)];
outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
lineLength += 4;
if (lineLength == maxLineLength) {
outBuff[e + 4] = NEW_LINE;
e++;
lineLength = 0;
} // end if: end of line
} // end for: each piece of array
if (d < len) {
encode3to4(source, d + off, len - d, outBuff, e, alphabet);
lineLength += 4;
if (lineLength == maxLineLength) {
// Add a last newline
outBuff[e + 4] = NEW_LINE;
e++;
}
e += 4;
}
assert (e == outBuff.length);
return outBuff;
}
/* ******** D E C O D I N G M E T H O D S ******** */
/**
* Decodes four bytes from array <var>source</var> and writes the resulting
* bytes (up to three of them) to <var>destination</var>. The source and
* destination arrays can be manipulated anywhere along their length by
* specifying <var>srcOffset</var> and <var>destOffset</var>. This method
* does not check to make sure your arrays are large enough to accommodate
* <var>srcOffset</var> + 4 for the <var>source</var> array or
* <var>destOffset</var> + 3 for the <var>destination</var> array. This
* method returns the actual number of bytes that were converted from the
* Base64 encoding.
*
*
* @param source
* the array to convert
* @param srcOffset
* the index where conversion begins
* @param destination
* the array to hold the conversion
* @param destOffset
* the index where output will be put
* @param decodabet
* the decodabet for decoding Base64 content
* @return the number of decoded bytes converted
* @since 1.3
*/
private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset, byte[] decodabet) {
// Example: Dk==
if (source[srcOffset + 2] == EQUALS_SIGN) {
int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
destination[destOffset] = (byte) (outBuff >>> 16);
return 1;
} else if (source[srcOffset + 3] == EQUALS_SIGN) {
// Example: DkL=
int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
destination[destOffset] = (byte) (outBuff >>> 16);
destination[destOffset + 1] = (byte) (outBuff >>> 8);
return 2;
} else {
// Example: DkLE
int outBuff = ((decodabet[source[srcOffset]] << 24) >>> 6) | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
| ((decodabet[source[srcOffset + 2]] << 24) >>> 18) | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
destination[destOffset] = (byte) (outBuff >> 16);
destination[destOffset + 1] = (byte) (outBuff >> 8);
destination[destOffset + 2] = (byte) (outBuff);
return 3;
}
} // end decodeToBytes
/**
* Decodes data from Base64 notation.
*
* @param s
* the string to decode (decoded in default encoding)
* @return the decoded data
* @since 1.4
*/
public static byte[] decode(String s) throws Base64DecoderException {
byte[] bytes = s.getBytes();
return decode(bytes, 0, bytes.length);
}
/**
* Decodes data from web safe Base64 notation. Web safe encoding uses '-'
* instead of '+', '_' instead of '/'
*
* @param s
* the string to decode (decoded in default encoding)
* @return the decoded data
*/
public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
byte[] bytes = s.getBytes();
return decodeWebSafe(bytes, 0, bytes.length);
}
/**
* Decodes Base64 content in byte array format and returns the decoded byte
* array.
*
* @param source
* The Base64 encoded data
* @return decoded data
* @since 1.3
* @throws Base64DecoderException
*/
public static byte[] decode(byte[] source) throws Base64DecoderException {
return decode(source, 0, source.length);
}
/**
* Decodes web safe Base64 content in byte array format and returns the
* decoded data. Web safe encoding uses '-' instead of '+', '_' instead of
* '/'
*
* @param source
* the string to decode (decoded in default encoding)
* @return the decoded data
*/
public static byte[] decodeWebSafe(byte[] source) throws Base64DecoderException {
return decodeWebSafe(source, 0, source.length);
}
/**
* Decodes Base64 content in byte array format and returns the decoded byte
* array.
*
* @param source
* the Base64 encoded data
* @param off
* the offset of where to begin decoding
* @param len
* the length of characters to decode
* @return decoded data
* @since 1.3
* @throws Base64DecoderException
*/
public static byte[] decode(byte[] source, int off, int len) throws Base64DecoderException {
return decode(source, off, len, DECODABET);
}
/**
* Decodes web safe Base64 content in byte array format and returns the
* decoded byte array. Web safe encoding uses '-' instead of '+', '_'
* instead of '/'
*
* @param source
* the Base64 encoded data
* @param off
* the offset of where to begin decoding
* @param len
* the length of characters to decode
* @return decoded data
*/
public static byte[] decodeWebSafe(byte[] source, int off, int len) throws Base64DecoderException {
return decode(source, off, len, WEBSAFE_DECODABET);
}
/**
* Decodes Base64 content using the supplied decodabet and returns the
* decoded byte array.
*
* @param source
* the Base64 encoded data
* @param off
* the offset of where to begin decoding
* @param len
* the length of characters to decode
* @param decodabet
* the decodabet for decoding Base64 content
* @return decoded data
*/
public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) throws Base64DecoderException {
int len34 = len * 3 / 4;
byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
int outBuffPosn = 0;
byte[] b4 = new byte[4];
int b4Posn = 0;
int i = 0;
byte sbiCrop = 0;
byte sbiDecode = 0;
for (i = 0; i < len; i++) {
sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven
// bits
sbiDecode = decodabet[sbiCrop];
if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or
// better
if (sbiDecode >= EQUALS_SIGN_ENC) {
// An equals sign (for padding) must not occur at position 0
// or 1
// and must be the last byte[s] in the encoded value
if (sbiCrop == EQUALS_SIGN) {
int bytesLeft = len - i;
byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
if (b4Posn == 0 || b4Posn == 1) {
throw new Base64DecoderException("invalid padding byte '=' at byte offset " + i);
} else if ((b4Posn == 3 && bytesLeft > 2) || (b4Posn == 4 && bytesLeft > 1)) {
throw new Base64DecoderException("padding byte '=' falsely signals end of encoded value " + "at offset " + i);
} else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
throw new Base64DecoderException("encoded value has invalid trailing byte");
}
break;
}
b4[b4Posn++] = sbiCrop;
if (b4Posn == 4) {
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
b4Posn = 0;
}
}
} else {
throw new Base64DecoderException("Bad Base64 input character at " + i + ": " + source[i + off] + "(decimal)");
}
}
// Because web safe encoding allows non padding base64 encodes, we
// need to pad the rest of the b4 buffer with equal signs when
// b4Posn != 0. There can be at most 2 equal signs at the end of
// four characters, so the b4 buffer must have two or three
// characters. This also catches the case where the input is
// padded with EQUALS_SIGN
if (b4Posn != 0) {
if (b4Posn == 1) {
// Ensure you have set your public key
throw new Base64DecoderException("single trailing character at offset " + (len - 1));
}
b4[b4Posn++] = EQUALS_SIGN;
outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
}
byte[] out = new byte[outBuffPosn];
System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
return out;
}
}

View File

@ -1,32 +0,0 @@
// Copyright 2002, Google, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.fox.ttrss.util;;
/**
* Exception thrown when encountering an invalid Base64 input character.
*
* @author nelson
*/
public class Base64DecoderException extends Exception {
public Base64DecoderException() {
super();
}
public Base64DecoderException(String s) {
super(s);
}
private static final long serialVersionUID = 1L;
}

View File

@ -1,120 +0,0 @@
package org.fox.ttrss.util;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.scheme.LayeredSocketFactory;
import org.apache.http.conn.scheme.SocketFactory;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;
public class EasySSLSocketFactory implements SocketFactory, LayeredSocketFactory
{
private SSLContext sslcontext = null;
private static SSLContext createEasySSLContext() throws IOException
{
try
{
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[] { new EasyX509TrustManager() }, null);
return context;
}
catch (Exception e)
{
throw new IOException(e.getMessage());
}
}
private SSLContext getSSLContext() throws IOException
{
if (this.sslcontext == null)
{
this.sslcontext = createEasySSLContext();
}
return this.sslcontext;
}
/**
* @see org.apache.http.conn.scheme.SocketFactory#connectSocket(java.net.Socket, java.lang.String, int,
* java.net.InetAddress, int, org.apache.http.params.HttpParams)
*/
public Socket connectSocket(Socket sock,
String host,
int port,
InetAddress localAddress,
int localPort,
HttpParams params)
throws IOException, UnknownHostException, ConnectTimeoutException
{
int connTimeout = HttpConnectionParams.getConnectionTimeout(params);
int soTimeout = HttpConnectionParams.getSoTimeout(params);
InetSocketAddress remoteAddress = new InetSocketAddress(host, port);
SSLSocket sslsock = (SSLSocket) ((sock != null) ? sock : createSocket());
if ((localAddress != null) || (localPort > 0))
{
// we need to bind explicitly
if (localPort < 0)
{
localPort = 0; // indicates "any"
}
InetSocketAddress isa = new InetSocketAddress(localAddress, localPort);
sslsock.bind(isa);
}
sslsock.connect(remoteAddress, connTimeout);
sslsock.setSoTimeout(soTimeout);
return sslsock;
}
/**
* @see org.apache.http.conn.scheme.SocketFactory#createSocket()
*/
public Socket createSocket() throws IOException {
return getSSLContext().getSocketFactory().createSocket();
}
/**
* @see org.apache.http.conn.scheme.SocketFactory#isSecure(java.net.Socket)
*/
public boolean isSecure(Socket socket) throws IllegalArgumentException {
return true;
}
/**
* @see org.apache.http.conn.scheme.LayeredSocketFactory#createSocket(java.net.Socket, java.lang.String, int,
* boolean)
*/
public Socket createSocket(Socket socket,
String host,
int port,
boolean autoClose) throws IOException,
UnknownHostException
{
return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
}
// -------------------------------------------------------------------
// javadoc in org.apache.http.conn.scheme.SocketFactory says :
// Both Object.equals() and Object.hashCode() must be overridden
// for the correct operation of some connection managers
// -------------------------------------------------------------------
public boolean equals(Object obj) {
return ((obj != null) && obj.getClass().equals(EasySSLSocketFactory.class));
}
public int hashCode() {
return EasySSLSocketFactory.class.hashCode();
}
}

View File

@ -1,26 +0,0 @@
package org.fox.ttrss.util;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
// http://stackoverflow.com/questions/6989116/httpget-not-working-due-to-not-trusted-server-certificate-but-it-works-with-ht
public class EasyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException { }
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException { }
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}

View File

@ -0,0 +1,82 @@
package org.fox.ttrss.util;
import java.lang.reflect.Type;
import java.util.List;
import org.fox.ttrss.ApiRequest;
import org.fox.ttrss.GlobalState;
import org.fox.ttrss.OnlineActivity;
import org.fox.ttrss.R;
import org.fox.ttrss.types.Article;
import org.fox.ttrss.types.ArticleList;
import android.content.Context;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.reflect.TypeToken;
public class HeadlinesRequest extends ApiRequest {
public static final int HEADLINES_REQUEST_SIZE = 30;
public static final int HEADLINES_BUFFER_MAX = 500;
private int m_offset = 0;
private OnlineActivity m_activity;
private ArticleList m_articles = GlobalState.getInstance().m_loadedArticles;
public HeadlinesRequest(Context context, OnlineActivity activity) {
super(context);
m_activity = activity;
}
protected void onPostExecute(JsonElement result) {
if (result != null) {
try {
JsonArray content = result.getAsJsonArray();
if (content != null) {
Type listType = new TypeToken<List<Article>>() {}.getType();
final List<Article> articles = new Gson().fromJson(content, listType);
while (m_articles.size() > HEADLINES_BUFFER_MAX)
m_articles.remove(0);
if (m_offset == 0)
m_articles.clear();
else
if (m_articles.get(m_articles.size()-1).id == -1)
m_articles.remove(m_articles.size()-1); // remove previous placeholder
for (Article f : articles)
m_articles.add(f);
if (articles.size() == HEADLINES_REQUEST_SIZE) {
Article placeholder = new Article(-1);
m_articles.add(placeholder);
}
if (m_articles.size() == 0)
m_activity.setLoadingStatus(R.string.no_headlines_to_display, false);
else
m_activity.setLoadingStatus(R.string.blank, false);
return;
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (m_lastError == ApiError.LOGIN_FAILED) {
m_activity.login();
} else {
m_activity.setLoadingStatus(getErrorMessage(), false);
}
}
public void setOffset(int skip) {
m_offset = skip;
}
}

View File

@ -10,7 +10,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.Date; import java.util.Date;
import org.fox.ttrss.MainActivity; import org.fox.ttrss.OnlineActivity;
import org.fox.ttrss.R; import org.fox.ttrss.R;
import org.fox.ttrss.offline.OfflineDownloadService; import org.fox.ttrss.offline.OfflineDownloadService;
@ -25,6 +25,7 @@ import android.os.Environment;
public class ImageCacheService extends IntentService { public class ImageCacheService extends IntentService {
@SuppressWarnings("unused")
private final String TAG = this.getClass().getSimpleName(); private final String TAG = this.getClass().getSimpleName();
public static final int NOTIFY_DOWNLOADING = 1; public static final int NOTIFY_DOWNLOADING = 1;
@ -123,12 +124,13 @@ public class ImageCacheService extends IntentService {
} }
} }
@SuppressWarnings("deprecation")
private void updateNotification(String msg) { private void updateNotification(String msg) {
Notification notification = new Notification(R.drawable.icon, Notification notification = new Notification(R.drawable.icon,
getString(R.string.notify_downloading_title), System.currentTimeMillis()); getString(R.string.notify_downloading_title), System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
new Intent(this, MainActivity.class), 0); new Intent(this, OnlineActivity.class), 0);
notification.flags |= Notification.FLAG_ONGOING_EVENT; notification.flags |= Notification.FLAG_ONGOING_EVENT;
notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE; notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE;