├── pkrss ├── .gitignore ├── gradle.properties ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── pkmmte │ │ │ │ └── pkrss │ │ │ │ ├── Callback.java │ │ │ │ ├── Utils.java │ │ │ │ ├── parser │ │ │ │ ├── Parser.java │ │ │ │ ├── AtomParser.java │ │ │ │ └── Rss2Parser.java │ │ │ │ ├── CallbackHandler.java │ │ │ │ ├── Category.java │ │ │ │ ├── downloader │ │ │ │ ├── Downloader.java │ │ │ │ ├── OkHttpDownloader.java │ │ │ │ ├── OkHttp3Downloader.java │ │ │ │ └── DefaultDownloader.java │ │ │ │ ├── Request.java │ │ │ │ ├── Enclosure.java │ │ │ │ ├── FavoriteDatabase.java │ │ │ │ ├── RequestCreator.java │ │ │ │ ├── PkRSS.java │ │ │ │ └── Article.java │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── com │ │ └── pkmmte │ │ └── pkrss │ │ ├── ApplicationTest.java │ │ └── PkRssTest.java ├── proguard-rules.pro ├── pom.xml └── build.gradle ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .travis.yml ├── .gitignore ├── PkRSS.iml ├── circle.yml ├── gradle.properties ├── pom.xml ├── gradlew.bat ├── README.md ├── gradlew └── LICENSE /pkrss/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | secring.gpg -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':pkrss' 2 | -------------------------------------------------------------------------------- /pkrss/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=PkRSS 2 | POM_ARTIFACT_ID=pkrss 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pkmmte/PkRSS/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/Callback.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import java.util.List; 4 | 5 | public interface Callback { 6 | void onPreload(); 7 | void onLoaded(List
newArticles); 8 | void onLoadFailed(); 9 | } -------------------------------------------------------------------------------- /pkrss/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Jul 05 19:42:24 PDT 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip 7 | -------------------------------------------------------------------------------- /pkrss/src/androidTest/java/com/pkmmte/pkrss/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | android: 3 | components: 4 | - platform-tools 5 | - tools 6 | 7 | # The BuildTools version used by your project 8 | - build-tools-22.0.1 9 | 10 | # The SDK version used to compile your project 11 | - android-22 12 | 13 | - extra-google-google_play_services 14 | - extra-google-m2repository 15 | - extra-android-m2repository 16 | 17 | licenses: 18 | - 'android-sdk-license-.+' 19 | - 'google-gdk-license-.+' 20 | 21 | jdk: 22 | - oraclejdk8 23 | 24 | script: ./gradlew assembleDebug 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dex VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Web Javadocs 12 | javadoc/ 13 | 14 | # Built native files 15 | *.o 16 | *.so 17 | 18 | # Generated files 19 | bin/ 20 | gen/ 21 | 22 | # Ignore gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (SDK path, etc.) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Eclipse Metadata 33 | .metadata/ 34 | 35 | # Mac OS X clutter 36 | *.DS_Store 37 | 38 | # Windows clutter 39 | Thumbs.db 40 | 41 | # Intellij IDEA 42 | *.iml 43 | .idea/ 44 | .navigation 45 | -------------------------------------------------------------------------------- /pkrss/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Program Files (x86)\Android\Android SDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | -keep class com.pkmmte.pkrss.Callback{ *; } 20 | -------------------------------------------------------------------------------- /PkRSS.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | java: 3 | version: oraclejdk8 4 | 5 | dependencies: 6 | pre: 7 | - echo y | android update sdk --no-ui --all --filter "tools" 8 | - echo y | android update sdk --no-ui --all --filter "platform-tools" 9 | - echo y | android update sdk --no-ui --all --filter "build-tools" 10 | - echo y | android update sdk --no-ui --all --filter "build-tools-22.0.1" 11 | - echo y | android update sdk --no-ui --all --filter "android-22" 12 | - echo y | android update sdk --no-ui --all --filter "extra-android-support" 13 | - echo y | android update sdk --no-ui --all --filter "extra-android-m2repository" 14 | post: # Avoids invoking maven command (See https://circleci.com/docs/configuration/#dependencies) 15 | - echo $HOME 16 | override: # Avoids invoking maven command (See https://circleci.com/docs/configuration/#dependencies) 17 | - echo $HOME 18 | 19 | test: 20 | override: 21 | - ./gradlew assembleDebug -------------------------------------------------------------------------------- /pkrss/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | 7 | com.pkmmte.pkrss 8 | pkrss-parent 9 | 1.0.1-SNAPSHOT 10 | ../pom.xml 11 | 12 | 13 | pkrss 14 | PkRSS 15 | 16 | 17 | 18 | com.squareup.okhttp 19 | okhttp 20 | false 21 | 22 | 23 | com.squareup.okhttp 24 | okhttp-urlconnection 25 | false 26 | 27 | 28 | 29 | com.intellij 30 | annotations 31 | 9.0.4 32 | provided 33 | 34 | 35 | 36 | com.google.android 37 | android 38 | provided 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /pkrss/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 14 9 | versionCode Integer.parseInt(project.VERSION_CODE) 10 | versionName project.VERSION_NAME 11 | } 12 | } 13 | 14 | dependencies { 15 | compile fileTree(dir: 'libs', include: ['*.jar']) 16 | compile group: 'com.squareup.okhttp', name: 'okhttp', version: '2.5.0' 17 | compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.2.0' 18 | compile group: 'com.squareup.okhttp', name: 'okhttp-urlconnection', version: '2.5.0' 19 | 20 | androidTestCompile 'com.android.support.test:runner:0.4' 21 | androidTestCompile 'com.android.support.test:rules:0.4' 22 | androidTestCompile 'com.squareup.okhttp:okhttp:2.5.0' 23 | androidTestCompile 'com.squareup.okhttp:okhttp-urlconnection:2.5.0' 24 | } 25 | 26 | task clearJar(type: Delete) { 27 | delete 'build/libs/' + project.POM_ARTIFACT_ID + '_' + project.VERSION_NAME + '.jar' 28 | } 29 | 30 | task makeJar(type: Copy) { 31 | from('build/intermediates/bundles/release/') 32 | into('build/outputs/jar/') 33 | include('classes.jar') 34 | rename ('classes.jar', project.POM_ARTIFACT_ID + '-' + project.VERSION_NAME + '.jar') 35 | } 36 | 37 | makeJar.dependsOn(clearJar, build) 38 | 39 | apply from: 'https://raw.github.com/chrisbanes/gradle-mvn-push/master/gradle-mvn-push.gradle' 40 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Settings specified in this file will override any Gradle settings 5 | # configured through the IDE. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | VERSION_NAME=1.2-SNAPSHOT 21 | VERSION_CODE=6 22 | GROUP=com.pkmmte.pkrss 23 | 24 | POM_DESCRIPTION=A powerful RSS feed manager for Android 25 | POM_URL=https://github.com/Pkmmte/PkRSS 26 | POM_SCM_URL=https://github.com/Pkmmte/PkRSS 27 | POM_SCM_CONNECTION=scm:git@github.com:Pkmmte/PkRSS.git 28 | POM_SCM_DEV_CONNECTION=scm:git@github.com:Pkmmte/PkRSS.git 29 | POM_LICENCE_NAME=The Apache Software License, Version 2.0 30 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt 31 | POM_LICENCE_DIST=repo 32 | POM_DEVELOPER_ID=Pkmmte 33 | POM_DEVELOPER_NAME=Pkmmte Xeleon -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.pkmmte.pkrss 6 | pkrss 7 | 1.0.1-SNAPSHOT 8 | aar 9 | PkRSS 10 | An RSS feed manager for Android 11 | https://github.com/Pkmmte/PkRSS 12 | 13 | 14 | The Apache Software License, Version 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.txt 16 | repo 17 | 18 | 19 | 20 | 21 | Pkmmte 22 | Pkmmte Xeleon 23 | 24 | 25 | 26 | scm:git@github.com:Pkmmte/PkRSS.git 27 | scm:git@github.com:Pkmmte/PkRSS.git 28 | https://github.com/Pkmmte/PkRSS 29 | 30 | 31 | 32 | com.squareup.okhttp 33 | okhttp-urlconnection 34 | 2.0.0 35 | compile 36 | 37 | 38 | com.squareup.okhttp 39 | okhttp 40 | 2.0.0 41 | compile 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/Utils.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | import com.pkmmte.pkrss.downloader.DefaultDownloader; 6 | import com.pkmmte.pkrss.downloader.Downloader; 7 | import com.pkmmte.pkrss.downloader.OkHttp3Downloader; 8 | import com.pkmmte.pkrss.downloader.OkHttpDownloader; 9 | import java.io.File; 10 | 11 | public class Utils { 12 | private static final String TAG = "Utils"; 13 | 14 | /** 15 | * Deletes the specified directory. Returns true if successful, false if not. 16 | * 17 | * @param dir Directory to delete. 18 | * @return {@code true} if successful, {@code false} if otherwise. 19 | */ 20 | public static boolean deleteDir(File dir) { 21 | if (dir != null && dir.isDirectory()) { 22 | String[] children = dir.list(); 23 | for (int i = 0; i < children.length; i++) { 24 | if (!deleteDir(new File(dir, children[i]))) 25 | return false; 26 | } 27 | } 28 | return dir.delete(); 29 | } 30 | 31 | /** 32 | * Creates a Downloader object depending on the dependencies present. 33 | * 34 | * @param context Application context. 35 | * @return {@link OkHttp3Downloader} or {@link OkHttpDownloader} if the OkHttp library 36 | * is present, {@link DefaultDownloader} if not. 37 | */ 38 | public static Downloader createDefaultDownloader(Context context) { 39 | Downloader downloaderInstance = null; 40 | 41 | try { 42 | Class.forName("com.squareup.okhttp.OkHttpClient"); 43 | downloaderInstance = new OkHttpDownloader(context); 44 | } catch (ClassNotFoundException ignored) {} 45 | 46 | 47 | try { 48 | Class.forName("okhttp3.OkHttpClient"); 49 | downloaderInstance = new OkHttp3Downloader(context); 50 | } catch (ClassNotFoundException ignored) {} 51 | 52 | if (downloaderInstance == null) { 53 | downloaderInstance = new DefaultDownloader(context); 54 | } 55 | 56 | Log.d(TAG, "Downloader is " + downloaderInstance); 57 | return downloaderInstance; 58 | } 59 | } -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/parser/Parser.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss.parser; 2 | 3 | import android.util.Log; 4 | import com.pkmmte.pkrss.Article; 5 | import com.pkmmte.pkrss.PkRSS; 6 | import java.util.List; 7 | 8 | /** 9 | * Base Parser class for Parser objects. 10 | *

