From 08397a47af403d64a012a7961e7444254ccaa9a2 Mon Sep 17 00:00:00 2001 From: Andrew Dolgov Date: Tue, 19 Jun 2012 14:18:00 +0400 Subject: categorize source files --- src/org/fox/ttrss/ApiRequest.java | 1 + src/org/fox/ttrss/AppRater.java | 100 -- src/org/fox/ttrss/Article.java | 87 -- src/org/fox/ttrss/ArticleFragment.java | 2 + src/org/fox/ttrss/ArticleList.java | 2 + src/org/fox/ttrss/ArticlePager.java | 3 + src/org/fox/ttrss/Attachment.java | 71 -- src/org/fox/ttrss/BillingConstants.java | 63 -- src/org/fox/ttrss/BillingHelper.java | 267 ----- src/org/fox/ttrss/BillingReceiver.java | 57 - src/org/fox/ttrss/BillingSecurity.java | 258 ----- src/org/fox/ttrss/BillingService.java | 60 - src/org/fox/ttrss/DatabaseHelper.java | 67 -- src/org/fox/ttrss/EasySSLSocketFactory.java | 120 -- src/org/fox/ttrss/EasyX509TrustManager.java | 26 - src/org/fox/ttrss/Feed.java | 76 -- src/org/fox/ttrss/FeedCategoriesFragment.java | 2 + src/org/fox/ttrss/FeedCategory.java | 54 - src/org/fox/ttrss/FeedCategoryList.java | 3 + src/org/fox/ttrss/FeedList.java | 2 + src/org/fox/ttrss/FeedsFragment.java | 3 + src/org/fox/ttrss/FragmentStatePagerAdapter.java | 226 ---- src/org/fox/ttrss/HeadlinesFragment.java | 3 + src/org/fox/ttrss/ImageCacheService.java | 205 ---- src/org/fox/ttrss/Label.java | 9 - src/org/fox/ttrss/MainActivity.java | 12 + src/org/fox/ttrss/OfflineActivity.java | 1171 ------------------- src/org/fox/ttrss/OfflineArticleFragment.java | 248 ---- src/org/fox/ttrss/OfflineArticlePager.java | 106 -- src/org/fox/ttrss/OfflineDownloadService.java | 352 ------ src/org/fox/ttrss/OfflineFeedsFragment.java | 292 ----- src/org/fox/ttrss/OfflineHeadlinesFragment.java | 477 -------- src/org/fox/ttrss/OfflineServices.java | 17 - src/org/fox/ttrss/OfflineUploadService.java | 259 ----- src/org/fox/ttrss/OnlineServices.java | 4 + src/org/fox/ttrss/PrefsBackupAgent.java | 19 - src/org/fox/ttrss/billing/BillingConstants.java | 63 ++ src/org/fox/ttrss/billing/BillingHelper.java | 268 +++++ src/org/fox/ttrss/billing/BillingReceiver.java | 57 + src/org/fox/ttrss/billing/BillingSecurity.java | 258 +++++ src/org/fox/ttrss/billing/BillingService.java | 60 + src/org/fox/ttrss/offline/OfflineActivity.java | 1183 ++++++++++++++++++++ .../fox/ttrss/offline/OfflineArticleFragment.java | 254 +++++ src/org/fox/ttrss/offline/OfflineArticlePager.java | 111 ++ .../fox/ttrss/offline/OfflineDownloadService.java | 361 ++++++ .../fox/ttrss/offline/OfflineFeedsFragment.java | 298 +++++ .../ttrss/offline/OfflineHeadlinesFragment.java | 483 ++++++++ src/org/fox/ttrss/offline/OfflineServices.java | 20 + .../fox/ttrss/offline/OfflineUploadService.java | 266 +++++ src/org/fox/ttrss/types/Article.java | 88 ++ src/org/fox/ttrss/types/Attachment.java | 71 ++ src/org/fox/ttrss/types/Feed.java | 76 ++ src/org/fox/ttrss/types/FeedCategory.java | 54 + src/org/fox/ttrss/types/Label.java | 9 + src/org/fox/ttrss/util/AppRater.java | 100 ++ src/org/fox/ttrss/util/DatabaseHelper.java | 67 ++ src/org/fox/ttrss/util/EasySSLSocketFactory.java | 120 ++ src/org/fox/ttrss/util/EasyX509TrustManager.java | 26 + .../fox/ttrss/util/FragmentStatePagerAdapter.java | 226 ++++ src/org/fox/ttrss/util/ImageCacheService.java | 211 ++++ src/org/fox/ttrss/util/PrefsBackupAgent.java | 19 + 61 files changed, 4786 insertions(+), 4687 deletions(-) delete mode 100644 src/org/fox/ttrss/AppRater.java delete mode 100644 src/org/fox/ttrss/Article.java delete mode 100644 src/org/fox/ttrss/Attachment.java delete mode 100644 src/org/fox/ttrss/BillingConstants.java delete mode 100644 src/org/fox/ttrss/BillingHelper.java delete mode 100644 src/org/fox/ttrss/BillingReceiver.java delete mode 100644 src/org/fox/ttrss/BillingSecurity.java delete mode 100644 src/org/fox/ttrss/BillingService.java delete mode 100644 src/org/fox/ttrss/DatabaseHelper.java delete mode 100644 src/org/fox/ttrss/EasySSLSocketFactory.java delete mode 100644 src/org/fox/ttrss/EasyX509TrustManager.java delete mode 100644 src/org/fox/ttrss/Feed.java delete mode 100644 src/org/fox/ttrss/FeedCategory.java delete mode 100644 src/org/fox/ttrss/FragmentStatePagerAdapter.java delete mode 100644 src/org/fox/ttrss/ImageCacheService.java delete mode 100644 src/org/fox/ttrss/Label.java delete mode 100644 src/org/fox/ttrss/OfflineActivity.java delete mode 100644 src/org/fox/ttrss/OfflineArticleFragment.java delete mode 100644 src/org/fox/ttrss/OfflineArticlePager.java delete mode 100644 src/org/fox/ttrss/OfflineDownloadService.java delete mode 100644 src/org/fox/ttrss/OfflineFeedsFragment.java delete mode 100644 src/org/fox/ttrss/OfflineHeadlinesFragment.java delete mode 100644 src/org/fox/ttrss/OfflineServices.java delete mode 100644 src/org/fox/ttrss/OfflineUploadService.java delete mode 100644 src/org/fox/ttrss/PrefsBackupAgent.java create mode 100644 src/org/fox/ttrss/billing/BillingConstants.java create mode 100644 src/org/fox/ttrss/billing/BillingHelper.java create mode 100644 src/org/fox/ttrss/billing/BillingReceiver.java create mode 100644 src/org/fox/ttrss/billing/BillingSecurity.java create mode 100644 src/org/fox/ttrss/billing/BillingService.java create mode 100644 src/org/fox/ttrss/offline/OfflineActivity.java create mode 100644 src/org/fox/ttrss/offline/OfflineArticleFragment.java create mode 100644 src/org/fox/ttrss/offline/OfflineArticlePager.java create mode 100644 src/org/fox/ttrss/offline/OfflineDownloadService.java create mode 100644 src/org/fox/ttrss/offline/OfflineFeedsFragment.java create mode 100644 src/org/fox/ttrss/offline/OfflineHeadlinesFragment.java create mode 100644 src/org/fox/ttrss/offline/OfflineServices.java create mode 100644 src/org/fox/ttrss/offline/OfflineUploadService.java create mode 100644 src/org/fox/ttrss/types/Article.java create mode 100644 src/org/fox/ttrss/types/Attachment.java create mode 100644 src/org/fox/ttrss/types/Feed.java create mode 100644 src/org/fox/ttrss/types/FeedCategory.java create mode 100644 src/org/fox/ttrss/types/Label.java create mode 100644 src/org/fox/ttrss/util/AppRater.java create mode 100644 src/org/fox/ttrss/util/DatabaseHelper.java create mode 100644 src/org/fox/ttrss/util/EasySSLSocketFactory.java create mode 100644 src/org/fox/ttrss/util/EasyX509TrustManager.java create mode 100644 src/org/fox/ttrss/util/FragmentStatePagerAdapter.java create mode 100644 src/org/fox/ttrss/util/ImageCacheService.java create mode 100644 src/org/fox/ttrss/util/PrefsBackupAgent.java (limited to 'src') diff --git a/src/org/fox/ttrss/ApiRequest.java b/src/org/fox/ttrss/ApiRequest.java index 26e7f270..39b5a576 100644 --- a/src/org/fox/ttrss/ApiRequest.java +++ b/src/org/fox/ttrss/ApiRequest.java @@ -20,6 +20,7 @@ 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.SharedPreferences; diff --git a/src/org/fox/ttrss/AppRater.java b/src/org/fox/ttrss/AppRater.java deleted file mode 100644 index 2bce4248..00000000 --- a/src/org/fox/ttrss/AppRater.java +++ /dev/null @@ -1,100 +0,0 @@ -package org.fox.ttrss; - -// From http://androidsnippets.com/prompt-engaged-users-to-rate-your-app-in-the-android-market-appirater - -import android.app.Dialog; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.net.Uri; -import android.util.Log; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.TextView; - -public class AppRater { - private final static String APP_TITLE = "Tiny Tiny RSS"; - private final static String APP_PNAME = "org.fox.ttrss"; - - private final static int DAYS_UNTIL_PROMPT = 3; - private final static int LAUNCHES_UNTIL_PROMPT = 7; - - public static void appLaunched(Context mContext) { - SharedPreferences prefs = mContext.getSharedPreferences("apprater", 0); - if (prefs.getBoolean("dontshowagain", false)) { return ; } - - SharedPreferences.Editor editor = prefs.edit(); - - // Increment launch counter - long launch_count = prefs.getLong("launch_count", 0) + 1; - editor.putLong("launch_count", launch_count); - - // Get date of first launch - Long date_firstLaunch = prefs.getLong("date_firstlaunch", 0); - if (date_firstLaunch == 0) { - date_firstLaunch = System.currentTimeMillis(); - editor.putLong("date_firstlaunch", date_firstLaunch); - } - - // Wait at least n days before opening - if (launch_count >= LAUNCHES_UNTIL_PROMPT) { - if (System.currentTimeMillis() >= date_firstLaunch + - (DAYS_UNTIL_PROMPT * 24 * 60 * 60 * 1000)) { - showRateDialog(mContext, editor); - } - } - - editor.commit(); - } - - public static void showRateDialog(final Context mContext, final SharedPreferences.Editor editor) { - final Dialog dialog = new Dialog(mContext); - dialog.setTitle("Rate " + APP_TITLE); - - LinearLayout ll = new LinearLayout(mContext); - ll.setOrientation(LinearLayout.VERTICAL); - - TextView tv = new TextView(mContext); - tv.setText("If you enjoy using " + APP_TITLE + ", please take a moment to rate it. Thanks for your support!"); - tv.setWidth(240); - tv.setPadding(4, 0, 4, 10); - ll.addView(tv); - - Button b1 = new Button(mContext); - b1.setText("Rate " + APP_TITLE); - b1.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - mContext.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + APP_PNAME))); - dialog.dismiss(); - } - }); - ll.addView(b1); - - Button b2 = new Button(mContext); - b2.setText("Remind me later"); - b2.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - dialog.dismiss(); - } - }); - ll.addView(b2); - - Button b3 = new Button(mContext); - b3.setText("No, thanks"); - b3.setOnClickListener(new OnClickListener() { - public void onClick(View v) { - if (editor != null) { - editor.putBoolean("dontshowagain", true); - editor.commit(); - } - dialog.dismiss(); - } - }); - ll.addView(b3); - - dialog.setContentView(ll); - dialog.show(); - } -} \ No newline at end of file diff --git a/src/org/fox/ttrss/Article.java b/src/org/fox/ttrss/Article.java deleted file mode 100644 index 05ddcb49..00000000 --- a/src/org/fox/ttrss/Article.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.fox.ttrss; -import java.util.ArrayList; -import java.util.List; - -import android.os.Parcel; -import android.os.Parcelable; - -// TODO: serialize Labels -public class Article implements Parcelable { - int id; - boolean unread; - boolean marked; - boolean published; - int updated; - boolean is_updated; - String title; - String link; - int feed_id; - List tags; - List attachments; - String content; - List> labels; - - public Article(Parcel in) { - readFromParcel(in); - } - - public Article(int id) { - this.id = id; - this.title = ""; - this.link = ""; - this.tags = new ArrayList(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(id); - out.writeInt(unread ? 1 : 0); - out.writeInt(marked ? 1 : 0); - out.writeInt(published ? 1 : 0); - out.writeInt(updated); - out.writeInt(is_updated ? 1 : 0); - out.writeString(title); - out.writeString(link); - out.writeInt(feed_id); - out.writeStringList(tags); - out.writeString(content); - out.writeList(attachments); - } - - public void readFromParcel(Parcel in) { - id = in.readInt(); - unread = in.readInt() == 1; - marked = in.readInt() == 1; - published = in.readInt() == 1; - updated = in.readInt(); - is_updated = in.readInt() == 1; - title = in.readString(); - link = in.readString(); - feed_id = in.readInt(); - - if (tags == null) tags = new ArrayList(); - in.readStringList(tags); - - content = in.readString(); - - attachments = new ArrayList(); - in.readList(attachments, Attachment.class.getClassLoader()); - } - - @SuppressWarnings("rawtypes") - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - public Article createFromParcel(Parcel in) { - return new Article(in); - } - - public Article[] newArray(int size) { - return new Article[size]; - } - }; -} diff --git a/src/org/fox/ttrss/ArticleFragment.java b/src/org/fox/ttrss/ArticleFragment.java index d4677203..098ff6f9 100644 --- a/src/org/fox/ttrss/ArticleFragment.java +++ b/src/org/fox/ttrss/ArticleFragment.java @@ -6,6 +6,8 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import org.fox.ttrss.types.Article; +import org.fox.ttrss.types.Attachment; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; diff --git a/src/org/fox/ttrss/ArticleList.java b/src/org/fox/ttrss/ArticleList.java index 77024c74..2ff61e59 100644 --- a/src/org/fox/ttrss/ArticleList.java +++ b/src/org/fox/ttrss/ArticleList.java @@ -2,6 +2,8 @@ package org.fox.ttrss; import java.util.ArrayList; +import org.fox.ttrss.types.Article; + import android.os.Parcel; import android.os.Parcelable; diff --git a/src/org/fox/ttrss/ArticlePager.java b/src/org/fox/ttrss/ArticlePager.java index 904d8a88..6cf62836 100644 --- a/src/org/fox/ttrss/ArticlePager.java +++ b/src/org/fox/ttrss/ArticlePager.java @@ -1,5 +1,8 @@ package org.fox.ttrss; +import org.fox.ttrss.types.Article; +import org.fox.ttrss.util.FragmentStatePagerAdapter; + import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; diff --git a/src/org/fox/ttrss/Attachment.java b/src/org/fox/ttrss/Attachment.java deleted file mode 100644 index 37c4a08a..00000000 --- a/src/org/fox/ttrss/Attachment.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.fox.ttrss; - -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; - -import android.os.Parcel; -import android.os.Parcelable; - -public class Attachment implements Parcelable { - int id; - String content_url; - String content_type; - String title; - String duration; - int post_id; - - public Attachment(Parcel in) { - readFromParcel(in); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(id); - out.writeString(content_url); - out.writeString(content_type); - out.writeString(title); - out.writeString(duration); - out.writeInt(post_id); - } - - public String toString() { - if (title != null && title.length() > 0) { - return title; - } else { - try { - URL url = new URL(content_url.trim()); - return new File(url.getFile()).getName(); - } catch (MalformedURLException e) { - return content_url; - } - } - } - - public void readFromParcel(Parcel in) { - id = in.readInt(); - content_url = in.readString(); - content_type = in.readString(); - title = in.readString(); - duration = in.readString(); - post_id = in.readInt(); - } - - @SuppressWarnings("rawtypes") - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - public Attachment createFromParcel(Parcel in) { - return new Attachment(in); - } - - public Attachment[] newArray(int size) { - return new Attachment[size]; - } - }; - -} diff --git a/src/org/fox/ttrss/BillingConstants.java b/src/org/fox/ttrss/BillingConstants.java deleted file mode 100644 index ea8b454c..00000000 --- a/src/org/fox/ttrss/BillingConstants.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.fox.ttrss; - - -public class BillingConstants { - - // The response codes for a request, defined by Android Market. - public enum ResponseCode { - RESULT_OK, - RESULT_USER_CANCELED, - RESULT_SERVICE_UNAVAILABLE, - RESULT_BILLING_UNAVAILABLE, - RESULT_ITEM_UNAVAILABLE, - RESULT_DEVELOPER_ERROR, - RESULT_ERROR; - - // Converts from an ordinal value to the ResponseCode - public static ResponseCode valueOf(int index) { - ResponseCode[] values = ResponseCode.values(); - if (index < 0 || index >= values.length) { - return RESULT_ERROR; - } - return values[index]; - } - } - - // The possible states of an in-app purchase, as defined by Android Market. - public enum PurchaseState { - // Responses to requestPurchase or restoreTransactions. - PURCHASED, // User was charged for the order. - CANCELED, // The charge failed on the server. - REFUNDED; // User received a refund for the order. - - // Converts from an ordinal value to the PurchaseState - public static PurchaseState valueOf(int index) { - PurchaseState[] values = PurchaseState.values(); - if (index < 0 || index >= values.length) { - return CANCELED; - } - return values[index]; - } - } - - // These are the names of the extras that are passed in an intent from - // Market to this application and cannot be changed. - public static final String NOTIFICATION_ID = "notification_id"; - public static final String INAPP_SIGNED_DATA = "inapp_signed_data"; - public static final String INAPP_SIGNATURE = "inapp_signature"; - public static final String INAPP_REQUEST_ID = "request_id"; - public static final String INAPP_RESPONSE_CODE = "response_code"; - - // Intent actions that we send from the BillingReceiver to the - // BillingService. Defined by this application. - public static final String ACTION_CONFIRM_NOTIFICATION = "com.example.dungeons.CONFIRM_NOTIFICATION"; - public static final String ACTION_GET_PURCHASE_INFORMATION = "com.example.dungeons.GET_PURCHASE_INFORMATION"; - public static final String ACTION_RESTORE_TRANSACTIONS = "com.example.dungeons.RESTORE_TRANSACTIONS"; - - // Intent actions that we receive in the BillingReceiver from Market. - // These are defined by Market and cannot be changed. - public static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY"; - public static final String ACTION_RESPONSE_CODE = "com.android.vending.billing.RESPONSE_CODE"; - public static final String ACTION_PURCHASE_STATE_CHANGED = "com.android.vending.billing.PURCHASE_STATE_CHANGED"; - -} diff --git a/src/org/fox/ttrss/BillingHelper.java b/src/org/fox/ttrss/BillingHelper.java deleted file mode 100644 index e29fd2f7..00000000 --- a/src/org/fox/ttrss/BillingHelper.java +++ /dev/null @@ -1,267 +0,0 @@ -package org.fox.ttrss; - -import java.util.ArrayList; - -import android.app.PendingIntent; -import android.app.PendingIntent.CanceledException; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteException; -import android.util.Log; - -import com.android.vending.billing.IMarketBillingService; -import org.fox.ttrss.BillingSecurity.VerifiedPurchase; -import org.fox.ttrss.BillingConstants.ResponseCode; - -public class BillingHelper { - - private static final String TAG = "BillingService"; - - private static IMarketBillingService mService; - private static Context mContext; - private static Handler mCompletedHandler; - - protected static VerifiedPurchase latestPurchase; - - protected static void instantiateHelper(Context context, IMarketBillingService service) { - mService = service; - mContext = context; - } - - protected static void setCompletedHandler(Handler handler){ - mCompletedHandler = handler; - } - - protected static boolean isBillingSupported() { - if (amIDead()) { - return false; - } - Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED"); - if (mService != null) { - try { - Bundle response = mService.sendBillingRequest(request); - ResponseCode code = ResponseCode.valueOf((Integer) response.get("RESPONSE_CODE")); - Log.i(TAG, "isBillingSupported response was: " + code.toString()); - if (ResponseCode.RESULT_OK.equals(code)) { - return true; - } else { - return false; - } - } catch (RemoteException e) { - Log.e(TAG, "isBillingSupported response was: RemoteException", e); - return false; - } - } else { - Log.i(TAG, "isBillingSupported response was: BillingService.mService = null"); - return false; - } - } - - /** - * A REQUEST_PURCHASE request also triggers two asynchronous responses (broadcast intents). - * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides error information about the request. (which I ignore) - * Next, if the request was successful, the Android Market application sends an IN_APP_NOTIFY broadcast intent. - * This message contains a notification ID, which you can use to retrieve the transaction details for the REQUEST_PURCHASE - * @param activityContext - * @param itemId - */ - protected static void requestPurchase(Context activityContext, String itemId){ - if (amIDead()) { - return; - } - Log.i(TAG, "requestPurchase()"); - Bundle request = makeRequestBundle("REQUEST_PURCHASE"); - request.putString("ITEM_ID", itemId); - try { - Bundle response = mService.sendBillingRequest(request); - - //The RESPONSE_CODE key provides you with the status of the request - Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); - //The PURCHASE_INTENT key provides you with a PendingIntent, which you can use to launch the checkout UI - PendingIntent pendingIntent = (PendingIntent) response.get("PURCHASE_INTENT"); - //The REQUEST_ID key provides you with a unique request identifier for the request - Long requestIndentifier = (Long) response.get("REQUEST_ID"); - Log.i(TAG, "current request is:" + requestIndentifier); - BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); - Log.i(TAG, "REQUEST_PURCHASE Sync Response code: "+responseCode.toString()); - - startBuyPageActivity(pendingIntent, new Intent(), activityContext); - } catch (RemoteException e) { - Log.e(TAG, "Failed, internet error maybe", e); - Log.e(TAG, "Billing supported: "+isBillingSupported()); - } - } - - /** - * A GET_PURCHASE_INFORMATION request also triggers two asynchronous responses (broadcast intents). - * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. (which I ignore) - * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent. - * This message contains detailed transaction information. - * The transaction information is contained in a signed JSON string (unencrypted). - * The message includes the signature so you can verify the integrity of the signed string - * @param notifyIds - */ - protected static void getPurchaseInformation(String[] notifyIds){ - if (amIDead()) { - return; - } - Log.i(TAG, "getPurchaseInformation()"); - Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION"); - // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate. - // The Android Market application returns this nonce with the PURCHASE_STATE_CHANGED broadcast intent so you can verify the integrity of the transaction information. - request.putLong("NONCE", BillingSecurity.generateNonce()); - // The NOTIFY_IDS key contains an array of notification IDs, which you received in the IN_APP_NOTIFY broadcast intent. - request.putStringArray("NOTIFY_IDS", notifyIds); - try { - Bundle response = mService.sendBillingRequest(request); - - //The REQUEST_ID key provides you with a unique request identifier for the request - Long requestIndentifier = (Long) response.get("REQUEST_ID"); - Log.i(TAG, "current request is:" + requestIndentifier); - //The RESPONSE_CODE key provides you with the status of the request - Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); - BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); - Log.i(TAG, "GET_PURCHASE_INFORMATION Sync Response code: "+responseCode.toString()); - - } catch (RemoteException e) { - Log.e(TAG, "Failed, internet error maybe", e); - Log.e(TAG, "Billing supported: "+isBillingSupported()); - } - } - - /** - * To acknowledge that you received transaction information you send a - * CONFIRM_NOTIFICATIONS request. - * - * A CONFIRM_NOTIFICATIONS request triggers a single asynchronous response�a RESPONSE_CODE broadcast intent. - * This broadcast intent provides status and error information about the request. - * - * Note: As a best practice, you should not send a CONFIRM_NOTIFICATIONS request for a purchased item until you have delivered the item to the user. - * This way, if your application crashes or something else prevents your application from delivering the product, - * your application will still receive an IN_APP_NOTIFY broadcast intent from Android Market indicating that you need to deliver the product - * @param notifyIds - */ - protected static void confirmTransaction(String[] notifyIds) { - if (amIDead()) { - return; - } - Log.i(TAG, "confirmTransaction()"); - Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS"); - request.putStringArray("NOTIFY_IDS", notifyIds); - try { - Bundle response = mService.sendBillingRequest(request); - - //The REQUEST_ID key provides you with a unique request identifier for the request - Long requestIndentifier = (Long) response.get("REQUEST_ID"); - Log.i(TAG, "current request is:" + requestIndentifier); - - //The RESPONSE_CODE key provides you with the status of the request - Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); - BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); - - Log.i(TAG, "CONFIRM_NOTIFICATIONS Sync Response code: "+responseCode.toString()); - } catch (RemoteException e) { - Log.e(TAG, "Failed, internet error maybe", e); - Log.e(TAG, "Billing supported: " + isBillingSupported()); - } - } - - /** - * - * Can be used for when a user has reinstalled the app to give back prior purchases. - * if an item for sale's purchase type is "managed per user account" this means google will have a record ofthis transaction - * - * A RESTORE_TRANSACTIONS request also triggers two asynchronous responses (broadcast intents). - * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. - * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent. - * This message contains the detailed transaction information. The transaction information is contained in a signed JSON string (unencrypted). - * The message includes the signature so you can verify the integrity of the signed string - * @param nonce - */ - protected static void restoreTransactionInformation(Long nonce) { - if (amIDead()) { - return; - } - Log.i(TAG, "confirmTransaction()"); - Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS"); - // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate - request.putLong("NONCE", nonce); - try { - Bundle response = mService.sendBillingRequest(request); - - //The REQUEST_ID key provides you with a unique request identifier for the request - Long requestIndentifier = (Long) response.get("REQUEST_ID"); - Log.i(TAG, "current request is:" + requestIndentifier); - - //The RESPONSE_CODE key provides you with the status of the request - Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); - BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); - Log.i(TAG, "RESTORE_TRANSACTIONS Sync Response code: "+responseCode.toString()); - } catch (RemoteException e) { - Log.e(TAG, "Failed, internet error maybe", e); - Log.e(TAG, "Billing supported: " + isBillingSupported()); - } - } - - private static boolean amIDead() { - if (mService == null || mContext == null) { - Log.e(TAG, "BillingHelper not fully instantiated"); - return true; - } else { - return false; - } - } - - private static Bundle makeRequestBundle(String method) { - Bundle request = new Bundle(); - request.putString("BILLING_REQUEST", method); - request.putInt("API_VERSION", 1); - request.putString("PACKAGE_NAME", mContext.getPackageName()); - return request; - } - - /** - * - * - * You must launch the pending intent from an activity context and not an application context - * You cannot use the singleTop launch mode to launch the pending intent - * @param pendingIntent - * @param intent - * @param context - */ - private static void startBuyPageActivity(PendingIntent pendingIntent, Intent intent, Context context){ - //TODO add above 2.0 implementation with reflection, for now just using 1.6 implem - - // This is on Android 1.6. The in-app checkout page activity will be on its - // own separate activity stack instead of on the activity stack of - // the application. - try { - pendingIntent.send(context, 0, intent); - } catch (CanceledException e){ - Log.e(TAG, "startBuyPageActivity CanceledException"); - } - } - - protected static void verifyPurchase(String signedData, String signature) { - ArrayList purchases = BillingSecurity.verifyPurchase(signedData, signature); - latestPurchase = purchases.get(0); - - confirmTransaction(new String[]{latestPurchase.notificationId}); - - if(mCompletedHandler != null){ - mCompletedHandler.sendEmptyMessage(0); - } else { - Log.e(TAG, "verifyPurchase error. Handler not instantiated. Have you called setCompletedHandler()?"); - } - } - - public static void stopService(){ - mContext.stopService(new Intent(mContext, BillingService.class)); - mService = null; - mContext = null; - mCompletedHandler = null; - Log.i(TAG, "Stopping Service"); - } -} diff --git a/src/org/fox/ttrss/BillingReceiver.java b/src/org/fox/ttrss/BillingReceiver.java deleted file mode 100644 index 3371f97b..00000000 --- a/src/org/fox/ttrss/BillingReceiver.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.fox.ttrss; - -import static org.fox.ttrss.BillingConstants.ACTION_NOTIFY; -import static org.fox.ttrss.BillingConstants.ACTION_PURCHASE_STATE_CHANGED; -import static org.fox.ttrss.BillingConstants.ACTION_RESPONSE_CODE; -import static org.fox.ttrss.BillingConstants.INAPP_REQUEST_ID; -import static org.fox.ttrss.BillingConstants.INAPP_RESPONSE_CODE; -import static org.fox.ttrss.BillingConstants.INAPP_SIGNATURE; -import static org.fox.ttrss.BillingConstants.INAPP_SIGNED_DATA; -import static org.fox.ttrss.BillingConstants.NOTIFICATION_ID; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -public class BillingReceiver extends BroadcastReceiver { - - private static final String TAG = "BillingService"; - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - Log.i(TAG, "Received action: " + action); - if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) { - String signedData = intent.getStringExtra(INAPP_SIGNED_DATA); - String signature = intent.getStringExtra(INAPP_SIGNATURE); - purchaseStateChanged(context, signedData, signature); - } else if (ACTION_NOTIFY.equals(action)) { - String notifyId = intent.getStringExtra(NOTIFICATION_ID); - notify(context, notifyId); - } else if (ACTION_RESPONSE_CODE.equals(action)) { - long requestId = intent.getLongExtra(INAPP_REQUEST_ID, -1); - int responseCodeIndex = intent.getIntExtra(INAPP_RESPONSE_CODE, BillingConstants.ResponseCode.RESULT_ERROR.ordinal()); - checkResponseCode(context, requestId, responseCodeIndex); - } else { - Log.e(TAG, "unexpected action: " + action); - } - } - - - private void purchaseStateChanged(Context context, String signedData, String signature) { - Log.i(TAG, "purchaseStateChanged got signedData: " + signedData); - Log.i(TAG, "purchaseStateChanged got signature: " + signature); - BillingHelper.verifyPurchase(signedData, signature); - } - - private void notify(Context context, String notifyId) { - Log.i(TAG, "notify got id: " + notifyId); - String[] notifyIds = {notifyId}; - BillingHelper.getPurchaseInformation(notifyIds); - } - - private void checkResponseCode(Context context, long requestId, int responseCodeIndex) { - Log.i(TAG, "checkResponseCode got requestId: " + requestId); - Log.i(TAG, "checkResponseCode got responseCode: " + BillingConstants.ResponseCode.valueOf(responseCodeIndex)); - } -} \ No newline at end of file diff --git a/src/org/fox/ttrss/BillingSecurity.java b/src/org/fox/ttrss/BillingSecurity.java deleted file mode 100644 index 26e19b3e..00000000 --- a/src/org/fox/ttrss/BillingSecurity.java +++ /dev/null @@ -1,258 +0,0 @@ -// Copyright 2010 Google Inc. All Rights Reserved. - -package org.fox.ttrss; - -import java.security.InvalidKeyException; -import java.security.KeyFactory; -import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.SecureRandom; -import java.security.Signature; -import java.security.SignatureException; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.X509EncodedKeySpec; -import java.util.ArrayList; -import java.util.HashSet; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import android.text.TextUtils; -import android.util.Log; - -import org.fox.ttrss.BillingConstants.PurchaseState; -import org.fox.ttrss.util.Base64; -import org.fox.ttrss.util.Base64DecoderException; - -/** - * Security-related methods. For a secure implementation, all of this code - * should be implemented on a server that communicates with the application on - * the device. For the sake of simplicity and clarity of this example, this code - * is included here and is executed on the device. If you must verify the - * purchases on the phone, you should obfuscate this code to make it harder for - * an attacker to replace the code with stubs that treat all purchases as - * verified. - */ -public class BillingSecurity { - private static final String TAG = "BillingService"; - - private static final String KEY_FACTORY_ALGORITHM = "RSA"; - private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; - private static final SecureRandom RANDOM = new SecureRandom(); - - /** - * This keeps track of the nonces that we generated and sent to the server. - * We need to keep track of these until we get back the purchase state and - * send a confirmation message back to Android Market. If we are killed and - * lose this list of nonces, it is not fatal. Android Market will send us a - * new "notify" message and we will re-generate a new nonce. This has to be - * "static" so that the {@link BillingReceiver} can check if a nonce exists. - */ - private static HashSet sKnownNonces = new HashSet(); - - /** - * A class to hold the verified purchase information. - */ - public static class VerifiedPurchase { - public PurchaseState purchaseState; - public String notificationId; - public String productId; - public String orderId; - public long purchaseTime; - public String developerPayload; - - public VerifiedPurchase(PurchaseState purchaseState, String notificationId, String productId, String orderId, long purchaseTime, - String developerPayload) { - this.purchaseState = purchaseState; - this.notificationId = notificationId; - this.productId = productId; - this.orderId = orderId; - this.purchaseTime = purchaseTime; - this.developerPayload = developerPayload; - } - - public boolean isPurchased(){ - return purchaseState.equals(PurchaseState.PURCHASED); - } - - - } - - /** Generates a nonce (a random number used once). */ - public static long generateNonce() { - long nonce = RANDOM.nextLong(); - Log.i(TAG, "Nonce generateD: "+nonce); - sKnownNonces.add(nonce); - return nonce; - } - - public static void removeNonce(long nonce) { - sKnownNonces.remove(nonce); - } - - public static boolean isNonceKnown(long nonce) { - return sKnownNonces.contains(nonce); - } - - /** - * Verifies that the data was signed with the given signature, and returns - * the list of verified purchases. The data is in JSON format and contains a - * nonce (number used once) that we generated and that was signed (as part - * of the whole data string) with a private key. The data also contains the - * {@link PurchaseState} and product ID of the purchase. In the general - * case, there can be an array of purchase transactions because there may be - * delays in processing the purchase on the backend and then several - * purchases can be batched together. - * - * @param signedData - * the signed JSON string (signed, not encrypted) - * @param signature - * the signature for the data, signed with the private key - */ - public static ArrayList verifyPurchase(String signedData, String signature) { - if (signedData == null) { - Log.e(TAG, "data is null"); - return null; - } - Log.i(TAG, "signedData: " + signedData); - boolean verified = false; - if (!TextUtils.isEmpty(signature)) { - /** - * Compute your public key (that you got from the Android Market - * publisher site). - * - * Instead of just storing the entire literal string here embedded - * in the program, construct the key at runtime from pieces or use - * bit manipulation (for example, XOR with some other string) to - * hide the actual key. The key itself is not secret information, - * but we don't want to make it easy for an adversary to replace the - * public key with one of their own and then fake messages from the - * server. - * - * Generally, encryption keys / passwords should only be kept in - * memory long enough to perform the operation they need to perform. - */ - String base64EncodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApLWBv8eFC4f7h6gz3VE87XX2nqJB2KL2yNnNawmgaL/0nd6nXvVRiZ3iXLLP9k8RpLJ6rZPV778z8WzDLZATV3b2nh21KgjSNoG4em1oSf7pW4+AujqjLfNVRsXoJIWG+OMMd9o9l/D2YJTCXzSvgFIfF5EJRg6APZHEVrVJo8iXwnYM1tFfLjPfp10MtjLmD5tZW8o3hTmXJ3ZMDI12PL22G4KaE+BuQqI6PZ22m/pA85R6AuhNo2IUSE4XFUE8i7ANWDvdfDzQ5J0TTWAeHmUQCstdZ48z+6AjqD3L2omS/dKoBnlYxEUZms3iUa1/Co40nWU7sc2hqpmfNiG5oQIDAQAB"; - PublicKey key = BillingSecurity.generatePublicKey(base64EncodedPublicKey); - verified = BillingSecurity.verify(key, signedData, signature); - if (!verified) { - Log.w(TAG, "signature does not match data."); - return null; - } - } - - JSONObject jObject; - JSONArray jTransactionsArray = null; - int numTransactions = 0; - long nonce = 0L; - try { - jObject = new JSONObject(signedData); - - // The nonce might be null if the user backed out of the buy page. - nonce = jObject.optLong("nonce"); - jTransactionsArray = jObject.optJSONArray("orders"); - if (jTransactionsArray != null) { - numTransactions = jTransactionsArray.length(); - } - } catch (JSONException e) { - return null; - } - - if (!BillingSecurity.isNonceKnown(nonce)) { - Log.w(TAG, "Nonce not found: " + nonce); - return null; - } - - ArrayList purchases = new ArrayList(); - try { - for (int i = 0; i < numTransactions; i++) { - JSONObject jElement = jTransactionsArray.getJSONObject(i); - int response = jElement.getInt("purchaseState"); - PurchaseState purchaseState = PurchaseState.valueOf(response); - String productId = jElement.getString("productId"); - String packageName = jElement.getString("packageName"); - long purchaseTime = jElement.getLong("purchaseTime"); - String orderId = jElement.optString("orderId", ""); - String notifyId = null; - if (jElement.has("notificationId")) { - notifyId = jElement.getString("notificationId"); - } - String developerPayload = jElement.optString("developerPayload", null); - - // If the purchase state is PURCHASED, then we require a - // verified nonce. - if (purchaseState == PurchaseState.PURCHASED && !verified) { - continue; - } - purchases.add(new VerifiedPurchase(purchaseState, notifyId, productId, orderId, purchaseTime, developerPayload)); - } - } catch (JSONException e) { - Log.e(TAG, "JSON exception: ", e); - return null; - } - removeNonce(nonce); - return purchases; - } - - /** - * Generates a PublicKey instance from a string containing the - * Base64-encoded public key. - * - * @param encodedPublicKey - * Base64-encoded public key - * @throws IllegalArgumentException - * if encodedPublicKey is invalid - */ - public static PublicKey generatePublicKey(String encodedPublicKey) { - try { - byte[] decodedKey = Base64.decode(encodedPublicKey); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); - return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException(e); - } catch (InvalidKeySpecException e) { - Log.e(TAG, "Invalid key specification."); - throw new IllegalArgumentException(e); - } catch (Base64DecoderException e) { - Log.e(TAG, "Base64DecoderException.", e); - return null; - } - } - - /** - * Verifies that the signature from the server matches the computed - * signature on the data. Returns true if the data is correctly signed. - * - * @param publicKey - * public key associated with the developer account - * @param signedData - * signed data from server - * @param signature - * server signature - * @return true if the data and signature match - */ - public static boolean verify(PublicKey publicKey, String signedData, String signature) { - Log.i(TAG, "signature: " + signature); - Signature sig; - try { - sig = Signature.getInstance(SIGNATURE_ALGORITHM); - sig.initVerify(publicKey); - sig.update(signedData.getBytes()); - if (!sig.verify(Base64.decode(signature))) { - Log.e(TAG, "Signature verification failed."); - return false; - } - return true; - } catch (NoSuchAlgorithmException e) { - Log.e(TAG, "NoSuchAlgorithmException."); - } catch (InvalidKeyException e) { - Log.e(TAG, "Invalid key specification."); - } catch (SignatureException e) { - Log.e(TAG, "Signature exception."); - } catch (Base64DecoderException e) { - Log.e(TAG, "Base64DecoderException.", e); - } - return false; - } -} diff --git a/src/org/fox/ttrss/BillingService.java b/src/org/fox/ttrss/BillingService.java deleted file mode 100644 index e003df32..00000000 --- a/src/org/fox/ttrss/BillingService.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.fox.ttrss; - -import android.app.Service; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; -import android.util.Log; - -import com.android.vending.billing.IMarketBillingService; - -public class BillingService extends Service implements ServiceConnection{ - - private static final String TAG = "BillingService"; - - /** The service connection to the remote MarketBillingService. */ - private IMarketBillingService mService; - - @Override - public void onCreate() { - super.onCreate(); - Log.i(TAG, "Service starting with onCreate"); - - try { - boolean bindResult = bindService(new Intent("com.android.vending.billing.MarketBillingService.BIND"), this, Context.BIND_AUTO_CREATE); - if(bindResult){ - Log.i(TAG,"Market Billing Service Successfully Bound"); - } else { - Log.e(TAG,"Market Billing Service could not be bound."); - //TODO stop user continuing - } - } catch (SecurityException e){ - Log.e(TAG,"Market Billing Service could not be bound. SecurityException: "+e); - //TODO stop user continuing - } - } - - public void setContext(Context context) { - attachBaseContext(context); - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - Log.i(TAG, "Market Billing Service Connected."); - mService = IMarketBillingService.Stub.asInterface(service); - BillingHelper.instantiateHelper(getBaseContext(), mService); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - - } - -} diff --git a/src/org/fox/ttrss/DatabaseHelper.java b/src/org/fox/ttrss/DatabaseHelper.java deleted file mode 100644 index 71020078..00000000 --- a/src/org/fox/ttrss/DatabaseHelper.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.fox.ttrss; -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.provider.BaseColumns; - - -public class DatabaseHelper extends SQLiteOpenHelper { - - @SuppressWarnings("unused") - private final String TAG = this.getClass().getSimpleName(); - public static final String DATABASE_NAME = "OfflineStorage.db"; - public static final int DATABASE_VERSION = 2; - - public DatabaseHelper(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase db) { - db.execSQL("DROP TABLE IF EXISTS feeds;"); - db.execSQL("DROP TABLE IF EXISTS articles;"); - db.execSQL("DROP VIEW IF EXISTS feeds_unread;"); - db.execSQL("DROP TRIGGER IF EXISTS articles_set_modified;"); - - db.execSQL("CREATE TABLE IF NOT EXISTS feeds (" + - BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - "feed_url TEXT, " + - "title TEXT, " + - "has_icon BOOLEAN, " + - "cat_id INTEGER" + - ");"); - - db.execSQL("CREATE TABLE IF NOT EXISTS articles (" + - BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - "unread BOOLEAN, " + - "marked BOOLEAN, " + - "published BOOLEAN, " + - "updated INTEGER, " + - "is_updated BOOLEAN, " + - "title TEXT, " + - "link TEXT, " + - "feed_id INTEGER, " + - "tags TEXT, " + - "content TEXT, " + - "selected BOOLEAN, " + - "modified BOOLEAN" + - ");"); - - db.execSQL("CREATE TRIGGER articles_set_modified UPDATE OF marked, published, unread ON articles " + - "BEGIN " + - " UPDATE articles SET modified = 1 WHERE " + BaseColumns._ID + " = " + "OLD." + BaseColumns._ID + "; " + - "END;"); - - db.execSQL("CREATE VIEW feeds_unread AS SELECT feeds."+BaseColumns._ID+" AS "+BaseColumns._ID+", " + - "feeds.title AS title, " + - "SUM(articles.unread) AS unread FROM feeds " + - "LEFT JOIN articles ON (articles.feed_id = feeds."+BaseColumns._ID+") " + - "GROUP BY feeds."+BaseColumns._ID+", feeds.title;"); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - onCreate(db); - } - -} diff --git a/src/org/fox/ttrss/EasySSLSocketFactory.java b/src/org/fox/ttrss/EasySSLSocketFactory.java deleted file mode 100644 index 2bb6ea14..00000000 --- a/src/org/fox/ttrss/EasySSLSocketFactory.java +++ /dev/null @@ -1,120 +0,0 @@ -package org.fox.ttrss; - -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(); - } -} \ No newline at end of file diff --git a/src/org/fox/ttrss/EasyX509TrustManager.java b/src/org/fox/ttrss/EasyX509TrustManager.java deleted file mode 100644 index 6842a1a6..00000000 --- a/src/org/fox/ttrss/EasyX509TrustManager.java +++ /dev/null @@ -1,26 +0,0 @@ - -package org.fox.ttrss; - -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]; - } - -} diff --git a/src/org/fox/ttrss/Feed.java b/src/org/fox/ttrss/Feed.java deleted file mode 100644 index 1f0af4cc..00000000 --- a/src/org/fox/ttrss/Feed.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.fox.ttrss; - -import android.os.Parcel; -import android.os.Parcelable; - -public class Feed implements Comparable, Parcelable { - String feed_url; - String title; - int id; - int unread; - boolean has_icon; - int cat_id; - int last_updated; - int order_id; - boolean is_cat; - - public Feed(int id, String title, boolean is_cat) { - this.id = id; - this.title = title; - this.is_cat = is_cat; - } - - public Feed(Parcel in) { - readFromParcel(in); - } - - @Override - public int compareTo(Feed feed) { - if (feed.unread != this.unread) - return feed.unread - this.unread; - else - return this.title.compareTo(feed.title); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeString(feed_url); - out.writeString(title); - out.writeInt(id); - out.writeInt(unread); - out.writeInt(has_icon ? 1 : 0); - out.writeInt(cat_id); - out.writeInt(last_updated); - out.writeInt(is_cat ? 1 : 0); - out.writeInt(order_id); - } - - public void readFromParcel(Parcel in) { - feed_url = in.readString(); - title = in.readString(); - id = in.readInt(); - unread = in.readInt(); - has_icon = in.readInt() == 1; - cat_id = in.readInt(); - last_updated = in.readInt(); - is_cat = in.readInt() == 1; - order_id = in.readInt(); - } - - @SuppressWarnings("rawtypes") - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - public Feed createFromParcel(Parcel in) { - return new Feed(in); - } - - public Feed[] newArray(int size) { - return new Feed[size]; - } - }; -} \ No newline at end of file diff --git a/src/org/fox/ttrss/FeedCategoriesFragment.java b/src/org/fox/ttrss/FeedCategoriesFragment.java index eb1cd0a8..b2b378bc 100644 --- a/src/org/fox/ttrss/FeedCategoriesFragment.java +++ b/src/org/fox/ttrss/FeedCategoriesFragment.java @@ -7,6 +7,8 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; +import org.fox.ttrss.types.FeedCategory; + import android.app.Activity; import android.app.Fragment; import android.content.Context; diff --git a/src/org/fox/ttrss/FeedCategory.java b/src/org/fox/ttrss/FeedCategory.java deleted file mode 100644 index 48c3b554..00000000 --- a/src/org/fox/ttrss/FeedCategory.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.fox.ttrss; - -import android.os.Parcel; -import android.os.Parcelable; - -public class FeedCategory implements Parcelable { - int id; - String title; - int unread; - int order_id; - - public FeedCategory(Parcel in) { - readFromParcel(in); - } - - public FeedCategory(int id, String title, int unread) { - this.id = id; - this.title = title; - this.unread = unread; - this.order_id = 0; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeInt(id); - out.writeString(title); - out.writeInt(unread); - out.writeInt(order_id); - } - - public void readFromParcel(Parcel in) { - id = in.readInt(); - title = in.readString(); - unread = in.readInt(); - order_id = in.readInt(); - } - - @SuppressWarnings("rawtypes") - public static final Parcelable.Creator CREATOR = - new Parcelable.Creator() { - public FeedCategory createFromParcel(Parcel in) { - return new FeedCategory(in); - } - - public FeedCategory[] newArray(int size) { - return new FeedCategory[size]; - } - }; -} diff --git a/src/org/fox/ttrss/FeedCategoryList.java b/src/org/fox/ttrss/FeedCategoryList.java index e78f8747..71e99161 100644 --- a/src/org/fox/ttrss/FeedCategoryList.java +++ b/src/org/fox/ttrss/FeedCategoryList.java @@ -2,6 +2,9 @@ package org.fox.ttrss; import java.util.ArrayList; +import org.fox.ttrss.types.Feed; +import org.fox.ttrss.types.FeedCategory; + import android.os.Parcel; import android.os.Parcelable; diff --git a/src/org/fox/ttrss/FeedList.java b/src/org/fox/ttrss/FeedList.java index c54ef780..2d3f0933 100644 --- a/src/org/fox/ttrss/FeedList.java +++ b/src/org/fox/ttrss/FeedList.java @@ -2,6 +2,8 @@ package org.fox.ttrss; import java.util.ArrayList; +import org.fox.ttrss.types.Feed; + import android.os.Parcel; import android.os.Parcelable; diff --git a/src/org/fox/ttrss/FeedsFragment.java b/src/org/fox/ttrss/FeedsFragment.java index 2ad43550..38e8b48c 100644 --- a/src/org/fox/ttrss/FeedsFragment.java +++ b/src/org/fox/ttrss/FeedsFragment.java @@ -24,6 +24,9 @@ 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.FeedCategory; +import org.fox.ttrss.util.EasySSLSocketFactory; import android.app.Activity; import android.app.Fragment; diff --git a/src/org/fox/ttrss/FragmentStatePagerAdapter.java b/src/org/fox/ttrss/FragmentStatePagerAdapter.java deleted file mode 100644 index 1d7210df..00000000 --- a/src/org/fox/ttrss/FragmentStatePagerAdapter.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * 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; - -import android.app.Fragment; -import android.app.FragmentManager; -import android.app.FragmentTransaction; -import android.os.Bundle; -import android.os.Parcelable; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.PagerAdapter; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; - -import java.util.ArrayList; - -/** - * Implementation of {@link android.support.v4.view.PagerAdapter} that - * uses a {@link Fragment} to manage each page. This class also handles - * saving and restoring of fragment's state. - * - *

