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/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 ++ 7 files changed, 769 insertions(+) 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/org/fox/ttrss/util') diff --git a/src/org/fox/ttrss/util/AppRater.java b/src/org/fox/ttrss/util/AppRater.java new file mode 100644 index 00000000..a50cc9d9 --- /dev/null +++ b/src/org/fox/ttrss/util/AppRater.java @@ -0,0 +1,100 @@ +package org.fox.ttrss.util; + +// 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/util/DatabaseHelper.java b/src/org/fox/ttrss/util/DatabaseHelper.java new file mode 100644 index 00000000..b8560589 --- /dev/null +++ b/src/org/fox/ttrss/util/DatabaseHelper.java @@ -0,0 +1,67 @@ +package org.fox.ttrss.util; +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/util/EasySSLSocketFactory.java b/src/org/fox/ttrss/util/EasySSLSocketFactory.java new file mode 100644 index 00000000..f0c2d3ad --- /dev/null +++ b/src/org/fox/ttrss/util/EasySSLSocketFactory.java @@ -0,0 +1,120 @@ +package org.fox.ttrss.util; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; + +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.scheme.LayeredSocketFactory; +import org.apache.http.conn.scheme.SocketFactory; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +public class EasySSLSocketFactory implements SocketFactory, LayeredSocketFactory +{ + private SSLContext sslcontext = null; + + private static SSLContext createEasySSLContext() throws IOException + { + try + { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new TrustManager[] { new EasyX509TrustManager() }, null); + return context; + } + catch (Exception e) + { + throw new IOException(e.getMessage()); + } + } + + private SSLContext getSSLContext() throws IOException + { + if (this.sslcontext == null) + { + this.sslcontext = createEasySSLContext(); + } + return this.sslcontext; + } + + /** + * @see org.apache.http.conn.scheme.SocketFactory#connectSocket(java.net.Socket, java.lang.String, int, + * java.net.InetAddress, int, org.apache.http.params.HttpParams) + */ + public Socket connectSocket(Socket sock, + String host, + int port, + InetAddress localAddress, + int localPort, + HttpParams params) + + throws IOException, UnknownHostException, ConnectTimeoutException + { + int connTimeout = HttpConnectionParams.getConnectionTimeout(params); + int soTimeout = HttpConnectionParams.getSoTimeout(params); + InetSocketAddress remoteAddress = new InetSocketAddress(host, port); + SSLSocket sslsock = (SSLSocket) ((sock != null) ? sock : createSocket()); + + if ((localAddress != null) || (localPort > 0)) + { + // we need to bind explicitly + if (localPort < 0) + { + localPort = 0; // indicates "any" + } + InetSocketAddress isa = new InetSocketAddress(localAddress, localPort); + sslsock.bind(isa); + } + + sslsock.connect(remoteAddress, connTimeout); + sslsock.setSoTimeout(soTimeout); + return sslsock; + } + + /** + * @see org.apache.http.conn.scheme.SocketFactory#createSocket() + */ + public Socket createSocket() throws IOException { + return getSSLContext().getSocketFactory().createSocket(); + } + + /** + * @see org.apache.http.conn.scheme.SocketFactory#isSecure(java.net.Socket) + */ + public boolean isSecure(Socket socket) throws IllegalArgumentException { + return true; + } + + /** + * @see org.apache.http.conn.scheme.LayeredSocketFactory#createSocket(java.net.Socket, java.lang.String, int, + * boolean) + */ + public Socket createSocket(Socket socket, + String host, + int port, + boolean autoClose) throws IOException, + UnknownHostException + { + return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose); + } + + // ------------------------------------------------------------------- + // javadoc in org.apache.http.conn.scheme.SocketFactory says : + // Both Object.equals() and Object.hashCode() must be overridden + // for the correct operation of some connection managers + // ------------------------------------------------------------------- + + public boolean equals(Object obj) { + return ((obj != null) && obj.getClass().equals(EasySSLSocketFactory.class)); + } + + public int hashCode() { + return EasySSLSocketFactory.class.hashCode(); + } +} \ No newline at end of file diff --git a/src/org/fox/ttrss/util/EasyX509TrustManager.java b/src/org/fox/ttrss/util/EasyX509TrustManager.java new file mode 100644 index 00000000..5ffc19bb --- /dev/null +++ b/src/org/fox/ttrss/util/EasyX509TrustManager.java @@ -0,0 +1,26 @@ + +package org.fox.ttrss.util; + +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509TrustManager; + +// http://stackoverflow.com/questions/6989116/httpget-not-working-due-to-not-trusted-server-certificate-but-it-works-with-ht + +public class EasyX509TrustManager implements X509TrustManager { + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + +} diff --git a/src/org/fox/ttrss/util/FragmentStatePagerAdapter.java b/src/org/fox/ttrss/util/FragmentStatePagerAdapter.java new file mode 100644 index 00000000..26494fdc --- /dev/null +++ b/src/org/fox/ttrss/util/FragmentStatePagerAdapter.java @@ -0,0 +1,226 @@ +/* + * 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.util; + +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/util/ImageCacheService.java b/src/org/fox/ttrss/util/ImageCacheService.java new file mode 100644 index 00000000..1541c249 --- /dev/null +++ b/src/org/fox/ttrss/util/ImageCacheService.java @@ -0,0 +1,211 @@ +package org.fox.ttrss.util; + +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 org.fox.ttrss.MainActivity; +import org.fox.ttrss.R; +import org.fox.ttrss.R.drawable; +import org.fox.ttrss.R.string; +import org.fox.ttrss.offline.OfflineDownloadService; + +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); + } + + public 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(); + } + + public 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(); + } + + public 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