11 | * Extend this class upon creating your own custom parser. You may 12 | * handle any type of data as long as you are able to make an Article 13 | * ArrayList out of it. 14 | */ 15 | public abstract class Parser { 16 | // For logging purposes 17 | final String TAG = "Parser"; 18 | PkRSS singleton; 19 | 20 | /** 21 | * Parses {@link Article} objects out of the passed String response. 22 | * @param rssStream String response to parse items from. 23 | * @return An {@link Article} {@link List} containing newly parsed items. 24 | */ 25 | public abstract List

parse(String rssStream); 26 | 27 | /** 28 | * Attaches a {@link PkRSS} singleton instance to this Parser for logging purposes. 29 | * @param singleton Singleton instance to attach to this Parser 30 | */ 31 | public final void attachInstance(PkRSS singleton) { 32 | this.singleton = singleton; 33 | } 34 | 35 | public final void log(String message) { 36 | log(TAG, message, Log.DEBUG); 37 | } 38 | 39 | public final void log(String tag, String message) { 40 | log(tag, message, Log.DEBUG); 41 | } 42 | 43 | public final void log(String message, int type) { 44 | log(TAG, message, type); 45 | } 46 | 47 | public final void log(String tag, String message, int type) { 48 | if(singleton == null || !singleton.isLoggingEnabled()) 49 | return; 50 | 51 | switch(type) { 52 | case Log.VERBOSE: 53 | Log.v(tag, message); 54 | break; 55 | case Log.DEBUG: 56 | Log.d(tag, message); 57 | break; 58 | case Log.INFO: 59 | Log.i(tag, message); 60 | break; 61 | case Log.WARN: 62 | Log.w(tag, message); 63 | break; 64 | case Log.ERROR: 65 | Log.e(tag, message); 66 | break; 67 | case Log.ASSERT: 68 | default: 69 | Log.wtf(tag, message); 70 | break; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /pkrss/src/androidTest/java/com/pkmmte/pkrss/PkRssTest.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.support.test.runner.AndroidJUnit4; 4 | import android.test.AndroidTestCase; 5 | 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | import java.io.IOException; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.concurrent.CountDownLatch; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * Testing Fundamentals 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class PkRssTest extends AndroidTestCase { 20 | 21 | private static final String RSS_URL = "http://lorem-rss.herokuapp.com/feed"; 22 | 23 | @Test 24 | public void testSynchGet() { 25 | try { 26 | List
articles = PkRSS.with(getContext()).load(RSS_URL).get(); 27 | assertTrue(!articles.isEmpty()); 28 | } catch (IOException e) { 29 | fail(); 30 | } 31 | } 32 | 33 | @Test 34 | public void testAsynchGet() { 35 | try { 36 | final CountDownLatch lock = new CountDownLatch(1); 37 | final List
articles = new ArrayList<>(); 38 | 39 | Callback callback = new Callback() { 40 | 41 | @Override 42 | public void onPreload() { 43 | // do nothing 44 | } 45 | 46 | @Override 47 | public void onLoaded(List
newArticles) { 48 | articles.addAll(newArticles); 49 | lock.countDown(); 50 | } 51 | 52 | @Override 53 | public void onLoadFailed() { 54 | lock.countDown(); 55 | fail(); 56 | } 57 | }; 58 | 59 | PkRSS.with(getContext()).load(RSS_URL).callback(callback).get(); 60 | lock.await(5000, TimeUnit.MILLISECONDS); 61 | assertTrue(!articles.isEmpty()); 62 | 63 | } catch (Exception e) { 64 | fail(); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/CallbackHandler.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.os.Handler; 4 | 5 | import java.lang.reflect.Method; 6 | import java.util.List; 7 | 8 | /** 9 | * A wrapper for Android's {@link Handler} class. 10 | * This provides clean and easy access to interface calls 11 | * without having to surround code every time. 12 | */ 13 | class CallbackHandler { 14 | private static final Class clazz = Callback.class; 15 | private Handler handler; 16 | 17 | CallbackHandler() { 18 | this(null); 19 | } 20 | 21 | CallbackHandler(Handler handler) { 22 | this.handler = handler; 23 | } 24 | 25 | protected void onPreload(final boolean safe, final Callback callback) { 26 | Method method = getDeclaredMethod(clazz, "onPreload"); 27 | invokeCallback(method, callback, safe); 28 | } 29 | 30 | protected void onLoaded(final boolean safe, final Callback callback, final List
newArticles) { 31 | Method method = getDeclaredMethod(clazz, "onLoaded", List.class); 32 | invokeCallback(method, callback, safe, newArticles); 33 | } 34 | 35 | protected void onLoadFailed(boolean safe, Callback callback) { 36 | Method method = getDeclaredMethod(clazz, "onLoadFailed"); 37 | invokeCallback(method, callback, safe); 38 | } 39 | 40 | private void invokeCallback(final Method method, final Callback callback, final boolean safe, final Object... args) { 41 | // Catch invalid calls before proceeding 42 | if(callback == null) 43 | return; 44 | 45 | // Create runnable containing invoked code 46 | Runnable call = new Runnable() { 47 | @Override 48 | public void run() { 49 | try { 50 | method.invoke(callback, args); 51 | } catch (Exception e) { 52 | if(safe) 53 | PkRSS.getInstance().log("Caught " + clazz.getSimpleName() + '.' + method.getName() + " exception! [" + e.getMessage() + ']'); 54 | else 55 | throw new RuntimeException(e); 56 | } 57 | } 58 | }; 59 | 60 | // Execute using handler if available, otherwise use default thread 61 | if (handler != null) 62 | handler.post(call); 63 | else 64 | call.run(); 65 | } 66 | 67 | private static Method getDeclaredMethod(Class clazz, String name, Class... parameterTypes) { 68 | try { 69 | return clazz.getDeclaredMethod(name, parameterTypes); 70 | } catch (NoSuchMethodException e) { 71 | throw new RuntimeException("No method named " + name + " in class " + clazz.getSimpleName() + " [" + e.getMessage() + ']'); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/Category.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class Category implements Parcelable { 9 | private String name; 10 | private String url; 11 | 12 | public Category(String name, String url) { 13 | this.name = name; 14 | this.url = url; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | 25 | public String getUrl() { 26 | return url; 27 | } 28 | 29 | public void setUrl(String url) { 30 | this.url = url; 31 | } 32 | 33 | @Override 34 | public boolean equals(Object o) { 35 | if (this == o) return true; 36 | if (o == null || !(o instanceof Category)) return false; 37 | 38 | Category category = (Category) o; 39 | 40 | if (name != null ? !name.equals(category.name) : category.name != null) return false; 41 | if (url != null ? !url.equals(category.url) : category.url != null) return false; 42 | 43 | return true; 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | int result = name != null ? name.hashCode() : 0; 49 | result = 31 * result + (url != null ? url.hashCode() : 0); 50 | return result; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | return "Category{" + 56 | "name='" + name + '\'' + 57 | ", url='" + url + '\'' + 58 | '}'; 59 | } 60 | 61 | protected Category(Parcel in) { 62 | name = in.readString(); 63 | url = in.readString(); 64 | } 65 | 66 | @Override 67 | public int describeContents() { 68 | return 0; 69 | } 70 | 71 | @Override 72 | public void writeToParcel(Parcel dest, int flags) { 73 | dest.writeString(name); 74 | dest.writeString(url); 75 | } 76 | 77 | @SuppressWarnings("unused") 78 | public static final Creator CREATOR = new Creator() { 79 | @Override 80 | public Category createFromParcel(Parcel in) { 81 | return new Category(in); 82 | } 83 | 84 | @Override 85 | public Category[] newArray(int size) { 86 | return new Category[size]; 87 | } 88 | }; 89 | 90 | public static class ListBuilder { 91 | private final List categoryList; 92 | 93 | public ListBuilder() { 94 | categoryList = new ArrayList(); 95 | } 96 | 97 | public ListBuilder add(String name, String url) { 98 | categoryList.add(new Category(name, url)); 99 | return this; 100 | } 101 | 102 | public List build() { 103 | return categoryList; 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/downloader/Downloader.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss.downloader; 2 | 3 | import android.util.Log; 4 | import com.pkmmte.pkrss.PkRSS; 5 | import com.pkmmte.pkrss.Request; 6 | import java.io.IOException; 7 | 8 | /** 9 | * Base Downloader class for Downloader objects. 10 | *

11 | * Extend this class upon creating your own custom downloader. You may 12 | * do anything you want with it as long as you're able to return data 13 | * and format URLs properly. 14 | */ 15 | public abstract class Downloader { 16 | // For logging purposes 17 | final String TAG = "Downloader"; 18 | PkRSS singleton; 19 | 20 | /** 21 | * Clears the {@link Downloader} cache. 22 | * @return {@code true} if successful, {@code false} if otherwise. 23 | */ 24 | public abstract boolean clearCache(); 25 | 26 | /** 27 | * Executes the specified request and returns the response String. 28 | * @param request PkRSS Request object containing all necessary parameters. 29 | * @return Response String to parse 30 | * @throws IllegalArgumentException 31 | * @throws IOException 32 | */ 33 | public abstract String execute(Request request) throws IllegalArgumentException, IOException; 34 | 35 | /** 36 | * Parses a request into a safe URL to be used for caching/tracking purposes. 37 | *

38 | * You'd normally want to build something similar as {@link #toUrl(Request)} with the exception 39 | * of certain parameters such as pagination or extra useless data. 40 | *

41 | * Note: Returning an invalid URL may cause caching errors and mishandled memory. 42 | * @param request PkRSS Request object containing all necessary parameters. 43 | * @return Safe URL String to be used for tracking and caching purposes. 44 | */ 45 | public abstract String toSafeUrl(Request request); 46 | 47 | /** 48 | * Parses a request into a URL to be used for execution. 49 | * @param request PkRSS Request object containing all necessary parameters. 50 | * @return A URL String to load valid data from. 51 | */ 52 | public abstract String toUrl(Request request); 53 | 54 | /** 55 | * Attaches a {@link PkRSS} singleton instance to this Downloader for logging purposes. 56 | * @param singleton Singleton instance to attach to this Parser 57 | */ 58 | public final void attachInstance(PkRSS singleton) { 59 | this.singleton = singleton; 60 | } 61 | 62 | final void log(String message) { 63 | log(TAG, message, Log.DEBUG); 64 | } 65 | 66 | final void log(String tag, String message) { 67 | log(tag, message, Log.DEBUG); 68 | } 69 | 70 | final void log(String message, int type) { 71 | log(TAG, message, type); 72 | } 73 | 74 | final void log(String tag, String message, int type) { 75 | if(singleton == null || !singleton.isLoggingEnabled()) 76 | return; 77 | 78 | switch(type) { 79 | case Log.VERBOSE: 80 | Log.v(tag, message); 81 | break; 82 | case Log.DEBUG: 83 | Log.d(tag, message); 84 | break; 85 | case Log.INFO: 86 | Log.i(tag, message); 87 | break; 88 | case Log.WARN: 89 | Log.w(tag, message); 90 | break; 91 | case Log.ERROR: 92 | Log.e(tag, message); 93 | break; 94 | case Log.ASSERT: 95 | default: 96 | Log.wtf(tag, message); 97 | break; 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PkRSS [![Build Status](https://travis-ci.org/Pkmmte/PkRSS.svg?branch=master)](https://travis-ci.org/Pkmmte/PkRSS) 2 | ===== 3 | 4 | A fluent and flexible RSS feed manager for Android 5 | 6 | For more information, please see [the website][1] 7 | 8 | Download 9 | -------- 10 | 11 | Download the latest JAR from the [release page][2] or grab via Gradle: 12 | ```groovy 13 | compile 'com.pkmmte.pkrss:pkrss:1.2' 14 | ``` 15 | or Maven: 16 | ```xml 17 | 18 | com.pkmmte.pkrss 19 | pkrss 20 | 1.2 21 | 22 | ``` 23 | 24 | Basic Usage 25 | -------- 26 | 27 | PkRSS features a fluent API which allows you to create flexible requests - often using only one line of code. For a full application sample, see the [TechDissected Source Code][3] 28 | 29 | #####Basic Loading 30 | This code loads the specified URL asynchronously. 31 | ```java 32 | PkRSS.with(this).load(url).async(); 33 | ``` 34 | 35 | #####Pagination & Callbacks 36 | The following loads the next page belonging to the specified url and assigns a callback. PkRSS keeps track of which page was last loaded using a PageTracker. You may also manually specify a page number using `page(int)`. 37 | ```java 38 | PkRSS.with(this).load(url).nextPage().callback(this).async(); 39 | ``` 40 | 41 | #####Search & Synchronous Requests 42 | You are able to query for a specific search term on a specified feed and get the result synchronously. Pagination is also supported for search queries. 43 | ```java 44 | PkRSS.with(this).load(url).search(query).page(int).get(); 45 | ``` 46 | 47 | There are a lot more APIs available such as custom parsers, mark articles as read/favorited, instance builder, custom article objects, request cancelling, and more! See [the website][1] for more info or [read the Javadoc][4]. 48 | 49 | ProGuard 50 | -------- 51 | 52 | If you are using ProGuard make sure you add the following option: 53 | 54 | ``` 55 | -keep class com.pkmmte.pkrss.Callback{ *; } 56 | -dontwarn com.squareup.okhttp.** 57 | ``` 58 | 59 | Developed By 60 | -------- 61 | 62 | Pkmmte Xeleon - www.pkmmte.com 63 | 64 | 65 | Follow me on Google+ 67 | 68 | 69 | Follow me on LinkedIn 71 | 72 | 73 | License 74 | -------- 75 | 76 | Copyright 2014 Pkmmte Xeleon 77 | 78 | Licensed under the Apache License, Version 2.0 (the "License"); 79 | you may not use this file except in compliance with the License. 80 | You may obtain a copy of the License at 81 | 82 | http://www.apache.org/licenses/LICENSE-2.0 83 | 84 | Unless required by applicable law or agreed to in writing, software 85 | distributed under the License is distributed on an "AS IS" BASIS, 86 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 87 | See the License for the specific language governing permissions and 88 | limitations under the License. 89 | 90 | [1]: http://pkmmte.github.io/PkRSS/ 91 | [2]: https://github.com/Pkmmte/PkRSS/releases 92 | [3]: https://github.com/Pkmmte/TechDissected 93 | [4]: http://pkmmte.github.io/PkRSS/javadoc/ 94 | -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/Request.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import com.pkmmte.pkrss.downloader.Downloader; 4 | import com.pkmmte.pkrss.parser.Parser; 5 | 6 | import java.lang.ref.WeakReference; 7 | import java.util.concurrent.atomic.AtomicLong; 8 | 9 | /** 10 | * Immutable data to be used for execution. 11 | */ 12 | public final class Request { 13 | private static final AtomicLong ID_GENERATOR = new AtomicLong(System.currentTimeMillis() * 100000); 14 | 15 | public final String tag; 16 | public final String url; 17 | public final String search; 18 | public final boolean individual; 19 | public final boolean skipCache; 20 | public final int page; 21 | public final Boolean safe; 22 | public final CallbackHandler handler; 23 | public final Downloader downloader; 24 | public final Parser parser; 25 | public final WeakReference callback; 26 | 27 | /* Hidden constructor */ 28 | public Request(Builder builder) { 29 | this.tag = builder.tag == null ? String.valueOf(ID_GENERATOR.incrementAndGet()) : builder.tag; 30 | this.url = builder.url; 31 | this.search = builder.search; 32 | this.individual = builder.individual; 33 | this.skipCache = builder.skipCache; 34 | this.page = builder.page; 35 | this.safe = builder.safe; 36 | this.handler = builder.handler; 37 | this.downloader = builder.downloader; 38 | this.parser = builder.parser; 39 | this.callback = builder.callback; 40 | } 41 | 42 | public static class Builder { 43 | private String tag; 44 | private String url; 45 | private String search; 46 | private boolean individual; 47 | private boolean skipCache; 48 | private int page; 49 | private Boolean safe; 50 | private CallbackHandler handler; 51 | private Downloader downloader; 52 | private Parser parser; 53 | private WeakReference callback; 54 | 55 | public Builder(String url) { 56 | this.tag = null; 57 | this.url = url; 58 | this.search = null; 59 | this.individual = false; 60 | this.skipCache = false; 61 | this.page = 1; 62 | this.safe = null; 63 | this.handler = null; 64 | this.downloader = null; 65 | this.parser = null; 66 | this.callback = null; 67 | } 68 | 69 | public Builder tag(String tag) { 70 | this.tag = tag; 71 | return this; 72 | } 73 | 74 | public Builder url(String url) { 75 | this.url = url; 76 | return this; 77 | } 78 | 79 | public Builder search(String search) { 80 | this.search = search; 81 | return this; 82 | } 83 | 84 | public Builder individual(boolean individual) { 85 | this.individual = individual; 86 | return this; 87 | } 88 | 89 | public Builder skipCache(boolean skipCache) { 90 | this.skipCache = skipCache; 91 | return this; 92 | } 93 | 94 | public Builder page(int page) { 95 | this.page = page; 96 | return this; 97 | } 98 | 99 | public Builder safe(boolean safe) { 100 | this.safe = safe; 101 | return this; 102 | } 103 | 104 | public Builder handler(CallbackHandler handler) { 105 | this.handler = handler; 106 | return this; 107 | } 108 | 109 | public Builder downloader(Downloader downloader) { 110 | this.downloader = downloader; 111 | return this; 112 | } 113 | 114 | public Builder parser(Parser parser) { 115 | this.parser = parser; 116 | return this; 117 | } 118 | 119 | public Builder callback(Callback callback) { 120 | this.callback = new WeakReference<>(callback); 121 | return this; 122 | } 123 | 124 | public Request build() { 125 | return new Request(this); 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/Enclosure.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | import org.xmlpull.v1.XmlPullParser; 7 | 8 | public class Enclosure implements Parcelable { 9 | private String url; 10 | private String length; 11 | private String mimeType; 12 | 13 | /** 14 | * Creates an Enclosure from the current event in an XmlPullParser. 15 | * @param xmlParser The XmlPullParser object. 16 | */ 17 | public Enclosure(XmlPullParser xmlParser) { 18 | for(int i = 0; i < xmlParser.getAttributeCount(); i++) { 19 | String val = xmlParser.getAttributeValue(i); 20 | String att = xmlParser.getAttributeName(i); 21 | 22 | if(att.equalsIgnoreCase("url")) 23 | this.setUrl(val); 24 | 25 | else if(att.equalsIgnoreCase("length")) 26 | this.setLength(val); 27 | 28 | else if(att.equalsIgnoreCase("type")) 29 | this.setMimeType(val); 30 | } 31 | } 32 | 33 | /** 34 | * Creates an Enclosure from a url, the size of the attachment, and it's mime-type. 35 | * @param url The url of the attachment. 36 | * @param length The size (in bytes) of the attachment. 37 | * @param mimeType The mime-type of the attachment. 38 | */ 39 | public Enclosure(String url, String length, String mimeType) { 40 | this.url = url; 41 | this.length = length; 42 | this.mimeType = mimeType; 43 | } 44 | 45 | /** 46 | * Creates an Enclosure from a Parcel. 47 | * @param in The Parcel object. 48 | */ 49 | protected Enclosure(Parcel in) { 50 | url = in.readString(); 51 | length = in.readString(); 52 | mimeType = in.readString(); 53 | } 54 | 55 | /** 56 | * @return The url of the attachment. 57 | */ 58 | public String getUrl() { 59 | return url; 60 | } 61 | 62 | /** 63 | * Sets the url of the attachment. 64 | * @param url The url of the attachment. 65 | */ 66 | public void setUrl(String url) { 67 | this.url = url; 68 | } 69 | 70 | /** 71 | * @return The size (in bytes) of the attachment. 72 | */ 73 | public long getLength() { 74 | return Long.valueOf(length); 75 | } 76 | 77 | /** 78 | * Sets the size of the attachment. 79 | * @param length The size (in bytes) of the attachment. 80 | */ 81 | public void setLength(String length) { 82 | this.length = length; 83 | } 84 | 85 | /** 86 | * @return The mime-type of the attachment. 87 | */ 88 | public String getMimeType() { 89 | return mimeType; 90 | } 91 | 92 | /** 93 | * Sets the url of the attachment. 94 | * @param mimeType The mime-type of the attachment. 95 | */ 96 | public void setMimeType(String mimeType) { 97 | this.mimeType = mimeType; 98 | } 99 | 100 | /** 101 | * @return A String representing the Enclosure. 102 | */ 103 | @Override 104 | public String toString() { 105 | return "Enclosure{" + 106 | "url='" + url + '\'' + 107 | ", length='" + length + '\'' + 108 | ", mimeType='" + mimeType + '\'' + 109 | '}'; 110 | } 111 | 112 | @Override 113 | public int describeContents() { 114 | return 0; 115 | } 116 | 117 | @Override 118 | public void writeToParcel(Parcel parcel, int i) { 119 | parcel.writeString(url); 120 | parcel.writeString(length); 121 | parcel.writeString(mimeType); 122 | } 123 | 124 | public static final Creator CREATOR = new Creator() { 125 | @Override 126 | public Enclosure createFromParcel(Parcel source) { 127 | return new Enclosure(source); 128 | } 129 | 130 | @Override 131 | public Enclosure[] newArray(int size) { 132 | return new Enclosure[size]; 133 | } 134 | }; 135 | } 136 | -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/downloader/OkHttpDownloader.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss.downloader; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | import android.util.Log; 6 | import com.pkmmte.pkrss.Request; 7 | import com.squareup.okhttp.Cache; 8 | import com.squareup.okhttp.OkHttpClient; 9 | import com.squareup.okhttp.Response; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | /** 15 | * An improved version of the DefaultDownloader using the OkHttp library. 16 | *

17 | * This Downloader class uses Square's OkHttp library for networking. It is 18 | * preferred over the DefaultDownloader and often performs better than the default 19 | * implementation although it requires you to include OkHttp as a dependency to your project. 20 | */ 21 | public class OkHttpDownloader extends Downloader { 22 | // OkHttpClient & configuration 23 | private final OkHttpClient client = new OkHttpClient(); 24 | private final String cacheDir = "/okhttp"; 25 | private final int cacheSize = 1024 * 1024; 26 | private final int cacheMaxAge = 2 * 60 * 60; 27 | private final long connectTimeout = 15; 28 | private final long readTimeout = 45; 29 | 30 | public OkHttpDownloader(Context context) { 31 | this.client.setConnectTimeout(connectTimeout, TimeUnit.SECONDS); 32 | this.client.setReadTimeout(readTimeout, TimeUnit.SECONDS); 33 | 34 | try { 35 | File cacheDir = new File(context.getCacheDir().getAbsolutePath() + this.cacheDir); 36 | this.client.setCache(new Cache(cacheDir, cacheSize)); 37 | } catch (Exception e) { 38 | Log.e(TAG, "Error configuring Downloader cache! \n" + e.getMessage()); 39 | } 40 | } 41 | 42 | @Override 43 | public boolean clearCache() { 44 | try { 45 | client.getCache().delete(); 46 | return true; 47 | } catch (IOException e) { 48 | return false; 49 | } 50 | } 51 | 52 | @Override 53 | public String execute(Request request) throws IllegalArgumentException, IOException { 54 | // Invalid URLs are a big no no 55 | if (request.url == null || request.url.isEmpty()) { 56 | throw new IllegalArgumentException("Invalid URL!"); 57 | } 58 | 59 | // Start tracking download time 60 | long time = System.currentTimeMillis(); 61 | 62 | // Empty response string placeholder 63 | String responseStr; 64 | 65 | // Handle cache 66 | int maxCacheAge = request.skipCache ? 0 : cacheMaxAge; 67 | 68 | // Build proper URL 69 | String requestUrl = toUrl(request); 70 | 71 | // Build the OkHttp request 72 | com.squareup.okhttp.Request httpRequest = new com.squareup.okhttp.Request.Builder() 73 | .addHeader("Cache-Control", "public, max-age=" + maxCacheAge) 74 | .url(requestUrl) 75 | .build(); 76 | 77 | try { 78 | // Execute the built request and log its data 79 | log("Making a request to " + requestUrl + (request.skipCache ? " [SKIP-CACHE]" : " [MAX-AGE " + maxCacheAge + "]")); 80 | Response response = client.newCall(httpRequest).execute(); 81 | 82 | // Was this retrieved from cache? 83 | if (response.cacheResponse() != null) log("Response retrieved from cache"); 84 | 85 | // Convert response body to a string 86 | responseStr = response.body().string(); 87 | log(TAG, "Request download took " + (System.currentTimeMillis() - time) + "ms", Log.INFO); 88 | } catch (Exception e) { 89 | log("Error executing/reading http request!", Log.ERROR); 90 | e.printStackTrace(); 91 | throw new IOException(e.getMessage()); 92 | } 93 | 94 | return responseStr; 95 | } 96 | 97 | @Override 98 | public String toSafeUrl(Request request) { 99 | // Copy base url 100 | String url = request.url; 101 | 102 | if (request.individual) { 103 | // Append feed URL if individual article 104 | url += "feed/?withoutcomments=1"; 105 | } 106 | else if (request.search != null) { 107 | // Append search query if available and not individual 108 | url += "?s=" + Uri.encode(request.search); 109 | } 110 | 111 | // Return safe url 112 | return url; 113 | } 114 | 115 | @Override 116 | public String toUrl(Request request) { 117 | // Copy base url 118 | String url = request.url; 119 | 120 | if (request.individual) { 121 | // Handle individual urls differently 122 | url += "feed/?withoutcomments=1"; 123 | } 124 | else { 125 | if (request.search != null) 126 | url += "?s=" + Uri.encode(request.search); 127 | if (request.page > 1) 128 | url += (request.search == null ? "?paged=" : "&paged=") + String.valueOf(request.page); 129 | } 130 | 131 | // Return safe url 132 | return url; 133 | } 134 | } -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/downloader/OkHttp3Downloader.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss.downloader; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | import android.util.Log; 6 | 7 | import com.pkmmte.pkrss.Request; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.concurrent.TimeUnit; 12 | 13 | import okhttp3.Cache; 14 | import okhttp3.OkHttpClient; 15 | import okhttp3.Response; 16 | 17 | public class OkHttp3Downloader extends Downloader { 18 | // OkHttpClient & configuration 19 | private OkHttpClient client; 20 | private final String cacheDir = "/okhttp"; 21 | private final int cacheSize = 1024 * 1024; 22 | private final int cacheMaxAge = 2 * 60 * 60; 23 | private final long connectTimeout = 15; 24 | private final long readTimeout = 45; 25 | 26 | public OkHttp3Downloader(Context context) { 27 | 28 | File cacheDir = new File(context.getCacheDir().getAbsolutePath() + this.cacheDir); 29 | client = new OkHttpClient.Builder() 30 | .connectTimeout(connectTimeout, TimeUnit.SECONDS) 31 | .readTimeout(readTimeout, TimeUnit.SECONDS) 32 | .cache(new Cache(cacheDir, cacheSize)) 33 | .build(); 34 | } 35 | 36 | @Override 37 | public boolean clearCache() { 38 | try { 39 | client.cache().delete(); 40 | return true; 41 | } catch (IOException e) { 42 | return false; 43 | } 44 | } 45 | 46 | @Override 47 | public String execute(Request request) throws IllegalArgumentException, IOException { 48 | // Invalid URLs are a big no no 49 | if (request.url == null || request.url.isEmpty()) { 50 | throw new IllegalArgumentException("Invalid URL!"); 51 | } 52 | 53 | // Start tracking download time 54 | long time = System.currentTimeMillis(); 55 | 56 | // Empty response string placeholder 57 | String responseStr; 58 | 59 | // Handle cache 60 | int maxCacheAge = request.skipCache ? 0 : cacheMaxAge; 61 | 62 | // Build proper URL 63 | String requestUrl = toUrl(request); 64 | 65 | // Build the OkHttp request 66 | okhttp3.Request httpRequest = new okhttp3.Request.Builder() 67 | .addHeader("Cache-Control", "public, max-age=" + maxCacheAge) 68 | .url(requestUrl) 69 | .build(); 70 | 71 | try { 72 | // Execute the built request and log its data 73 | log("Making a request to " + requestUrl + (request.skipCache ? " [SKIP-CACHE]" : " [MAX-AGE " + maxCacheAge + "]")); 74 | Response response = client.newCall(httpRequest).execute(); 75 | 76 | // Was this retrieved from cache? 77 | if (response.cacheResponse() != null) { 78 | log("Response retrieved from cache"); 79 | } 80 | 81 | // Convert response body to a string 82 | responseStr = response.body().string(); 83 | log(TAG, "Request download took " + (System.currentTimeMillis() - time) + "ms", Log.INFO); 84 | } catch (Exception e) { 85 | log("Error executing/reading http request!", Log.ERROR); 86 | e.printStackTrace(); 87 | throw new IOException(e.getMessage()); 88 | } 89 | 90 | return responseStr; 91 | } 92 | 93 | @Override 94 | public String toSafeUrl(Request request) { 95 | // Copy base url 96 | String url = request.url; 97 | 98 | if (request.individual) { 99 | // Append feed URL if individual article 100 | url += "feed/?withoutcomments=1"; 101 | } else if (request.search != null) { 102 | // Append search query if available and not individual 103 | url += "?s=" + Uri.encode(request.search); 104 | } 105 | 106 | // Return safe url 107 | return url; 108 | } 109 | 110 | @Override 111 | public String toUrl(Request request) { 112 | // Copy base url 113 | String url = request.url; 114 | 115 | if (request.individual) { 116 | // Handle individual urls differently 117 | url += "feed/?withoutcomments=1"; 118 | } else { 119 | if (request.search != null) 120 | url += "?s=" + Uri.encode(request.search); 121 | if (request.page > 1) 122 | url += (request.search == null ? "?paged=" : "&paged=") + String.valueOf(request.page); 123 | } 124 | 125 | // Return safe url 126 | return url; 127 | } 128 | } -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/downloader/DefaultDownloader.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss.downloader; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | import android.net.http.HttpResponseCache; 6 | import android.util.Log; 7 | import com.pkmmte.pkrss.Request; 8 | import com.pkmmte.pkrss.Utils; 9 | import java.io.BufferedReader; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.net.HttpURLConnection; 14 | import java.net.URL; 15 | 16 | /** 17 | * The Default Downloader object used for general purposes. 18 | *

19 | * This Downloader class uses Android's built-in HttpUrlConnection for 20 | * networking. It is recommended to use the OkHttpDownloader instead as 21 | * it is more stable and potentially performs better. 22 | */ 23 | public class DefaultDownloader extends Downloader { 24 | // OkHttpClient & configuration 25 | private final File cacheDir; 26 | private final int cacheSize = 1024 * 1024; 27 | private final int cacheMaxAge = 2 * 60 * 60; 28 | private final long connectTimeout = 15000; 29 | private final long readTimeout = 45000; 30 | 31 | public DefaultDownloader(Context context) { 32 | cacheDir = new File(context.getCacheDir(), "http"); 33 | try { 34 | HttpResponseCache.install(cacheDir, cacheSize); 35 | } 36 | catch (IOException e) { 37 | Log.i(TAG, "HTTP response cache installation failed:" + e); 38 | } 39 | } 40 | 41 | @Override 42 | public boolean clearCache() { 43 | return Utils.deleteDir(cacheDir); 44 | } 45 | 46 | @Override 47 | public String execute(Request request) throws IllegalArgumentException, IOException { 48 | // Invalid URLs are a big no no 49 | if (request.url == null || request.url.isEmpty()) { 50 | throw new IllegalArgumentException("Invalid URL!"); 51 | } 52 | 53 | // Start tracking download time 54 | long time = System.currentTimeMillis(); 55 | 56 | // Empty response string placeholder 57 | String responseStr; 58 | 59 | // Handle cache 60 | int maxCacheAge = request.skipCache ? 0 : cacheMaxAge; 61 | 62 | // Build proper URL 63 | String requestUrl = toUrl(request); 64 | URL url = new URL(requestUrl); 65 | 66 | // Open a connection and configure timeouts/cache 67 | HttpURLConnection connection = (HttpURLConnection) url.openConnection(); 68 | connection.setRequestProperty("Cache-Control", "public, max-age=" + maxCacheAge); 69 | connection.setConnectTimeout((int) connectTimeout); 70 | connection.setReadTimeout((int) readTimeout); 71 | 72 | // Execute the request and log its data 73 | log("Making a request to " + requestUrl + (request.skipCache ? " [SKIP-CACHE]" : " [MAX-AGE " + maxCacheAge + "]")); 74 | connection.connect(); 75 | 76 | // Read stream 77 | InputStreamReader streamReader = null; 78 | BufferedReader bufferedReader = null; 79 | try { 80 | streamReader = new InputStreamReader(url.openStream(), "UTF-8"); 81 | bufferedReader = new BufferedReader(streamReader); 82 | StringBuilder sb = new StringBuilder(); 83 | String readLine; 84 | while ((readLine = bufferedReader.readLine()) != null) { 85 | sb.append(readLine); 86 | } 87 | 88 | responseStr = sb.toString(); 89 | log(TAG, "Request download took " + (System.currentTimeMillis() - time) + "ms", Log.INFO); 90 | } catch (Exception e) { 91 | log("Error executing/reading http request!", Log.ERROR); 92 | e.printStackTrace(); 93 | throw new IOException(e.getMessage()); 94 | } finally { 95 | if (bufferedReader != null) 96 | bufferedReader.close(); 97 | if (streamReader != null) 98 | streamReader.close(); 99 | 100 | connection.disconnect(); 101 | } 102 | 103 | return responseStr; 104 | } 105 | 106 | @Override 107 | public String toSafeUrl(Request request) { 108 | // Copy base url 109 | String url = request.url; 110 | 111 | if (request.individual) { 112 | // Append feed URL if individual article 113 | url += "feed/?withoutcomments=1"; 114 | } 115 | else if (request.search != null) { 116 | // Append search query if available and not individual 117 | url += "?s=" + Uri.encode(request.search); 118 | } 119 | 120 | // Return safe url 121 | return url; 122 | } 123 | 124 | @Override 125 | public String toUrl(Request request) { 126 | // Copy base url 127 | String url = request.url; 128 | 129 | if (request.individual) { 130 | // Handle individual urls differently 131 | url += "feed/?withoutcomments=1"; 132 | } 133 | else { 134 | if (request.search != null) 135 | url += "?s=" + Uri.encode(request.search); 136 | if (request.page > 1) 137 | url += (request.search == null ? "?paged=" : "&paged=") + String.valueOf(request.page); 138 | } 139 | 140 | // Return safe url 141 | return url; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/FavoriteDatabase.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.content.ContentValues; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.database.sqlite.SQLiteDatabase; 7 | import android.database.sqlite.SQLiteOpenHelper; 8 | import android.net.Uri; 9 | import android.text.TextUtils; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | /** 16 | * A rather simple SQLite database used for storing Article objects upon marked them as favorites.

Warning: Extra article properties 17 | * will not be saved! 18 | */ 19 | class FavoriteDatabase extends SQLiteOpenHelper { 20 | // Basic Database Info 21 | private static final int DATABASE_VERSION = 1; 22 | private static final String DATABASE_NAME = "db.pkrss.favorites"; 23 | private static final String TABLE_ARTICLES = "articles"; 24 | 25 | // Column Keys 26 | private static final String KEY_TAGS = "TAGS"; 27 | private static final String KEY_MEDIA_CONTENT = "MEDIA_CONTENT"; 28 | private static final String KEY_SOURCE = "SOURCE"; 29 | private static final String KEY_IMAGE = "IMAGE"; 30 | private static final String KEY_TITLE = "TITLE"; 31 | private static final String KEY_DESCRIPTION = "DESCRIPTION"; 32 | private static final String KEY_CONTENT = "CONTENT"; 33 | private static final String KEY_COMMENTS = "COMMENTS"; 34 | private static final String KEY_AUTHOR = "AUTHOR"; 35 | private static final String KEY_DATE = "DATE"; 36 | private static final String KEY_ID = "ID"; 37 | 38 | public FavoriteDatabase(Context context) { 39 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 40 | } 41 | 42 | @Override 43 | public void onCreate(SQLiteDatabase db) { 44 | String CREATE_ARTICLES_TABLE = "CREATE TABLE " 45 | + TABLE_ARTICLES 46 | + " ( " 47 | + KEY_TAGS 48 | + " TEXT , " 49 | + KEY_MEDIA_CONTENT 50 | + " BLOB , " 51 | + KEY_SOURCE 52 | + " TEXT , " 53 | + KEY_IMAGE 54 | + " TEXT ," 55 | + KEY_TITLE 56 | + " TEXT ," 57 | + KEY_DESCRIPTION 58 | + " TEXT , " 59 | + KEY_CONTENT 60 | + " TEXT , " 61 | + KEY_COMMENTS 62 | + " TEXT , " 63 | + KEY_AUTHOR 64 | + " TEXT , " 65 | + KEY_DATE 66 | + " TEXT , " 67 | + KEY_ID 68 | + " INTEGER NOT NULL UNIQUE " 69 | + ")"; 70 | db.execSQL(CREATE_ARTICLES_TABLE); 71 | } 72 | 73 | @Override 74 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 75 | db.execSQL("DROP TABLE IF EXISTS " + TABLE_ARTICLES); 76 | onCreate(db); 77 | } 78 | 79 | /** 80 | * Inserts an Article object to this database. 81 | * 82 | * @param article Object to save into database. 83 | */ 84 | public void add(Article article) { 85 | // Get Write Access 86 | SQLiteDatabase db = this.getWritableDatabase(); 87 | 88 | // Build Content Values 89 | ContentValues values = new ContentValues(); 90 | values.put(KEY_TAGS, TextUtils.join("_PCX_", article.getTags())); 91 | values.put(KEY_MEDIA_CONTENT, Article.MediaContent.toByteArray(article.getMediaContent())); 92 | values.put(KEY_SOURCE, article.getSource().toString()); 93 | values.put(KEY_IMAGE, article.getImage().toString()); 94 | values.put(KEY_TITLE, article.getTitle()); 95 | values.put(KEY_DESCRIPTION, article.getDescription()); 96 | values.put(KEY_CONTENT, article.getContent()); 97 | values.put(KEY_COMMENTS, article.getComments()); 98 | values.put(KEY_AUTHOR, article.getAuthor()); 99 | values.put(KEY_DATE, article.getDate()); 100 | values.put(KEY_ID, article.getId()); 101 | 102 | // Insert & Close 103 | db.insert(TABLE_ARTICLES, null, values); 104 | db.close(); 105 | } 106 | 107 | /** 108 | * @param id ID to search for. 109 | * @return An Article object with the specified ID. May return null if none was found. 110 | */ 111 | public Article get(int id) { 112 | // Get Read Access 113 | SQLiteDatabase db = this.getReadableDatabase(); 114 | 115 | // Execute query with specified id 116 | Cursor cursor = db.query(TABLE_ARTICLES, 117 | new String[] {KEY_TAGS, KEY_MEDIA_CONTENT, KEY_SOURCE, KEY_IMAGE, KEY_TITLE, KEY_DESCRIPTION, KEY_CONTENT, KEY_COMMENTS, KEY_AUTHOR, 118 | KEY_DATE, KEY_ID}, KEY_ID + "=?", new String[] {String.valueOf(id)}, null, null, null, null); 119 | Article article = null; 120 | 121 | try { 122 | // Attempt to retrieve article 123 | if (cursor != null) { 124 | cursor.moveToFirst(); 125 | article = new Article(null, Arrays.asList(cursor.getString(0).split("_PCX_")), Article.MediaContent.fromByteArray(cursor.getBlob(1)), Uri.parse(cursor.getString(2)), 126 | Uri.parse(cursor.getString(3)), cursor.getString(4), cursor.getString(5), cursor.getString(6), 127 | cursor.getString(7), cursor.getString(8), cursor.getLong(9), cursor.getInt(10)); 128 | } 129 | } finally { 130 | // Close Cursor 131 | if (cursor != null) cursor.close(); 132 | } 133 | 134 | // Close & Return 135 | db.close(); 136 | return article; 137 | } 138 | 139 | /** 140 | * @return A backwards Article ArrayList ordered from last added to end. 141 | */ 142 | public List

getAll() { 143 | // Init List & Build Query 144 | List
articleList = new ArrayList
(); 145 | String selectQuery = "SELECT * FROM " + TABLE_ARTICLES; 146 | 147 | // Get Write Access & Execute Query 148 | SQLiteDatabase db = this.getWritableDatabase(); 149 | Cursor cursor = db.rawQuery(selectQuery, null); 150 | 151 | // Read the query backwards 152 | if (cursor.moveToLast()) { 153 | do { 154 | articleList.add(new Article(null, Arrays.asList(cursor.getString(0).split("_PCX_")), Article.MediaContent.fromByteArray(cursor.getBlob(1)), Uri.parse(cursor.getString(2)), 155 | Uri.parse(cursor.getString(3)), cursor.getString(4), cursor.getString(5), cursor.getString(6), 156 | cursor.getString(7), cursor.getString(8), cursor.getLong(9), cursor.getInt(10))); 157 | } while (cursor.moveToPrevious()); 158 | } 159 | cursor.close(); 160 | db.close(); 161 | 162 | return articleList; 163 | } 164 | 165 | /** 166 | * @param id ID to search for. 167 | * @return {@code true} if found or {@code false} if otherwise. 168 | */ 169 | public boolean contains(int id) { 170 | // Get Read Access & Execute Query 171 | SQLiteDatabase db = this.getReadableDatabase(); 172 | Cursor cursor = db.query(TABLE_ARTICLES, 173 | new String[] {KEY_TAGS, KEY_MEDIA_CONTENT, KEY_SOURCE, KEY_IMAGE, KEY_TITLE, KEY_DESCRIPTION, KEY_CONTENT, KEY_COMMENTS, KEY_AUTHOR, 174 | KEY_DATE, KEY_ID}, KEY_ID + "=?", new String[] {String.valueOf(id)}, null, null, null, null); 175 | 176 | // Check if any row exists for this query 177 | boolean exists = cursor.moveToFirst(); 178 | 179 | // Close & Return 180 | cursor.close(); 181 | db.close(); 182 | return exists; 183 | } 184 | 185 | /** 186 | * Removes a specified Article from this database based on its ID value. 187 | * @param article Article to remove. May contain dummy data as long as the id is valid. 188 | */ 189 | public void delete(Article article) { 190 | SQLiteDatabase db = this.getWritableDatabase(); 191 | db.delete(TABLE_ARTICLES, KEY_ID + " = ?", new String[] {String.valueOf(article.getId())}); 192 | db.close(); 193 | } 194 | 195 | /** 196 | * Removes ALL content stored in this database! 197 | */ 198 | public void deleteAll() { 199 | SQLiteDatabase db = this.getWritableDatabase(); 200 | db.delete(TABLE_ARTICLES, null, null); 201 | db.close(); 202 | } 203 | } -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/parser/AtomParser.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss.parser; 2 | 3 | import android.net.Uri; 4 | import android.text.Html; 5 | import android.util.Log; 6 | import com.pkmmte.pkrss.Article; 7 | import com.pkmmte.pkrss.PkRSS; 8 | import java.io.ByteArrayInputStream; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.StringReader; 12 | import java.text.DateFormat; 13 | import java.text.ParseException; 14 | import java.text.SimpleDateFormat; 15 | import java.util.ArrayList; 16 | import java.util.Calendar; 17 | import java.util.List; 18 | import java.util.Locale; 19 | import java.util.regex.Pattern; 20 | import org.xmlpull.v1.XmlPullParser; 21 | import org.xmlpull.v1.XmlPullParserException; 22 | import org.xmlpull.v1.XmlPullParserFactory; 23 | 24 | /** 25 | * Custom PkRSS parser for parsing feeds using the Atom format. 26 | * This is the default parser. Use {@link PkRSS.Builder} to apply your own custom parser 27 | * or modify an existing one. 28 | */ 29 | public class AtomParser extends Parser { 30 | private final List
articleList = new ArrayList
(); 31 | private final DateFormat dateFormat; 32 | private final Pattern pattern; 33 | private final XmlPullParser xmlParser; 34 | 35 | public AtomParser() { 36 | // Initialize DateFormat object with the default date formatting 37 | dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US); 38 | dateFormat.setTimeZone(Calendar.getInstance().getTimeZone()); 39 | pattern = Pattern.compile("-\\d{1,4}x\\d{1,4}"); 40 | 41 | // Initialize XmlPullParser object with a common configuration 42 | XmlPullParser parser = null; 43 | try { 44 | XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 45 | factory.setNamespaceAware(false); 46 | parser = factory.newPullParser(); 47 | } 48 | catch (XmlPullParserException e) { 49 | e.printStackTrace(); 50 | } 51 | xmlParser = parser; 52 | } 53 | 54 | @Override 55 | public List
parse(String rssStream) { 56 | // Clear previous list and start timing execution time 57 | articleList.clear(); 58 | long time = System.currentTimeMillis(); 59 | 60 | try { 61 | // Get InputStream from String and set it to our XmlPullParser 62 | InputStream input = new ByteArrayInputStream(rssStream.getBytes()); 63 | xmlParser.setInput(input, null); 64 | 65 | // Reuse Article object and event holder 66 | Article article = new Article(); 67 | int eventType = xmlParser.getEventType(); 68 | 69 | // Loop through the entire xml feed 70 | while (eventType != XmlPullParser.END_DOCUMENT) { 71 | String tagname = xmlParser.getName(); 72 | switch (eventType) { 73 | case XmlPullParser.START_TAG: 74 | if (tagname.equalsIgnoreCase("entry")) // Start a new instance 75 | article = new Article(); 76 | else // Handle this node if not an entry tag 77 | handleNode(tagname, article); 78 | break; 79 | case XmlPullParser.END_TAG: 80 | if (tagname.equalsIgnoreCase("entry")) { 81 | // Generate ID 82 | article.setId(Math.abs(article.hashCode())); 83 | 84 | // Remove content thumbnail 85 | if(article.getImage() != null && article.getContent() != null) 86 | article.setContent(article.getContent().replaceFirst("", "")); 87 | 88 | // (Optional) Log a minimized version of the toString() output 89 | log(TAG, article.toShortString(), Log.INFO); 90 | 91 | // Add article object to list 92 | articleList.add(article); 93 | } 94 | break; 95 | default: 96 | break; 97 | } 98 | eventType = xmlParser.next(); 99 | } 100 | } 101 | catch (IOException e) { 102 | // Uh oh 103 | e.printStackTrace(); 104 | } 105 | catch (XmlPullParserException e) { 106 | // Oh noes 107 | e.printStackTrace(); 108 | } 109 | 110 | // Output execution time and return list of newly parsed articles 111 | log(TAG, "Parsing took " + (System.currentTimeMillis() - time) + "ms"); 112 | return articleList; 113 | } 114 | 115 | /** 116 | * Handles a node from the tag node and assigns it to the correct article value. 117 | * @param tag The tag which to handle. 118 | * @param article Article object to assign the node value to. 119 | * @return True if a proper tag was given or handled. False if improper tag was given or 120 | * if an exception if triggered. 121 | */ 122 | private boolean handleNode(String tag, Article article) { 123 | try { 124 | if (tag.equalsIgnoreCase("category")) 125 | article.setNewTag(xmlParser.getAttributeValue(null, "term")); 126 | else if (tag.equalsIgnoreCase("link")) { 127 | String rel = xmlParser.getAttributeValue(null, "rel"); 128 | if (rel.equalsIgnoreCase("alternate")) 129 | article.setSource(Uri.parse(xmlParser.getAttributeValue(null, "href"))); 130 | else if (rel.equalsIgnoreCase("replies")) 131 | article.setComments(xmlParser.getAttributeValue(null, "href")); 132 | } 133 | 134 | if(xmlParser.next() != XmlPullParser.TEXT) 135 | return false; 136 | 137 | if (tag.equalsIgnoreCase("title")) 138 | article.setTitle(xmlParser.getText()); 139 | else if (tag.equalsIgnoreCase("summary")) { 140 | String encoded = xmlParser.getText(); 141 | article.setImage(Uri.parse(pullImageLink(encoded))); 142 | article.setDescription(Html.fromHtml(encoded.replaceAll("", "")).toString()); 143 | } 144 | else if (tag.equalsIgnoreCase("content")) 145 | article.setContent(xmlParser.getText().replaceAll("[<](/)?div[^>]*[>]", "")); 146 | else if (tag.equalsIgnoreCase("category")) 147 | article.setNewTag(xmlParser.getText()); 148 | else if (tag.equalsIgnoreCase("name")) 149 | article.setAuthor(xmlParser.getText()); 150 | else if (tag.equalsIgnoreCase("published")) { 151 | article.setDate(getParsedDate(xmlParser.getText())); 152 | } 153 | 154 | return true; 155 | } 156 | catch (IOException e) { 157 | e.printStackTrace(); 158 | return false; 159 | } 160 | catch (XmlPullParserException e) { 161 | e.printStackTrace(); 162 | return false; 163 | } 164 | } 165 | 166 | /** 167 | * Converts a date in the "EEE, d MMM yyyy HH:mm:ss Z" format to a long value. 168 | * @param encodedDate The encoded date which to convert. 169 | * @return A long value for the passed date String or 0 if improperly parsed. 170 | */ 171 | private long getParsedDate(String encodedDate) { 172 | try { 173 | return dateFormat.parse(dateFormat.format(dateFormat.parseObject(encodedDate.replaceAll("Z$", "+0000")))).getTime(); 174 | } 175 | catch (ParseException e) { 176 | log(TAG, "Error parsing date " + encodedDate, Log.WARN); 177 | e.printStackTrace(); 178 | return 0; 179 | } 180 | } 181 | 182 | /** 183 | * Pulls an image URL from an encoded String. 184 | * 185 | * @param encoded The String which to extract an image URL from. 186 | * @return The first image URL found on the encoded String. May return an 187 | * empty String if none were found. 188 | */ 189 | private String pullImageLink(String encoded) { 190 | try { 191 | XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 192 | XmlPullParser xpp = factory.newPullParser(); 193 | 194 | xpp.setInput(new StringReader(encoded)); 195 | int eventType = xpp.getEventType(); 196 | while (eventType != XmlPullParser.END_DOCUMENT) { 197 | if (eventType == XmlPullParser.START_TAG && "img".equals(xpp.getName())) { 198 | int count = xpp.getAttributeCount(); 199 | for (int x = 0; x < count; x++) { 200 | if (xpp.getAttributeName(x).equalsIgnoreCase("src")) 201 | return pattern.matcher(xpp.getAttributeValue(x)).replaceAll(""); 202 | } 203 | } 204 | eventType = xpp.next(); 205 | } 206 | } 207 | catch (Exception e) { 208 | log(TAG, "Error pulling image link from description!\n" + e.getMessage(), Log.WARN); 209 | } 210 | 211 | return ""; 212 | } 213 | } -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/RequestCreator.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.os.AsyncTask; 4 | import android.os.Handler; 5 | import android.util.Log; 6 | import com.pkmmte.pkrss.downloader.Downloader; 7 | import com.pkmmte.pkrss.parser.Parser; 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | /** 15 | * Fluent API for building an RSS load request. 16 | */ 17 | public class RequestCreator { 18 | private static final List activeRequests = Collections.synchronizedList(new ArrayList()); 19 | 20 | private final PkRSS singleton; 21 | private final Request.Builder data; 22 | 23 | private long delay = 0; 24 | private boolean ignoreIfRunning = false; 25 | 26 | protected RequestCreator(PkRSS singleton, String url) { 27 | this.singleton = singleton; 28 | this.data = new Request.Builder(url); 29 | } 30 | 31 | /** 32 | * Time delay before executing this request asynchronously. 33 | * Defaults to 0 for immediate execution. 34 | * @param delay Time in milliseconds. 35 | */ 36 | public RequestCreator delay(long delay) { 37 | this.delay = delay; 38 | return this; 39 | } 40 | 41 | /** 42 | * Indicate whether or not to ignore this request if another 43 | * request with the same .tag() is already running. 44 | * @param ignoreIfRunning 45 | */ 46 | public RequestCreator ignoreIfRunning(boolean ignoreIfRunning) { 47 | this.ignoreIfRunning = ignoreIfRunning; 48 | return this; 49 | } 50 | 51 | /** 52 | * Assigns a reference tag to this request. 53 | * Leaving this empty will automatically generate a tag. 54 | * @param tag 55 | */ 56 | public RequestCreator tag(String tag) { 57 | this.data.tag(tag); 58 | return this; 59 | } 60 | 61 | /** 62 | * Looks up a specific query on the RSS feed. 63 | * The query string is automatically encoded. 64 | * @param search 65 | */ 66 | public RequestCreator search(String search) { 67 | this.data.search(search); 68 | return this; 69 | } 70 | 71 | /** 72 | * Threats this request as an individual article, 73 | * rather than full feed. Use only if you are sure that 74 | * the load URL belongs to a single article. 75 | */ 76 | public RequestCreator individual() { 77 | this.data.individual(true); 78 | return this; 79 | } 80 | 81 | /** 82 | * Ignores already cached responses when making this 83 | * request. Useful for refreshing feeds/articles. 84 | */ 85 | public RequestCreator skipCache() { 86 | this.data.skipCache(true); 87 | return this; 88 | } 89 | 90 | /** 91 | * Loads a specific page of the RSS feed. 92 | * @param page Page to load. 93 | */ 94 | public RequestCreator page(int page) { 95 | this.data.page(page); 96 | return this; 97 | } 98 | 99 | /** 100 | * Loads the next page of the current RSS feed. 101 | * If no page was previously loaded, this will 102 | * request the first page. 103 | */ 104 | public RequestCreator nextPage() { 105 | Request request = data.build(); 106 | String url = request.url; 107 | int page = request.page; 108 | 109 | if(request.search != null) 110 | url += "?s=" + request.search; 111 | 112 | Map pageTracker = singleton.getPageTracker(); 113 | if(pageTracker.containsKey(url)) 114 | page = pageTracker.get(url); 115 | 116 | this.data.page(page + 1); 117 | return this; 118 | } 119 | 120 | /** 121 | * Choose whether to handle callbacks safely. 122 | * Setting to true will automatically catch any exceptions thrown. 123 | * @param safe 124 | */ 125 | public RequestCreator safe(Boolean safe) { 126 | this.data.safe(safe); 127 | return this; 128 | } 129 | 130 | /** 131 | * Assigns a different callback handler to this specific request. 132 | * This is useful for handling this request in a specific thread but leaving the rest 133 | * as before. If you'd like to force the main thread handler, use new Handler(Looper.getMainLooper()); 134 | * @param handler Handler thread in which to run the callback for this request. 135 | */ 136 | public RequestCreator handler(Handler handler) { 137 | this.data.handler(new CallbackHandler(handler)); 138 | return this; 139 | } 140 | 141 | /** 142 | * Assigns a different downloader to handle this specific request. 143 | * This may be useful under certain rare conditions or for miscellaneous purposes. 144 | * @param downloader Custom downloader which to override the set downloader 145 | * for this request and this request only. 146 | */ 147 | public RequestCreator downloader(Downloader downloader) { 148 | this.data.downloader(downloader); 149 | return this; 150 | } 151 | 152 | /** 153 | * Assigns a different parser to handle this specific request. 154 | * This is useful for parsing separate data such as comments feeds. 155 | * @param parser Custom parser which to override the set parser 156 | * for this request and this request only. 157 | */ 158 | public RequestCreator parser(Parser parser) { 159 | this.data.parser(parser); 160 | return this; 161 | } 162 | 163 | /** 164 | * Adds a callback listener to this request. 165 | * @param callback Callback interface to respond to. 166 | */ 167 | public RequestCreator callback(Callback callback) { 168 | this.data.callback(callback); 169 | return this; 170 | } 171 | 172 | /** 173 | * Executes request and returns a full list containing all 174 | * articles loaded from this request's URL. 175 | *

176 | * If this request is marked as individual, the list will 177 | * contain only 1 index. It is recommended to use getFirst() 178 | * for individual requests instead. 179 | */ 180 | public List

get() throws IOException { 181 | final Request request = data.build(); 182 | singleton.load(request); 183 | return singleton.get(request.individual ? request.url + "feed/?withoutcomments=1" : request.url, request.search); 184 | } 185 | 186 | /** 187 | * Executes request and returns the first Article associated 188 | * with this request. This is useful for individual Article requests. 189 | *

190 | * May return null. 191 | * @return Returns the first article associated with this request. 192 | */ 193 | public Article getFirst() { 194 | try { 195 | List

articleList = get(); 196 | if(articleList == null || articleList.size() < 1) 197 | return null; 198 | 199 | return articleList.get(0); 200 | } catch (IOException e) { 201 | return null; 202 | } 203 | } 204 | 205 | /** 206 | * Executes request asynchronously. 207 | *

208 | * Be sure to add a callback to handle this. 209 | */ 210 | public void async() { 211 | final Request request = data.build(); 212 | final CallbackHandler handler = request.handler != null ? request.handler : singleton.handler; 213 | final boolean safe = request.safe != null ? request.safe : singleton.safe; 214 | 215 | // Ignore current request if already running (ignoreIfRunning) 216 | synchronized (activeRequests) { 217 | if (ignoreIfRunning && activeRequests.contains(request.tag)) { 218 | singleton.log(request.tag + " request already running! Ignoring..."); 219 | return; 220 | } 221 | activeRequests.add(request.tag); 222 | } 223 | 224 | new AsyncTask() { 225 | @Override 226 | protected Void doInBackground(Void... params) { 227 | try { 228 | // Delay thread if specified 229 | if (delay > 0) { 230 | singleton.log("Delaying " + request.tag + " request for " + delay + "ms"); 231 | Thread.sleep(delay); 232 | } 233 | 234 | // Execute request 235 | try { 236 | singleton.load(request); 237 | } catch (IOException e) { 238 | singleton.log("Error executing request " + request.tag + " asynchronously! " + e.getMessage(), Log.ERROR); 239 | handler.onLoadFailed(safe, request.callback.get()); 240 | } 241 | } catch (InterruptedException e) { 242 | singleton.log(request.tag + " thread interrupted!"); 243 | } finally { 244 | synchronized (activeRequests) { 245 | activeRequests.remove(request.tag); 246 | } 247 | } 248 | return null; 249 | } 250 | 251 | @Override 252 | protected void onCancelled(Void aVoid) { 253 | super.onCancelled(aVoid); 254 | 255 | // Double check to make sure this request is removed 256 | synchronized (activeRequests) { 257 | activeRequests.remove(request.tag); 258 | } 259 | } 260 | }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 261 | } 262 | } -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/parser/Rss2Parser.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss.parser; 2 | 3 | import android.net.Uri; 4 | import android.text.Html; 5 | import android.util.Log; 6 | 7 | import com.pkmmte.pkrss.Article; 8 | import com.pkmmte.pkrss.Enclosure; 9 | import com.pkmmte.pkrss.PkRSS; 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.StringReader; 14 | import java.text.DateFormat; 15 | import java.text.ParseException; 16 | import java.text.SimpleDateFormat; 17 | import java.util.ArrayList; 18 | import java.util.Calendar; 19 | import java.util.List; 20 | import java.util.Locale; 21 | import java.util.regex.Pattern; 22 | import org.xmlpull.v1.XmlPullParser; 23 | import org.xmlpull.v1.XmlPullParserException; 24 | import org.xmlpull.v1.XmlPullParserFactory; 25 | 26 | /** 27 | * Custom PkRSS parser for parsing feeds using the RSS2 standard format. 28 | * This is the default parser. Use {@link PkRSS.Builder} to apply your own custom parser 29 | * or modify an existing one. 30 | */ 31 | public class Rss2Parser extends Parser { 32 | private final List

articleList = new ArrayList
(); 33 | private final DateFormat dateFormat; 34 | private final Pattern pattern; 35 | private final XmlPullParser xmlParser; 36 | 37 | public Rss2Parser() { 38 | // Initialize DateFormat object with the default date formatting 39 | dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US); 40 | dateFormat.setTimeZone(Calendar.getInstance().getTimeZone()); 41 | pattern = Pattern.compile("-\\d{1,4}x\\d{1,4}"); 42 | 43 | // Initialize XmlPullParser object with a common configuration 44 | XmlPullParser parser = null; 45 | try { 46 | XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 47 | factory.setNamespaceAware(false); 48 | parser = factory.newPullParser(); 49 | } 50 | catch (XmlPullParserException e) { 51 | e.printStackTrace(); 52 | } 53 | xmlParser = parser; 54 | } 55 | 56 | @Override 57 | public List
parse(String rssStream) { 58 | // Clear previous list and start timing execution time 59 | articleList.clear(); 60 | long time = System.currentTimeMillis(); 61 | 62 | try { 63 | // Get InputStream from String and set it to our XmlPullParser 64 | InputStream input = new ByteArrayInputStream(rssStream.getBytes()); 65 | xmlParser.setInput(input, null); 66 | 67 | // Reuse Article object and event holder 68 | Article article = new Article(); 69 | int eventType = xmlParser.getEventType(); 70 | 71 | // Loop through the entire xml feed 72 | while (eventType != XmlPullParser.END_DOCUMENT) { 73 | String tagname = xmlParser.getName(); 74 | switch (eventType) { 75 | case XmlPullParser.START_TAG: 76 | if (tagname.equalsIgnoreCase("item")) // Start a new instance 77 | article = new Article(); 78 | // Enclosures not readable as text by XmlPullParser in Android and will fail in handleNode, considered not a bug 79 | // https://code.google.com/p/android/issues/detail?id=18658 80 | else if (tagname.equalsIgnoreCase("enclosure")) { 81 | article.setEnclosure(new Enclosure(xmlParser)); 82 | } else if (tagname.equalsIgnoreCase("media:content")) { 83 | handleMediaContent(tagname, article); 84 | } else // Handle this node if not an entry tag 85 | handleNode(tagname, article); 86 | break; 87 | case XmlPullParser.END_TAG: 88 | if (tagname.equalsIgnoreCase("item")) { 89 | // Generate ID 90 | article.setId(Math.abs(article.hashCode())); 91 | 92 | // Remove content thumbnail 93 | if(article.getImage() != null && article.getContent() != null) 94 | article.setContent(article.getContent().replaceFirst("", "")); 95 | 96 | // (Optional) Log a minimized version of the toString() output 97 | log(TAG, article.toShortString(), Log.INFO); 98 | 99 | // Add article object to list 100 | articleList.add(article); 101 | } 102 | break; 103 | default: 104 | break; 105 | } 106 | eventType = xmlParser.next(); 107 | } 108 | } 109 | catch (IOException e) { 110 | // Uh oh 111 | e.printStackTrace(); 112 | } 113 | catch (XmlPullParserException e) { 114 | // Oh noes 115 | e.printStackTrace(); 116 | } 117 | 118 | // Output execution time and return list of newly parsed articles 119 | log(TAG, "Parsing took " + (System.currentTimeMillis() - time) + "ms"); 120 | return articleList; 121 | } 122 | 123 | /** 124 | * Handles a node from the tag node and assigns it to the correct article value. 125 | * @param tag The tag which to handle. 126 | * @param article Article object to assign the node value to. 127 | * @return True if a proper tag was given or handled. False if improper tag was given or 128 | * if an exception if triggered. 129 | */ 130 | private boolean handleNode(String tag, Article article) { 131 | try { 132 | if(xmlParser.next() != XmlPullParser.TEXT) 133 | return false; 134 | 135 | if (tag.equalsIgnoreCase("link")) 136 | article.setSource(Uri.parse(xmlParser.getText())); 137 | else if (tag.equalsIgnoreCase("title")) 138 | article.setTitle(xmlParser.getText()); 139 | else if (tag.equalsIgnoreCase("description")) { 140 | String encoded = xmlParser.getText(); 141 | article.setImage(Uri.parse(pullImageLink(encoded))); 142 | article.setDescription(Html.fromHtml(encoded.replaceAll("", "")).toString()); 143 | } 144 | else if (tag.equalsIgnoreCase("content:encoded")) 145 | article.setContent(xmlParser.getText().replaceAll("[<](/)?div[^>]*[>]", "")); 146 | else if (tag.equalsIgnoreCase("wfw:commentRss")) 147 | article.setComments(xmlParser.getText()); 148 | else if (tag.equalsIgnoreCase("category")) 149 | article.setNewTag(xmlParser.getText()); 150 | else if (tag.equalsIgnoreCase("dc:creator")) 151 | article.setAuthor(xmlParser.getText()); 152 | else if (tag.equalsIgnoreCase("pubDate")) { 153 | article.setDate(getParsedDate(xmlParser.getText())); 154 | } 155 | 156 | return true; 157 | } 158 | catch (IOException e) { 159 | e.printStackTrace(); 160 | return false; 161 | } 162 | catch (XmlPullParserException e) { 163 | e.printStackTrace(); 164 | return false; 165 | } 166 | } 167 | 168 | /** 169 | * Parses the media content of the entry 170 | * @param tag The tag which to handle. 171 | * @param article Article object to assign the node value to. 172 | */ 173 | private void handleMediaContent(String tag, Article article) { 174 | String url = xmlParser.getAttributeValue(null, "url"); 175 | if(url == null) { 176 | throw new IllegalArgumentException("Url argument must not be null"); 177 | } 178 | 179 | Article.MediaContent mc = new Article.MediaContent(); 180 | article.addMediaContent(mc); 181 | mc.setUrl(url); 182 | 183 | if(xmlParser.getAttributeValue(null, "type") != null) { 184 | mc.setType(xmlParser.getAttributeValue(null, "type")); 185 | } 186 | 187 | if(xmlParser.getAttributeValue(null, "fileSize") != null) { 188 | mc.setFileSize(Integer.parseInt(xmlParser.getAttributeValue(null, "fileSize"))); 189 | } 190 | 191 | 192 | if(xmlParser.getAttributeValue(null, "medium") != null) { 193 | mc.setMedium(xmlParser.getAttributeValue(null, "medium")); 194 | } 195 | 196 | if(xmlParser.getAttributeValue(null, "isDefault") != null) { 197 | mc.setIsDefault(Boolean.parseBoolean(xmlParser.getAttributeValue(null, "isDefault"))); 198 | } 199 | 200 | if(xmlParser.getAttributeValue(null, "expression") != null) { 201 | mc.setExpression(xmlParser.getAttributeValue(null, "expression")); 202 | } 203 | 204 | if(xmlParser.getAttributeValue(null, "bitrate") != null) { 205 | mc.setBitrate(Integer.parseInt(xmlParser.getAttributeValue(null, "bitrate"))); 206 | } 207 | 208 | if(xmlParser.getAttributeValue(null, "framerate") != null) { 209 | mc.setFramerate(Integer.parseInt(xmlParser.getAttributeValue(null, "framerate"))); 210 | } 211 | 212 | if(xmlParser.getAttributeValue(null, "samplingrate") != null) { 213 | mc.setSamplingrate(Integer.parseInt(xmlParser.getAttributeValue(null, "samplingrate"))); 214 | } 215 | 216 | if(xmlParser.getAttributeValue(null, "channels") != null) { 217 | mc.setChannels(Integer.parseInt(xmlParser.getAttributeValue(null, "channels"))); 218 | } 219 | 220 | if(xmlParser.getAttributeValue(null, "duration") != null) { 221 | mc.setDuration(Integer.parseInt(xmlParser.getAttributeValue(null, "duration"))); 222 | } 223 | 224 | if(xmlParser.getAttributeValue(null, "height") != null) { 225 | mc.setHeight(Integer.parseInt(xmlParser.getAttributeValue(null, "height"))); 226 | } 227 | 228 | if(xmlParser.getAttributeValue(null, "width") != null) { 229 | mc.setWidth(Integer.parseInt(xmlParser.getAttributeValue(null, "width"))); 230 | } 231 | 232 | if(xmlParser.getAttributeValue(null, "lang") != null) { 233 | mc.setLang(xmlParser.getAttributeValue(null, "lang")); 234 | } 235 | } 236 | 237 | /** 238 | * Converts a date in the "EEE, d MMM yyyy HH:mm:ss Z" format to a long value. 239 | * @param encodedDate The encoded date which to convert. 240 | * @return A long value for the passed date String or 0 if improperly parsed. 241 | */ 242 | private long getParsedDate(String encodedDate) { 243 | try { 244 | return dateFormat.parse(dateFormat.format(dateFormat.parseObject(encodedDate))).getTime(); 245 | } 246 | catch (ParseException e) { 247 | log(TAG, "Error parsing date " + encodedDate, Log.WARN); 248 | e.printStackTrace(); 249 | return 0; 250 | } 251 | } 252 | 253 | /** 254 | * Pulls an image URL from an encoded String. 255 | * 256 | * @param encoded The String which to extract an image URL from. 257 | * @return The first image URL found on the encoded String. May return an 258 | * empty String if none were found. 259 | */ 260 | private String pullImageLink(String encoded) { 261 | try { 262 | XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); 263 | XmlPullParser xpp = factory.newPullParser(); 264 | 265 | xpp.setInput(new StringReader(encoded)); 266 | int eventType = xpp.getEventType(); 267 | while (eventType != XmlPullParser.END_DOCUMENT) { 268 | if (eventType == XmlPullParser.START_TAG && "img".equals(xpp.getName())) { 269 | int count = xpp.getAttributeCount(); 270 | for (int x = 0; x < count; x++) { 271 | if (xpp.getAttributeName(x).equalsIgnoreCase("src")) 272 | return pattern.matcher(xpp.getAttributeValue(x)).replaceAll(""); 273 | } 274 | } 275 | eventType = xpp.next(); 276 | } 277 | } 278 | catch (Exception e) { 279 | log(TAG, "Error pulling image link from description!\n" + e.getMessage(), Log.WARN); 280 | } 281 | 282 | return ""; 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/PkRSS.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.net.Uri; 6 | import android.os.AsyncTask; 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | import android.util.Log; 10 | import android.util.SparseBooleanArray; 11 | import com.pkmmte.pkrss.downloader.DefaultDownloader; 12 | import com.pkmmte.pkrss.downloader.Downloader; 13 | import com.pkmmte.pkrss.downloader.OkHttpDownloader; 14 | import com.pkmmte.pkrss.parser.Parser; 15 | import com.pkmmte.pkrss.parser.Rss2Parser; 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | 22 | /** 23 | * A powerful RSS feed manager for Android 24 | *

25 | * Use {@link #with(android.content.Context)} for the global singleton instance or construct your 26 | * own instance with {@link PkRSS.Builder}. 27 | */ 28 | public class PkRSS { 29 | // General public constant keys 30 | public static final String KEY_ARTICLE = "ARTICLE"; 31 | public static final String KEY_ARTICLE_ID = "ARTICLE ID"; 32 | public static final String KEY_ARTICLE_URL = "ARTICLE URL"; 33 | public static final String KEY_FEED_URL = "FEED URL"; 34 | public static final String KEY_CATEGORY_NAME = "CATEGORY NAME"; 35 | public static final String KEY_CATEGORY = "CATEGORY"; 36 | public static final String KEY_SEARCH = "SEARCH TERM"; 37 | public static final String KEY_READ_ARRAY = "READ ARRAY"; 38 | public static final String KEY_FAVORITES = "FAVORITES"; 39 | 40 | // Global singleton instance 41 | private static PkRSS singleton = null; 42 | 43 | // For issue tracking purposes 44 | private volatile boolean loggingEnabled; 45 | protected static final String TAG = "PkRSS"; 46 | 47 | // Callback Thread Handler 48 | protected final CallbackHandler handler; 49 | 50 | // Safely handle callbacks 51 | protected final boolean safe; 52 | 53 | // Context is always useful for some reason. 54 | private final Context mContext; 55 | 56 | // For storing & reading read data 57 | private final SharedPreferences mPrefs; 58 | 59 | // Our handy client for getting XML feed data 60 | private final Downloader downloader; 61 | 62 | // Reusable XML Parser 63 | private final Parser parser; 64 | 65 | // List of stored articles 66 | private final Map> articleMap = new HashMap>(); 67 | 68 | // Keep track of pages already loaded on specific feeds 69 | private final Map pageTracker = new HashMap(); 70 | 71 | // Persistent SparseArray for checking an article's read state 72 | private final SparseBooleanArray readList = new SparseBooleanArray(); 73 | 74 | // Database storing all articles marked as favorite 75 | private final FavoriteDatabase favoriteDatabase; 76 | 77 | /** 78 | * The global default {@link PkRSS} instance. 79 | *

80 | * This instance is automatically initialized with defaults that are suitable to most 81 | * implementations. 82 | *

83 | * If these settings do not meet the requirements of your application, you can construct your own 84 | * instance with full control over the configuration by using {@link PkRSS.Builder}. 85 | * You may then use it directly or replace the current singleton instance via {@link #setSingleton(PkRSS)}. 86 | */ 87 | public static PkRSS with(Context context) { 88 | if(singleton == null) 89 | singleton = new Builder(context).build(); 90 | return singleton; 91 | } 92 | 93 | /** 94 | * Returns the global singleton instance. It may be null so use wisely! 95 | * @return Global singleton instance. May be null. 96 | */ 97 | protected static PkRSS getInstance() { 98 | return singleton; 99 | } 100 | 101 | public static void setSingleton(PkRSS singleton) { 102 | PkRSS.singleton = singleton; 103 | } 104 | 105 | PkRSS(Context context, CallbackHandler handler, Downloader downloader, Parser parser, boolean loggingEnabled, boolean safe) { 106 | this.mContext = context; 107 | this.handler = handler; 108 | this.downloader = downloader; 109 | this.downloader.attachInstance(this); 110 | this.parser = parser; 111 | this.parser.attachInstance(this); 112 | this.loggingEnabled = loggingEnabled; 113 | this.safe = safe; 114 | this.mPrefs = context.getSharedPreferences(TAG, Context.MODE_PRIVATE); 115 | getRead(); 116 | favoriteDatabase = new FavoriteDatabase(context); 117 | } 118 | 119 | /** 120 | * Toggle whether debug logging is enabled. 121 | */ 122 | public void setLoggingEnabled(boolean enabled) { 123 | loggingEnabled = enabled; 124 | Log.d(TAG, "Logging is now " + (enabled ? "enabled" : "disabled")); 125 | } 126 | 127 | /** 128 | * {@code true} if debug logging is enabled. 129 | */ 130 | public boolean isLoggingEnabled() { 131 | return loggingEnabled; 132 | } 133 | 134 | /** 135 | * Starts a request with the specified URL. 136 | *

137 | * URLs may be anything you wish as long as the {@link Parser} and/or 138 | * {@link Downloader} supports it. See {@link PkRSS.Builder} for using 139 | * your own custom Downloaders/Parsers. If necessary, you may use a custom 140 | * parser for this specific request via {@link RequestCreator#parser(Parser)}. 141 | * @param url URL to load feed from. 142 | * @return A chained Request. 143 | */ 144 | public RequestCreator load(String url) { 145 | return new RequestCreator(this, url); 146 | } 147 | 148 | /** 149 | * Handles the specified {@link Request}. May throw an {@link IOException} for 150 | * mishandled URLs or timeouts. 151 | * @param request Request to execute. 152 | * @throws IOException 153 | */ 154 | protected void load(final Request request) throws IOException { 155 | log("load(" + request + ')'); 156 | final CallbackHandler handler = request.handler != null ? request.handler : this.handler; 157 | final boolean safe = request.safe != null ? request.safe : this.safe; 158 | Callback callback = request.callback != null ? request.callback.get() : null; 159 | 160 | // Don't load if URL is the favorites key 161 | if(request.url.equals(KEY_FAVORITES)) { 162 | log("Favorites URL detected, skipping load..."); 163 | return; 164 | } 165 | 166 | // Notify callback 167 | handler.onPreload(safe, callback); 168 | 169 | // Create safe url for pagination/indexing purposes 170 | String safeUrl = request.downloader == null ? downloader.toSafeUrl(request) : request.downloader.toSafeUrl(request); 171 | 172 | // Put the page index into the request's HashMap 173 | pageTracker.put(safeUrl, request.page); 174 | 175 | // Get response from this request 176 | String response = request.downloader == null ? downloader.execute(request) : request.downloader.execute(request); 177 | 178 | // Parse articles from response and inset into global list 179 | List

newArticles = request.parser == null ? parser.parse(response) : request.parser.parse(response); 180 | insert(safeUrl, newArticles); 181 | 182 | // Notify callback 183 | handler.onLoaded(safe, callback, newArticles); 184 | } 185 | 186 | /** 187 | * Returns a {@link HashMap} containing all loaded 188 | * Article objects. The map key being the safe url. 189 | * @return 190 | */ 191 | public Map> get() { 192 | return articleMap; 193 | } 194 | 195 | /** 196 | * Looks up the specified URL String from the saved HashMap. 197 | * @param url Safe URL to look up loaded articles from. May also be {@link PkRSS#KEY_FAVORITES}. 198 | * @return A {@link List} containing all loaded articles associated with that 199 | * URL. May be null if no such URL has yet been loaded. 200 | */ 201 | public List
get(String url) { 202 | if(url.equals(KEY_FAVORITES)) 203 | return getFavorites(); 204 | 205 | return articleMap.get(url); 206 | } 207 | 208 | /** 209 | * Similar to {@link PkRSS#get(String)} but also looks for the search term. 210 | * @param url Safe URL to look up loaded articles from. 211 | * @param search Search term. 212 | * @return A {@link List} containing all loaded articles associated with that 213 | * URL and query. May be null if no such URL has yet been loaded. 214 | */ 215 | public List
get(String url, String search) { 216 | if(search == null) 217 | return articleMap.get(url); 218 | 219 | return articleMap.get(url + "?s=" + Uri.encode(search)); 220 | } 221 | 222 | /** 223 | * Returns an {@link Article} object associated with the specified id. 224 | * @param id ID belonging to such article. 225 | * @return The Article associated with the specified id. May return null if 226 | * no such article was found with that ID. 227 | */ 228 | public Article get(int id) { 229 | long time = System.currentTimeMillis(); 230 | 231 | // Look for an article with this id in the Article HashMap 232 | for(List
articleList : articleMap.values()) { 233 | for(Article article : articleList) { 234 | if(article.getId() == id) { 235 | log("get(" + id + ") took " + (System.currentTimeMillis() - time) + "ms"); 236 | return article; 237 | } 238 | } 239 | } 240 | 241 | // If none was found, try searching in the favorites database 242 | for(Article article : favoriteDatabase.getAll()) { 243 | if(article.getId() == id) { 244 | log("get(" + id + ") took " + (System.currentTimeMillis() - time) + "ms"); 245 | return article; 246 | } 247 | } 248 | 249 | log("Could not find Article with id " + id, Log.WARN); 250 | log("get(" + id + ") took " + (System.currentTimeMillis() - time) + "ms"); 251 | return null; 252 | } 253 | 254 | /** 255 | * Retrieves an ArrayList of articles from the Favorite Database. 256 | * @return Either an Article List or null if database wasn't properly started. 257 | */ 258 | public List
getFavorites() { 259 | return favoriteDatabase == null ? null : favoriteDatabase.getAll(); 260 | } 261 | 262 | /** 263 | * Marks/Unmarks all loaded articles as read. This includes those loaded from 264 | * all URLs and those stored in the favorites database. Those loaded after this 265 | * was called will still be unread. 266 | * @param read 267 | */ 268 | public void markAllRead(boolean read) { 269 | long time = System.currentTimeMillis(); 270 | 271 | // Just clear the array and return, if marking as unread 272 | if(!read) { 273 | readList.clear(); 274 | writeRead(); 275 | log("markAllRead(" + String.valueOf(read) + ") took " + (System.currentTimeMillis() - time) + "ms"); 276 | return; 277 | } 278 | 279 | // Look for an article with this id in the Article HashMap 280 | for(List
articleList : articleMap.values()) { 281 | for(Article article : articleList) 282 | readList.put(article.getId(), read); 283 | } 284 | 285 | // If none was found, try searching in the favorites database 286 | for(Article article : favoriteDatabase.getAll()) { 287 | readList.put(article.getId(), read); 288 | } 289 | 290 | writeRead(); 291 | log("markAllRead(" + String.valueOf(read) + ") took " + (System.currentTimeMillis() - time) + "ms"); 292 | } 293 | 294 | /** 295 | * Marks an article id as read. 296 | * @param id Article id to store its read state. 297 | * @param read Whether or not to mark this id as read. 298 | */ 299 | public void markRead(int id, boolean read) { 300 | readList.put(id, read); 301 | writeRead(); 302 | } 303 | 304 | /** 305 | * @param id Article ID to check for its read state. 306 | * @return {@code true} if such id was previously marked as read, 307 | * {@code false} if it has not yet been marked as read. 308 | */ 309 | public boolean isRead(int id) { 310 | return readList.get(id, false); 311 | } 312 | 313 | /** 314 | * Saves an {@link Article} object to the favorites database. 315 | *

316 | * If possible, use the Article object itself instead to increase performance! 317 | * @param id ID of the article to save. 318 | * @return {@code true} if successful, {@code false} if otherwise. 319 | */ 320 | public boolean saveFavorite(int id) { 321 | return saveFavorite(get(id), true); 322 | } 323 | 324 | /** 325 | * Saves/Deletes an {@link Article} object to the favorites database. 326 | *

327 | * If possible, use the Article object itself instead to increase performance! 328 | * @param id ID of the article to save. 329 | * @param favorite Whether to save or delete. {@code true} to save; {@code false} to delete. 330 | * @return {@code true} if successful, {@code false} if otherwise. 331 | */ 332 | public boolean saveFavorite(int id, boolean favorite) { 333 | return saveFavorite(get(id), favorite); 334 | } 335 | 336 | /** 337 | * Saves an {@link Article} object to the favorites database. 338 | * @param article Article object to save. 339 | * @return {@code true} if successful, {@code false} if otherwise. 340 | */ 341 | public boolean saveFavorite(Article article) { 342 | return saveFavorite(article, true); 343 | } 344 | 345 | /** 346 | * Saves/Deletes an {@link Article} object to the favorites database. 347 | * @param article Article object to save. 348 | * @param favorite Whether to save or delete. {@code true} to save; {@code false} to delete. 349 | * @return {@code true} if successful, {@code false} if otherwise. 350 | */ 351 | public boolean saveFavorite(Article article, boolean favorite) { 352 | long time = System.currentTimeMillis(); 353 | log("Adding article " + article.getId() + " to favorites..."); 354 | try { 355 | if (favorite) 356 | favoriteDatabase.add(article); 357 | else 358 | favoriteDatabase.delete(article); 359 | } 360 | catch (Exception e) { 361 | log("Error " + (favorite ? "saving article to" : "deleting article from") + " favorites database.", Log.ERROR); 362 | } 363 | 364 | log("Saving article " + article.getId() + " to favorites took " + (System.currentTimeMillis() - time) + "ms"); 365 | return true; 366 | } 367 | 368 | /** 369 | * Clears the favorites database. 370 | */ 371 | public void deleteAllFavorites() { 372 | long time = System.currentTimeMillis(); 373 | log("Deleting all favorites..."); 374 | favoriteDatabase.deleteAll(); 375 | log("Deleting all favorites took " + (System.currentTimeMillis() - time) + "ms"); 376 | } 377 | 378 | /** 379 | * Searches the FavoriteDatabase for the specified ID. 380 | * @param id Article ID which to search for. 381 | * @return {@code true} if database contains it or {@code false} if otherwise 382 | * or database not yet started. 383 | */ 384 | public boolean containsFavorite(int id) { 385 | if(favoriteDatabase == null) 386 | return false; 387 | 388 | return favoriteDatabase.contains(id); 389 | } 390 | 391 | /** 392 | * Clears {@link Downloader} cache. 393 | * @return {@code true} if successfully cleared or {@code false} if otherwise. 394 | */ 395 | public boolean clearCache() { 396 | return downloader.clearCache(); 397 | } 398 | 399 | /** 400 | * Clears all PkRSS data including Downloader cache, all articles in Favorite 401 | * Database, and all articles marked as read. 402 | * @return {@code true} if successfully cleared or {@code false} if otherwise. 403 | */ 404 | public boolean clearData() { 405 | if (!downloader.clearCache()) 406 | return false; 407 | deleteAllFavorites(); 408 | markAllRead(false); 409 | return true; 410 | } 411 | 412 | /** 413 | * @return The {@link Map} used for storing page states. 414 | */ 415 | protected Map getPageTracker() { 416 | return pageTracker; 417 | } 418 | 419 | /** 420 | * Inserts the passed list into the article map database. 421 | * This will be cleared once the instance dies. 422 | * @param url URL to associate this list with. 423 | * @param newArticles Article list to store. 424 | */ 425 | private void insert(String url, List

newArticles) { 426 | if(!articleMap.containsKey(url)) 427 | articleMap.put(url, new ArrayList
()); 428 | 429 | List
articleList = articleMap.get(url); 430 | articleList.addAll(newArticles); 431 | 432 | log("New size for " + url + " is " + articleList.size()); 433 | } 434 | 435 | /** 436 | * Asynchronously loads read data. 437 | */ 438 | private void getRead() { 439 | // Execute on background thread as we don't know how large this is 440 | new AsyncTask() { 441 | @Override 442 | protected Void doInBackground(Void... params) { 443 | int size = mPrefs.getInt("READ_ARRAY_SIZE", 0); 444 | boolean value; 445 | if(size < 1) 446 | return null; 447 | 448 | for(int i = 0, key; i < size; i++) { 449 | key = mPrefs.getInt("READ_ARRAY_KEY_" + i, 0); 450 | value = mPrefs.getBoolean("READ_ARRAY_VALUE_" + i, false); 451 | 452 | readList.put(key, value); 453 | } 454 | return null; 455 | } 456 | }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 457 | } 458 | 459 | /** 460 | * Asynchronously saves read data. 461 | */ 462 | private void writeRead() { 463 | // Execute on background thread as we don't know how large this is 464 | new AsyncTask() { 465 | @Override 466 | protected Void doInBackground(Void... params) { 467 | // Get editor & basic variables 468 | SharedPreferences.Editor editor = mPrefs.edit(); 469 | int size = readList.size(); 470 | boolean value; 471 | 472 | editor.putInt("READ_ARRAY_SIZE", size); 473 | for(int i = 0, key; i < size; i++) { 474 | key = readList.keyAt(i); 475 | value = readList.get(key); 476 | 477 | editor.putInt("READ_ARRAY_KEY_" + i, key); 478 | editor.putBoolean("READ_ARRAY_VALUE_" + i, value); 479 | } 480 | editor.commit(); 481 | 482 | return null; 483 | } 484 | }.executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 485 | } 486 | 487 | protected final void log(String message) { 488 | log(TAG, message, Log.DEBUG); 489 | } 490 | 491 | protected final void log(String tag, String message) { 492 | log(tag, message, Log.DEBUG); 493 | } 494 | 495 | protected final void log(String message, int type) { 496 | log(TAG, message, type); 497 | } 498 | 499 | protected final void log(String tag, String message, int type) { 500 | if(!loggingEnabled) 501 | return; 502 | 503 | switch(type) { 504 | case Log.VERBOSE: 505 | Log.v(tag, message); 506 | break; 507 | case Log.DEBUG: 508 | Log.d(tag, message); 509 | break; 510 | case Log.INFO: 511 | Log.i(tag, message); 512 | break; 513 | case Log.WARN: 514 | Log.w(tag, message); 515 | break; 516 | case Log.ERROR: 517 | Log.e(tag, message); 518 | break; 519 | case Log.ASSERT: 520 | default: 521 | Log.wtf(tag, message); 522 | break; 523 | } 524 | } 525 | 526 | /** Fluent API for creating {@link PkRSS} instances. */ 527 | public static class Builder { 528 | private final Context context; 529 | private CallbackHandler handler; 530 | private Downloader downloader; 531 | private Parser parser; 532 | private boolean loggingEnabled; 533 | private boolean safe; 534 | 535 | /** 536 | * Start building a new {@link PkRSS} instance. 537 | */ 538 | public Builder(Context context) { 539 | if (context == null) 540 | throw new IllegalArgumentException("Context must not be null!"); 541 | 542 | this.context = context.getApplicationContext(); 543 | this.handler = new CallbackHandler(new Handler(Looper.getMainLooper())); 544 | } 545 | 546 | /** 547 | * Specifies the thread for which to execute callbacks on. 548 | * Setting this to null executes callbacks on the same thread in which the request was called.
549 | * Default: new Handler(Lopper.getMainLooper()) 550 | */ 551 | public Builder handler(Handler handler) { 552 | this.handler = new CallbackHandler(handler); 553 | return this; 554 | } 555 | 556 | /** 557 | * Specifies a custom {@link Downloader} Object for which to load data with.
558 | * Default: Either {@link DefaultDownloader} or {@link OkHttpDownloader} (See more... {@link Utils#createDefaultDownloader(Context)}) 559 | */ 560 | public Builder downloader(Downloader downloader) { 561 | this.downloader = downloader; 562 | return this; 563 | } 564 | 565 | /** 566 | * Specifies a custom {@link Parser} Object for which to parse data with.
567 | * Default: {@link Rss2Parser} 568 | */ 569 | public Builder parser(Parser parser) { 570 | this.parser = parser; 571 | return this; 572 | } 573 | 574 | /** 575 | * Toggle whether debug logging is enabled. 576 | * Default: {@code false} 577 | */ 578 | public Builder loggingEnabled(boolean enabled) { 579 | this.loggingEnabled = enabled; 580 | return this; 581 | } 582 | 583 | /** 584 | * Toggle whether or not to automatically catch exceptions from callbacks. 585 | * Default: {@code false} 586 | */ 587 | public Builder safe(boolean safe) { 588 | this.safe = safe; 589 | return this; 590 | } 591 | 592 | /** 593 | * Creates a {@link PkRSS} instance. 594 | */ 595 | public PkRSS build() { 596 | if(parser == null) 597 | parser = new Rss2Parser(); 598 | 599 | if(downloader == null) 600 | downloader = Utils.createDefaultDownloader(context); 601 | 602 | if(handler == null) 603 | handler = new CallbackHandler(); 604 | 605 | return new PkRSS(context, handler, downloader, parser, loggingEnabled, safe); 606 | } 607 | } 608 | } -------------------------------------------------------------------------------- /pkrss/src/main/java/com/pkmmte/pkrss/Article.java: -------------------------------------------------------------------------------- 1 | package com.pkmmte.pkrss; 2 | 3 | import android.net.Uri; 4 | import android.os.Bundle; 5 | import android.os.Parcel; 6 | import android.os.Parcelable; 7 | 8 | import java.io.ByteArrayInputStream; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.IOException; 11 | import java.io.ObjectInput; 12 | import java.io.ObjectInputStream; 13 | import java.io.ObjectOutputStream; 14 | import java.io.Serializable; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Vector; 18 | 19 | /** 20 | * Main Article class for storing all parsed/downloaded data. 21 | */ 22 | public class Article implements Parcelable { 23 | private Bundle extras; 24 | private List tags; 25 | private Vector mediaContentVec; 26 | private Uri source; 27 | private Uri image; 28 | private String title; 29 | private String description; 30 | private String content; 31 | private String comments; 32 | private String author; 33 | private long date; 34 | private int id; 35 | private Enclosure enclosure; 36 | 37 | public Article() { 38 | this.extras = new Bundle(); 39 | this.tags = new ArrayList(); 40 | this.mediaContentVec = null; 41 | this.source = null; 42 | this.image = null; 43 | this.title = null; 44 | this.description = null; 45 | this.content = null; 46 | this.comments = null; 47 | this.author = null; 48 | this.date = 0; 49 | this.id = -1; 50 | this.enclosure = null; 51 | } 52 | 53 | public Article(Bundle extras, List tags, Vector mediaContent, Uri source, 54 | Uri image, String title, String description, String content, String comments, 55 | String author, long date, int id) { 56 | this.extras = extras == null ? new Bundle() : extras; 57 | this.tags = tags == null ? new ArrayList() : tags; 58 | this.mediaContentVec = mediaContent == null ? new Vector() : mediaContent; 59 | this.source = source; 60 | this.image = image; 61 | this.title = title; 62 | this.description = description; 63 | this.content = content; 64 | this.comments = comments; 65 | this.author = author; 66 | this.date = date; 67 | this.id = id; 68 | } 69 | 70 | /** 71 | * Returns the entry with the given key as an object. 72 | * @param key A String key. 73 | * @return An Object, or null. 74 | */ 75 | public Object getExtra(String key) { 76 | return extras.get(key); 77 | } 78 | 79 | /** 80 | * Returns the value associated with the given key. 81 | * @param key A String key. 82 | * @return A String value, or null. 83 | */ 84 | public String getExtraString(String key) { 85 | return extras.getString(key); 86 | } 87 | 88 | /** 89 | * Returns the value associated with the given key. 90 | * @param key A String key. 91 | * @return An int value, or null. 92 | */ 93 | public int getExtraInt(String key) { 94 | return extras.getInt(key); 95 | } 96 | 97 | /** 98 | * Returns the value associated with the given key. 99 | * @param key A String key. 100 | * @return A boolean value, or null. 101 | */ 102 | public boolean getExtraBoolean(String key) { 103 | return extras.getBoolean(key); 104 | } 105 | 106 | /** 107 | * @return The Bundle object used to store all extra values. 108 | */ 109 | public Bundle getExtras() { 110 | return extras; 111 | } 112 | 113 | /** 114 | * Inserts a given value into a Bundle associated with this Article instance. 115 | * @param key A String key. 116 | * @param value Value to insert. 117 | */ 118 | public Article putExtra(String key, String value) { 119 | this.extras.putString(key, value); 120 | return this; 121 | } 122 | 123 | /** 124 | * Inserts a given value into a Bundle associated with this Article instance. 125 | * @param key A String key. 126 | * @param value Value to insert. 127 | */ 128 | public Article putExtra(String key, int value) { 129 | this.extras.putInt(key, value); 130 | return this; 131 | } 132 | 133 | /** 134 | * Inserts a given value into a Bundle associated with this Article instance. 135 | * @param key A String key. 136 | * @param value Value to insert. 137 | */ 138 | public Article putExtra(String key, boolean value) { 139 | this.extras.putBoolean(key, value); 140 | return this; 141 | } 142 | 143 | /** 144 | * Replaces all article extras with the passed extras Bundle. 145 | * @param extras Bundle to override the current one with. 146 | */ 147 | public Article setExtras(Bundle extras) { 148 | this.extras = extras; 149 | return this; 150 | } 151 | 152 | /** 153 | * @return A String List containing this article's tags. 154 | */ 155 | public List getTags() { 156 | return tags; 157 | } 158 | 159 | /** 160 | * Overrides the current list of tags with a new one. 161 | * @param tags Tag List to override the current one with. 162 | */ 163 | public Article setTags(List tags) { 164 | this.tags = tags; 165 | return this; 166 | } 167 | 168 | /** 169 | * @return A list with media content pairs 170 | */ 171 | public Vector getMediaContent() { 172 | return mediaContentVec; 173 | } 174 | 175 | /** 176 | * Overrides the current list of media content with a new one. 177 | * @param mediaContentVec Media content list to override the current one 178 | */ 179 | public Article setMediaContent(Vector mediaContentVec) { 180 | this.mediaContentVec = mediaContentVec; 181 | return this; 182 | } 183 | 184 | /** 185 | * Adds a single media content item to the list 186 | * @param mediaContent The media content object to add 187 | */ 188 | public Article addMediaContent(MediaContent mediaContent) { 189 | if(mediaContent == null) 190 | return this; 191 | 192 | if(this.mediaContentVec == null) 193 | this.mediaContentVec = new Vector<>(); 194 | 195 | this.mediaContentVec.add(mediaContent); 196 | 197 | return this; 198 | } 199 | 200 | /** 201 | * Removes a single media content item from the list 202 | * @param mediaContent The media content object to remove 203 | */ 204 | public Article removeMediaContent(MediaContent mediaContent) { 205 | if(mediaContent == null || this.mediaContentVec == null) 206 | return this; 207 | 208 | this.mediaContentVec.remove(mediaContent); 209 | 210 | return this; 211 | } 212 | 213 | /** 214 | * Adds a new tag to this article. 215 | * @param tag String value to add as a tag. 216 | */ 217 | public Article setNewTag(String tag) { 218 | this.tags.add(tag); 219 | return this; 220 | } 221 | 222 | /** 223 | * @return A Uri containing the source address of this article. 224 | * (In other words, a link to this article) 225 | */ 226 | public Uri getSource() { 227 | return source; 228 | } 229 | 230 | /** 231 | * Sets the source of the article. 232 | * @param source Simple Uri object referencing the source. It may be a URL or null. 233 | */ 234 | public Article setSource(Uri source) { 235 | this.source = source; 236 | return this; 237 | } 238 | 239 | /** 240 | * @return A Uri containing the main image source. It may be a URL, resource, asset, or null. 241 | */ 242 | public Uri getImage() { 243 | return image; 244 | } 245 | 246 | /** 247 | * Sets the main article image. 248 | * @param image Simple Uri object referencing the image source. It may be a URL, resource, asset, or null. 249 | */ 250 | public Article setImage(Uri image) { 251 | this.image = image; 252 | return this; 253 | } 254 | 255 | /** 256 | * @return String containing the article's title. May be null. 257 | */ 258 | public String getTitle() { 259 | return title; 260 | } 261 | 262 | /** 263 | * Sets the article's title. 264 | * @param title String containing this article's title. 265 | */ 266 | public Article setTitle(String title) { 267 | this.title = title; 268 | return this; 269 | } 270 | 271 | /** 272 | * @return String containing the article's description. May be null. 273 | */ 274 | public String getDescription() { 275 | return description; 276 | } 277 | 278 | /** 279 | * Sets the article's description. 280 | * @param description String containing this article's description. 281 | */ 282 | public Article setDescription(String description) { 283 | this.description = description; 284 | return this; 285 | } 286 | 287 | /** 288 | * @return String containing the article's content. May be null. 289 | */ 290 | public String getContent() { 291 | return content; 292 | } 293 | 294 | /** 295 | * Sets the article's content. 296 | * @param content String containing this article's content. 297 | */ 298 | public Article setContent(String content) { 299 | this.content = content; 300 | return this; 301 | } 302 | 303 | /** 304 | * @return String containing the source to this article's comments. May be null. 305 | */ 306 | public String getComments() { 307 | return comments; 308 | } 309 | 310 | /** 311 | * Sets the article's comment source. 312 | * @param comments String containing the source to this article's comments. 313 | */ 314 | public Article setComments(String comments) { 315 | this.comments = comments; 316 | return this; 317 | } 318 | 319 | /** 320 | * @return Enclosure which contains the URL, length, and mime type of the article's enclosure 321 | */ 322 | public Enclosure getEnclosure() { 323 | return this.enclosure; 324 | } 325 | 326 | /** 327 | * Sets the article's enclosure. 328 | * @param enclosure Enclosure which contains the URL, length, and mime type 329 | */ 330 | public Article setEnclosure(Enclosure enclosure) { 331 | this.enclosure = enclosure; 332 | return this; 333 | } 334 | 335 | /** 336 | * @return String containing the article's author. May be null. 337 | */ 338 | public String getAuthor() { 339 | return author; 340 | } 341 | 342 | /** 343 | * Sets the article's author. 344 | * @param author String containing this article's author. 345 | */ 346 | public Article setAuthor(String author) { 347 | this.author = author; 348 | return this; 349 | } 350 | 351 | /** 352 | * @return long containing the article's raw date. 353 | */ 354 | public long getDate() { 355 | return date; 356 | } 357 | 358 | /** 359 | * Sets the article's raw date. 360 | * @param date String containing this article's raw date. Usually expressed in milliseconds. 361 | */ 362 | public Article setDate(long date) { 363 | this.date = date; 364 | return this; 365 | } 366 | 367 | /** 368 | * @return long containing the article's id. IDs are normally generated based on the properties' hashcodes combined. 369 | */ 370 | public int getId() { 371 | return id; 372 | } 373 | 374 | /** 375 | * Sets the article's id. 376 | * @param id Long containing this article's id. Be sure to provide a unique id as it will be used for indexing. 377 | */ 378 | public Article setId(int id) { 379 | this.id = id; 380 | return this; 381 | } 382 | 383 | /** 384 | * Looks up the read index for this article's id. 385 | * @return {@code true} if this article's id has been marked as read, 386 | * {@code false} if otherwise or instance has not yet been created. 387 | */ 388 | public boolean isRead() { 389 | return PkRSS.getInstance() == null ? false : PkRSS.getInstance().isRead(id); 390 | } 391 | 392 | /** 393 | * Adds this article's id to the read index. 394 | * @return {@code true} if successful, {@code false} if otherwise. 395 | */ 396 | public boolean markRead() { 397 | return markRead(true); 398 | } 399 | 400 | /** 401 | * Adds this article's id to the read index. 402 | * @param read Whether or not to mark it as read. 403 | * @return {@code true} if successful, {@code false} if otherwise. 404 | */ 405 | public boolean markRead(boolean read) { 406 | if (PkRSS.getInstance() == null) return false; 407 | 408 | PkRSS.getInstance().markRead(id, read); 409 | return true; 410 | } 411 | 412 | /** 413 | * Looks up the favorite database for this article's id. 414 | * @return {@code true} if this article is in the favorites database, 415 | * {@code false} if otherwise, instance has not yet been created, or an error occurred. 416 | */ 417 | public boolean isFavorite() { 418 | return PkRSS.getInstance() == null ? false : PkRSS.getInstance().containsFavorite(id); 419 | } 420 | 421 | /** 422 | * Adds this article into the favorites database. 423 | * @return {@code true} if successful, {@code false} if otherwise. 424 | */ 425 | public boolean saveFavorite() { 426 | return saveFavorite(true); 427 | } 428 | 429 | /** 430 | * Adds this article into the favorites database. 431 | * @param favorite Whether to add it or remove it. 432 | * @return {@code true} if successful, {@code false} if otherwise. 433 | */ 434 | public boolean saveFavorite(boolean favorite) { 435 | if (PkRSS.getInstance() == null) return false; 436 | 437 | return PkRSS.getInstance().saveFavorite(this, favorite); 438 | } 439 | 440 | /** 441 | * Similar to {@link #toString()} but ommits the content and description. 442 | * @return A small-ish String describing this article's properties. 443 | */ 444 | public String toShortString() { 445 | return "Article{" + 446 | "extras=" + extras + 447 | ", tags=" + tags + 448 | ", source=" + source + 449 | ", image=" + image + 450 | ", title='" + title + '\'' + 451 | ", comments='" + comments + '\'' + 452 | ", author='" + author + '\'' + 453 | ", date=" + date + 454 | ", id=" + id + 455 | '}'; 456 | } 457 | 458 | @Override 459 | public String toString() { 460 | return "Article{" + 461 | "extras=" + extras + 462 | ", tags=" + tags + 463 | ", source=" + source + 464 | ", image=" + image + 465 | ", title='" + title + '\'' + 466 | ", description='" + description + '\'' + 467 | ", content='" + content + '\'' + 468 | ", comments='" + comments + '\'' + 469 | ", author='" + author + '\'' + 470 | ", date=" + date + 471 | ", id=" + id + 472 | '}'; 473 | } 474 | 475 | @Override 476 | public boolean equals(Object o) { 477 | if (this == o) return true; 478 | if (o == null || !(o instanceof Article)) return false; 479 | 480 | Article article = (Article) o; 481 | 482 | if (date != article.date) return false; 483 | if (id != article.id) return false; 484 | if (author != null ? !author.equals(article.author) : article.author != null) return false; 485 | if (comments != null ? !comments.equals(article.comments) : article.comments != null) return false; 486 | if (content != null ? !content.equals(article.content) : article.content != null) return false; 487 | if (description != null ? !description.equals(article.description) : article.description != null) return false; 488 | if (!extras.equals(article.extras)) return false; 489 | if (image != null ? !image.equals(article.image) : article.image != null) return false; 490 | if (source != null ? !source.equals(article.source) : article.source != null) return false; 491 | if (!tags.equals(article.tags)) return false; 492 | if (title != null ? !title.equals(article.title) : article.title != null) return false; 493 | 494 | return true; 495 | } 496 | 497 | @Override 498 | public int hashCode() { 499 | int result = extras.hashCode(); 500 | result = 31 * result + tags.hashCode(); 501 | result = 31 * result + (source != null ? source.hashCode() : 0); 502 | result = 31 * result + (image != null ? image.hashCode() : 0); 503 | result = 31 * result + (title != null ? title.hashCode() : 0); 504 | result = 31 * result + (description != null ? description.hashCode() : 0); 505 | result = 31 * result + (content != null ? content.hashCode() : 0); 506 | result = 31 * result + (comments != null ? comments.hashCode() : 0); 507 | result = 31 * result + (author != null ? author.hashCode() : 0); 508 | result = 31 * result + (int) (date ^ (date >>> 32)); 509 | result = 31 * result + id; 510 | return result; 511 | } 512 | 513 | protected Article(Parcel in) { 514 | extras = in.readBundle(); 515 | if (in.readByte() == 0x01) { 516 | tags = new ArrayList(); 517 | in.readList(tags, String.class.getClassLoader()); 518 | } 519 | else { 520 | tags = null; 521 | } 522 | source = (Uri) in.readValue(Uri.class.getClassLoader()); 523 | image = (Uri) in.readValue(Uri.class.getClassLoader()); 524 | title = in.readString(); 525 | description = in.readString(); 526 | content = in.readString(); 527 | author = in.readString(); 528 | date = in.readLong(); 529 | id = in.readInt(); 530 | } 531 | 532 | @Override 533 | public int describeContents() { 534 | return 0; 535 | } 536 | 537 | @Override 538 | public void writeToParcel(Parcel dest, int flags) { 539 | dest.writeBundle(extras); 540 | if (tags == null) { 541 | dest.writeByte((byte) (0x00)); 542 | } 543 | else { 544 | dest.writeByte((byte) (0x01)); 545 | dest.writeList(tags); 546 | } 547 | dest.writeValue(source); 548 | dest.writeValue(image); 549 | dest.writeString(title); 550 | dest.writeString(description); 551 | dest.writeString(content); 552 | dest.writeString(author); 553 | dest.writeLong(date); 554 | dest.writeInt(id); 555 | } 556 | 557 | public static final Creator
CREATOR = new Creator
() { 558 | @Override 559 | public Article createFromParcel(Parcel in) { 560 | return new Article(in); 561 | } 562 | 563 | @Override 564 | public Article[] newArray(int size) { 565 | return new Article[size]; 566 | } 567 | }; 568 | 569 | public static class MediaContent implements Parcelable, Serializable { 570 | private String url; 571 | private int fileSize; 572 | private String type; 573 | private String medium; 574 | private boolean isDefault; 575 | private String expression; 576 | private int bitrate; 577 | private float framerate; 578 | private float samplingrate; 579 | private int channels; 580 | private long duration; 581 | private int height; 582 | private int width; 583 | private String lang; 584 | 585 | public MediaContent() { 586 | } 587 | 588 | public MediaContent(String url, int fileSize, String type, String medium, boolean isDefault, 589 | String expression, int bitrate, int framerate, int samplingrate, int channels, 590 | int duration, int height, int width, String lang) { 591 | this.url = url; 592 | this.fileSize = fileSize; 593 | this.type = type; 594 | this.medium = medium; 595 | this.isDefault = isDefault; 596 | this.expression = expression; 597 | this.bitrate = bitrate; 598 | this.framerate = framerate; 599 | this.samplingrate = samplingrate; 600 | this.channels = channels; 601 | this.duration = duration; 602 | this.height = height; 603 | this.width = width; 604 | this.lang = lang; 605 | } 606 | 607 | /** 608 | * Returns media content's url. This is a required attribute. 609 | * @return Content url 610 | */ 611 | public String getUrl() { 612 | return url; 613 | } 614 | 615 | /** 616 | * Sets media content's url. This is a required attribute. 617 | * @param url Url of the content 618 | */ 619 | public void setUrl(String url) { 620 | this.url = url; 621 | } 622 | 623 | /** 624 | * Returns media content's type. This is an optional attribute. 625 | * @return Type of the content. May be null 626 | */ 627 | public String getType() { 628 | return type; 629 | } 630 | 631 | /** 632 | * Sets media content's type. This is an optional attribute. 633 | * @param type Type of the content 634 | */ 635 | public void setType(String type) { 636 | this.type = type; 637 | } 638 | 639 | /** 640 | * Gets media file size. This is an optional attribute. 641 | * @return Filesize of media item 642 | */ 643 | public int getFileSize() { 644 | return fileSize; 645 | } 646 | 647 | /** 648 | * Sets medias filesize. This is an optional attribute. 649 | * @param fileSize Filesize of media 650 | */ 651 | public void setFileSize(int fileSize) { 652 | this.fileSize = fileSize; 653 | } 654 | 655 | /** 656 | * Gets the medium of the object. This is an optional attribute. 657 | * @return Medium of media 658 | */ 659 | public String getMedium() { 660 | return medium; 661 | } 662 | 663 | /** 664 | * Sets the medium. This is an optional attribute. 665 | * @param medium The medium of the media object 666 | */ 667 | public void setMedium(String medium) { 668 | this.medium = medium; 669 | } 670 | 671 | /** 672 | * Gets whether this media is the default one. This is an optional attribute. 673 | * @return True if default 674 | */ 675 | public boolean isDefault() { 676 | return isDefault; 677 | } 678 | 679 | /** 680 | * Sets whether this media is the default media. This is an optional attribute. 681 | * @param isDefault The default state 682 | */ 683 | public void setIsDefault(boolean isDefault) { 684 | this.isDefault = isDefault; 685 | } 686 | 687 | /** 688 | * Gets the expression of the media object. This is an optional attribute. 689 | * @return The expression of the media object. 690 | */ 691 | public String getExpression() { 692 | return expression; 693 | } 694 | 695 | /** 696 | * Sets the expression of the media object. This is an optional attribute. 697 | * @param expression The bitrate of the object 698 | */ 699 | public void setExpression(String expression) { 700 | this.expression = expression; 701 | } 702 | 703 | /** 704 | * Gets the bitrate of the media object. This is an optional attribute. 705 | * @return The bitrate of the media object. 706 | */ 707 | public int getBitrate() { 708 | return bitrate; 709 | } 710 | 711 | /** 712 | * Sets the bitrate of the media object. This is an optional attribute. 713 | * @param bitrate The bitrate of the object 714 | */ 715 | public void setBitrate(int bitrate) { 716 | this.bitrate = bitrate; 717 | } 718 | 719 | /** 720 | * Gets the framerate of the media object. This is an optional attribute. 721 | * @return The framerate of the media object. 722 | */ 723 | public float getFramerate() { 724 | return framerate; 725 | } 726 | 727 | /** 728 | * Sets the framerate of the media object. This is an optional attribute. 729 | * @param framerate The framerate of the object 730 | */ 731 | public void setFramerate(float framerate) { 732 | this.framerate = framerate; 733 | } 734 | 735 | /** 736 | * Gets the samplingrate of the media object. This is an optional attribute. 737 | * @return The samplingrate of the media object. 738 | */ 739 | public float getSamplingrate() { 740 | return samplingrate; 741 | } 742 | 743 | /** 744 | * Sets the samplingrate of the media object. This is an optional attribute. 745 | * @param samplingrate The samplingrate of the object 746 | */ 747 | public void setSamplingrate(float samplingrate) { 748 | this.samplingrate = samplingrate; 749 | } 750 | 751 | /** 752 | * Gets the channels of the media object. This is an optional attribute. 753 | * @return The channels of the media object. 754 | */ 755 | public int getChannels() { 756 | return channels; 757 | } 758 | 759 | /** 760 | * Sets the channels of the media object. This is an optional attribute. 761 | * @param channels The channel count of the object 762 | */ 763 | public void setChannels(int channels) { 764 | this.channels = channels; 765 | } 766 | 767 | /** 768 | * Gets the duration of the media object. This is an optional attribute. 769 | * @return The duration of the media object. 770 | */ 771 | public long getDuration() { 772 | return duration; 773 | } 774 | 775 | /** 776 | * Sets the duration of the media object. This is an optional attribute. 777 | * @param duration The width of the object 778 | */ 779 | public void setDuration(long duration) { 780 | this.duration = duration; 781 | } 782 | 783 | /** 784 | * Gets the heigth of the media object. This is an optional attribute. 785 | * @return The heigth of the media object. 786 | */ 787 | public int getHeight() { 788 | return height; 789 | } 790 | 791 | /** 792 | * Sets the height of the media object. This is an optional attribute. 793 | * @param height The width of the object 794 | */ 795 | public void setHeight(int height) { 796 | this.height = height; 797 | } 798 | 799 | /** 800 | * Gets the width of the media object. This is an optional attribute. 801 | * @return The width of the media object. 802 | */ 803 | public int getWidth() { 804 | return width; 805 | } 806 | 807 | /** 808 | * Sets the width of the media object. This is an optional attribute. 809 | * @param width The width of the object 810 | */ 811 | public void setWidth(int width) { 812 | this.width = width; 813 | } 814 | 815 | /** 816 | * Gets the default language code according to RFC 3066 817 | * This is an optional attribute. 818 | * @return The language code 819 | */ 820 | public String getLang() { 821 | return lang; 822 | } 823 | 824 | /** 825 | * Sets the medias primary language. Possible codes are detailed in RFC 3066. 826 | * This is an optional attribute. 827 | * @param lang The language code 828 | */ 829 | public void setLang(String lang) { 830 | this.lang = lang; 831 | } 832 | 833 | @Override public int describeContents() { 834 | return 0; 835 | } 836 | 837 | @Override public void writeToParcel(Parcel dest, int flags) { 838 | dest.writeString(this.url); 839 | dest.writeInt(this.fileSize); 840 | dest.writeString(this.type); 841 | dest.writeString(this.medium); 842 | dest.writeByte(isDefault ? (byte) 1 : (byte) 0); 843 | dest.writeString(this.expression); 844 | dest.writeInt(this.bitrate); 845 | dest.writeFloat(this.framerate); 846 | dest.writeFloat(this.samplingrate); 847 | dest.writeInt(this.channels); 848 | dest.writeLong(this.duration); 849 | dest.writeInt(this.height); 850 | dest.writeInt(this.width); 851 | dest.writeString(this.lang); 852 | } 853 | 854 | protected MediaContent(Parcel in) { 855 | this.url = in.readString(); 856 | this.fileSize = in.readInt(); 857 | this.type = in.readString(); 858 | this.medium = in.readString(); 859 | this.isDefault = in.readByte() != 0; 860 | this.expression = in.readString(); 861 | this.bitrate = in.readInt(); 862 | this.framerate = in.readFloat(); 863 | this.samplingrate = in.readFloat(); 864 | this.channels = in.readInt(); 865 | this.duration = in.readLong(); 866 | this.height = in.readInt(); 867 | this.width = in.readInt(); 868 | this.lang = in.readString(); 869 | } 870 | 871 | public static final Creator CREATOR = new Creator() { 872 | public MediaContent createFromParcel(Parcel source) { 873 | return new MediaContent(source); 874 | } 875 | 876 | public MediaContent[] newArray(int size) { 877 | return new MediaContent[size]; 878 | } 879 | }; 880 | 881 | public static Vector fromByteArray(byte[] byteArray) { 882 | try { 883 | ByteArrayInputStream bais = new ByteArrayInputStream (byteArray); 884 | ObjectInput in = new ObjectInputStream(bais); 885 | 886 | @SuppressWarnings("unchecked") 887 | Vector vec = (Vector < MediaContent >)in.readObject(); 888 | 889 | return vec; 890 | } catch (IOException e) { 891 | e.printStackTrace(); 892 | return null; 893 | } catch (ClassNotFoundException e) { 894 | e.printStackTrace(); 895 | return null; 896 | } 897 | } 898 | 899 | public static byte[] toByteArray(Vector contentArray) { 900 | byte[] array = null; 901 | try { 902 | ByteArrayOutputStream byteArrayStream = new ByteArrayOutputStream(); 903 | ObjectOutputStream objectOutputStream = new ObjectOutputStream( byteArrayStream ); 904 | objectOutputStream.writeObject(contentArray); 905 | objectOutputStream.close(); 906 | array = byteArrayStream.toByteArray(); 907 | } catch (IOException e) { 908 | e.printStackTrace(); 909 | } 910 | return array; 911 | } 912 | } 913 | } --------------------------------------------------------------------------------