This version of the pager is more useful when there are a large number - * of pages, working more like a list view. When pages are not visible to - * the user, their entire fragment may be destroyed, only keeping the saved - * state of that fragment. This allows the pager to hold on to much less - * memory associated with each visited page as compared to - * {@link FragmentPagerAdapter} at the cost of potentially more overhead when - * switching between pages. - * - *

When using FragmentPagerAdapter the host ViewPager must have a - * valid ID set.

- * - *

Subclasses only need to implement {@link #getItem(int)} - * and {@link #getCount()} to have a working adapter. - * - *

Here is an example implementation of a pager containing fragments of - * lists: - * - * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentStatePagerSupport.java - * complete} - * - *

The R.layout.fragment_pager resource of the top-level fragment is: - * - * {@sample development/samples/Support4Demos/res/layout/fragment_pager.xml - * complete} - * - *

The R.layout.fragment_pager_list resource containing each - * individual fragment's layout is: - * - * {@sample development/samples/Support4Demos/res/layout/fragment_pager_list.xml - * complete} - */ -public abstract class FragmentStatePagerAdapter extends PagerAdapter { - private static final String TAG = "FragmentStatePagerAdapter"; - private static final boolean DEBUG = false; - - private final FragmentManager mFragmentManager; - private FragmentTransaction mCurTransaction = null; - - private ArrayList mSavedState = new ArrayList(); - private ArrayList mFragments = new ArrayList(); - private Fragment mCurrentPrimaryItem = null; - - public FragmentStatePagerAdapter(FragmentManager fm) { - mFragmentManager = fm; - } - - /** - * Return the Fragment associated with a specified position. - */ - public abstract Fragment getItem(int position); - - @Override - public void startUpdate(ViewGroup container) { - } - - @Override - public Object instantiateItem(ViewGroup container, int position) { - // If we already have this item instantiated, there is nothing - // to do. This can happen when we are restoring the entire pager - // from its saved state, where the fragment manager has already - // taken care of restoring the fragments we previously had instantiated. - if (mFragments.size() > position) { - Fragment f = mFragments.get(position); - if (f != null) { - return f; - } - } - - if (mCurTransaction == null) { - mCurTransaction = mFragmentManager.beginTransaction(); - } - - Fragment fragment = getItem(position); - if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); - if (mSavedState.size() > position) { - Fragment.SavedState fss = mSavedState.get(position); - if (fss != null) { - fragment.setInitialSavedState(fss); - } - } - while (mFragments.size() <= position) { - mFragments.add(null); - } - fragment.setMenuVisibility(false); - mFragments.set(position, fragment); - mCurTransaction.add(container.getId(), fragment); - - return fragment; - } - - @Override - public void destroyItem(ViewGroup container, int position, Object object) { - Fragment fragment = (Fragment)object; - - if (mCurTransaction == null) { - mCurTransaction = mFragmentManager.beginTransaction(); - } - if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object - + " v=" + ((Fragment)object).getView()); - while (mSavedState.size() <= position) { - mSavedState.add(null); - } - mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); - mFragments.set(position, null); - - mCurTransaction.remove(fragment); - } - - @Override - public void setPrimaryItem(ViewGroup container, int position, Object object) { - Fragment fragment = (Fragment)object; - if (fragment != mCurrentPrimaryItem) { - if (mCurrentPrimaryItem != null) { - fragment.setMenuVisibility(false); - } - if (fragment != null) { - fragment.setMenuVisibility(true); - } - mCurrentPrimaryItem = fragment; - } - } - - @Override - public void finishUpdate(ViewGroup container) { - if (mCurTransaction != null) { - mCurTransaction.commitAllowingStateLoss(); - mCurTransaction = null; - mFragmentManager.executePendingTransactions(); - } - } - - @Override - public boolean isViewFromObject(View view, Object object) { - return ((Fragment)object).getView() == view; - } - - @Override - public Parcelable saveState() { - Bundle state = null; - if (mSavedState.size() > 0) { - state = new Bundle(); - Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; - mSavedState.toArray(fss); - state.putParcelableArray("states", fss); - } - for (int i=0; i keys = bundle.keySet(); - for (String key: keys) { - if (key.startsWith("f")) { - int index = Integer.parseInt(key.substring(1)); - Fragment f = mFragmentManager.getFragment(bundle, key); - if (f != null) { - while (mFragments.size() <= index) { - mFragments.add(null); - } - f.setMenuVisibility(false); - mFragments.set(index, f); - } else { - Log.w(TAG, "Bad fragment at key " + key); - } - } - } - } - } -} diff --git a/src/org/fox/ttrss/HeadlinesFragment.java b/src/org/fox/ttrss/HeadlinesFragment.java index 9141f48f..acfeeb27 100644 --- a/src/org/fox/ttrss/HeadlinesFragment.java +++ b/src/org/fox/ttrss/HeadlinesFragment.java @@ -11,6 +11,9 @@ import java.util.HashMap; import java.util.List; import java.util.TimeZone; +import org.fox.ttrss.types.Article; +import org.fox.ttrss.types.Attachment; +import org.fox.ttrss.types.Feed; import org.jsoup.Jsoup; import android.app.Activity; diff --git a/src/org/fox/ttrss/ImageCacheService.java b/src/org/fox/ttrss/ImageCacheService.java deleted file mode 100644 index 6b0dc139..00000000 --- a/src/org/fox/ttrss/ImageCacheService.java +++ /dev/null @@ -1,205 +0,0 @@ -package org.fox.ttrss; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLConnection; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Date; - -import android.app.ActivityManager; -import android.app.IntentService; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.ActivityManager.RunningServiceInfo; -import android.content.Intent; -import android.os.Environment; -import android.util.Log; - -public class ImageCacheService extends IntentService { - - private final String TAG = this.getClass().getSimpleName(); - - public static final int NOTIFY_DOWNLOADING = 1; - - private static final String CACHE_PATH = "/data/org.fox.ttrss/image-cache/"; - - private int m_imagesDownloaded = 0; - - private NotificationManager m_nmgr; - - public ImageCacheService() { - super("ImageCacheService"); - } - - private boolean isDownloadServiceRunning() { - ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE); - for (RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { - if ("org.fox.ttrss.OfflineDownloadService".equals(service.service.getClassName())) { - return true; - } - } - return false; - } - - - @Override - public void onCreate() { - super.onCreate(); - m_nmgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); - } - - protected static boolean isUrlCached(String url) { - String hashedUrl = md5(url); - - File storage = Environment.getExternalStorageDirectory(); - - File file = new File(storage.getAbsolutePath() + CACHE_PATH + "/" + hashedUrl + ".png"); - - return file.exists(); - } - - protected static String getCacheFileName(String url) { - String hashedUrl = md5(url); - - File storage = Environment.getExternalStorageDirectory(); - - File file = new File(storage.getAbsolutePath() + CACHE_PATH + "/" + hashedUrl + ".png"); - - return file.getAbsolutePath(); - } - - protected static void cleanupCache(boolean deleteAll) { - if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { - File storage = Environment.getExternalStorageDirectory(); - File cachePath = new File(storage.getAbsolutePath() + CACHE_PATH); - - long now = new Date().getTime(); - - if (cachePath.isDirectory()) { - for (File file : cachePath.listFiles()) { - if (deleteAll || now - file.lastModified() > 1000*60*60*24*7) { - file.delete(); - } - } - } - } - } - - protected static String md5(String s) { - try { - MessageDigest digest = java.security.MessageDigest.getInstance("MD5"); - digest.update(s.getBytes()); - byte messageDigest[] = digest.digest(); - - StringBuffer hexString = new StringBuffer(); - for (int i=0; i 0 && m_activeFeedId != 0) { - SQLiteStatement stmt = getWritableDb() - .compileStatement( - "UPDATE articles SET unread = NOT unread WHERE selected = 1 AND feed_id = ?"); - stmt.bindLong(1, m_activeFeedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - return true; - case R.id.selection_toggle_marked: - if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { - SQLiteStatement stmt = getWritableDb() - .compileStatement( - "UPDATE articles SET marked = NOT marked WHERE selected = 1 AND feed_id = ?"); - stmt.bindLong(1, m_activeFeedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - return true; - case R.id.selection_toggle_published: - if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { - SQLiteStatement stmt = getWritableDb() - .compileStatement( - "UPDATE articles SET published = NOT published WHERE selected = 1 AND feed_id = ?"); - stmt.bindLong(1, m_activeFeedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - return true; - case R.id.toggle_published: - if (m_selectedArticleId != 0) { - SQLiteStatement stmt = getWritableDb().compileStatement( - "UPDATE articles SET published = NOT published WHERE " - + BaseColumns._ID + " = ?"); - stmt.bindLong(1, m_selectedArticleId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - return true; - case R.id.catchup_above: - if (m_selectedArticleId != 0 && m_activeFeedId != 0) { - SQLiteStatement stmt = getWritableDb().compileStatement( - "UPDATE articles SET unread = 0 WHERE updated >= " - + "(SELECT updated FROM articles WHERE " - + BaseColumns._ID + " = ?) AND feed_id = ?"); - stmt.bindLong(1, m_selectedArticleId); - stmt.bindLong(2, m_activeFeedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - return true; - case R.id.set_unread: - if (m_selectedArticleId != 0) { - SQLiteStatement stmt = getWritableDb().compileStatement( - "UPDATE articles SET unread = 1 WHERE " - + BaseColumns._ID + " = ?"); - stmt.bindLong(1, m_selectedArticleId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - return true; - case R.id.show_feeds: - setUnreadOnly(!getUnreadOnly()); - - if (getUnreadOnly()) { - item.setTitle(R.string.menu_all_feeds); - } else { - item.setTitle(R.string.menu_unread_feeds); - } - - return true; - default: - Log.d(TAG, - "onOptionsItemSelected, unhandled id=" + item.getItemId()); - return super.onOptionsItemSelected(item); - } - } - - private void refreshFeeds() { - OfflineFeedsFragment frag = (OfflineFeedsFragment) getFragmentManager() - .findFragmentById(R.id.feeds_fragment); - - if (frag != null) { - frag.refresh(); - } - } - - private void closeArticle() { - if (m_smallScreenMode) { - findViewById(R.id.main).setAnimation( - AnimationUtils.loadAnimation(this, R.anim.slide_right)); - } - - if (m_smallScreenMode) { - findViewById(R.id.article_fragment).setVisibility(View.GONE); - findViewById(R.id.headlines_fragment).setVisibility(View.VISIBLE); - } else { - findViewById(R.id.article_fragment).setVisibility(View.GONE); - findViewById(R.id.feeds_fragment).setVisibility(View.VISIBLE); - - } - - // we don't want to lose selected article in headlines so we refresh them before setting selected id to 0 - refreshViews(); - - m_selectedArticleId = 0; - - FragmentTransaction ft = getFragmentManager().beginTransaction(); - ft.replace(R.id.article_fragment, new DummyFragment()); - ft.commit(); - - initMainMenu(); - } - - private int getSelectedArticleCount() { - Cursor c = getReadableDb().query("articles", - new String[] { "COUNT(*)" }, "selected = 1", null, null, null, - null); - c.moveToFirst(); - int selected = c.getInt(0); - c.close(); - - return selected; - } - - @Override - public void initMainMenu() { - if (m_menu != null) { - int numSelected = getSelectedArticleCount(); - - m_menu.setGroupVisible(R.id.menu_group_feeds, false); - m_menu.setGroupVisible(R.id.menu_group_headlines, false); - m_menu.setGroupVisible(R.id.menu_group_headlines_selection, false); - m_menu.setGroupVisible(R.id.menu_group_article, false); - - if (numSelected != 0) { - if (m_headlinesActionMode == null) - m_headlinesActionMode = startActionMode(m_headlinesActionModeCallback); - } else if (m_selectedArticleId != 0) { - m_menu.setGroupVisible(R.id.menu_group_article, true); - } else if (m_activeFeedId != 0) { - m_menu.setGroupVisible(R.id.menu_group_headlines, true); - - MenuItem search = m_menu.findItem(R.id.search); - - SearchView searchView = (SearchView) search.getActionView(); - searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - private String query = ""; - - @Override - public boolean onQueryTextSubmit(String query) { - OfflineHeadlinesFragment frag = (OfflineHeadlinesFragment) getFragmentManager() - .findFragmentById(R.id.headlines_fragment); - - if (frag != null) { - frag.setSearchQuery(query); - this.query = query; - } - - return false; - } - - @Override - public boolean onQueryTextChange(String newText) { - if (newText.equals("") && !newText.equals(this.query)) { - OfflineHeadlinesFragment frag = (OfflineHeadlinesFragment) getFragmentManager() - .findFragmentById(R.id.headlines_fragment); - - if (frag != null) { - frag.setSearchQuery(newText); - this.query = newText; - } - } - - return false; - } - }); - - } else { - m_menu.setGroupVisible(R.id.menu_group_feeds, true); - } - - if (numSelected == 0 && m_headlinesActionMode != null) { - m_headlinesActionMode.finish(); - } - - if (m_activeFeedId != 0) { - Cursor feed = getFeedById(m_activeFeedId); - - if (feed != null) { - getActionBar().setTitle(feed.getString(feed.getColumnIndex("title"))); - } - } else { - getActionBar().setTitle(R.string.app_name); - } - - if (!m_smallScreenMode) { - getActionBar().setDisplayHomeAsUpEnabled(m_selectedArticleId != 0); - } else { - getActionBar().setDisplayHomeAsUpEnabled(m_selectedArticleId != 0 || m_activeFeedId != 0); - } - } - } - - @Override - public void onPause() { - super.onPause(); - - } - - @Override - public void onDestroy() { - super.onDestroy(); - - m_readableDb.close(); - m_writableDb.close(); - - } - - private void refreshViews() { - refreshFeeds(); - refreshHeadlines(); - } - - @Override - public boolean onContextItemSelected(MenuItem item) { - AdapterContextMenuInfo info = (AdapterContextMenuInfo) item - .getMenuInfo(); - - OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getFragmentManager() - .findFragmentById(R.id.headlines_fragment); - OfflineFeedsFragment ff = (OfflineFeedsFragment) getFragmentManager() - .findFragmentById(R.id.feeds_fragment); - - switch (item.getItemId()) { - case R.id.article_link_copy: - if (m_selectedArticleId != 0) { - Cursor article = null; - - if (m_selectedArticleId != 0) { - article = getArticleById(m_selectedArticleId); - } else if (info != null) { - article = hf.getArticleAtPosition(info.position); - } - - if (article != null) { - if (android.os.Build.VERSION.SDK_INT < 11) { - @SuppressWarnings("deprecation") - android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - clipboard.setText(article.getString(article.getColumnIndex("link"))); - } else { - android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - clipboard.setText(article.getString(article.getColumnIndex("link"))); - } - - article.close(); - - Toast toast = Toast.makeText(OfflineActivity.this, R.string.text_copied_to_clipboard, Toast.LENGTH_SHORT); - toast.show(); - } - } - return true; - case R.id.article_link_share: - if (m_selectedArticleId != 0) { - shareArticle(m_selectedArticleId); - } - return true; - - case R.id.browse_articles: - // TODO cat stuff - return true; - case R.id.browse_feeds: - // TODO cat stuff - return true; - case R.id.catchup_category: - // TODO cat stuff - return true; - case R.id.catchup_feed: - if (ff != null) { - int feedId = ff.getFeedIdAtPosition(info.position); - - if (feedId != 0) { - SQLiteStatement stmt = getWritableDb().compileStatement( - "UPDATE articles SET unread = 0 WHERE feed_id = ?"); - stmt.bindLong(1, feedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - } - return true; - case R.id.selection_toggle_unread: - if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { - SQLiteStatement stmt = getWritableDb() - .compileStatement( - "UPDATE articles SET unread = NOT unread WHERE selected = 1 AND feed_id = ?"); - stmt.bindLong(1, m_activeFeedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } else { - int articleId = hf.getArticleIdAtPosition(info.position); - if (articleId != 0) { - SQLiteStatement stmt = getWritableDb().compileStatement( - "UPDATE articles SET unread = NOT unread WHERE " - + BaseColumns._ID + " = ?"); - stmt.bindLong(1, articleId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - } - return true; - case R.id.selection_toggle_marked: - if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { - SQLiteStatement stmt = getWritableDb() - .compileStatement( - "UPDATE articles SET marked = NOT marked WHERE selected = 1 AND feed_id = ?"); - stmt.bindLong(1, m_activeFeedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } else { - int articleId = hf.getArticleIdAtPosition(info.position); - if (articleId != 0) { - SQLiteStatement stmt = getWritableDb().compileStatement( - "UPDATE articles SET marked = NOT marked WHERE " - + BaseColumns._ID + " = ?"); - stmt.bindLong(1, articleId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - } - return true; - case R.id.selection_toggle_published: - if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { - SQLiteStatement stmt = getWritableDb() - .compileStatement( - "UPDATE articles SET published = NOT published WHERE selected = 1 AND feed_id = ?"); - stmt.bindLong(1, m_activeFeedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } else { - int articleId = hf.getArticleIdAtPosition(info.position); - if (articleId != 0) { - SQLiteStatement stmt = getWritableDb().compileStatement( - "UPDATE articles SET published = NOT published WHERE " - + BaseColumns._ID + " = ?"); - stmt.bindLong(1, articleId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - } - return true; - case R.id.share_article: - Cursor article = hf.getArticleAtPosition(info.position); - - if (article != null) { - shareArticle(article); - } - return true; - case R.id.catchup_above: - int articleId = hf.getArticleIdAtPosition(info.position); - - if (articleId != 0 && m_activeFeedId != 0) { - SQLiteStatement stmt = 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_activeFeedId); - stmt.execute(); - stmt.close(); - refreshViews(); - } - return true; - default: - Log.d(TAG, - "onContextItemSelected, unhandled id=" + item.getItemId()); - return super.onContextItemSelected(item); - } - } - - @Override - public boolean dispatchKeyEvent(KeyEvent event) { - int action = event.getAction(); - int keyCode = event.getKeyCode(); - switch (keyCode) { - case KeyEvent.KEYCODE_VOLUME_DOWN: - if (action == KeyEvent.ACTION_DOWN) { - - OfflineHeadlinesFragment ohf = (OfflineHeadlinesFragment) getFragmentManager() - .findFragmentById(R.id.headlines_fragment); - - int nextId = getRelativeArticleId(m_selectedArticleId, - m_activeFeedId, RelativeArticle.AFTER); - - if (nextId != 0 && ohf != null) { - if (m_prefs.getBoolean("combined_mode", false)) { - ohf.setActiveArticleId(nextId); - - SQLiteStatement stmt = getWritableDb() - .compileStatement( - "UPDATE articles SET unread = 0 " - + "WHERE " + BaseColumns._ID - + " = ?"); - - stmt.bindLong(1, nextId); - stmt.execute(); - stmt.close(); - - } else { - openArticle(nextId, 0); - } - } - } - return true; - case KeyEvent.KEYCODE_VOLUME_UP: - if (action == KeyEvent.ACTION_UP) { - - OfflineHeadlinesFragment ohf = (OfflineHeadlinesFragment) getFragmentManager() - .findFragmentById(R.id.headlines_fragment); - - int prevId = getRelativeArticleId(m_selectedArticleId, - m_activeFeedId, RelativeArticle.BEFORE); - - if (prevId != 0 && ohf != null) { - if (m_prefs.getBoolean("combined_mode", false)) { - ohf.setActiveArticleId(prevId); - - SQLiteStatement stmt = getWritableDb() - .compileStatement( - "UPDATE articles SET unread = 0 " - + "WHERE " + BaseColumns._ID - + " = ?"); - - stmt.bindLong(1, prevId); - stmt.execute(); - stmt.close(); - - } else { - openArticle(prevId, 0); - } - } - } - return true; - default: - return super.dispatchKeyEvent(event); - } - } - - private void deselectAllArticles() { - getWritableDb().execSQL("UPDATE articles SET selected = 0 "); - } - - @Override - public int getRelativeArticleId(int baseId, int feedId, - OnlineServices.RelativeArticle mode) { - - Cursor c; - - /* - * if (baseId == 0) { c = getReadableDb().query("articles", null, - * "feed_id = ?", new String[] { String.valueOf(feedId) }, null, null, - * "updated DESC LIMIT 1"); - * - * if (c.moveToFirst()) { baseId = c.getInt(0); } - * - * c.close(); - * - * return baseId; } - */ - - if (mode == RelativeArticle.BEFORE) { - c = getReadableDb().query( - "articles", - null, - "updated > (SELECT updated FROM articles WHERE " - + BaseColumns._ID + " = ?) AND feed_id = ?", - new String[] { String.valueOf(baseId), - String.valueOf(feedId) }, null, null, - "updated LIMIT 1"); - - } else { - c = getReadableDb().query( - "articles", - null, - "updated < (SELECT updated FROM articles WHERE " - + BaseColumns._ID + " = ?) AND feed_id = ?", - new String[] { String.valueOf(baseId), - String.valueOf(feedId) }, null, null, - "updated DESC LIMIT 1"); - } - - int id = 0; - - if (c.moveToFirst()) { - id = c.getInt(0); - } - - c.close(); - - return id; - } - - @Override - public void viewFeed(int feedId) { - m_activeFeedId = feedId; - - initMainMenu(); - - if (m_smallScreenMode) { - findViewById(R.id.feeds_fragment).setVisibility(View.GONE); - findViewById(R.id.headlines_fragment).setVisibility(View.VISIBLE); - } else { - findViewById(R.id.headlines_fragment).setVisibility(View.VISIBLE); - } - - deselectAllArticles(); - - if (m_menu != null) { - MenuItem search = m_menu.findItem(R.id.search); - - if (search != null) { - SearchView sv = (SearchView) search.getActionView(); - sv.setQuery("", false); - } - } - - FragmentTransaction ft = getFragmentManager().beginTransaction(); - OfflineHeadlinesFragment frag = new OfflineHeadlinesFragment(); - ft.replace(R.id.headlines_fragment, frag); - ft.commit(); - - } - - @Override - public void openArticle(int articleId, int compatAnimation) { - m_selectedArticleId = articleId; - - initMainMenu(); - - OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getFragmentManager() - .findFragmentById(R.id.headlines_fragment); - - if (hf != null) { - hf.setActiveArticleId(articleId); - } - - SQLiteStatement stmt = getWritableDb().compileStatement( - "UPDATE articles SET unread = 0 " + "WHERE " + BaseColumns._ID - + " = ?"); - - stmt.bindLong(1, articleId); - stmt.execute(); - stmt.close(); - - Fragment frag; - - if (m_smallScreenMode) { - frag = new OfflineArticlePager(articleId); - } else { - frag = new OfflineArticleFragment(articleId); - } - - FragmentTransaction ft = getFragmentManager().beginTransaction(); - ft.replace(R.id.article_fragment, frag); - ft.commit(); - - if (m_smallScreenMode) { - if (compatAnimation == 0) - findViewById(R.id.main).setAnimation( - AnimationUtils.loadAnimation(this, R.anim.slide_left)); - else - findViewById(R.id.main).setAnimation( - AnimationUtils.loadAnimation(this, compatAnimation)); - } - - if (m_smallScreenMode) { - findViewById(R.id.headlines_fragment).setVisibility(View.GONE); - findViewById(R.id.article_fragment).setVisibility(View.VISIBLE); - } else { - findViewById(R.id.feeds_fragment).setVisibility(View.GONE); - findViewById(R.id.cats_fragment).setVisibility(View.GONE); - findViewById(R.id.article_fragment).setVisibility(View.VISIBLE); - } - - } - - @Override - public int getSelectedArticleId() { - return m_selectedArticleId; - } - - @Override - public void setSelectedArticleId(int articleId) { - m_selectedArticleId = articleId; - refreshViews(); - } -} \ No newline at end of file diff --git a/src/org/fox/ttrss/OfflineArticleFragment.java b/src/org/fox/ttrss/OfflineArticleFragment.java deleted file mode 100644 index 241a6ea0..00000000 --- a/src/org/fox/ttrss/OfflineArticleFragment.java +++ /dev/null @@ -1,248 +0,0 @@ -package org.fox.ttrss; - -import java.text.SimpleDateFormat; -import java.util.Date; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import android.app.Activity; -import android.app.Fragment; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.provider.BaseColumns; -import android.text.Html; -import android.text.method.LinkMovementMethod; -import android.util.TypedValue; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.widget.TextView; - -public class OfflineArticleFragment extends Fragment { - @SuppressWarnings("unused") - private final String TAG = this.getClass().getSimpleName(); - - private SharedPreferences m_prefs; - private int m_articleId; - private Cursor m_cursor; - private OfflineServices m_offlineServices; - - public OfflineArticleFragment() { - super(); - } - - public OfflineArticleFragment(int articleId) { - super(); - m_articleId = articleId; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - - getActivity().getMenuInflater().inflate(R.menu.article_link_context_menu, menu); - menu.setHeaderTitle(m_cursor.getString(m_cursor.getColumnIndex("title"))); - - super.onCreateContextMenu(menu, v, menuInfo); - - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - - if (savedInstanceState != null) { - m_articleId = savedInstanceState.getInt("articleId"); - } - - View view = inflater.inflate(R.layout.article_fragment, container, false); - - - // TODO change to interface? - Activity activity = getActivity(); - - if (activity != null) { - int orientation = activity.getWindowManager().getDefaultDisplay().getOrientation(); - - if (!m_offlineServices.isSmallScreen()) { - if (orientation % 2 == 0) { - view.findViewById(R.id.splitter_horizontal).setVisibility(View.GONE); - } else { - view.findViewById(R.id.splitter_vertical).setVisibility(View.GONE); - } - } else { - view.findViewById(R.id.splitter_vertical).setVisibility(View.GONE); - view.findViewById(R.id.splitter_horizontal).setVisibility(View.GONE); - } - } else { - view.findViewById(R.id.splitter_horizontal).setVisibility(View.GONE); - } - - m_cursor = m_offlineServices.getReadableDb().query("articles", null, BaseColumns._ID + "=?", - new String[] { String.valueOf(m_articleId) }, null, null, null); - - m_cursor.moveToFirst(); - - if (m_cursor.isFirst()) { - - TextView title = (TextView)view.findViewById(R.id.title); - - if (title != null) { - - String titleStr; - - if (m_cursor.getString(m_cursor.getColumnIndex("title")).length() > 200) - titleStr = m_cursor.getString(m_cursor.getColumnIndex("title")).substring(0, 200) + "..."; - else - titleStr = m_cursor.getString(m_cursor.getColumnIndex("title")); - - title.setMovementMethod(LinkMovementMethod.getInstance()); - title.setText(Html.fromHtml("" + titleStr + "")); - registerForContextMenu(title); - } - - WebView web = (WebView)view.findViewById(R.id.content); - - if (web != null) { - - String content; - String cssOverride = ""; - - WebSettings ws = web.getSettings(); - ws.setSupportZoom(true); - ws.setBuiltInZoomControls(true); - - TypedValue tv = new TypedValue(); - getActivity().getTheme().resolveAttribute(R.attr.linkColor, tv, true); - - // prevent flicker in ics - if (android.os.Build.VERSION.SDK_INT >= 11) { - web.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } - - if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) { - cssOverride = "body { background : transparent; color : #e0e0e0}"; - //view.setBackgroundColor(android.R.color.black); - web.setBackgroundColor(getResources().getColor(android.R.color.transparent)); - } else { - cssOverride = ""; - } - - String hexColor = String.format("#%06X", (0xFFFFFF & tv.data)); - cssOverride += " a:link {color: "+hexColor+";} a:visited { color: "+hexColor+";}"; - - String articleContent = m_cursor.getString(m_cursor.getColumnIndex("content")); - Document doc = Jsoup.parse(articleContent); - - if (doc != null) { - if (m_prefs.getBoolean("offline_image_cache_enabled", false)) { - - Elements images = doc.select("img"); - - for (Element img : images) { - String url = img.attr("src"); - - if (ImageCacheService.isUrlCached(url)) { - img.attr("src", "file://" + ImageCacheService.getCacheFileName(url)); - } - } - } - - // thanks webview for crashing on

articles = new Gson().fromJson(content, listType); - - SQLiteStatement stmtInsert = getWritableDb().compileStatement("INSERT INTO articles " + - "("+BaseColumns._ID+", unread, marked, published, updated, is_updated, title, link, feed_id, tags, content) " + - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"); - - for (Article article : articles) { - - String tagsString = ""; - - for (String t : article.tags) { - tagsString += t + ", "; - } - - tagsString = tagsString.replaceAll(", $", ""); - - stmtInsert.bindLong(1, article.id); - stmtInsert.bindLong(2, article.unread ? 1 : 0); - stmtInsert.bindLong(3, article.marked ? 1 : 0); - stmtInsert.bindLong(4, article.published ? 1 : 0); - stmtInsert.bindLong(5, article.updated); - stmtInsert.bindLong(6, article.is_updated ? 1 : 0); - stmtInsert.bindString(7, article.title); - stmtInsert.bindString(8, article.link); - stmtInsert.bindLong(9, article.feed_id); - stmtInsert.bindString(10, tagsString); // comma-separated tags - stmtInsert.bindString(11, article.content); - - if (m_downloadImages) { - Document doc = Jsoup.parse(article.content); - - if (doc != null) { - Elements images = doc.select("img"); - - for (Element img : images) { - String url = img.attr("src"); - - if (url.indexOf("://") != -1) { - if (!ImageCacheService.isUrlCached(url)) { - Intent intent = new Intent(OfflineDownloadService.this, - ImageCacheService.class); - - intent.putExtra("url", url); - startService(intent); - } - } - } - } - } - - try { - stmtInsert.execute(); - } catch (Exception e) { - e.printStackTrace(); - } - - } - - stmtInsert.close(); - - //m_canGetMoreArticles = articles.size() == 30; - m_articleOffset += articles.size(); - - Log.d(TAG, "offline: received " + articles.size() + " articles"); - - if (articles.size() == OFFLINE_SYNC_SEQ && m_articleOffset < m_syncMax) { - downloadArticles(); - } else { - downloadComplete(); - } - - return; - - } catch (Exception e) { - updateNotification(R.string.offline_switch_error); - Log.d(TAG, "offline: failed: exception when loading articles"); - e.printStackTrace(); - downloadFailed(); - } - - } else { - Log.d(TAG, "offline: failed: " + getErrorMessage()); - updateNotification(getErrorMessage()); - downloadFailed(); - } - } - } - - @Override - protected void onHandleIntent(Intent intent) { - m_sessionId = intent.getStringExtra("sessionId"); - - if (!m_downloadInProgress) { - if (m_downloadImages) ImageCacheService.cleanupCache(false); - - updateNotification(R.string.notify_downloading_init); - m_downloadInProgress = true; - - downloadFeeds(); - } - } -} diff --git a/src/org/fox/ttrss/OfflineFeedsFragment.java b/src/org/fox/ttrss/OfflineFeedsFragment.java deleted file mode 100644 index 41ef6e02..00000000 --- a/src/org/fox/ttrss/OfflineFeedsFragment.java +++ /dev/null @@ -1,292 +0,0 @@ -package org.fox.ttrss; - -import java.io.File; - -import android.app.Activity; -import android.app.Fragment; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.os.Bundle; -import android.os.Environment; -import android.preference.PreferenceManager; -import android.provider.BaseColumns; -import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.SimpleCursorAdapter; -import android.widget.TextView; - -public class OfflineFeedsFragment extends Fragment implements OnItemClickListener, OnSharedPreferenceChangeListener { - private final String TAG = this.getClass().getSimpleName(); - private SharedPreferences m_prefs; - private FeedListAdapter m_adapter; - private static final String ICON_PATH = "/data/org.fox.ttrss/icons/"; - private int m_selectedFeedId; - private boolean m_enableFeedIcons; - private Cursor m_cursor; - private OfflineServices m_offlineServices; - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - - getActivity().getMenuInflater().inflate(R.menu.feed_menu, menu); - - AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; - Cursor cursor = (Cursor)m_adapter.getItem(info.position); - - if (cursor != null) - menu.setHeaderTitle(cursor.getString(cursor.getColumnIndex("title"))); - - super.onCreateContextMenu(menu, v, menuInfo); - - } - - public Cursor createCursor() { - String unreadOnly = m_offlineServices.getUnreadOnly() ? "unread > 0" : null; - - String order = m_prefs.getBoolean("sort_feeds_by_unread", false) ? "unread DESC, title" : "title"; - - return m_offlineServices.getReadableDb().query("feeds_unread", - null, unreadOnly, null, null, null, order); - } - - public void refresh() { - if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close(); - - m_cursor = createCursor(); - - if (m_cursor != null) { - m_adapter.changeCursor(m_cursor); - m_adapter.notifyDataSetChanged(); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - - if (savedInstanceState != null) { - m_selectedFeedId = savedInstanceState.getInt("selectedFeedId"); - } - - View view = inflater.inflate(R.layout.feeds_fragment, container, false); - - ListView list = (ListView)view.findViewById(R.id.feeds); - - m_cursor = createCursor(); - - m_adapter = new FeedListAdapter(getActivity(), R.layout.feeds_row, m_cursor, - new String[] { "title", "unread" }, new int[] { R.id.title, R.id.unread_counter }, 0); - - list.setAdapter(m_adapter); - list.setOnItemClickListener(this); - list.setEmptyView(view.findViewById(R.id.no_feeds)); - registerForContextMenu(list); - - view.findViewById(R.id.loading_container).setVisibility(View.GONE); - - m_enableFeedIcons = m_prefs.getBoolean("download_feed_icons", false); - - return view; - } - - @Override - public void onDestroy() { - super.onDestroy(); - - if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close(); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - m_offlineServices = (OfflineServices)activity; - - m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); - m_prefs.registerOnSharedPreferenceChangeListener(this); - - } - - @Override - public void onSaveInstanceState (Bundle out) { - super.onSaveInstanceState(out); - - out.putInt("selectedFeedId", m_selectedFeedId); - } - - @Override - public void onItemClick(AdapterView av, View view, int position, long id) { - ListView list = (ListView)getActivity().findViewById(R.id.feeds); - - if (list != null) { - Cursor cursor = (Cursor) list.getItemAtPosition(position); - - if (cursor != null) { - int feedId = (int) cursor.getLong(0); - Log.d(TAG, "clicked on feed " + feedId); - - m_offlineServices.viewFeed(feedId); - - m_selectedFeedId = feedId; - - m_adapter.notifyDataSetChanged(); - } - } - } - - public void setLoadingStatus(int status, boolean showProgress) { - if (getView() != null) { - TextView tv = (TextView)getView().findViewById(R.id.loading_message); - - if (tv != null) { - tv.setText(status); - } - - View pb = getView().findViewById(R.id.loading_progress); - - if (pb != null) { - pb.setVisibility(showProgress ? View.VISIBLE : View.GONE); - } - } - } - - private class FeedListAdapter extends SimpleCursorAdapter { - - - public FeedListAdapter(Context context, int layout, Cursor c, - String[] from, int[] to, int flags) { - super(context, layout, c, from, to, flags); - } - - public static final int VIEW_NORMAL = 0; - public static final int VIEW_SELECTED = 1; - - public static final int VIEW_COUNT = VIEW_SELECTED+1; - - @Override - public int getViewTypeCount() { - return VIEW_COUNT; - } - - @Override - public int getItemViewType(int position) { - Cursor cursor = (Cursor) this.getItem(position); - - if (cursor.getLong(0) == m_selectedFeedId) { - return VIEW_SELECTED; - } else { - return VIEW_NORMAL; - } - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View v = convertView; - - Cursor cursor = (Cursor)getItem(position); - - if (v == null) { - int layoutId = R.layout.feeds_row; - - switch (getItemViewType(position)) { - case VIEW_SELECTED: - layoutId = R.layout.feeds_row_selected; - break; - } - - LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - v = vi.inflate(layoutId, null); - - } - - TextView tt = (TextView) v.findViewById(R.id.title); - - if (tt != null) { - tt.setText(cursor.getString(cursor.getColumnIndex("title"))); - } - - TextView tu = (TextView) v.findViewById(R.id.unread_counter); - - if (tu != null) { - tu.setText(String.valueOf(cursor.getInt(cursor.getColumnIndex("unread")))); - tu.setVisibility((cursor.getInt(cursor.getColumnIndex("unread")) > 0) ? View.VISIBLE : View.INVISIBLE); - } - - ImageView icon = (ImageView)v.findViewById(R.id.icon); - - if (icon != null) { - - if (m_enableFeedIcons) { - - File storage = Environment.getExternalStorageDirectory(); - - File iconFile = new File(storage.getAbsolutePath() + ICON_PATH + cursor.getInt(cursor.getColumnIndex(BaseColumns._ID)) + ".ico"); - if (iconFile.exists()) { - Bitmap bmpOrig = BitmapFactory.decodeFile(iconFile.getAbsolutePath()); - if (bmpOrig != null) { - icon.setImageBitmap(bmpOrig); - } - } else { - icon.setImageResource(cursor.getInt(cursor.getColumnIndex("unread")) > 0 ? R.drawable.ic_rss : R.drawable.ic_rss_bw); - } - - } else { - icon.setImageResource(cursor.getInt(cursor.getColumnIndex("unread")) > 0 ? R.drawable.ic_rss : R.drawable.ic_rss_bw); - } - - } - - return v; - } - } - - public void sortFeeds() { - try { - refresh(); - } catch (NullPointerException e) { - // activity is gone? - } catch (IllegalStateException e) { - // we're probably closing and DB is gone already - } - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { - - sortFeeds(); - m_enableFeedIcons = m_prefs.getBoolean("download_feed_icons", false); - - } - - public int getFeedIdAtPosition(int position) { - Cursor c = (Cursor)m_adapter.getItem(position); - - if (c != null) { - int feedId = c.getInt(0); - c.close(); - return feedId; - } - - return 0; - } - - public void setSelectedFeedId(int feedId) { - m_selectedFeedId = feedId; - refresh(); - } - -} diff --git a/src/org/fox/ttrss/OfflineHeadlinesFragment.java b/src/org/fox/ttrss/OfflineHeadlinesFragment.java deleted file mode 100644 index e4a40b13..00000000 --- a/src/org/fox/ttrss/OfflineHeadlinesFragment.java +++ /dev/null @@ -1,477 +0,0 @@ -package org.fox.ttrss; - -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -import org.jsoup.Jsoup; - -import android.app.Activity; -import android.app.Fragment; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.database.sqlite.SQLiteStatement; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.provider.BaseColumns; -import android.text.Html; -import android.text.Html.ImageGetter; -import android.text.method.LinkMovementMethod; -import android.util.Log; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.OnClickListener; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.AdapterContextMenuInfo; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.SimpleCursorAdapter; -import android.widget.TextView; - -public class OfflineHeadlinesFragment extends Fragment implements OnItemClickListener { - public static enum ArticlesSelection { ALL, NONE, UNREAD }; - - private final String TAG = this.getClass().getSimpleName(); - - private int m_feedId; - private int m_activeArticleId; - private boolean m_combinedMode = true; - private String m_searchQuery = ""; - - private SharedPreferences m_prefs; - - private Cursor m_cursor; - private ArticleListAdapter m_adapter; - - private OfflineServices m_offlineServices; - - private ImageGetter m_dummyGetter = new ImageGetter() { - - @Override - public Drawable getDrawable(String source) { - return new BitmapDrawable(); - } - - }; - - @Override - public void onDestroy() { - super.onDestroy(); - - if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close(); - } - - public int getSelectedArticleCount() { - Cursor c = m_offlineServices.getReadableDb().query("articles", - new String[] { "COUNT(*)" }, "selected = 1", null, null, null, null); - c.moveToFirst(); - int selected = c.getInt(0); - c.close(); - - return selected; - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - - getActivity().getMenuInflater().inflate(R.menu.headlines_menu, menu); - - if (getSelectedArticleCount() > 0) { - menu.setHeaderTitle(R.string.headline_context_multiple); - menu.setGroupVisible(R.id.menu_group_single_article, false); - } else { - AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo; - Cursor c = getArticleAtPosition(info.position); - menu.setHeaderTitle(c.getString(c.getColumnIndex("title"))); - //c.close(); - menu.setGroupVisible(R.id.menu_group_single_article, true); - } - - super.onCreateContextMenu(menu, v, menuInfo); - - } - - public void refresh() { - if (m_cursor != null && !m_cursor.isClosed()) m_cursor.close(); - - m_cursor = createCursor(); - - if (m_cursor != null) { - m_adapter.changeCursor(m_cursor); - setActiveArticleId(m_offlineServices.getSelectedArticleId()); - m_adapter.notifyDataSetChanged(); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - - if (savedInstanceState != null) { - m_feedId = savedInstanceState.getInt("feedId"); - m_activeArticleId = savedInstanceState.getInt("activeArticleId"); - //m_selectedArticles = savedInstanceState.getParcelableArrayList("selectedArticles"); - m_combinedMode = savedInstanceState.getBoolean("combinedMode"); - m_searchQuery = (String) savedInstanceState.getCharSequence("searchQuery"); - } - - View view = inflater.inflate(R.layout.headlines_fragment, container, false); - - m_cursor = createCursor(); - - ListView list = (ListView)view.findViewById(R.id.headlines); - m_adapter = new ArticleListAdapter(getActivity(), R.layout.headlines_row, m_cursor, - new String[] { "title" }, new int[] { R.id.title }, 0); - - list.setAdapter(m_adapter); - list.setOnItemClickListener(this); - list.setEmptyView(view.findViewById(R.id.no_headlines)); - registerForContextMenu(list); - - view.findViewById(R.id.loading_progress).setVisibility(View.GONE); - - return view; - } - - public Cursor createCursor() { - if (m_searchQuery.equals("")) { - return m_offlineServices.getReadableDb().query("articles", - null, "feed_id = ?", new String[] { String.valueOf(m_feedId) }, null, null, "updated DESC"); - } else { - return m_offlineServices.getReadableDb().query("articles", - null, "feed_id = ? AND (title LIKE '%' || ? || '%' OR content LIKE '%' || ? || '%')", - new String[] { String.valueOf(m_feedId), m_searchQuery, m_searchQuery }, null, null, "updated DESC"); - } - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - m_offlineServices = (OfflineServices)activity; - - m_feedId = m_offlineServices.getActiveFeedId(); - m_prefs = PreferenceManager.getDefaultSharedPreferences(getActivity().getApplicationContext()); - m_combinedMode = m_prefs.getBoolean("combined_mode", false); - } - - @Override - public void onItemClick(AdapterView av, View view, int position, long id) { - ListView list = (ListView)av; - - Log.d(TAG, "onItemClick=" + position); - - if (list != null) { - Cursor cursor = (Cursor)list.getItemAtPosition(position); - - m_activeArticleId = cursor.getInt(0); - - SQLiteStatement stmtUpdate = m_offlineServices.getWritableDb().compileStatement("UPDATE articles SET unread = 0 " + - "WHERE " + BaseColumns._ID + " = ?"); - - stmtUpdate.bindLong(1, m_activeArticleId); - stmtUpdate.execute(); - stmtUpdate.close(); - - if (!m_combinedMode) { - m_offlineServices.openArticle(m_activeArticleId, 0); - } - - refresh(); - } - } - - @Override - public void onSaveInstanceState (Bundle out) { - super.onSaveInstanceState(out); - - out.putInt("feedId", m_feedId); - out.putInt("activeArticleId", m_activeArticleId); - //out.putParcelableArrayList("selectedArticles", m_selectedArticles); - out.putBoolean("combinedMode", m_combinedMode); - out.putCharSequence("searchQuery", m_searchQuery); - } - - public void setLoadingStatus(int status, boolean showProgress) { - if (getView() != null) { - TextView tv = (TextView)getView().findViewById(R.id.loading_message); - - if (tv != null) { - tv.setText(status); - } - - View pb = getView().findViewById(R.id.loading_progress); - - if (pb != null) { - pb.setVisibility(showProgress ? View.VISIBLE : View.GONE); - } - } - } - - private class ArticleListAdapter extends SimpleCursorAdapter { - public ArticleListAdapter(Context context, int layout, Cursor c, - String[] from, int[] to, int flags) { - super(context, layout, c, from, to, flags); - // TODO Auto-generated constructor stub - } - - public static final int VIEW_NORMAL = 0; - public static final int VIEW_UNREAD = 1; - public static final int VIEW_SELECTED = 2; - public static final int VIEW_LOADMORE = 3; - - public static final int VIEW_COUNT = VIEW_LOADMORE+1; - - - public int getViewTypeCount() { - return VIEW_COUNT; - } - - @Override - public int getItemViewType(int position) { - Cursor c = (Cursor) getItem(position); - - //Log.d(TAG, "@gIVT " + position + " " + c.getInt(0) + " vs " + m_activeArticleId); - - if (c.getInt(0) == m_activeArticleId) { - return VIEW_SELECTED; - } else if (c.getInt(c.getColumnIndex("unread")) == 1) { - return VIEW_UNREAD; - } else { - return VIEW_NORMAL; - } - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - - View v = convertView; - - Cursor article = (Cursor)getItem(position); - final int articleId = article.getInt(0); - - if (v == null) { - int layoutId = R.layout.headlines_row; - - switch (getItemViewType(position)) { - case VIEW_LOADMORE: - layoutId = R.layout.headlines_row_loadmore; - break; - case VIEW_UNREAD: - layoutId = R.layout.headlines_row_unread; - break; - case VIEW_SELECTED: - layoutId = R.layout.headlines_row_selected; - break; - } - - LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - v = vi.inflate(layoutId, null); - - // http://code.google.com/p/android/issues/detail?id=3414 - ((ViewGroup)v).setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); - } - - TextView tt = (TextView)v.findViewById(R.id.title); - - if (tt != null) { - if (m_combinedMode) { - tt.setMovementMethod(LinkMovementMethod.getInstance()); - tt.setText(Html.fromHtml("" + - article.getString(article.getColumnIndex("title")) + "")); - } else { - tt.setText(Html.fromHtml(article.getString(article.getColumnIndex("title")))); - } - } - - ImageView marked = (ImageView)v.findViewById(R.id.marked); - - if (marked != null) { - marked.setImageResource(article.getInt(article.getColumnIndex("marked")) == 1 ? android.R.drawable.star_on : android.R.drawable.star_off); - - marked.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - SQLiteStatement stmtUpdate = m_offlineServices.getWritableDb().compileStatement("UPDATE articles SET marked = NOT marked " + - "WHERE " + BaseColumns._ID + " = ?"); - - stmtUpdate.bindLong(1, articleId); - stmtUpdate.execute(); - stmtUpdate.close(); - - refresh(); - } - }); - } - - ImageView published = (ImageView)v.findViewById(R.id.published); - - if (published != null) { - published.setImageResource(article.getInt(article.getColumnIndex("published")) == 1 ? R.drawable.ic_rss : R.drawable.ic_rss_bw); - - published.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - SQLiteStatement stmtUpdate = m_offlineServices.getWritableDb().compileStatement("UPDATE articles SET published = NOT published " + - "WHERE " + BaseColumns._ID + " = ?"); - - stmtUpdate.bindLong(1, articleId); - stmtUpdate.execute(); - stmtUpdate.close(); - - refresh(); - } - }); - } - - TextView te = (TextView)v.findViewById(R.id.excerpt); - - if (te != null) { - if (!m_combinedMode) { - String excerpt = Jsoup.parse(article.getString(article.getColumnIndex("content"))).text(); - - if (excerpt.length() > 100) - excerpt = excerpt.substring(0, 100) + "..."; - - te.setText(excerpt); - } else { - te.setVisibility(View.GONE); - } - } - - ImageView separator = (ImageView)v.findViewById(R.id.headlines_separator); - - if (separator != null && m_offlineServices.isSmallScreen()) { - separator.setVisibility(View.GONE); - } - - TextView content = (TextView)v.findViewById(R.id.content); - - if (content != null) { - if (m_combinedMode) { - content.setMovementMethod(LinkMovementMethod.getInstance()); - - content.setText(Html.fromHtml(article.getString(article.getColumnIndex("content")), m_dummyGetter, null)); - - switch (Integer.parseInt(m_prefs.getString("font_size", "0"))) { - case 0: - content.setTextSize(15F); - break; - case 1: - content.setTextSize(18F); - break; - case 2: - content.setTextSize(21F); - break; - } - } else { - content.setVisibility(View.GONE); - } - } - - v.findViewById(R.id.attachments_holder).setVisibility(View.GONE); - - TextView dv = (TextView) v.findViewById(R.id.date); - - if (dv != null) { - Date d = new Date((long)article.getInt(article.getColumnIndex("updated")) * 1000); - DateFormat df = new SimpleDateFormat("MMM dd, HH:mm"); - df.setTimeZone(TimeZone.getDefault()); - dv.setText(df.format(d)); - } - - CheckBox cb = (CheckBox) v.findViewById(R.id.selected); - - if (cb != null) { - cb.setChecked(article.getInt(article.getColumnIndex("selected")) == 1); - cb.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View view) { - CheckBox cb = (CheckBox)view; - - SQLiteStatement stmtUpdate = m_offlineServices.getWritableDb().compileStatement("UPDATE articles SET selected = ? " + - "WHERE " + BaseColumns._ID + " = ?"); - - stmtUpdate.bindLong(1, cb.isChecked() ? 1 : 0); - stmtUpdate.bindLong(2, articleId); - stmtUpdate.execute(); - stmtUpdate.close(); - - refresh(); - - m_offlineServices.initMainMenu(); - - } - }); - } - - return v; - } - } - - public void notifyUpdated() { - m_adapter.notifyDataSetChanged(); - } - - public void setActiveArticleId(int articleId) { - m_activeArticleId = articleId; - // m_adapter.notifyDataSetChanged(); - - ListView list = (ListView)getView().findViewById(R.id.headlines); - - if (list != null) { - list.setSelection(getArticleIdPosition(articleId)); - } - } - - public Cursor getArticleAtPosition(int position) { - return (Cursor) m_adapter.getItem(position); - } - - public int getArticleIdAtPosition(int position) { - /*Cursor c = getArticleAtPosition(position); - - if (c != null) { - int id = c.getInt(0); - return id; - } */ - - return (int) m_adapter.getItemId(position); - } - - public int getActiveArticleId() { - return m_activeArticleId; - } - - public int getArticleIdPosition(int articleId) { - for (int i = 0; i < m_adapter.getCount(); i++) { - if (articleId == m_adapter.getItemId(i)) - return i; - } - - return 0; - } - - public int getArticleCount() { - return m_adapter.getCount(); - } - - public void setSearchQuery(String query) { - if (!m_searchQuery.equals(query)) { - m_searchQuery = query; - refresh(); - } - } - -} diff --git a/src/org/fox/ttrss/OfflineServices.java b/src/org/fox/ttrss/OfflineServices.java deleted file mode 100644 index e5fdb92e..00000000 --- a/src/org/fox/ttrss/OfflineServices.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.fox.ttrss; - -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 viewFeed(int feedId); - public void openArticle(int articleId, int compatAnimation); - public boolean getUnreadOnly(); - public int getSelectedArticleId(); - public void initMainMenu(); - public boolean isSmallScreen(); - public void setSelectedArticleId(int articleId); -} diff --git a/src/org/fox/ttrss/OfflineUploadService.java b/src/org/fox/ttrss/OfflineUploadService.java deleted file mode 100644 index 9eb028ac..00000000 --- a/src/org/fox/ttrss/OfflineUploadService.java +++ /dev/null @@ -1,259 +0,0 @@ -package org.fox.ttrss; - -import java.util.HashMap; - -import com.google.gson.JsonElement; - -import android.app.IntentService; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Intent; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.util.Log; - -public class OfflineUploadService extends IntentService { - private final String TAG = this.getClass().getSimpleName(); - - public static final int NOTIFY_UPLOADING = 2; - public static final String INTENT_ACTION_SUCCESS = "org.fox.ttrss.intent.action.UploadComplete"; - - private SQLiteDatabase m_writableDb; - private SQLiteDatabase m_readableDb; - private String m_sessionId; - private NotificationManager m_nmgr; - private boolean m_uploadInProgress = false; - - public OfflineUploadService() { - super("OfflineUploadService"); - } - - @Override - public void onCreate() { - super.onCreate(); - m_nmgr = (NotificationManager)getSystemService(NOTIFICATION_SERVICE); - initDatabase(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - - m_nmgr.cancel(NOTIFY_UPLOADING); - } - - private void updateNotification(String msg) { - Notification notification = new Notification(R.drawable.icon, - getString(R.string.notify_uploading_title), System.currentTimeMillis()); - - PendingIntent contentIntent = PendingIntent.getActivity(this, 0, - new Intent(this, MainActivity.class), 0); - - notification.flags |= Notification.FLAG_ONGOING_EVENT; - notification.flags |= Notification.FLAG_ONLY_ALERT_ONCE; - - notification.setLatestEventInfo(this, getString(R.string.notify_uploading_title), msg, contentIntent); - - m_nmgr.notify(NOTIFY_UPLOADING, notification); - } - - private void updateNotification(int msgResId) { - updateNotification(getString(msgResId)); - } - - private void initDatabase() { - DatabaseHelper dh = new DatabaseHelper(getApplicationContext()); - m_writableDb = dh.getWritableDatabase(); - m_readableDb = dh.getReadableDatabase(); - } - - private synchronized SQLiteDatabase getReadableDb() { - return m_readableDb; - } - - private synchronized SQLiteDatabase getWritableDb() { - return m_writableDb; - } - - private void uploadRead() { - Log.d(TAG, "syncing modified offline data... (read)"); - - final String ids = getModifiedIds(ModifiedCriteria.READ); - - if (ids.length() > 0) { - ApiRequest req = new ApiRequest(getApplicationContext()) { - @Override - protected void onPostExecute(JsonElement result) { - if (result != null) { - uploadMarked(); - } else { - updateNotification(getErrorMessage()); - uploadFailed(); - } - } - }; - - @SuppressWarnings("serial") - HashMap map = new HashMap() { - { - put("sid", m_sessionId); - put("op", "updateArticle"); - put("article_ids", ids); - put("mode", "0"); - put("field", "2"); - } - }; - - req.execute(map); - } else { - uploadMarked(); - } - } - - private enum ModifiedCriteria { - READ, MARKED, PUBLISHED - }; - - private String getModifiedIds(ModifiedCriteria criteria) { - - String criteriaStr = ""; - - switch (criteria) { - case READ: - criteriaStr = "unread = 0"; - break; - case MARKED: - criteriaStr = "marked = 1"; - break; - case PUBLISHED: - criteriaStr = "published = 1"; - break; - } - - Cursor c = getReadableDb().query("articles", null, - "modified = 1 AND " + criteriaStr, null, null, null, null); - - String tmp = ""; - - while (c.moveToNext()) { - tmp += c.getInt(0) + ","; - } - - tmp = tmp.replaceAll(",$", ""); - - c.close(); - - return tmp; - } - - private void uploadMarked() { - Log.d(TAG, "syncing modified offline data... (marked)"); - - final String ids = getModifiedIds(ModifiedCriteria.MARKED); - - if (ids.length() > 0) { - ApiRequest req = new ApiRequest(getApplicationContext()) { - @Override - protected void onPostExecute(JsonElement result) { - if (result != null) { - uploadPublished(); - } else { - updateNotification(getErrorMessage()); - uploadFailed(); - } - } - }; - - @SuppressWarnings("serial") - HashMap map = new HashMap() { - { - put("sid", m_sessionId); - put("op", "updateArticle"); - put("article_ids", ids); - put("mode", "0"); - put("field", "0"); - } - }; - - req.execute(map); - } else { - uploadPublished(); - } - } - - private void uploadFailed() { - m_readableDb.close(); - m_writableDb.close(); - - // TODO send notification to activity? - - m_uploadInProgress = false; - } - - private void uploadSuccess() { - getWritableDb().execSQL("UPDATE articles SET modified = 0"); - - Intent intent = new Intent(); - intent.setAction(INTENT_ACTION_SUCCESS); - intent.addCategory(Intent.CATEGORY_DEFAULT); - sendBroadcast(intent); - - m_readableDb.close(); - m_writableDb.close(); - - m_uploadInProgress = false; - - m_nmgr.cancel(NOTIFY_UPLOADING); - } - - private void uploadPublished() { - Log.d(TAG, "syncing modified offline data... (published)"); - - final String ids = getModifiedIds(ModifiedCriteria.MARKED); - - if (ids.length() > 0) { - ApiRequest req = new ApiRequest(getApplicationContext()) { - @Override - protected void onPostExecute(JsonElement result) { - if (result != null) { - uploadSuccess(); - } else { - updateNotification(getErrorMessage()); - uploadFailed(); - } - } - }; - - @SuppressWarnings("serial") - HashMap map = new HashMap() { - { - put("sid", m_sessionId); - put("op", "updateArticle"); - put("article_ids", ids); - put("mode", "0"); - put("field", "1"); - } - }; - - req.execute(map); - } else { - uploadSuccess(); - } - } - - - @Override - protected void onHandleIntent(Intent intent) { - m_sessionId = intent.getStringExtra("sessionId"); - - if (!m_uploadInProgress) { - m_uploadInProgress = true; - - updateNotification(R.string.notify_uploading_sending_data); - - uploadRead(); - } - } - -} diff --git a/src/org/fox/ttrss/OnlineServices.java b/src/org/fox/ttrss/OnlineServices.java index a4a10f08..3f61276a 100644 --- a/src/org/fox/ttrss/OnlineServices.java +++ b/src/org/fox/ttrss/OnlineServices.java @@ -1,5 +1,9 @@ package org.fox.ttrss; +import org.fox.ttrss.types.Article; +import org.fox.ttrss.types.Feed; +import org.fox.ttrss.types.FeedCategory; + public interface OnlineServices { public enum RelativeArticle { BEFORE, AFTER }; diff --git a/src/org/fox/ttrss/PrefsBackupAgent.java b/src/org/fox/ttrss/PrefsBackupAgent.java deleted file mode 100644 index 0a7e5dec..00000000 --- a/src/org/fox/ttrss/PrefsBackupAgent.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.fox.ttrss; - -import android.app.backup.BackupAgentHelper; -import android.app.backup.SharedPreferencesBackupHelper; - -public class PrefsBackupAgent extends BackupAgentHelper { - // The name of the SharedPreferences file - static final String PREFS = "org.fox.ttrss_preferences"; - - // A key to uniquely identify the set of backup data - static final String PREFS_BACKUP_KEY = "prefs"; - - // Allocate a helper and add it to the backup agent - @Override - public void onCreate() { - SharedPreferencesBackupHelper helper = new SharedPreferencesBackupHelper(this, PREFS); - addHelper(PREFS_BACKUP_KEY, helper); - } -} diff --git a/src/org/fox/ttrss/billing/BillingConstants.java b/src/org/fox/ttrss/billing/BillingConstants.java new file mode 100644 index 00000000..eb440219 --- /dev/null +++ b/src/org/fox/ttrss/billing/BillingConstants.java @@ -0,0 +1,63 @@ +package org.fox.ttrss.billing; + + +public class BillingConstants { + + // The response codes for a request, defined by Android Market. + public enum ResponseCode { + RESULT_OK, + RESULT_USER_CANCELED, + RESULT_SERVICE_UNAVAILABLE, + RESULT_BILLING_UNAVAILABLE, + RESULT_ITEM_UNAVAILABLE, + RESULT_DEVELOPER_ERROR, + RESULT_ERROR; + + // Converts from an ordinal value to the ResponseCode + public static ResponseCode valueOf(int index) { + ResponseCode[] values = ResponseCode.values(); + if (index < 0 || index >= values.length) { + return RESULT_ERROR; + } + return values[index]; + } + } + + // The possible states of an in-app purchase, as defined by Android Market. + public enum PurchaseState { + // Responses to requestPurchase or restoreTransactions. + PURCHASED, // User was charged for the order. + CANCELED, // The charge failed on the server. + REFUNDED; // User received a refund for the order. + + // Converts from an ordinal value to the PurchaseState + public static PurchaseState valueOf(int index) { + PurchaseState[] values = PurchaseState.values(); + if (index < 0 || index >= values.length) { + return CANCELED; + } + return values[index]; + } + } + + // These are the names of the extras that are passed in an intent from + // Market to this application and cannot be changed. + public static final String NOTIFICATION_ID = "notification_id"; + public static final String INAPP_SIGNED_DATA = "inapp_signed_data"; + public static final String INAPP_SIGNATURE = "inapp_signature"; + public static final String INAPP_REQUEST_ID = "request_id"; + public static final String INAPP_RESPONSE_CODE = "response_code"; + + // Intent actions that we send from the BillingReceiver to the + // BillingService. Defined by this application. + public static final String ACTION_CONFIRM_NOTIFICATION = "com.example.dungeons.CONFIRM_NOTIFICATION"; + public static final String ACTION_GET_PURCHASE_INFORMATION = "com.example.dungeons.GET_PURCHASE_INFORMATION"; + public static final String ACTION_RESTORE_TRANSACTIONS = "com.example.dungeons.RESTORE_TRANSACTIONS"; + + // Intent actions that we receive in the BillingReceiver from Market. + // These are defined by Market and cannot be changed. + public static final String ACTION_NOTIFY = "com.android.vending.billing.IN_APP_NOTIFY"; + public static final String ACTION_RESPONSE_CODE = "com.android.vending.billing.RESPONSE_CODE"; + public static final String ACTION_PURCHASE_STATE_CHANGED = "com.android.vending.billing.PURCHASE_STATE_CHANGED"; + +} diff --git a/src/org/fox/ttrss/billing/BillingHelper.java b/src/org/fox/ttrss/billing/BillingHelper.java new file mode 100644 index 00000000..dcf29322 --- /dev/null +++ b/src/org/fox/ttrss/billing/BillingHelper.java @@ -0,0 +1,268 @@ +package org.fox.ttrss.billing; + +import java.util.ArrayList; + +import android.app.PendingIntent; +import android.app.PendingIntent.CanceledException; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Log; + +import com.android.vending.billing.IMarketBillingService; + +import org.fox.ttrss.billing.BillingConstants.ResponseCode; +import org.fox.ttrss.billing.BillingSecurity.VerifiedPurchase; + +public class BillingHelper { + + private static final String TAG = "BillingService"; + + private static IMarketBillingService mService; + private static Context mContext; + private static Handler mCompletedHandler; + + protected static VerifiedPurchase latestPurchase; + + protected static void instantiateHelper(Context context, IMarketBillingService service) { + mService = service; + mContext = context; + } + + protected static void setCompletedHandler(Handler handler){ + mCompletedHandler = handler; + } + + public static boolean isBillingSupported() { + if (amIDead()) { + return false; + } + Bundle request = makeRequestBundle("CHECK_BILLING_SUPPORTED"); + if (mService != null) { + try { + Bundle response = mService.sendBillingRequest(request); + ResponseCode code = ResponseCode.valueOf((Integer) response.get("RESPONSE_CODE")); + Log.i(TAG, "isBillingSupported response was: " + code.toString()); + if (ResponseCode.RESULT_OK.equals(code)) { + return true; + } else { + return false; + } + } catch (RemoteException e) { + Log.e(TAG, "isBillingSupported response was: RemoteException", e); + return false; + } + } else { + Log.i(TAG, "isBillingSupported response was: BillingService.mService = null"); + return false; + } + } + + /** + * A REQUEST_PURCHASE request also triggers two asynchronous responses (broadcast intents). + * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides error information about the request. (which I ignore) + * Next, if the request was successful, the Android Market application sends an IN_APP_NOTIFY broadcast intent. + * This message contains a notification ID, which you can use to retrieve the transaction details for the REQUEST_PURCHASE + * @param activityContext + * @param itemId + */ + public static void requestPurchase(Context activityContext, String itemId){ + if (amIDead()) { + return; + } + Log.i(TAG, "requestPurchase()"); + Bundle request = makeRequestBundle("REQUEST_PURCHASE"); + request.putString("ITEM_ID", itemId); + try { + Bundle response = mService.sendBillingRequest(request); + + //The RESPONSE_CODE key provides you with the status of the request + Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); + //The PURCHASE_INTENT key provides you with a PendingIntent, which you can use to launch the checkout UI + PendingIntent pendingIntent = (PendingIntent) response.get("PURCHASE_INTENT"); + //The REQUEST_ID key provides you with a unique request identifier for the request + Long requestIndentifier = (Long) response.get("REQUEST_ID"); + Log.i(TAG, "current request is:" + requestIndentifier); + BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); + Log.i(TAG, "REQUEST_PURCHASE Sync Response code: "+responseCode.toString()); + + startBuyPageActivity(pendingIntent, new Intent(), activityContext); + } catch (RemoteException e) { + Log.e(TAG, "Failed, internet error maybe", e); + Log.e(TAG, "Billing supported: "+isBillingSupported()); + } + } + + /** + * A GET_PURCHASE_INFORMATION request also triggers two asynchronous responses (broadcast intents). + * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. (which I ignore) + * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent. + * This message contains detailed transaction information. + * The transaction information is contained in a signed JSON string (unencrypted). + * The message includes the signature so you can verify the integrity of the signed string + * @param notifyIds + */ + protected static void getPurchaseInformation(String[] notifyIds){ + if (amIDead()) { + return; + } + Log.i(TAG, "getPurchaseInformation()"); + Bundle request = makeRequestBundle("GET_PURCHASE_INFORMATION"); + // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate. + // The Android Market application returns this nonce with the PURCHASE_STATE_CHANGED broadcast intent so you can verify the integrity of the transaction information. + request.putLong("NONCE", BillingSecurity.generateNonce()); + // The NOTIFY_IDS key contains an array of notification IDs, which you received in the IN_APP_NOTIFY broadcast intent. + request.putStringArray("NOTIFY_IDS", notifyIds); + try { + Bundle response = mService.sendBillingRequest(request); + + //The REQUEST_ID key provides you with a unique request identifier for the request + Long requestIndentifier = (Long) response.get("REQUEST_ID"); + Log.i(TAG, "current request is:" + requestIndentifier); + //The RESPONSE_CODE key provides you with the status of the request + Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); + BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); + Log.i(TAG, "GET_PURCHASE_INFORMATION Sync Response code: "+responseCode.toString()); + + } catch (RemoteException e) { + Log.e(TAG, "Failed, internet error maybe", e); + Log.e(TAG, "Billing supported: "+isBillingSupported()); + } + } + + /** + * To acknowledge that you received transaction information you send a + * CONFIRM_NOTIFICATIONS request. + * + * A CONFIRM_NOTIFICATIONS request triggers a single asynchronous response�a RESPONSE_CODE broadcast intent. + * This broadcast intent provides status and error information about the request. + * + * Note: As a best practice, you should not send a CONFIRM_NOTIFICATIONS request for a purchased item until you have delivered the item to the user. + * This way, if your application crashes or something else prevents your application from delivering the product, + * your application will still receive an IN_APP_NOTIFY broadcast intent from Android Market indicating that you need to deliver the product + * @param notifyIds + */ + protected static void confirmTransaction(String[] notifyIds) { + if (amIDead()) { + return; + } + Log.i(TAG, "confirmTransaction()"); + Bundle request = makeRequestBundle("CONFIRM_NOTIFICATIONS"); + request.putStringArray("NOTIFY_IDS", notifyIds); + try { + Bundle response = mService.sendBillingRequest(request); + + //The REQUEST_ID key provides you with a unique request identifier for the request + Long requestIndentifier = (Long) response.get("REQUEST_ID"); + Log.i(TAG, "current request is:" + requestIndentifier); + + //The RESPONSE_CODE key provides you with the status of the request + Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); + BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); + + Log.i(TAG, "CONFIRM_NOTIFICATIONS Sync Response code: "+responseCode.toString()); + } catch (RemoteException e) { + Log.e(TAG, "Failed, internet error maybe", e); + Log.e(TAG, "Billing supported: " + isBillingSupported()); + } + } + + /** + * + * Can be used for when a user has reinstalled the app to give back prior purchases. + * if an item for sale's purchase type is "managed per user account" this means google will have a record ofthis transaction + * + * A RESTORE_TRANSACTIONS request also triggers two asynchronous responses (broadcast intents). + * First, the Android Market application sends a RESPONSE_CODE broadcast intent, which provides status and error information about the request. + * Next, if the request was successful, the Android Market application sends a PURCHASE_STATE_CHANGED broadcast intent. + * This message contains the detailed transaction information. The transaction information is contained in a signed JSON string (unencrypted). + * The message includes the signature so you can verify the integrity of the signed string + * @param nonce + */ + protected static void restoreTransactionInformation(Long nonce) { + if (amIDead()) { + return; + } + Log.i(TAG, "confirmTransaction()"); + Bundle request = makeRequestBundle("RESTORE_TRANSACTIONS"); + // The REQUEST_NONCE key contains a cryptographically secure nonce (number used once) that you must generate + request.putLong("NONCE", nonce); + try { + Bundle response = mService.sendBillingRequest(request); + + //The REQUEST_ID key provides you with a unique request identifier for the request + Long requestIndentifier = (Long) response.get("REQUEST_ID"); + Log.i(TAG, "current request is:" + requestIndentifier); + + //The RESPONSE_CODE key provides you with the status of the request + Integer responseCodeIndex = (Integer) response.get("RESPONSE_CODE"); + BillingConstants.ResponseCode responseCode = BillingConstants.ResponseCode.valueOf(responseCodeIndex); + Log.i(TAG, "RESTORE_TRANSACTIONS Sync Response code: "+responseCode.toString()); + } catch (RemoteException e) { + Log.e(TAG, "Failed, internet error maybe", e); + Log.e(TAG, "Billing supported: " + isBillingSupported()); + } + } + + private static boolean amIDead() { + if (mService == null || mContext == null) { + Log.e(TAG, "BillingHelper not fully instantiated"); + return true; + } else { + return false; + } + } + + private static Bundle makeRequestBundle(String method) { + Bundle request = new Bundle(); + request.putString("BILLING_REQUEST", method); + request.putInt("API_VERSION", 1); + request.putString("PACKAGE_NAME", mContext.getPackageName()); + return request; + } + + /** + * + * + * You must launch the pending intent from an activity context and not an application context + * You cannot use the singleTop launch mode to launch the pending intent + * @param pendingIntent + * @param intent + * @param context + */ + private static void startBuyPageActivity(PendingIntent pendingIntent, Intent intent, Context context){ + //TODO add above 2.0 implementation with reflection, for now just using 1.6 implem + + // This is on Android 1.6. The in-app checkout page activity will be on its + // own separate activity stack instead of on the activity stack of + // the application. + try { + pendingIntent.send(context, 0, intent); + } catch (CanceledException e){ + Log.e(TAG, "startBuyPageActivity CanceledException"); + } + } + + protected static void verifyPurchase(String signedData, String signature) { + ArrayList purchases = BillingSecurity.verifyPurchase(signedData, signature); + latestPurchase = purchases.get(0); + + confirmTransaction(new String[]{latestPurchase.notificationId}); + + if(mCompletedHandler != null){ + mCompletedHandler.sendEmptyMessage(0); + } else { + Log.e(TAG, "verifyPurchase error. Handler not instantiated. Have you called setCompletedHandler()?"); + } + } + + public static void stopService(){ + mContext.stopService(new Intent(mContext, BillingService.class)); + mService = null; + mContext = null; + mCompletedHandler = null; + Log.i(TAG, "Stopping Service"); + } +} diff --git a/src/org/fox/ttrss/billing/BillingReceiver.java b/src/org/fox/ttrss/billing/BillingReceiver.java new file mode 100644 index 00000000..9b772054 --- /dev/null +++ b/src/org/fox/ttrss/billing/BillingReceiver.java @@ -0,0 +1,57 @@ +package org.fox.ttrss.billing; + +import static org.fox.ttrss.billing.BillingConstants.ACTION_NOTIFY; +import static org.fox.ttrss.billing.BillingConstants.ACTION_PURCHASE_STATE_CHANGED; +import static org.fox.ttrss.billing.BillingConstants.ACTION_RESPONSE_CODE; +import static org.fox.ttrss.billing.BillingConstants.INAPP_REQUEST_ID; +import static org.fox.ttrss.billing.BillingConstants.INAPP_RESPONSE_CODE; +import static org.fox.ttrss.billing.BillingConstants.INAPP_SIGNATURE; +import static org.fox.ttrss.billing.BillingConstants.INAPP_SIGNED_DATA; +import static org.fox.ttrss.billing.BillingConstants.NOTIFICATION_ID; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class BillingReceiver extends BroadcastReceiver { + + private static final String TAG = "BillingService"; + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + Log.i(TAG, "Received action: " + action); + if (ACTION_PURCHASE_STATE_CHANGED.equals(action)) { + String signedData = intent.getStringExtra(INAPP_SIGNED_DATA); + String signature = intent.getStringExtra(INAPP_SIGNATURE); + purchaseStateChanged(context, signedData, signature); + } else if (ACTION_NOTIFY.equals(action)) { + String notifyId = intent.getStringExtra(NOTIFICATION_ID); + notify(context, notifyId); + } else if (ACTION_RESPONSE_CODE.equals(action)) { + long requestId = intent.getLongExtra(INAPP_REQUEST_ID, -1); + int responseCodeIndex = intent.getIntExtra(INAPP_RESPONSE_CODE, BillingConstants.ResponseCode.RESULT_ERROR.ordinal()); + checkResponseCode(context, requestId, responseCodeIndex); + } else { + Log.e(TAG, "unexpected action: " + action); + } + } + + + private void purchaseStateChanged(Context context, String signedData, String signature) { + Log.i(TAG, "purchaseStateChanged got signedData: " + signedData); + Log.i(TAG, "purchaseStateChanged got signature: " + signature); + BillingHelper.verifyPurchase(signedData, signature); + } + + private void notify(Context context, String notifyId) { + Log.i(TAG, "notify got id: " + notifyId); + String[] notifyIds = {notifyId}; + BillingHelper.getPurchaseInformation(notifyIds); + } + + private void checkResponseCode(Context context, long requestId, int responseCodeIndex) { + Log.i(TAG, "checkResponseCode got requestId: " + requestId); + Log.i(TAG, "checkResponseCode got responseCode: " + BillingConstants.ResponseCode.valueOf(responseCodeIndex)); + } +} \ No newline at end of file diff --git a/src/org/fox/ttrss/billing/BillingSecurity.java b/src/org/fox/ttrss/billing/BillingSecurity.java new file mode 100644 index 00000000..513d6f34 --- /dev/null +++ b/src/org/fox/ttrss/billing/BillingSecurity.java @@ -0,0 +1,258 @@ +// Copyright 2010 Google Inc. All Rights Reserved. + +package org.fox.ttrss.billing; + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.Signature; +import java.security.SignatureException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.HashSet; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import android.text.TextUtils; +import android.util.Log; + +import org.fox.ttrss.billing.BillingConstants.PurchaseState; +import org.fox.ttrss.util.Base64; +import org.fox.ttrss.util.Base64DecoderException; + +/** + * Security-related methods. For a secure implementation, all of this code + * should be implemented on a server that communicates with the application on + * the device. For the sake of simplicity and clarity of this example, this code + * is included here and is executed on the device. If you must verify the + * purchases on the phone, you should obfuscate this code to make it harder for + * an attacker to replace the code with stubs that treat all purchases as + * verified. + */ +public class BillingSecurity { + private static final String TAG = "BillingService"; + + private static final String KEY_FACTORY_ALGORITHM = "RSA"; + private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; + private static final SecureRandom RANDOM = new SecureRandom(); + + /** + * This keeps track of the nonces that we generated and sent to the server. + * We need to keep track of these until we get back the purchase state and + * send a confirmation message back to Android Market. If we are killed and + * lose this list of nonces, it is not fatal. Android Market will send us a + * new "notify" message and we will re-generate a new nonce. This has to be + * "static" so that the {@link BillingReceiver} can check if a nonce exists. + */ + private static HashSet sKnownNonces = new HashSet(); + + /** + * A class to hold the verified purchase information. + */ + public static class VerifiedPurchase { + public PurchaseState purchaseState; + public String notificationId; + public String productId; + public String orderId; + public long purchaseTime; + public String developerPayload; + + public VerifiedPurchase(PurchaseState purchaseState, String notificationId, String productId, String orderId, long purchaseTime, + String developerPayload) { + this.purchaseState = purchaseState; + this.notificationId = notificationId; + this.productId = productId; + this.orderId = orderId; + this.purchaseTime = purchaseTime; + this.developerPayload = developerPayload; + } + + public boolean isPurchased(){ + return purchaseState.equals(PurchaseState.PURCHASED); + } + + + } + + /** Generates a nonce (a random number used once). */ + public static long generateNonce() { + long nonce = RANDOM.nextLong(); + Log.i(TAG, "Nonce generateD: "+nonce); + sKnownNonces.add(nonce); + return nonce; + } + + public static void removeNonce(long nonce) { + sKnownNonces.remove(nonce); + } + + public static boolean isNonceKnown(long nonce) { + return sKnownNonces.contains(nonce); + } + + /** + * Verifies that the data was signed with the given signature, and returns + * the list of verified purchases. The data is in JSON format and contains a + * nonce (number used once) that we generated and that was signed (as part + * of the whole data string) with a private key. The data also contains the + * {@link PurchaseState} and product ID of the purchase. In the general + * case, there can be an array of purchase transactions because there may be + * delays in processing the purchase on the backend and then several + * purchases can be batched together. + * + * @param signedData + * the signed JSON string (signed, not encrypted) + * @param signature + * the signature for the data, signed with the private key + */ + public static ArrayList verifyPurchase(String signedData, String signature) { + if (signedData == null) { + Log.e(TAG, "data is null"); + return null; + } + Log.i(TAG, "signedData: " + signedData); + boolean verified = false; + if (!TextUtils.isEmpty(signature)) { + /** + * Compute your public key (that you got from the Android Market + * publisher site). + * + * Instead of just storing the entire literal string here embedded + * in the program, construct the key at runtime from pieces or use + * bit manipulation (for example, XOR with some other string) to + * hide the actual key. The key itself is not secret information, + * but we don't want to make it easy for an adversary to replace the + * public key with one of their own and then fake messages from the + * server. + * + * Generally, encryption keys / passwords should only be kept in + * memory long enough to perform the operation they need to perform. + */ + String base64EncodedPublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApLWBv8eFC4f7h6gz3VE87XX2nqJB2KL2yNnNawmgaL/0nd6nXvVRiZ3iXLLP9k8RpLJ6rZPV778z8WzDLZATV3b2nh21KgjSNoG4em1oSf7pW4+AujqjLfNVRsXoJIWG+OMMd9o9l/D2YJTCXzSvgFIfF5EJRg6APZHEVrVJo8iXwnYM1tFfLjPfp10MtjLmD5tZW8o3hTmXJ3ZMDI12PL22G4KaE+BuQqI6PZ22m/pA85R6AuhNo2IUSE4XFUE8i7ANWDvdfDzQ5J0TTWAeHmUQCstdZ48z+6AjqD3L2omS/dKoBnlYxEUZms3iUa1/Co40nWU7sc2hqpmfNiG5oQIDAQAB"; + PublicKey key = BillingSecurity.generatePublicKey(base64EncodedPublicKey); + verified = BillingSecurity.verify(key, signedData, signature); + if (!verified) { + Log.w(TAG, "signature does not match data."); + return null; + } + } + + JSONObject jObject; + JSONArray jTransactionsArray = null; + int numTransactions = 0; + long nonce = 0L; + try { + jObject = new JSONObject(signedData); + + // The nonce might be null if the user backed out of the buy page. + nonce = jObject.optLong("nonce"); + jTransactionsArray = jObject.optJSONArray("orders"); + if (jTransactionsArray != null) { + numTransactions = jTransactionsArray.length(); + } + } catch (JSONException e) { + return null; + } + + if (!BillingSecurity.isNonceKnown(nonce)) { + Log.w(TAG, "Nonce not found: " + nonce); + return null; + } + + ArrayList purchases = new ArrayList(); + try { + for (int i = 0; i < numTransactions; i++) { + JSONObject jElement = jTransactionsArray.getJSONObject(i); + int response = jElement.getInt("purchaseState"); + PurchaseState purchaseState = PurchaseState.valueOf(response); + String productId = jElement.getString("productId"); + String packageName = jElement.getString("packageName"); + long purchaseTime = jElement.getLong("purchaseTime"); + String orderId = jElement.optString("orderId", ""); + String notifyId = null; + if (jElement.has("notificationId")) { + notifyId = jElement.getString("notificationId"); + } + String developerPayload = jElement.optString("developerPayload", null); + + // If the purchase state is PURCHASED, then we require a + // verified nonce. + if (purchaseState == PurchaseState.PURCHASED && !verified) { + continue; + } + purchases.add(new VerifiedPurchase(purchaseState, notifyId, productId, orderId, purchaseTime, developerPayload)); + } + } catch (JSONException e) { + Log.e(TAG, "JSON exception: ", e); + return null; + } + removeNonce(nonce); + return purchases; + } + + /** + * Generates a PublicKey instance from a string containing the + * Base64-encoded public key. + * + * @param encodedPublicKey + * Base64-encoded public key + * @throws IllegalArgumentException + * if encodedPublicKey is invalid + */ + public static PublicKey generatePublicKey(String encodedPublicKey) { + try { + byte[] decodedKey = Base64.decode(encodedPublicKey); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); + return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (InvalidKeySpecException e) { + Log.e(TAG, "Invalid key specification."); + throw new IllegalArgumentException(e); + } catch (Base64DecoderException e) { + Log.e(TAG, "Base64DecoderException.", e); + return null; + } + } + + /** + * Verifies that the signature from the server matches the computed + * signature on the data. Returns true if the data is correctly signed. + * + * @param publicKey + * public key associated with the developer account + * @param signedData + * signed data from server + * @param signature + * server signature + * @return true if the data and signature match + */ + public static boolean verify(PublicKey publicKey, String signedData, String signature) { + Log.i(TAG, "signature: " + signature); + Signature sig; + try { + sig = Signature.getInstance(SIGNATURE_ALGORITHM); + sig.initVerify(publicKey); + sig.update(signedData.getBytes()); + if (!sig.verify(Base64.decode(signature))) { + Log.e(TAG, "Signature verification failed."); + return false; + } + return true; + } catch (NoSuchAlgorithmException e) { + Log.e(TAG, "NoSuchAlgorithmException."); + } catch (InvalidKeyException e) { + Log.e(TAG, "Invalid key specification."); + } catch (SignatureException e) { + Log.e(TAG, "Signature exception."); + } catch (Base64DecoderException e) { + Log.e(TAG, "Base64DecoderException.", e); + } + return false; + } +} diff --git a/src/org/fox/ttrss/billing/BillingService.java b/src/org/fox/ttrss/billing/BillingService.java new file mode 100644 index 00000000..2ae53234 --- /dev/null +++ b/src/org/fox/ttrss/billing/BillingService.java @@ -0,0 +1,60 @@ +package org.fox.ttrss.billing; + +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; + +import com.android.vending.billing.IMarketBillingService; + +public class BillingService extends Service implements ServiceConnection{ + + private static final String TAG = "BillingService"; + + /** The service connection to the remote MarketBillingService. */ + private IMarketBillingService mService; + + @Override + public void onCreate() { + super.onCreate(); + Log.i(TAG, "Service starting with onCreate"); + + try { + boolean bindResult = bindService(new Intent("com.android.vending.billing.MarketBillingService.BIND"), this, Context.BIND_AUTO_CREATE); + if(bindResult){ + Log.i(TAG,"Market Billing Service Successfully Bound"); + } else { + Log.e(TAG,"Market Billing Service could not be bound."); + //TODO stop user continuing + } + } catch (SecurityException e){ + Log.e(TAG,"Market Billing Service could not be bound. SecurityException: "+e); + //TODO stop user continuing + } + } + + public void setContext(Context context) { + attachBaseContext(context); + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.i(TAG, "Market Billing Service Connected."); + mService = IMarketBillingService.Stub.asInterface(service); + BillingHelper.instantiateHelper(getBaseContext(), mService); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + + } + +} diff --git a/src/org/fox/ttrss/offline/OfflineActivity.java b/src/org/fox/ttrss/offline/OfflineActivity.java new file mode 100644 index 00000000..8849d67e --- /dev/null +++ b/src/org/fox/ttrss/offline/OfflineActivity.java @@ -0,0 +1,1183 @@ +package org.fox.ttrss.offline; + +import org.fox.ttrss.DummyFragment; +import org.fox.ttrss.MainActivity; +import org.fox.ttrss.OnlineServices; +import org.fox.ttrss.PreferencesActivity; +import org.fox.ttrss.R; +import org.fox.ttrss.OnlineServices.RelativeArticle; +import org.fox.ttrss.R.anim; +import org.fox.ttrss.R.id; +import org.fox.ttrss.R.layout; +import org.fox.ttrss.R.menu; +import org.fox.ttrss.R.string; +import org.fox.ttrss.R.style; +import org.fox.ttrss.util.DatabaseHelper; + +import android.animation.LayoutTransition; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.app.NotificationManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.provider.BaseColumns; +import android.util.Log; +import android.view.ActionMode; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.EditText; +import android.widget.SearchView; +import android.widget.TextView; +import android.widget.Toast; + +public class OfflineActivity extends Activity implements + OfflineServices { + private final String TAG = this.getClass().getSimpleName(); + + private SharedPreferences m_prefs; + private String m_themeName = ""; + private Menu m_menu; + private boolean m_smallScreenMode; + private boolean m_unreadOnly = true; + private boolean m_unreadArticlesOnly = true; + private boolean m_enableCats = false; + + private int m_activeFeedId = 0; + private int m_selectedArticleId = 0; + + private SQLiteDatabase m_readableDb; + private SQLiteDatabase m_writableDb; + + @Override + public boolean isSmallScreen() { + return m_smallScreenMode; + } + + private ActionMode m_headlinesActionMode; + private HeadlinesActionModeCallback m_headlinesActionModeCallback; + + private class HeadlinesActionModeCallback implements ActionMode.Callback { + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + deselectAllArticles(); + m_headlinesActionMode = null; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.headlines_action_menu, menu); + + return true; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + onOptionsItemSelected(item); + return false; + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + initDatabase(); + + 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); + + NotificationManager nmgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nmgr.cancel(OfflineDownloadService.NOTIFY_DOWNLOADING); + + m_themeName = m_prefs.getString("theme", "THEME_DARK"); + + if (savedInstanceState != null) { + m_unreadOnly = savedInstanceState.getBoolean("unreadOnly"); + m_unreadArticlesOnly = savedInstanceState + .getBoolean("unreadArticlesOnly"); + m_activeFeedId = savedInstanceState.getInt("offlineActiveFeedId"); + m_selectedArticleId = savedInstanceState.getInt("offlineArticleId"); + } + + m_enableCats = m_prefs.getBoolean("enable_cats", false); + + m_smallScreenMode = (getResources().getConfiguration().screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) != + Configuration.SCREENLAYOUT_SIZE_XLARGE; + + setContentView(R.layout.main); + + Log.d(TAG, "m_smallScreenMode=" + m_smallScreenMode); + + if (android.os.Build.VERSION.SDK_INT < 14 /* || android.os.Build.VERSION.SDK_INT == 15 */) { + if (!m_smallScreenMode) { + LayoutTransition transitioner = new LayoutTransition(); + ((ViewGroup) findViewById(R.id.main)).setLayoutTransition(transitioner); + } + } + + m_headlinesActionModeCallback = new HeadlinesActionModeCallback(); + + initMainMenu(); + + findViewById(R.id.loading_container).setVisibility(View.GONE); + + if (m_smallScreenMode) { + if (m_selectedArticleId != 0) { + findViewById(R.id.feeds_fragment).setVisibility(View.GONE); + findViewById(R.id.cats_fragment).setVisibility(View.GONE); + findViewById(R.id.headlines_fragment).setVisibility(View.GONE); + } else if (m_activeFeedId != 0) { + findViewById(R.id.feeds_fragment).setVisibility(View.GONE); + findViewById(R.id.article_fragment).setVisibility(View.GONE); + findViewById(R.id.cats_fragment).setVisibility(View.GONE); + } else { + //findViewById(R.id.headlines_fragment).setVisibility(View.GONE); + // findViewById(R.id.article_fragment).setVisibility(View.GONE); + + /* + * if (m_enableCats && m_activeCategory == null) { + * findViewById(R.id.feeds_fragment).setVisibility(View.GONE); + * findViewById(R.id.cats_fragment).setVisibility(View.VISIBLE); + * } else { + * findViewById(R.id.cats_fragment).setVisibility(View.GONE); } + */ + + findViewById(R.id.cats_fragment).setVisibility(View.GONE); + } + } else { + if (m_selectedArticleId == 0) { + findViewById(R.id.article_fragment).setVisibility(View.GONE); + + /* + * if (!m_enableCats || m_activeCategory != null) + * findViewById(R.id.cats_fragment).setVisibility(View.GONE); + * else + * findViewById(R.id.feeds_fragment).setVisibility(View.GONE); + */ + + findViewById(R.id.cats_fragment).setVisibility(View.GONE); + + } else { + findViewById(R.id.feeds_fragment).setVisibility(View.GONE); + findViewById(R.id.cats_fragment).setVisibility(View.GONE); + } + } + + if (m_activeFeedId == 0) { + FragmentTransaction ft = getFragmentManager() + .beginTransaction(); + OfflineFeedsFragment frag = new OfflineFeedsFragment(); + ft.replace(R.id.feeds_fragment, frag); + ft.commit(); + } + } + + private void initDatabase() { + DatabaseHelper dh = new DatabaseHelper(getApplicationContext()); + m_writableDb = dh.getWritableDatabase(); + m_readableDb = dh.getReadableDatabase(); + } + + @Override + public synchronized SQLiteDatabase getReadableDb() { + return m_readableDb; + } + + @Override + public synchronized SQLiteDatabase getWritableDb() { + return m_writableDb; + } + + private void switchOnline() { + SharedPreferences localPrefs = getSharedPreferences("localprefs", Context.MODE_PRIVATE); + SharedPreferences.Editor editor = localPrefs.edit(); + editor.putBoolean("offline_mode_active", false); + editor.commit(); + + Intent refresh = new Intent(this, MainActivity.class); + startActivity(refresh); + finish(); + } + + @Override + public int getActiveFeedId() { + return m_activeFeedId; + } + + private void setLoadingStatus(int status, boolean showProgress) { + TextView tv = (TextView) findViewById(R.id.loading_message); + + if (tv != null) { + tv.setText(status); + } + + View pb = findViewById(R.id.loading_progress); + + if (pb != null) { + pb.setVisibility(showProgress ? View.VISIBLE : View.GONE); + } + } + + @Override + public void onSaveInstanceState(Bundle out) { + super.onSaveInstanceState(out); + + out.putBoolean("unreadOnly", m_unreadOnly); + out.putBoolean("unreadArticlesOnly", m_unreadArticlesOnly); + out.putInt("offlineActiveFeedId", m_activeFeedId); + out.putInt("offlineArticleId", m_selectedArticleId); + } + + private void setUnreadOnly(boolean unread) { + m_unreadOnly = unread; + + refreshViews(); + + /* + * if (!m_enableCats || m_activeCategory != null ) refreshFeeds(); else + * refreshCategories(); + */ + } + + @Override + public boolean getUnreadOnly() { + return m_unreadOnly; + } + + @Override + public void onResume() { + super.onResume(); + + boolean needRefresh = !m_prefs.getString("theme", "THEME_DARK").equals( + m_themeName) + || m_prefs.getBoolean("enable_cats", false) != m_enableCats; + + if (needRefresh) { + Intent refresh = new Intent(this, OfflineActivity.class); + startActivity(refresh); + finish(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.offline_menu, menu); + + m_menu = menu; + + initMainMenu(); + + MenuItem item = menu.findItem(R.id.show_feeds); + + if (getUnreadOnly()) { + item.setTitle(R.string.menu_all_feeds); + } else { + item.setTitle(R.string.menu_unread_feeds); + } + + return true; + } + + private void setMenuLabel(int id, int labelId) { + MenuItem mi = m_menu.findItem(id); + + if (mi != null) { + mi.setTitle(labelId); + } + } + + private void goBack(boolean allowQuit) { + if (m_smallScreenMode) { + if (m_selectedArticleId != 0) { + closeArticle(); + } else if (m_activeFeedId != 0) { + //if (m_compatMode) { + findViewById(R.id.main).setAnimation( + AnimationUtils.loadAnimation(this, + R.anim.slide_right)); + //} + + /* + * if (m_activeFeed != null && m_activeFeed.is_cat) { + * findViewById + * (R.id.headlines_fragment).setVisibility(View.GONE); + * findViewById(R.id.cats_fragment).setVisibility(View.VISIBLE); + * + * refreshCategories(); } else { + */ + findViewById(R.id.headlines_fragment).setVisibility(View.GONE); + findViewById(R.id.feeds_fragment).setVisibility(View.VISIBLE); + // } + m_activeFeedId = 0; + + OfflineFeedsFragment ff = (OfflineFeedsFragment) getFragmentManager() + .findFragmentById(R.id.feeds_fragment); + + if (ff != null) { + ff.setSelectedFeedId(0); + } + + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.replace(R.id.headlines_fragment, new OfflineHeadlinesFragment()); + ft.commit(); + + refreshViews(); + initMainMenu(); + + } else if (allowQuit) { + finish(); + } + } else { + if (m_selectedArticleId != 0) { + closeArticle(); + } else if (m_activeFeedId != 0) { + findViewById(R.id.headlines_fragment).setVisibility(View.INVISIBLE); + m_activeFeedId = 0; + + OfflineFeedsFragment ff = (OfflineFeedsFragment) getFragmentManager() + .findFragmentById(R.id.feeds_fragment); + + if (ff != null) { + ff.setSelectedFeedId(0); + } + + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.replace(R.id.headlines_fragment, new OfflineHeadlinesFragment()); + ft.commit(); + + refreshViews(); + initMainMenu(); + + } else if (allowQuit) { + finish(); + } + } + } + + @Override + public void onBackPressed() { + goBack(true); + } + + /* + * @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if + * (keyCode == KeyEvent.KEYCODE_BACK) { + * + * if (m_smallScreenMode) { if (m_selectedArticleId != 0) { closeArticle(); + * } else if (m_activeFeedId != 0) { if (m_compatMode) { + * findViewById(R.id.main).setAnimation(AnimationUtils.loadAnimation(this, + * R.anim.slide_right)); } + */ + + /* + * if (m_activeFeed != null && m_activeFeed.is_cat) { + * findViewById(R.id.headlines_fragment).setVisibility(View.GONE); + * findViewById(R.id.cats_fragment).setVisibility(View.VISIBLE); + * + * refreshCategories(); } else { + *//* + * findViewById(R.id.headlines_fragment).setVisibility(View.GONE); + * findViewById(R.id.feeds_fragment).setVisibility(View.VISIBLE); //} + * m_activeFeedId = 0; refreshViews(); initMainMenu(); + * + * } else { finish(); } } else { if (m_selectedArticleId != 0) { + * closeArticle(); } else { finish(); } } + * + * return false; } return super.onKeyDown(keyCode, event); } + */ + + private Cursor getArticleById(int articleId) { + Cursor c = getReadableDb().query("articles", null, + BaseColumns._ID + "=?", + new String[] { String.valueOf(articleId) }, null, null, null); + + c.moveToFirst(); + + return c; + } + + private Cursor getFeedById(int feedId) { + Cursor c = getReadableDb().query("feeds", null, + BaseColumns._ID + "=?", + new String[] { String.valueOf(feedId) }, null, null, null); + + c.moveToFirst(); + + return c; + } + + private Intent getShareIntent(Cursor article) { + String title = article.getString(article.getColumnIndex("title")); + String link = article.getString(article.getColumnIndex("link")); + + Intent intent = new Intent(Intent.ACTION_SEND); + + intent.setType("text/plain"); + //intent.putExtra(Intent.EXTRA_SUBJECT, title); + intent.putExtra(Intent.EXTRA_TEXT, title + " " + link); + + return intent; + } + + private void shareArticle(int articleId) { + + Cursor article = getArticleById(articleId); + + if (article != null) { + shareArticle(article); + article.close(); + } + } + + private void shareArticle(Cursor article) { + if (article != null) { + Intent intent = getShareIntent(article); + + startActivity(Intent.createChooser(intent, + getString(R.id.share_article))); + } + } + + private void refreshHeadlines() { + OfflineHeadlinesFragment ohf = (OfflineHeadlinesFragment) getFragmentManager() + .findFragmentById(R.id.headlines_fragment); + + if (ohf != null) { + ohf.refresh(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + final OfflineHeadlinesFragment ohf = (OfflineHeadlinesFragment) getFragmentManager() + .findFragmentById(R.id.headlines_fragment); + + switch (item.getItemId()) { + case android.R.id.home: + goBack(false); + return true; + case R.id.preferences: + Intent intent = new Intent(this, PreferencesActivity.class); + startActivityForResult(intent, 0); + return true; + case R.id.go_online: + switchOnline(); + return true; + case R.id.headlines_select: + if (ohf != null) { + Dialog dialog = new Dialog(this); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.headlines_select_dialog); + + builder.setSingleChoiceItems(new String[] { + getString(R.string.headlines_select_all), + getString(R.string.headlines_select_unread), + getString(R.string.headlines_select_none) }, 0, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + switch (which) { + case 0: + SQLiteStatement stmtSelectAll = getWritableDb() + .compileStatement( + "UPDATE articles SET selected = 1 WHERE feed_id = ?"); + stmtSelectAll.bindLong(1, m_activeFeedId); + stmtSelectAll.execute(); + stmtSelectAll.close(); + break; + case 1: + SQLiteStatement stmtSelectUnread = getWritableDb() + .compileStatement( + "UPDATE articles SET selected = 1 WHERE feed_id = ? AND unread = 1"); + stmtSelectUnread + .bindLong(1, m_activeFeedId); + stmtSelectUnread.execute(); + stmtSelectUnread.close(); + break; + case 2: + deselectAllArticles(); + break; + } + + refreshViews(); + initMainMenu(); + + dialog.cancel(); + } + }); + + dialog = builder.create(); + dialog.show(); + } + return true; + case R.id.headlines_mark_as_read: + if (m_activeFeedId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET unread = 0 WHERE feed_id = ?"); + stmt.bindLong(1, m_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + case R.id.share_article: + shareArticle(m_selectedArticleId); + return true; + case R.id.toggle_marked: + if (m_selectedArticleId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET marked = NOT marked WHERE " + + BaseColumns._ID + " = ?"); + stmt.bindLong(1, m_selectedArticleId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + case R.id.selection_select_none: + deselectAllArticles(); + return true; + case R.id.selection_toggle_unread: + if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { + SQLiteStatement stmt = getWritableDb() + .compileStatement( + "UPDATE articles SET unread = NOT unread WHERE selected = 1 AND feed_id = ?"); + stmt.bindLong(1, m_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + case R.id.selection_toggle_marked: + if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { + SQLiteStatement stmt = getWritableDb() + .compileStatement( + "UPDATE articles SET marked = NOT marked WHERE selected = 1 AND feed_id = ?"); + stmt.bindLong(1, m_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + case R.id.selection_toggle_published: + if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { + SQLiteStatement stmt = getWritableDb() + .compileStatement( + "UPDATE articles SET published = NOT published WHERE selected = 1 AND feed_id = ?"); + stmt.bindLong(1, m_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + case R.id.toggle_published: + if (m_selectedArticleId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET published = NOT published WHERE " + + BaseColumns._ID + " = ?"); + stmt.bindLong(1, m_selectedArticleId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + case R.id.catchup_above: + if (m_selectedArticleId != 0 && m_activeFeedId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET unread = 0 WHERE updated >= " + + "(SELECT updated FROM articles WHERE " + + BaseColumns._ID + " = ?) AND feed_id = ?"); + stmt.bindLong(1, m_selectedArticleId); + stmt.bindLong(2, m_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + case R.id.set_unread: + if (m_selectedArticleId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET unread = 1 WHERE " + + BaseColumns._ID + " = ?"); + stmt.bindLong(1, m_selectedArticleId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + case R.id.show_feeds: + setUnreadOnly(!getUnreadOnly()); + + if (getUnreadOnly()) { + item.setTitle(R.string.menu_all_feeds); + } else { + item.setTitle(R.string.menu_unread_feeds); + } + + return true; + default: + Log.d(TAG, + "onOptionsItemSelected, unhandled id=" + item.getItemId()); + return super.onOptionsItemSelected(item); + } + } + + private void refreshFeeds() { + OfflineFeedsFragment frag = (OfflineFeedsFragment) getFragmentManager() + .findFragmentById(R.id.feeds_fragment); + + if (frag != null) { + frag.refresh(); + } + } + + private void closeArticle() { + if (m_smallScreenMode) { + findViewById(R.id.main).setAnimation( + AnimationUtils.loadAnimation(this, R.anim.slide_right)); + } + + if (m_smallScreenMode) { + findViewById(R.id.article_fragment).setVisibility(View.GONE); + findViewById(R.id.headlines_fragment).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.article_fragment).setVisibility(View.GONE); + findViewById(R.id.feeds_fragment).setVisibility(View.VISIBLE); + + } + + // we don't want to lose selected article in headlines so we refresh them before setting selected id to 0 + refreshViews(); + + m_selectedArticleId = 0; + + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.replace(R.id.article_fragment, new DummyFragment()); + ft.commit(); + + initMainMenu(); + } + + private int getSelectedArticleCount() { + Cursor c = getReadableDb().query("articles", + new String[] { "COUNT(*)" }, "selected = 1", null, null, null, + null); + c.moveToFirst(); + int selected = c.getInt(0); + c.close(); + + return selected; + } + + @Override + public void initMainMenu() { + if (m_menu != null) { + int numSelected = getSelectedArticleCount(); + + m_menu.setGroupVisible(R.id.menu_group_feeds, false); + m_menu.setGroupVisible(R.id.menu_group_headlines, false); + m_menu.setGroupVisible(R.id.menu_group_headlines_selection, false); + m_menu.setGroupVisible(R.id.menu_group_article, false); + + if (numSelected != 0) { + if (m_headlinesActionMode == null) + m_headlinesActionMode = startActionMode(m_headlinesActionModeCallback); + } else if (m_selectedArticleId != 0) { + m_menu.setGroupVisible(R.id.menu_group_article, true); + } else if (m_activeFeedId != 0) { + m_menu.setGroupVisible(R.id.menu_group_headlines, true); + + MenuItem search = m_menu.findItem(R.id.search); + + SearchView searchView = (SearchView) search.getActionView(); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + private String query = ""; + + @Override + public boolean onQueryTextSubmit(String query) { + OfflineHeadlinesFragment frag = (OfflineHeadlinesFragment) getFragmentManager() + .findFragmentById(R.id.headlines_fragment); + + if (frag != null) { + frag.setSearchQuery(query); + this.query = query; + } + + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (newText.equals("") && !newText.equals(this.query)) { + OfflineHeadlinesFragment frag = (OfflineHeadlinesFragment) getFragmentManager() + .findFragmentById(R.id.headlines_fragment); + + if (frag != null) { + frag.setSearchQuery(newText); + this.query = newText; + } + } + + return false; + } + }); + + } else { + m_menu.setGroupVisible(R.id.menu_group_feeds, true); + } + + if (numSelected == 0 && m_headlinesActionMode != null) { + m_headlinesActionMode.finish(); + } + + if (m_activeFeedId != 0) { + Cursor feed = getFeedById(m_activeFeedId); + + if (feed != null) { + getActionBar().setTitle(feed.getString(feed.getColumnIndex("title"))); + } + } else { + getActionBar().setTitle(R.string.app_name); + } + + if (!m_smallScreenMode) { + getActionBar().setDisplayHomeAsUpEnabled(m_selectedArticleId != 0); + } else { + getActionBar().setDisplayHomeAsUpEnabled(m_selectedArticleId != 0 || m_activeFeedId != 0); + } + } + } + + @Override + public void onPause() { + super.onPause(); + + } + + @Override + public void onDestroy() { + super.onDestroy(); + + m_readableDb.close(); + m_writableDb.close(); + + } + + private void refreshViews() { + refreshFeeds(); + refreshHeadlines(); + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + AdapterContextMenuInfo info = (AdapterContextMenuInfo) item + .getMenuInfo(); + + OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getFragmentManager() + .findFragmentById(R.id.headlines_fragment); + OfflineFeedsFragment ff = (OfflineFeedsFragment) getFragmentManager() + .findFragmentById(R.id.feeds_fragment); + + switch (item.getItemId()) { + case R.id.article_link_copy: + if (m_selectedArticleId != 0) { + Cursor article = null; + + if (m_selectedArticleId != 0) { + article = getArticleById(m_selectedArticleId); + } else if (info != null) { + article = hf.getArticleAtPosition(info.position); + } + + if (article != null) { + if (android.os.Build.VERSION.SDK_INT < 11) { + @SuppressWarnings("deprecation") + android.text.ClipboardManager clipboard = (android.text.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + clipboard.setText(article.getString(article.getColumnIndex("link"))); + } else { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + clipboard.setText(article.getString(article.getColumnIndex("link"))); + } + + article.close(); + + Toast toast = Toast.makeText(OfflineActivity.this, R.string.text_copied_to_clipboard, Toast.LENGTH_SHORT); + toast.show(); + } + } + return true; + case R.id.article_link_share: + if (m_selectedArticleId != 0) { + shareArticle(m_selectedArticleId); + } + return true; + + case R.id.browse_articles: + // TODO cat stuff + return true; + case R.id.browse_feeds: + // TODO cat stuff + return true; + case R.id.catchup_category: + // TODO cat stuff + return true; + case R.id.catchup_feed: + if (ff != null) { + int feedId = ff.getFeedIdAtPosition(info.position); + + if (feedId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET unread = 0 WHERE feed_id = ?"); + stmt.bindLong(1, feedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + } + return true; + case R.id.selection_toggle_unread: + if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { + SQLiteStatement stmt = getWritableDb() + .compileStatement( + "UPDATE articles SET unread = NOT unread WHERE selected = 1 AND feed_id = ?"); + stmt.bindLong(1, m_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } else { + int articleId = hf.getArticleIdAtPosition(info.position); + if (articleId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET unread = NOT unread WHERE " + + BaseColumns._ID + " = ?"); + stmt.bindLong(1, articleId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + } + return true; + case R.id.selection_toggle_marked: + if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { + SQLiteStatement stmt = getWritableDb() + .compileStatement( + "UPDATE articles SET marked = NOT marked WHERE selected = 1 AND feed_id = ?"); + stmt.bindLong(1, m_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } else { + int articleId = hf.getArticleIdAtPosition(info.position); + if (articleId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET marked = NOT marked WHERE " + + BaseColumns._ID + " = ?"); + stmt.bindLong(1, articleId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + } + return true; + case R.id.selection_toggle_published: + if (getSelectedArticleCount() > 0 && m_activeFeedId != 0) { + SQLiteStatement stmt = getWritableDb() + .compileStatement( + "UPDATE articles SET published = NOT published WHERE selected = 1 AND feed_id = ?"); + stmt.bindLong(1, m_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } else { + int articleId = hf.getArticleIdAtPosition(info.position); + if (articleId != 0) { + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET published = NOT published WHERE " + + BaseColumns._ID + " = ?"); + stmt.bindLong(1, articleId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + } + return true; + case R.id.share_article: + Cursor article = hf.getArticleAtPosition(info.position); + + if (article != null) { + shareArticle(article); + } + return true; + case R.id.catchup_above: + int articleId = hf.getArticleIdAtPosition(info.position); + + if (articleId != 0 && m_activeFeedId != 0) { + SQLiteStatement stmt = 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_activeFeedId); + stmt.execute(); + stmt.close(); + refreshViews(); + } + return true; + default: + Log.d(TAG, + "onContextItemSelected, unhandled id=" + item.getItemId()); + return super.onContextItemSelected(item); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + int action = event.getAction(); + int keyCode = event.getKeyCode(); + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_DOWN: + if (action == KeyEvent.ACTION_DOWN) { + + OfflineHeadlinesFragment ohf = (OfflineHeadlinesFragment) getFragmentManager() + .findFragmentById(R.id.headlines_fragment); + + int nextId = getRelativeArticleId(m_selectedArticleId, + m_activeFeedId, RelativeArticle.AFTER); + + if (nextId != 0 && ohf != null) { + if (m_prefs.getBoolean("combined_mode", false)) { + ohf.setActiveArticleId(nextId); + + SQLiteStatement stmt = getWritableDb() + .compileStatement( + "UPDATE articles SET unread = 0 " + + "WHERE " + BaseColumns._ID + + " = ?"); + + stmt.bindLong(1, nextId); + stmt.execute(); + stmt.close(); + + } else { + openArticle(nextId, 0); + } + } + } + return true; + case KeyEvent.KEYCODE_VOLUME_UP: + if (action == KeyEvent.ACTION_UP) { + + OfflineHeadlinesFragment ohf = (OfflineHeadlinesFragment) getFragmentManager() + .findFragmentById(R.id.headlines_fragment); + + int prevId = getRelativeArticleId(m_selectedArticleId, + m_activeFeedId, RelativeArticle.BEFORE); + + if (prevId != 0 && ohf != null) { + if (m_prefs.getBoolean("combined_mode", false)) { + ohf.setActiveArticleId(prevId); + + SQLiteStatement stmt = getWritableDb() + .compileStatement( + "UPDATE articles SET unread = 0 " + + "WHERE " + BaseColumns._ID + + " = ?"); + + stmt.bindLong(1, prevId); + stmt.execute(); + stmt.close(); + + } else { + openArticle(prevId, 0); + } + } + } + return true; + default: + return super.dispatchKeyEvent(event); + } + } + + private void deselectAllArticles() { + getWritableDb().execSQL("UPDATE articles SET selected = 0 "); + } + + @Override + public int getRelativeArticleId(int baseId, int feedId, + OnlineServices.RelativeArticle mode) { + + Cursor c; + + /* + * if (baseId == 0) { c = getReadableDb().query("articles", null, + * "feed_id = ?", new String[] { String.valueOf(feedId) }, null, null, + * "updated DESC LIMIT 1"); + * + * if (c.moveToFirst()) { baseId = c.getInt(0); } + * + * c.close(); + * + * return baseId; } + */ + + if (mode == RelativeArticle.BEFORE) { + c = getReadableDb().query( + "articles", + null, + "updated > (SELECT updated FROM articles WHERE " + + BaseColumns._ID + " = ?) AND feed_id = ?", + new String[] { String.valueOf(baseId), + String.valueOf(feedId) }, null, null, + "updated LIMIT 1"); + + } else { + c = getReadableDb().query( + "articles", + null, + "updated < (SELECT updated FROM articles WHERE " + + BaseColumns._ID + " = ?) AND feed_id = ?", + new String[] { String.valueOf(baseId), + String.valueOf(feedId) }, null, null, + "updated DESC LIMIT 1"); + } + + int id = 0; + + if (c.moveToFirst()) { + id = c.getInt(0); + } + + c.close(); + + return id; + } + + @Override + public void viewFeed(int feedId) { + m_activeFeedId = feedId; + + initMainMenu(); + + if (m_smallScreenMode) { + findViewById(R.id.feeds_fragment).setVisibility(View.GONE); + findViewById(R.id.headlines_fragment).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.headlines_fragment).setVisibility(View.VISIBLE); + } + + deselectAllArticles(); + + if (m_menu != null) { + MenuItem search = m_menu.findItem(R.id.search); + + if (search != null) { + SearchView sv = (SearchView) search.getActionView(); + sv.setQuery("", false); + } + } + + FragmentTransaction ft = getFragmentManager().beginTransaction(); + OfflineHeadlinesFragment frag = new OfflineHeadlinesFragment(); + ft.replace(R.id.headlines_fragment, frag); + ft.commit(); + + } + + @Override + public void openArticle(int articleId, int compatAnimation) { + m_selectedArticleId = articleId; + + initMainMenu(); + + OfflineHeadlinesFragment hf = (OfflineHeadlinesFragment) getFragmentManager() + .findFragmentById(R.id.headlines_fragment); + + if (hf != null) { + hf.setActiveArticleId(articleId); + } + + SQLiteStatement stmt = getWritableDb().compileStatement( + "UPDATE articles SET unread = 0 " + "WHERE " + BaseColumns._ID + + " = ?"); + + stmt.bindLong(1, articleId); + stmt.execute(); + stmt.close(); + + Fragment frag; + + if (m_smallScreenMode) { + frag = new OfflineArticlePager(articleId); + } else { + frag = new OfflineArticleFragment(articleId); + } + + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.replace(R.id.article_fragment, frag); + ft.commit(); + + if (m_smallScreenMode) { + if (compatAnimation == 0) + findViewById(R.id.main).setAnimation( + AnimationUtils.loadAnimation(this, R.anim.slide_left)); + else + findViewById(R.id.main).setAnimation( + AnimationUtils.loadAnimation(this, compatAnimation)); + } + + if (m_smallScreenMode) { + findViewById(R.id.headlines_fragment).setVisibility(View.GONE); + findViewById(R.id.article_fragment).setVisibility(View.VISIBLE); + } else { + findViewById(R.id.feeds_fragment).setVisibility(View.GONE); + findViewById(R.id.cats_fragment).setVisibility(View.GONE); + findViewById(R.id.article_fragment).setVisibility(View.VISIBLE); + } + + } + + @Override + public int getSelectedArticleId() { + return m_selectedArticleId; + } + + @Override + public void setSelectedArticleId(int articleId) { + m_selectedArticleId = articleId; + refreshViews(); + } +} \ No newline at end of file diff --git a/src/org/fox/ttrss/offline/OfflineArticleFragment.java b/src/org/fox/ttrss/offline/OfflineArticleFragment.java new file mode 100644 index 00000000..893a7d28 --- /dev/null +++ b/src/org/fox/ttrss/offline/OfflineArticleFragment.java @@ -0,0 +1,254 @@ +package org.fox.ttrss.offline; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.fox.ttrss.R; +import org.fox.ttrss.R.attr; +import org.fox.ttrss.R.id; +import org.fox.ttrss.R.layout; +import org.fox.ttrss.R.menu; +import org.fox.ttrss.util.ImageCacheService; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import android.app.Activity; +import android.app.Fragment; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.provider.BaseColumns; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.util.TypedValue; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.TextView; + +public class OfflineArticleFragment extends Fragment { + @SuppressWarnings("unused") + private final String TAG = this.getClass().getSimpleName(); + + private SharedPreferences m_prefs; + private int m_articleId; + private Cursor m_cursor; + private OfflineServices m_offlineServices; + + public OfflineArticleFragment() { + super(); + } + + public OfflineArticleFragment(int articleId) { + super(); + m_articleId = articleId; + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + + getActivity().getMenuInflater().inflate(R.menu.article_link_context_menu, menu); + menu.setHeaderTitle(m_cursor.getString(m_cursor.getColumnIndex("title"))); + + super.onCreateContextMenu(menu, v, menuInfo); + + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + if (savedInstanceState != null) { + m_articleId = savedInstanceState.getInt("articleId"); + } + + View view = inflater.inflate(R.layout.article_fragment, container, false); + + + // TODO change to interface? + Activity activity = getActivity(); + + if (activity != null) { + int orientation = activity.getWindowManager().getDefaultDisplay().getOrientation(); + + if (!m_offlineServices.isSmallScreen()) { + if (orientation % 2 == 0) { + view.findViewById(R.id.splitter_horizontal).setVisibility(View.GONE); + } else { + view.findViewById(R.id.splitter_vertical).setVisibility(View.GONE); + } + } else { + view.findViewById(R.id.splitter_vertical).setVisibility(View.GONE); + view.findViewById(R.id.splitter_horizontal).setVisibility(View.GONE); + } + } else { + view.findViewById(R.id.splitter_horizontal).setVisibility(View.GONE); + } + + m_cursor = m_offlineServices.getReadableDb().query("articles", null, BaseColumns._ID + "=?", + new String[] { String.valueOf(m_articleId) }, null, null, null); + + m_cursor.moveToFirst(); + + if (m_cursor.isFirst()) { + + TextView title = (TextView)view.findViewById(R.id.title); + + if (title != null) { + + String titleStr; + + if (m_cursor.getString(m_cursor.getColumnIndex("title")).length() > 200) + titleStr = m_cursor.getString(m_cursor.getColumnIndex("title")).substring(0, 200) + "..."; + else + titleStr = m_cursor.getString(m_cursor.getColumnIndex("title")); + + title.setMovementMethod(LinkMovementMethod.getInstance()); + title.setText(Html.fromHtml("" + titleStr + "")); + registerForContextMenu(title); + } + + WebView web = (WebView)view.findViewById(R.id.content); + + if (web != null) { + + String content; + String cssOverride = ""; + + WebSettings ws = web.getSettings(); + ws.setSupportZoom(true); + ws.setBuiltInZoomControls(true); + + TypedValue tv = new TypedValue(); + getActivity().getTheme().resolveAttribute(R.attr.linkColor, tv, true); + + // prevent flicker in ics + if (android.os.Build.VERSION.SDK_INT >= 11) { + web.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + + if (m_prefs.getString("theme", "THEME_DARK").equals("THEME_DARK")) { + cssOverride = "body { background : transparent; color : #e0e0e0}"; + //view.setBackgroundColor(android.R.color.black); + web.setBackgroundColor(getResources().getColor(android.R.color.transparent)); + } else { + cssOverride = ""; + } + + String hexColor = String.format("#%06X", (0xFFFFFF & tv.data)); + cssOverride += " a:link {color: "+hexColor+";} a:visited { color: "+hexColor+";}"; + + String articleContent = m_cursor.getString(m_cursor.getColumnIndex("content")); + Document doc = Jsoup.parse(articleContent); + + if (doc != null) { + if (m_prefs.getBoolean("offline_image_cache_enabled", false)) { + + Elements images = doc.select("img"); + + for (Element img : images) { + String url = img.attr("src"); + + if (ImageCacheService.isUrlCached(url)) { + img.attr("src", "file://" + ImageCacheService.getCacheFileName(url)); + } + } + } + + // thanks webview for crashing on