commit e760f10124b28bf87c5ec976cf88ef1d59997d71 Author: danijoo Date: Sat Nov 7 20:05:17 2015 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee9258c --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +Thumbs.db#built application files +*.apk +*.ap_ + +# files for the dex VM +*.dex + +# Java class files +*.class + +# generated files +bin/ +gen/ + +# Local configuration file (sdk path, etc) +local.properties + +# Windows thumbnail db +Thumbs.db + +# OSX files +.DS_Store + +# Eclipse project files +.classpath +.project + +# Android Studio +.idea +.idea/workspace.xml +.gradle +build/ +captures/ + +*.iws +*.iml + +# calabash +screenshot* +.irb-history +test_servers + +# crashlytics +crashlytics-build.properties +com_crashlytics_export_strings.xml + +gradle.properties + +screenshot* + +# sbt +00-gradle-generated.sbt +target/ +project/project + + diff --git a/XYZReader/build.gradle b/XYZReader/build.gradle new file mode 100644 index 0000000..dfbe82b --- /dev/null +++ b/XYZReader/build.gradle @@ -0,0 +1,34 @@ +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.1.0' + } +} +apply plugin: 'com.android.application' + +repositories { + mavenCentral() +} + +android { + compileSdkVersion 22 + buildToolsVersion "22.0.1" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 22 + } +} + +dependencies { + compile 'com.android.support:support-v4:22.0.0' + compile 'com.android.support:support-v13:22.0.0' + compile 'com.android.support:appcompat-v7:22.0.0' + compile 'com.android.support:palette-v7:22.0.0' + compile 'com.android.support:recyclerview-v7:22.0.0' + compile 'com.android.support:cardview-v7:22.0.0' + compile 'com.squareup.okhttp:okhttp:1.1.0' + compile files('libs/volley.jar') +} diff --git a/XYZReader/build.sbt b/XYZReader/build.sbt new file mode 100644 index 0000000..dba0d18 --- /dev/null +++ b/XYZReader/build.sbt @@ -0,0 +1 @@ +protifySettings \ No newline at end of file diff --git a/XYZReader/gradle/wrapper/gradle-wrapper.jar b/XYZReader/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8c0fb64 Binary files /dev/null and b/XYZReader/gradle/wrapper/gradle-wrapper.jar differ diff --git a/XYZReader/gradle/wrapper/gradle-wrapper.properties b/XYZReader/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0c71e76 --- /dev/null +++ b/XYZReader/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Apr 10 15:27:10 PDT 2013 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/XYZReader/gradlew b/XYZReader/gradlew new file mode 100644 index 0000000..91a7e26 --- /dev/null +++ b/XYZReader/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/XYZReader/gradlew.bat b/XYZReader/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/XYZReader/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/XYZReader/libs/volley.jar b/XYZReader/libs/volley.jar new file mode 100644 index 0000000..4f8134e Binary files /dev/null and b/XYZReader/libs/volley.jar differ diff --git a/XYZReader/src/main/AndroidManifest.xml b/XYZReader/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5a720d3 --- /dev/null +++ b/XYZReader/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XYZReader/src/main/assets/Rosario-Regular.ttf b/XYZReader/src/main/assets/Rosario-Regular.ttf new file mode 100644 index 0000000..0646b7b Binary files /dev/null and b/XYZReader/src/main/assets/Rosario-Regular.ttf differ diff --git a/XYZReader/src/main/java/com/example/xyzreader/data/ArticleLoader.java b/XYZReader/src/main/java/com/example/xyzreader/data/ArticleLoader.java new file mode 100644 index 0000000..69d4829 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/data/ArticleLoader.java @@ -0,0 +1,44 @@ +package com.example.xyzreader.data; + +import android.content.Context; +import android.content.CursorLoader; +import android.net.Uri; + +/** + * Helper for loading a list of articles or a single article. + */ +public class ArticleLoader extends CursorLoader { + public static ArticleLoader newAllArticlesInstance(Context context) { + return new ArticleLoader(context, ItemsContract.Items.buildDirUri()); + } + + public static ArticleLoader newInstanceForItemId(Context context, long itemId) { + return new ArticleLoader(context, ItemsContract.Items.buildItemUri(itemId)); + } + + private ArticleLoader(Context context, Uri uri) { + super(context, uri, Query.PROJECTION, null, null, ItemsContract.Items.DEFAULT_SORT); + } + + public interface Query { + String[] PROJECTION = { + ItemsContract.Items._ID, + ItemsContract.Items.TITLE, + ItemsContract.Items.PUBLISHED_DATE, + ItemsContract.Items.AUTHOR, + ItemsContract.Items.THUMB_URL, + ItemsContract.Items.PHOTO_URL, + ItemsContract.Items.ASPECT_RATIO, + ItemsContract.Items.BODY, + }; + + int _ID = 0; + int TITLE = 1; + int PUBLISHED_DATE = 2; + int AUTHOR = 3; + int THUMB_URL = 4; + int PHOTO_URL = 5; + int ASPECT_RATIO = 6; + int BODY = 7; + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/data/ItemsContract.java b/XYZReader/src/main/java/com/example/xyzreader/data/ItemsContract.java new file mode 100644 index 0000000..517b40f --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/data/ItemsContract.java @@ -0,0 +1,54 @@ +package com.example.xyzreader.data; + +import android.net.Uri; + +public class ItemsContract { + public static final String CONTENT_AUTHORITY = "com.example.xyzreader"; + public static final Uri BASE_URI = Uri.parse("content://com.example.xyzreader"); + + interface ItemsColumns { + /** Type: INTEGER PRIMARY KEY AUTOINCREMENT */ + String _ID = "_id"; + /** Type: TEXT */ + String SERVER_ID = "server_id"; + /** Type: TEXT NOT NULL */ + String TITLE = "title"; + /** Type: TEXT NOT NULL */ + String AUTHOR = "author"; + /** Type: TEXT NOT NULL */ + String BODY = "body"; + /** Type: TEXT NOT NULL */ + String THUMB_URL = "thumb_url"; + /** Type: TEXT NOT NULL */ + String PHOTO_URL = "photo_url"; + /** Type: REAL NOT NULL DEFAULT 1.5 */ + String ASPECT_RATIO = "aspect_ratio"; + /** Type: INTEGER NOT NULL DEFAULT 0 */ + String PUBLISHED_DATE = "published_date"; + } + + public static class Items implements ItemsColumns { + public static final String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.com.example.xyzreader.items"; + public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/vnd.com.example.xyzreader.items"; + + public static final String DEFAULT_SORT = PUBLISHED_DATE + " DESC"; + + /** Matches: /items/ */ + public static Uri buildDirUri() { + return BASE_URI.buildUpon().appendPath("items").build(); + } + + /** Matches: /items/[_id]/ */ + public static Uri buildItemUri(long _id) { + return BASE_URI.buildUpon().appendPath("items").appendPath(Long.toString(_id)).build(); + } + + /** Read item ID item detail URI. */ + public static long getItemId(Uri itemUri) { + return Long.parseLong(itemUri.getPathSegments().get(1)); + } + } + + private ItemsContract() { + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/data/ItemsDatabase.java b/XYZReader/src/main/java/com/example/xyzreader/data/ItemsDatabase.java new file mode 100644 index 0000000..831b581 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/data/ItemsDatabase.java @@ -0,0 +1,37 @@ +package com.example.xyzreader.data; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import static com.example.xyzreader.data.ItemsProvider.Tables; + +public class ItemsDatabase extends SQLiteOpenHelper { + private static final String DATABASE_NAME = "xyzreader.db"; + private static final int DATABASE_VERSION = 1; + + public ItemsDatabase(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL("CREATE TABLE " + Tables.ITEMS + " (" + + ItemsContract.ItemsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ItemsContract.ItemsColumns.SERVER_ID + " TEXT," + + ItemsContract.ItemsColumns.TITLE + " TEXT NOT NULL," + + ItemsContract.ItemsColumns.AUTHOR + " TEXT NOT NULL," + + ItemsContract.ItemsColumns.BODY + " TEXT NOT NULL," + + ItemsContract.ItemsColumns.THUMB_URL + " TEXT NOT NULL," + + ItemsContract.ItemsColumns.PHOTO_URL + " TEXT NOT NULL," + + ItemsContract.ItemsColumns.ASPECT_RATIO + " REAL NOT NULL DEFAULT 1.5," + + ItemsContract.ItemsColumns.PUBLISHED_DATE + " INTEGER NOT NULL DEFAULT 0" + + ")" ); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS " + Tables.ITEMS); + onCreate(db); + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/data/ItemsProvider.java b/XYZReader/src/main/java/com/example/xyzreader/data/ItemsProvider.java new file mode 100644 index 0000000..3579112 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/data/ItemsProvider.java @@ -0,0 +1,143 @@ + +package com.example.xyzreader.data; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentProviderResult; +import android.content.ContentValues; +import android.content.OperationApplicationException; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; + +import java.util.ArrayList; +import java.util.List; + +public class ItemsProvider extends ContentProvider { + private SQLiteOpenHelper mOpenHelper; + + interface Tables { + String ITEMS = "items"; + } + + private static final int ITEMS = 0; + private static final int ITEMS__ID = 1; + + private static final UriMatcher sUriMatcher = buildUriMatcher(); + + private static UriMatcher buildUriMatcher() { + final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); + final String authority = ItemsContract.CONTENT_AUTHORITY; + matcher.addURI(authority, "items", ITEMS); + matcher.addURI(authority, "items/#", ITEMS__ID); + return matcher; + } + + @Override + public boolean onCreate() { + mOpenHelper = new ItemsDatabase(getContext()); + return true; + } + + @Override + public String getType(Uri uri) { + final int match = sUriMatcher.match(uri); + switch (match) { + case ITEMS: + return ItemsContract.Items.CONTENT_TYPE; + case ITEMS__ID: + return ItemsContract.Items.CONTENT_ITEM_TYPE; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + final SelectionBuilder builder = buildSelection(uri); + Cursor cursor = builder.where(selection, selectionArgs).query(db, projection, sortOrder); + if (cursor != null) { + cursor.setNotificationUri(getContext().getContentResolver(), uri); + } + return cursor; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + switch (match) { + case ITEMS: { + final long _id = db.insertOrThrow(Tables.ITEMS, null, values); + getContext().getContentResolver().notifyChange(uri, null); + return ItemsContract.Items.buildItemUri(_id); + } + default: { + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final SelectionBuilder builder = buildSelection(uri); + getContext().getContentResolver().notifyChange(uri, null); + return builder.where(selection, selectionArgs).update(db, values); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final SelectionBuilder builder = buildSelection(uri); + getContext().getContentResolver().notifyChange(uri, null); + return builder.where(selection, selectionArgs).delete(db); + } + + private SelectionBuilder buildSelection(Uri uri) { + final SelectionBuilder builder = new SelectionBuilder(); + final int match = sUriMatcher.match(uri); + return buildSelection(uri, match, builder); + } + + private SelectionBuilder buildSelection(Uri uri, int match, SelectionBuilder builder) { + final List paths = uri.getPathSegments(); + switch (match) { + case ITEMS: { + return builder.table(Tables.ITEMS); + } + case ITEMS__ID: { + final String _id = paths.get(1); + return builder.table(Tables.ITEMS).where(ItemsContract.Items._ID + "=?", _id); + } + default: { + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + } + + /** + * Apply the given set of {@link ContentProviderOperation}, executing inside + * a {@link SQLiteDatabase} transaction. All changes will be rolled back if + * any single one fails. + */ + public ContentProviderResult[] applyBatch(ArrayList operations) + throws OperationApplicationException { + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + db.beginTransaction(); + try { + final int numOperations = operations.size(); + final ContentProviderResult[] results = new ContentProviderResult[numOperations]; + for (int i = 0; i < numOperations; i++) { + results[i] = operations.get(i).apply(this, results, i); + } + db.setTransactionSuccessful(); + return results; + } finally { + db.endTransaction(); + } + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/data/SelectionBuilder.java b/XYZReader/src/main/java/com/example/xyzreader/data/SelectionBuilder.java new file mode 100644 index 0000000..e205a74 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/data/SelectionBuilder.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Modifications: + * -Imported from AOSP frameworks/base/core/java/com/android/internal/content + * -Changed package name + */ + +package com.example.xyzreader.data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; + +/** + * Helper for building selection clauses for {@link SQLiteDatabase}. Each + * appended clause is combined using {@code AND}. This class is not + * thread safe. + */ +public class SelectionBuilder { + private String mTable = null; + private HashMap mProjectionMap; + private StringBuilder mSelection; + private ArrayList mSelectionArgs; + + /** + * Reset any internal state, allowing this builder to be recycled. + */ + public SelectionBuilder reset() { + mTable = null; + if (mProjectionMap != null) { + mProjectionMap.clear(); + } + if (mSelection != null) { + mSelection.setLength(0); + } + if (mSelectionArgs != null) { + mSelectionArgs.clear(); + } + return this; + } + + /** + * Append the given selection clause to the internal state. Each clause is + * surrounded with parenthesis and combined using {@code AND}. + */ + public SelectionBuilder where(String selection, String... selectionArgs) { + if (TextUtils.isEmpty(selection)) { + if (selectionArgs != null && selectionArgs.length > 0) { + throw new IllegalArgumentException( + "Valid selection required when including arguments="); + } + + // Shortcut when clause is empty + return this; + } + + ensureSelection(selection.length()); + if (mSelection.length() > 0) { + mSelection.append(" AND "); + } + + mSelection.append("(").append(selection).append(")"); + if (selectionArgs != null) { + ensureSelectionArgs(); + for (String arg : selectionArgs) { + mSelectionArgs.add(arg); + } + } + + return this; + } + + public SelectionBuilder table(String table) { + mTable = table; + return this; + } + + private void assertTable() { + if (mTable == null) { + throw new IllegalStateException("Table not specified"); + } + } + + private void ensureProjectionMap() { + if (mProjectionMap == null) { + mProjectionMap = new HashMap(); + } + } + + private void ensureSelection(int lengthHint) { + if (mSelection == null) { + mSelection = new StringBuilder(lengthHint + 8); + } + } + + private void ensureSelectionArgs() { + if (mSelectionArgs == null) { + mSelectionArgs = new ArrayList(); + } + } + + public SelectionBuilder mapToTable(String column, String table) { + ensureProjectionMap(); + mProjectionMap.put(column, table + "." + column); + return this; + } + + public SelectionBuilder map(String fromColumn, String toClause) { + ensureProjectionMap(); + mProjectionMap.put(fromColumn, toClause + " AS " + fromColumn); + return this; + } + + /** + * Return selection string for current internal state. + * + * @see #getSelectionArgs() + */ + public String getSelection() { + if (mSelection != null) { + return mSelection.toString(); + } else { + return null; + } + } + + /** + * Return selection arguments for current internal state. + * + * @see #getSelection() + */ + public String[] getSelectionArgs() { + if (mSelectionArgs != null) { + return mSelectionArgs.toArray(new String[mSelectionArgs.size()]); + } else { + return null; + } + } + + private void mapColumns(String[] columns) { + if (mProjectionMap == null) return; + for (int i = 0; i < columns.length; i++) { + final String target = mProjectionMap.get(columns[i]); + if (target != null) { + columns[i] = target; + } + } + } + + @Override + public String toString() { + return "SelectionBuilder[table=" + mTable + ", selection=" + getSelection() + + ", selectionArgs=" + Arrays.toString(getSelectionArgs()) + "]"; + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) { + return query(db, columns, null, null, orderBy, null); + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String groupBy, + String having, String orderBy, String limit) { + assertTable(); + if (columns != null) mapColumns(columns); + return db.query(mTable, columns, getSelection(), getSelectionArgs(), groupBy, having, + orderBy, limit); + } + + /** + * Execute update using the current internal state as {@code WHERE} clause. + */ + public int update(SQLiteDatabase db, ContentValues values) { + assertTable(); + return db.update(mTable, values, getSelection(), getSelectionArgs()); + } + + /** + * Execute delete using the current internal state as {@code WHERE} clause. + */ + public int delete(SQLiteDatabase db) { + assertTable(); + return db.delete(mTable, getSelection(), getSelectionArgs()); + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/data/UpdaterService.java b/XYZReader/src/main/java/com/example/xyzreader/data/UpdaterService.java new file mode 100644 index 0000000..8444d33 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/data/UpdaterService.java @@ -0,0 +1,87 @@ +package com.example.xyzreader.data; + +import android.app.IntentService; +import android.content.ContentProviderOperation; +import android.content.ContentValues; +import android.content.Intent; +import android.content.OperationApplicationException; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.Uri; +import android.os.RemoteException; +import android.text.format.Time; +import android.util.Log; + +import com.example.xyzreader.remote.RemoteEndpointUtil; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; + +public class UpdaterService extends IntentService { + private static final String TAG = "UpdaterService"; + + public static final String BROADCAST_ACTION_STATE_CHANGE + = "com.example.xyzreader.intent.action.STATE_CHANGE"; + public static final String EXTRA_REFRESHING + = "com.example.xyzreader.intent.extra.REFRESHING"; + + public UpdaterService() { + super(TAG); + } + + @Override + protected void onHandleIntent(Intent intent) { + Time time = new Time(); + + ConnectivityManager cm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE); + NetworkInfo ni = cm.getActiveNetworkInfo(); + if (ni == null || !ni.isConnected()) { + Log.w(TAG, "Not online, not refreshing."); + return; + } + + sendStickyBroadcast( + new Intent(BROADCAST_ACTION_STATE_CHANGE).putExtra(EXTRA_REFRESHING, true)); + + // Don't even inspect the intent, we only do one thing, and that's fetch content. + ArrayList cpo = new ArrayList(); + + Uri dirUri = ItemsContract.Items.buildDirUri(); + + // Delete all items + cpo.add(ContentProviderOperation.newDelete(dirUri).build()); + + try { + JSONArray array = RemoteEndpointUtil.fetchJsonArray(); + if (array == null) { + throw new JSONException("Invalid parsed item array" ); + } + + for (int i = 0; i < array.length(); i++) { + ContentValues values = new ContentValues(); + JSONObject object = array.getJSONObject(i); + values.put(ItemsContract.Items.SERVER_ID, object.getString("id" )); + values.put(ItemsContract.Items.AUTHOR, object.getString("author" )); + values.put(ItemsContract.Items.TITLE, object.getString("title" )); + values.put(ItemsContract.Items.BODY, object.getString("body" )); + values.put(ItemsContract.Items.THUMB_URL, object.getString("thumb" )); + values.put(ItemsContract.Items.PHOTO_URL, object.getString("photo" )); + values.put(ItemsContract.Items.ASPECT_RATIO, object.getString("aspect_ratio" )); + time.parse3339(object.getString("published_date")); + values.put(ItemsContract.Items.PUBLISHED_DATE, time.toMillis(false)); + cpo.add(ContentProviderOperation.newInsert(dirUri).withValues(values).build()); + } + + getContentResolver().applyBatch(ItemsContract.CONTENT_AUTHORITY, cpo); + + } catch (JSONException | RemoteException | OperationApplicationException e) { + Log.e(TAG, "Error updating content.", e); + } + + sendStickyBroadcast( + new Intent(BROADCAST_ACTION_STATE_CHANGE).putExtra(EXTRA_REFRESHING, false)); + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/remote/Config.java b/XYZReader/src/main/java/com/example/xyzreader/remote/Config.java new file mode 100644 index 0000000..b87a75d --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/remote/Config.java @@ -0,0 +1,19 @@ +package com.example.xyzreader.remote; + +import java.net.MalformedURLException; +import java.net.URL; + +public class Config { + public static final URL BASE_URL; + + static { + URL url = null; + try { + url = new URL("https://dl.dropboxusercontent.com/u/231329/xyzreader_data/data.json" ); + } catch (MalformedURLException ignored) { + // TODO: throw a real error + } + + BASE_URL = url; + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/remote/RemoteEndpointUtil.java b/XYZReader/src/main/java/com/example/xyzreader/remote/RemoteEndpointUtil.java new file mode 100644 index 0000000..a5a5df7 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/remote/RemoteEndpointUtil.java @@ -0,0 +1,72 @@ +package com.example.xyzreader.remote; + +import android.util.Log; + +import com.squareup.okhttp.OkHttpClient; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONTokener; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; + +public class RemoteEndpointUtil { + private static final String TAG = "RemoteEndpointUtil"; + + private RemoteEndpointUtil() { + } + + public static JSONArray fetchJsonArray() { + String itemsJson = null; + try { + itemsJson = fetchPlainText(Config.BASE_URL); + } catch (IOException e) { + Log.e(TAG, "Error fetching items JSON", e); + return null; + } + + // Parse JSON + try { + JSONTokener tokener = new JSONTokener(itemsJson); + Object val = tokener.nextValue(); + if (!(val instanceof JSONArray)) { + throw new JSONException("Expected JSONArray"); + } + return (JSONArray) val; + } catch (JSONException e) { + Log.e(TAG, "Error parsing items JSON", e); + } + + return null; + } + + static String fetchPlainText(URL url) throws IOException { + return new String(fetch(url), "UTF-8" ); + } + + static byte[] fetch(URL url) throws IOException { + InputStream in = null; + + try { + OkHttpClient client = new OkHttpClient(); + HttpURLConnection conn = client.open(url); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + in = conn.getInputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = in.read(buffer)) > 0) { + out.write(buffer, 0, bytesRead); + } + return out.toByteArray(); + + } finally { + if (in != null) { + in.close(); + } + } + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleDetailActivity.java b/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleDetailActivity.java new file mode 100644 index 0000000..0de370f --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleDetailActivity.java @@ -0,0 +1,180 @@ +package com.example.xyzreader.ui; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.LoaderManager; +import android.content.Loader; +import android.database.Cursor; +import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.os.Bundle; +import android.support.v13.app.FragmentStatePagerAdapter; +import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBarActivity; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; + +import com.example.xyzreader.R; +import com.example.xyzreader.data.ArticleLoader; +import com.example.xyzreader.data.ItemsContract; + +/** + * An activity representing a single Article detail screen, letting you swipe between articles. + */ +public class ArticleDetailActivity extends ActionBarActivity + implements LoaderManager.LoaderCallbacks { + + private Cursor mCursor; + private long mStartId; + + private long mSelectedItemId; + private int mSelectedItemUpButtonFloor = Integer.MAX_VALUE; + private int mTopInset; + + private ViewPager mPager; + private MyPagerAdapter mPagerAdapter; + private View mUpButtonContainer; + private View mUpButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + getWindow().getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | + View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + setContentView(R.layout.activity_article_detail); + + getLoaderManager().initLoader(0, null, this); + + mPagerAdapter = new MyPagerAdapter(getFragmentManager()); + mPager = (ViewPager) findViewById(R.id.pager); + mPager.setAdapter(mPagerAdapter); + mPager.setPageMargin((int) TypedValue + .applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics())); + mPager.setPageMarginDrawable(new ColorDrawable(0x22000000)); + + mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { + @Override + public void onPageScrollStateChanged(int state) { + super.onPageScrollStateChanged(state); + mUpButton.animate() + .alpha((state == ViewPager.SCROLL_STATE_IDLE) ? 1f : 0f) + .setDuration(300); + } + + @Override + public void onPageSelected(int position) { + if (mCursor != null) { + mCursor.moveToPosition(position); + } + mSelectedItemId = mCursor.getLong(ArticleLoader.Query._ID); + updateUpButtonPosition(); + } + }); + + mUpButtonContainer = findViewById(R.id.up_container); + + mUpButton = findViewById(R.id.action_up); + mUpButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + onSupportNavigateUp(); + } + }); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mUpButtonContainer.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { + view.onApplyWindowInsets(windowInsets); + mTopInset = windowInsets.getSystemWindowInsetTop(); + mUpButtonContainer.setTranslationY(mTopInset); + updateUpButtonPosition(); + return windowInsets; + } + }); + } + + if (savedInstanceState == null) { + if (getIntent() != null && getIntent().getData() != null) { + mStartId = ItemsContract.Items.getItemId(getIntent().getData()); + mSelectedItemId = mStartId; + } + } + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return ArticleLoader.newAllArticlesInstance(this); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + mCursor = cursor; + mPagerAdapter.notifyDataSetChanged(); + + // Select the start ID + if (mStartId > 0) { + mCursor.moveToFirst(); + // TODO: optimize + while (!mCursor.isAfterLast()) { + if (mCursor.getLong(ArticleLoader.Query._ID) == mStartId) { + final int position = mCursor.getPosition(); + mPager.setCurrentItem(position, false); + break; + } + mCursor.moveToNext(); + } + mStartId = 0; + } + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + mCursor = null; + mPagerAdapter.notifyDataSetChanged(); + } + + public void onUpButtonFloorChanged(long itemId, ArticleDetailFragment fragment) { + if (itemId == mSelectedItemId) { + mSelectedItemUpButtonFloor = fragment.getUpButtonFloor(); + updateUpButtonPosition(); + } + } + + private void updateUpButtonPosition() { + int upButtonNormalBottom = mTopInset + mUpButton.getHeight(); + mUpButton.setTranslationY(Math.min(mSelectedItemUpButtonFloor - upButtonNormalBottom, 0)); + } + + private class MyPagerAdapter extends FragmentStatePagerAdapter { + public MyPagerAdapter(FragmentManager fm) { + super(fm); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + super.setPrimaryItem(container, position, object); + ArticleDetailFragment fragment = (ArticleDetailFragment) object; + if (fragment != null) { + mSelectedItemUpButtonFloor = fragment.getUpButtonFloor(); + updateUpButtonPosition(); + } + } + + @Override + public Fragment getItem(int position) { + mCursor.moveToPosition(position); + return ArticleDetailFragment.newInstance(mCursor.getLong(ArticleLoader.Query._ID)); + } + + @Override + public int getCount() { + return (mCursor != null) ? mCursor.getCount() : 0; + } + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleDetailFragment.java b/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleDetailFragment.java new file mode 100644 index 0000000..8542fbc --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleDetailFragment.java @@ -0,0 +1,268 @@ +package com.example.xyzreader.ui; + +import android.app.Fragment; +import android.app.LoaderManager; +import android.content.Intent; +import android.content.Loader; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.support.v4.app.ShareCompat; +import android.support.v7.graphics.Palette; +import android.text.Html; +import android.text.format.DateUtils; +import android.text.method.LinkMovementMethod; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.volley.VolleyError; +import com.android.volley.toolbox.ImageLoader; +import com.example.xyzreader.R; +import com.example.xyzreader.data.ArticleLoader; + +/** + * A fragment representing a single Article detail screen. This fragment is + * either contained in a {@link ArticleListActivity} in two-pane mode (on + * tablets) or a {@link ArticleDetailActivity} on handsets. + */ +public class ArticleDetailFragment extends Fragment implements + LoaderManager.LoaderCallbacks { + private static final String TAG = "ArticleDetailFragment"; + + public static final String ARG_ITEM_ID = "item_id"; + private static final float PARALLAX_FACTOR = 1.25f; + + private Cursor mCursor; + private long mItemId; + private View mRootView; + private int mMutedColor = 0xFF333333; + private ObservableScrollView mScrollView; + private DrawInsetsFrameLayout mDrawInsetsFrameLayout; + private ColorDrawable mStatusBarColorDrawable; + + private int mTopInset; + private View mPhotoContainerView; + private ImageView mPhotoView; + private int mScrollY; + private boolean mIsCard = false; + private int mStatusBarFullOpacityBottom; + + /** + * Mandatory empty constructor for the fragment manager to instantiate the + * fragment (e.g. upon screen orientation changes). + */ + public ArticleDetailFragment() { + } + + public static ArticleDetailFragment newInstance(long itemId) { + Bundle arguments = new Bundle(); + arguments.putLong(ARG_ITEM_ID, itemId); + ArticleDetailFragment fragment = new ArticleDetailFragment(); + fragment.setArguments(arguments); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (getArguments().containsKey(ARG_ITEM_ID)) { + mItemId = getArguments().getLong(ARG_ITEM_ID); + } + + mIsCard = getResources().getBoolean(R.bool.detail_is_card); + mStatusBarFullOpacityBottom = getResources().getDimensionPixelSize( + R.dimen.detail_card_top_margin); + setHasOptionsMenu(true); + } + + public ArticleDetailActivity getActivityCast() { + return (ArticleDetailActivity) getActivity(); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + // In support library r8, calling initLoader for a fragment in a FragmentPagerAdapter in + // the fragment's onCreate may cause the same LoaderManager to be dealt to multiple + // fragments because their mIndex is -1 (haven't been added to the activity yet). Thus, + // we do this in onActivityCreated. + getLoaderManager().initLoader(0, null, this); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + mRootView = inflater.inflate(R.layout.fragment_article_detail, container, false); + mDrawInsetsFrameLayout = (DrawInsetsFrameLayout) + mRootView.findViewById(R.id.draw_insets_frame_layout); + mDrawInsetsFrameLayout.setOnInsetsCallback(new DrawInsetsFrameLayout.OnInsetsCallback() { + @Override + public void onInsetsChanged(Rect insets) { + mTopInset = insets.top; + } + }); + + mScrollView = (ObservableScrollView) mRootView.findViewById(R.id.scrollview); + mScrollView.setCallbacks(new ObservableScrollView.Callbacks() { + @Override + public void onScrollChanged() { + mScrollY = mScrollView.getScrollY(); + getActivityCast().onUpButtonFloorChanged(mItemId, ArticleDetailFragment.this); + mPhotoContainerView.setTranslationY((int) (mScrollY - mScrollY / PARALLAX_FACTOR)); + updateStatusBar(); + } + }); + + mPhotoView = (ImageView) mRootView.findViewById(R.id.photo); + mPhotoContainerView = mRootView.findViewById(R.id.photo_container); + + mStatusBarColorDrawable = new ColorDrawable(0); + + mRootView.findViewById(R.id.share_fab).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(Intent.createChooser(ShareCompat.IntentBuilder.from(getActivity()) + .setType("text/plain") + .setText("Some sample text") + .getIntent(), getString(R.string.action_share))); + } + }); + + bindViews(); + updateStatusBar(); + return mRootView; + } + + private void updateStatusBar() { + int color = 0; + if (mPhotoView != null && mTopInset != 0 && mScrollY > 0) { + float f = progress(mScrollY, + mStatusBarFullOpacityBottom - mTopInset * 3, + mStatusBarFullOpacityBottom - mTopInset); + color = Color.argb((int) (255 * f), + (int) (Color.red(mMutedColor) * 0.9), + (int) (Color.green(mMutedColor) * 0.9), + (int) (Color.blue(mMutedColor) * 0.9)); + } + mStatusBarColorDrawable.setColor(color); + mDrawInsetsFrameLayout.setInsetBackground(mStatusBarColorDrawable); + } + + static float progress(float v, float min, float max) { + return constrain((v - min) / (max - min), 0, 1); + } + + static float constrain(float val, float min, float max) { + if (val < min) { + return min; + } else if (val > max) { + return max; + } else { + return val; + } + } + + private void bindViews() { + if (mRootView == null) { + return; + } + + TextView titleView = (TextView) mRootView.findViewById(R.id.article_title); + TextView bylineView = (TextView) mRootView.findViewById(R.id.article_byline); + bylineView.setMovementMethod(new LinkMovementMethod()); + TextView bodyView = (TextView) mRootView.findViewById(R.id.article_body); + bodyView.setTypeface(Typeface.createFromAsset(getResources().getAssets(), "Rosario-Regular.ttf")); + + if (mCursor != null) { + mRootView.setAlpha(0); + mRootView.setVisibility(View.VISIBLE); + mRootView.animate().alpha(1); + titleView.setText(mCursor.getString(ArticleLoader.Query.TITLE)); + bylineView.setText(Html.fromHtml( + DateUtils.getRelativeTimeSpanString( + mCursor.getLong(ArticleLoader.Query.PUBLISHED_DATE), + System.currentTimeMillis(), DateUtils.HOUR_IN_MILLIS, + DateUtils.FORMAT_ABBREV_ALL).toString() + + " by " + + mCursor.getString(ArticleLoader.Query.AUTHOR) + + "")); + bodyView.setText(Html.fromHtml(mCursor.getString(ArticleLoader.Query.BODY))); + ImageLoaderHelper.getInstance(getActivity()).getImageLoader() + .get(mCursor.getString(ArticleLoader.Query.PHOTO_URL), new ImageLoader.ImageListener() { + @Override + public void onResponse(ImageLoader.ImageContainer imageContainer, boolean b) { + Bitmap bitmap = imageContainer.getBitmap(); + if (bitmap != null) { + Palette p = Palette.generate(bitmap, 12); + mMutedColor = p.getDarkMutedColor(0xFF333333); + mPhotoView.setImageBitmap(imageContainer.getBitmap()); + mRootView.findViewById(R.id.meta_bar) + .setBackgroundColor(mMutedColor); + updateStatusBar(); + } + } + + @Override + public void onErrorResponse(VolleyError volleyError) { + + } + }); + } else { + mRootView.setVisibility(View.GONE); + titleView.setText("N/A"); + bylineView.setText("N/A" ); + bodyView.setText("N/A"); + } + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return ArticleLoader.newInstanceForItemId(getActivity(), mItemId); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + if (!isAdded()) { + if (cursor != null) { + cursor.close(); + } + return; + } + + mCursor = cursor; + if (mCursor != null && !mCursor.moveToFirst()) { + Log.e(TAG, "Error reading item detail cursor"); + mCursor.close(); + mCursor = null; + } + + bindViews(); + } + + @Override + public void onLoaderReset(Loader cursorLoader) { + mCursor = null; + bindViews(); + } + + public int getUpButtonFloor() { + if (mPhotoContainerView == null || mPhotoView.getHeight() == 0) { + return Integer.MAX_VALUE; + } + + // account for parallax + return mIsCard + ? (int) mPhotoContainerView.getTranslationY() + mPhotoView.getHeight() - mScrollY + : mPhotoView.getHeight() - mScrollY; + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleListActivity.java b/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleListActivity.java new file mode 100644 index 0000000..6cca410 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/ArticleListActivity.java @@ -0,0 +1,177 @@ +package com.example.xyzreader.ui; + +import android.app.LoaderManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.Loader; +import android.database.Cursor; +import android.os.Bundle; +import android.support.v4.widget.SwipeRefreshLayout; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.StaggeredGridLayoutManager; +import android.support.v7.widget.Toolbar; +import android.text.format.DateUtils; +import android.util.TypedValue; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.example.xyzreader.R; +import com.example.xyzreader.data.ArticleLoader; +import com.example.xyzreader.data.ItemsContract; +import com.example.xyzreader.data.UpdaterService; + +/** + * An activity representing a list of Articles. This activity has different presentations for + * handset and tablet-size devices. On handsets, the activity presents a list of items, which when + * touched, lead to a {@link ArticleDetailActivity} representing item details. On tablets, the + * activity presents a grid of items as cards. + */ +public class ArticleListActivity extends ActionBarActivity implements + LoaderManager.LoaderCallbacks { + + private Toolbar mToolbar; + private SwipeRefreshLayout mSwipeRefreshLayout; + private RecyclerView mRecyclerView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_article_list); + + mToolbar = (Toolbar) findViewById(R.id.toolbar); + + + final View toolbarContainerView = findViewById(R.id.toolbar_container); + + mSwipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_refresh_layout); + + mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view); + getLoaderManager().initLoader(0, null, this); + + if (savedInstanceState == null) { + refresh(); + } + } + + private void refresh() { + startService(new Intent(this, UpdaterService.class)); + } + + @Override + protected void onStart() { + super.onStart(); + registerReceiver(mRefreshingReceiver, + new IntentFilter(UpdaterService.BROADCAST_ACTION_STATE_CHANGE)); + } + + @Override + protected void onStop() { + super.onStop(); + unregisterReceiver(mRefreshingReceiver); + } + + private boolean mIsRefreshing = false; + + private BroadcastReceiver mRefreshingReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (UpdaterService.BROADCAST_ACTION_STATE_CHANGE.equals(intent.getAction())) { + mIsRefreshing = intent.getBooleanExtra(UpdaterService.EXTRA_REFRESHING, false); + updateRefreshingUI(); + } + } + }; + + private void updateRefreshingUI() { + mSwipeRefreshLayout.setRefreshing(mIsRefreshing); + } + + @Override + public Loader onCreateLoader(int i, Bundle bundle) { + return ArticleLoader.newAllArticlesInstance(this); + } + + @Override + public void onLoadFinished(Loader cursorLoader, Cursor cursor) { + Adapter adapter = new Adapter(cursor); + adapter.setHasStableIds(true); + mRecyclerView.setAdapter(adapter); + int columnCount = getResources().getInteger(R.integer.list_column_count); + StaggeredGridLayoutManager sglm = + new StaggeredGridLayoutManager(columnCount, StaggeredGridLayoutManager.VERTICAL); + mRecyclerView.setLayoutManager(sglm); + } + + @Override + public void onLoaderReset(Loader loader) { + mRecyclerView.setAdapter(null); + } + + private class Adapter extends RecyclerView.Adapter { + private Cursor mCursor; + + public Adapter(Cursor cursor) { + mCursor = cursor; + } + + @Override + public long getItemId(int position) { + mCursor.moveToPosition(position); + return mCursor.getLong(ArticleLoader.Query._ID); + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = getLayoutInflater().inflate(R.layout.list_item_article, parent, false); + final ViewHolder vh = new ViewHolder(view); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + startActivity(new Intent(Intent.ACTION_VIEW, + ItemsContract.Items.buildItemUri(getItemId(vh.getAdapterPosition())))); + } + }); + return vh; + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + mCursor.moveToPosition(position); + holder.titleView.setText(mCursor.getString(ArticleLoader.Query.TITLE)); + holder.subtitleView.setText( + DateUtils.getRelativeTimeSpanString( + mCursor.getLong(ArticleLoader.Query.PUBLISHED_DATE), + System.currentTimeMillis(), DateUtils.HOUR_IN_MILLIS, + DateUtils.FORMAT_ABBREV_ALL).toString() + + " by " + + mCursor.getString(ArticleLoader.Query.AUTHOR)); + holder.thumbnailView.setImageUrl( + mCursor.getString(ArticleLoader.Query.THUMB_URL), + ImageLoaderHelper.getInstance(ArticleListActivity.this).getImageLoader()); + holder.thumbnailView.setAspectRatio(mCursor.getFloat(ArticleLoader.Query.ASPECT_RATIO)); + } + + @Override + public int getItemCount() { + return mCursor.getCount(); + } + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public DynamicHeightNetworkImageView thumbnailView; + public TextView titleView; + public TextView subtitleView; + + public ViewHolder(View view) { + super(view); + thumbnailView = (DynamicHeightNetworkImageView) view.findViewById(R.id.thumbnail); + titleView = (TextView) view.findViewById(R.id.article_title); + subtitleView = (TextView) view.findViewById(R.id.article_subtitle); + } + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/DividerItemDecoration.java b/XYZReader/src/main/java/com/example/xyzreader/ui/DividerItemDecoration.java new file mode 100644 index 0000000..e1cb4cf --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/DividerItemDecoration.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.example.xyzreader.ui; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.View; + +public class DividerItemDecoration extends RecyclerView.ItemDecoration { + + private static final int[] ATTRS = new int[]{ + android.R.attr.listDivider + }; + + public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; + + public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; + + private Drawable mDivider; + + private int mOrientation; + + public DividerItemDecoration(Context context, Drawable divider, int orientation) { + mDivider = divider; + setOrientation(orientation); + } + + public void setOrientation(int orientation) { + if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { + throw new IllegalArgumentException("invalid orientation"); + } + mOrientation = orientation; + } + + @Override + public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + drawVertical(c, parent); + } else { + drawHorizontal(c, parent); + } + } + + public void drawVertical(Canvas c, RecyclerView parent) { + final int left = parent.getPaddingLeft(); + final int right = parent.getWidth() - parent.getPaddingRight(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int top = child.getBottom() + params.bottomMargin; + final int bottom = top + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + public void drawHorizontal(Canvas c, RecyclerView parent) { + final int top = parent.getPaddingTop(); + final int bottom = parent.getHeight() - parent.getPaddingBottom(); + + final int childCount = parent.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = parent.getChildAt(i); + final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child + .getLayoutParams(); + final int left = child.getRight() + params.rightMargin; + final int right = left + mDivider.getIntrinsicHeight(); + mDivider.setBounds(left, top, right, bottom); + mDivider.draw(c); + } + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + if (mOrientation == VERTICAL_LIST) { + outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); + } else { + outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); + } + } +} \ No newline at end of file diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/DrawInsetsFrameLayout.java b/XYZReader/src/main/java/com/example/xyzreader/ui/DrawInsetsFrameLayout.java new file mode 100644 index 0000000..dfe3875 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/DrawInsetsFrameLayout.java @@ -0,0 +1,159 @@ +/* + * Copyright 2014 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.xyzreader.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.AttributeSet; +import android.view.WindowInsets; +import android.widget.FrameLayout; + +import com.example.xyzreader.R; + + +public class DrawInsetsFrameLayout extends FrameLayout { + private Drawable mInsetBackground; + private Drawable mTopInsetBackground; + private Drawable mBottomInsetBackground; + private Drawable mSideInsetBackground; + + private Rect mInsets; + private Rect mTempRect = new Rect(); + private OnInsetsCallback mOnInsetsCallback; + + public DrawInsetsFrameLayout(Context context) { + super(context); + init(context, null, 0); + } + + public DrawInsetsFrameLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0); + } + + public DrawInsetsFrameLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.DrawInsetsFrameLayout, defStyle, 0); + assert a != null; + + mInsetBackground = a.getDrawable(R.styleable.DrawInsetsFrameLayout_insetBackground); + + a.recycle(); + } + + public void setInsetBackground(Drawable insetBackground) { + if (mInsetBackground != null) { + mInsetBackground.setCallback(null); + } + + if (insetBackground != null) { + insetBackground.setCallback(this); + } + + mInsetBackground = insetBackground; + postInvalidateOnAnimation(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + requestApplyInsets(); + } + if (mInsetBackground != null) { + mInsetBackground.setCallback(this); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mInsetBackground != null) { + mInsetBackground.setCallback(null); + } + } + + public void setOnInsetsCallback(OnInsetsCallback onInsetsCallback) { + mOnInsetsCallback = onInsetsCallback; + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + insets = super.onApplyWindowInsets(insets); + mInsets = new Rect( + insets.getSystemWindowInsetLeft(), + insets.getSystemWindowInsetTop(), + insets.getSystemWindowInsetRight(), + insets.getSystemWindowInsetBottom()); + setWillNotDraw(false); + postInvalidateOnAnimation(); + if (mOnInsetsCallback != null) { + mOnInsetsCallback.onInsetsChanged(mInsets); + } + return insets; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + int width = getWidth(); + int height = getHeight(); + + if (mInsets != null) { + // Top + mTempRect.set(0, 0, width, mInsets.top); + if (mInsetBackground != null) { + mInsetBackground.setBounds(mTempRect); + mInsetBackground.draw(canvas); + } + + // Bottom + mTempRect.set(0, height - mInsets.bottom, width, height); + if (mInsetBackground != null) { + mInsetBackground.setBounds(mTempRect); + mInsetBackground.draw(canvas); + } + + // Left + mTempRect.set(0, mInsets.top, mInsets.left, height - mInsets.bottom); + if (mInsetBackground != null) { + mInsetBackground.setBounds(mTempRect); + mInsetBackground.draw(canvas); + } + + // Right + mTempRect.set(width - mInsets.right, mInsets.top, width, height - mInsets.bottom); + if (mInsetBackground != null) { + mInsetBackground.setBounds(mTempRect); + mInsetBackground.draw(canvas); + } + } + } + + public static interface OnInsetsCallback { + public void onInsetsChanged(Rect insets); + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/DynamicHeightNetworkImageView.java b/XYZReader/src/main/java/com/example/xyzreader/ui/DynamicHeightNetworkImageView.java new file mode 100644 index 0000000..50d0f64 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/DynamicHeightNetworkImageView.java @@ -0,0 +1,34 @@ +package com.example.xyzreader.ui; + +import android.content.Context; +import android.util.AttributeSet; + +import com.android.volley.toolbox.NetworkImageView; + +public class DynamicHeightNetworkImageView extends NetworkImageView { + private float mAspectRatio = 1.5f; + + public DynamicHeightNetworkImageView(Context context) { + super(context); + } + + public DynamicHeightNetworkImageView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public DynamicHeightNetworkImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + public void setAspectRatio(float aspectRatio) { + mAspectRatio = aspectRatio; + requestLayout(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int measuredWidth = getMeasuredWidth(); + setMeasuredDimension(measuredWidth, (int) (measuredWidth / mAspectRatio)); + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/ImageLoaderHelper.java b/XYZReader/src/main/java/com/example/xyzreader/ui/ImageLoaderHelper.java new file mode 100644 index 0000000..e6f34b8 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/ImageLoaderHelper.java @@ -0,0 +1,44 @@ +package com.example.xyzreader.ui; + +import android.content.Context; +import android.graphics.Bitmap; +import android.support.v4.util.LruCache; + +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.Volley; + +public class ImageLoaderHelper { + private static ImageLoaderHelper sInstance; + + public static ImageLoaderHelper getInstance(Context context) { + if (sInstance == null) { + sInstance = new ImageLoaderHelper(context.getApplicationContext()); + } + + return sInstance; + } + + private final LruCache mImageCache = new LruCache(20); + private ImageLoader mImageLoader; + + private ImageLoaderHelper(Context applicationContext) { + RequestQueue queue = Volley.newRequestQueue(applicationContext); + ImageLoader.ImageCache imageCache = new ImageLoader.ImageCache() { + @Override + public void putBitmap(String key, Bitmap value) { + mImageCache.put(key, value); + } + + @Override + public Bitmap getBitmap(String key) { + return mImageCache.get(key); + } + }; + mImageLoader = new ImageLoader(queue, imageCache); + } + + public ImageLoader getImageLoader() { + return mImageLoader; + } +} diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/MaxWidthLinearLayout.java b/XYZReader/src/main/java/com/example/xyzreader/ui/MaxWidthLinearLayout.java new file mode 100644 index 0000000..0c50d38 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/MaxWidthLinearLayout.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.xyzreader.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +/** + * A simple {@link LinearLayout} subclass that has a maxWidth + */ +public class MaxWidthLinearLayout extends LinearLayout { + private static final int[] ATTRS = { + android.R.attr.maxWidth + }; + + private int mMaxWidth = Integer.MAX_VALUE; + + public MaxWidthLinearLayout(Context context) { + super(context); + init(context, null, 0, 0); + } + + public MaxWidthLinearLayout(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, 0); + } + + public MaxWidthLinearLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle, 0); + } + + public MaxWidthLinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, defStyleRes); + } + + public void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + mMaxWidth = a.getLayoutDimension(0, Integer.MAX_VALUE); + a.recycle(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int newSpecWidth = Math.min(MeasureSpec.getSize(widthMeasureSpec), mMaxWidth); + widthMeasureSpec = MeasureSpec.makeMeasureSpec(newSpecWidth, MeasureSpec.getMode(widthMeasureSpec)); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} \ No newline at end of file diff --git a/XYZReader/src/main/java/com/example/xyzreader/ui/ObservableScrollView.java b/XYZReader/src/main/java/com/example/xyzreader/ui/ObservableScrollView.java new file mode 100644 index 0000000..990fb91 --- /dev/null +++ b/XYZReader/src/main/java/com/example/xyzreader/ui/ObservableScrollView.java @@ -0,0 +1,65 @@ +/* + * Copyright 2012 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.xyzreader.ui; + +import android.content.Context; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.widget.ScrollView; + +/** + * A custom ScrollView that can accept a scroll listener. + */ +public class ObservableScrollView extends ScrollView { + private Callbacks mCallbacks; + + public ObservableScrollView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + if (mCallbacks != null) { + mCallbacks.onScrollChanged(); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + int scrollY = getScrollY(); + // hack to call onScrollChanged on screen rotate + if (scrollY > 0 && mCallbacks != null) { + mCallbacks.onScrollChanged(); + } + } + + @Override + public int computeVerticalScrollRange() { + return super.computeVerticalScrollRange(); + } + + public void setCallbacks(Callbacks listener) { + mCallbacks = listener; + } + + public static interface Callbacks { + public void onScrollChanged(); + } +} \ No newline at end of file diff --git a/XYZReader/src/main/res/anim-v21/fab_state_list_anim.xml b/XYZReader/src/main/res/anim-v21/fab_state_list_anim.xml new file mode 100644 index 0000000..41590d4 --- /dev/null +++ b/XYZReader/src/main/res/anim-v21/fab_state_list_anim.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + diff --git a/XYZReader/src/main/res/drawable-nodpi/empty_detail.png b/XYZReader/src/main/res/drawable-nodpi/empty_detail.png new file mode 100644 index 0000000..5dded1d Binary files /dev/null and b/XYZReader/src/main/res/drawable-nodpi/empty_detail.png differ diff --git a/XYZReader/src/main/res/drawable-v21/white_selectable_item_background_circle.xml b/XYZReader/src/main/res/drawable-v21/white_selectable_item_background_circle.xml new file mode 100644 index 0000000..1ee4c8a --- /dev/null +++ b/XYZReader/src/main/res/drawable-v21/white_selectable_item_background_circle.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/XYZReader/src/main/res/drawable-xxhdpi/ic_arrow_back.png b/XYZReader/src/main/res/drawable-xxhdpi/ic_arrow_back.png new file mode 100644 index 0000000..746d775 Binary files /dev/null and b/XYZReader/src/main/res/drawable-xxhdpi/ic_arrow_back.png differ diff --git a/XYZReader/src/main/res/drawable-xxhdpi/ic_share.png b/XYZReader/src/main/res/drawable-xxhdpi/ic_share.png new file mode 100644 index 0000000..a35b3cd Binary files /dev/null and b/XYZReader/src/main/res/drawable-xxhdpi/ic_share.png differ diff --git a/XYZReader/src/main/res/drawable-xxhdpi/logo.png b/XYZReader/src/main/res/drawable-xxhdpi/logo.png new file mode 100644 index 0000000..77bc3a0 Binary files /dev/null and b/XYZReader/src/main/res/drawable-xxhdpi/logo.png differ diff --git a/XYZReader/src/main/res/drawable/add_fab_background.xml b/XYZReader/src/main/res/drawable/add_fab_background.xml new file mode 100644 index 0000000..2162af9 --- /dev/null +++ b/XYZReader/src/main/res/drawable/add_fab_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/XYZReader/src/main/res/drawable/padded_divider.xml b/XYZReader/src/main/res/drawable/padded_divider.xml new file mode 100644 index 0000000..53054cf --- /dev/null +++ b/XYZReader/src/main/res/drawable/padded_divider.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/XYZReader/src/main/res/drawable/photo_background_protection.xml b/XYZReader/src/main/res/drawable/photo_background_protection.xml new file mode 100644 index 0000000..1230f50 --- /dev/null +++ b/XYZReader/src/main/res/drawable/photo_background_protection.xml @@ -0,0 +1,9 @@ + + + diff --git a/XYZReader/src/main/res/drawable/white_selectable_item_background_circle.xml b/XYZReader/src/main/res/drawable/white_selectable_item_background_circle.xml new file mode 100644 index 0000000..3fcfa1f --- /dev/null +++ b/XYZReader/src/main/res/drawable/white_selectable_item_background_circle.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/XYZReader/src/main/res/layout/activity_article_detail.xml b/XYZReader/src/main/res/layout/activity_article_detail.xml new file mode 100644 index 0000000..6151b44 --- /dev/null +++ b/XYZReader/src/main/res/layout/activity_article_detail.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/XYZReader/src/main/res/layout/activity_article_list.xml b/XYZReader/src/main/res/layout/activity_article_list.xml new file mode 100644 index 0000000..9259aac --- /dev/null +++ b/XYZReader/src/main/res/layout/activity_article_list.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/XYZReader/src/main/res/layout/fragment_article_detail.xml b/XYZReader/src/main/res/layout/fragment_article_detail.xml new file mode 100644 index 0000000..e5daf05 --- /dev/null +++ b/XYZReader/src/main/res/layout/fragment_article_detail.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/XYZReader/src/main/res/layout/list_item_article.xml b/XYZReader/src/main/res/layout/list_item_article.xml new file mode 100644 index 0000000..4f4c434 --- /dev/null +++ b/XYZReader/src/main/res/layout/list_item_article.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + diff --git a/XYZReader/src/main/res/menu/main.xml b/XYZReader/src/main/res/menu/main.xml new file mode 100644 index 0000000..0b74a02 --- /dev/null +++ b/XYZReader/src/main/res/menu/main.xml @@ -0,0 +1,5 @@ + + + diff --git a/XYZReader/src/main/res/mipmap-hdpi/ic_launcher.png b/XYZReader/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..f775b9e Binary files /dev/null and b/XYZReader/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/XYZReader/src/main/res/mipmap-mdpi/ic_launcher.png b/XYZReader/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..5df4609 Binary files /dev/null and b/XYZReader/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/XYZReader/src/main/res/mipmap-xhdpi/ic_launcher.png b/XYZReader/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..379cb5f Binary files /dev/null and b/XYZReader/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/XYZReader/src/main/res/mipmap-xxhdpi/ic_launcher.png b/XYZReader/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..302142e Binary files /dev/null and b/XYZReader/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/XYZReader/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/XYZReader/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..c72629f Binary files /dev/null and b/XYZReader/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/XYZReader/src/main/res/values-land/macro_layout.xml b/XYZReader/src/main/res/values-land/macro_layout.xml new file mode 100644 index 0000000..1ca1bbf --- /dev/null +++ b/XYZReader/src/main/res/values-land/macro_layout.xml @@ -0,0 +1,5 @@ + + + 224dp + 112dp + \ No newline at end of file diff --git a/XYZReader/src/main/res/values-sw600dp-land/macro_layout.xml b/XYZReader/src/main/res/values-sw600dp-land/macro_layout.xml new file mode 100644 index 0000000..6ae7b5f --- /dev/null +++ b/XYZReader/src/main/res/values-sw600dp-land/macro_layout.xml @@ -0,0 +1,7 @@ + + + true + + 600dp + 256dp + diff --git a/XYZReader/src/main/res/values-sw600dp/dimens.xml b/XYZReader/src/main/res/values-sw600dp/dimens.xml new file mode 100644 index 0000000..b2cf6ae --- /dev/null +++ b/XYZReader/src/main/res/values-sw600dp/dimens.xml @@ -0,0 +1,7 @@ + + + 42sp + 20sp + 1.3 + 24dp + \ No newline at end of file diff --git a/XYZReader/src/main/res/values-sw600dp/macro_layout.xml b/XYZReader/src/main/res/values-sw600dp/macro_layout.xml new file mode 100644 index 0000000..413c02e --- /dev/null +++ b/XYZReader/src/main/res/values-sw600dp/macro_layout.xml @@ -0,0 +1,9 @@ + + + 384dp + 384dp + 64dp + 32dp + 8dp + 96dp + diff --git a/XYZReader/src/main/res/values-w800dp/macro_layout.xml b/XYZReader/src/main/res/values-w800dp/macro_layout.xml new file mode 100644 index 0000000..fdce508 --- /dev/null +++ b/XYZReader/src/main/res/values-w800dp/macro_layout.xml @@ -0,0 +1,9 @@ + + + 64dp + 52dp + 3 + + + 704dp + \ No newline at end of file diff --git a/XYZReader/src/main/res/values/attrs.xml b/XYZReader/src/main/res/values/attrs.xml new file mode 100644 index 0000000..11f8381 --- /dev/null +++ b/XYZReader/src/main/res/values/attrs.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/XYZReader/src/main/res/values/colors.xml b/XYZReader/src/main/res/values/colors.xml new file mode 100644 index 0000000..247642b --- /dev/null +++ b/XYZReader/src/main/res/values/colors.xml @@ -0,0 +1,25 @@ + + + + #ccc + + #04561A + #04561A + #04561A + #ffe0e0e0 + diff --git a/XYZReader/src/main/res/values/dimens.xml b/XYZReader/src/main/res/values/dimens.xml new file mode 100644 index 0000000..f8bad8c --- /dev/null +++ b/XYZReader/src/main/res/values/dimens.xml @@ -0,0 +1,11 @@ + + + 32sp + 22sp + 1.25 + + 2dp + 6dp + 24dp + + diff --git a/XYZReader/src/main/res/values/macro_layout.xml b/XYZReader/src/main/res/values/macro_layout.xml new file mode 100644 index 0000000..09e9ae0 --- /dev/null +++ b/XYZReader/src/main/res/values/macro_layout.xml @@ -0,0 +1,17 @@ + + + 4dp + 0dp + 2 + + + false + 304dp + @dimen/detail_photo_height + 16dp + 10000dp + 16dp + 4dp + 16dp + 88dp + \ No newline at end of file diff --git a/XYZReader/src/main/res/values/strings.xml b/XYZReader/src/main/res/values/strings.xml new file mode 100644 index 0000000..e120fcd --- /dev/null +++ b/XYZReader/src/main/res/values/strings.xml @@ -0,0 +1,12 @@ + + + XYZ Reader + Welcome to XYZ Reader! + + Refresh + Settings + Share + + Up + + diff --git a/XYZReader/src/main/res/values/styles.xml b/XYZReader/src/main/res/values/styles.xml new file mode 100644 index 0000000..bce9969 --- /dev/null +++ b/XYZReader/src/main/res/values/styles.xml @@ -0,0 +1,28 @@ + + + + + + + + + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..495c503 --- /dev/null +++ b/build.gradle @@ -0,0 +1 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..8c0fb64 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..24d17ac --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sat Apr 18 23:16:49 EDT 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..91a7e26 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/project/gradle.scala b/project/gradle.scala new file mode 100644 index 0000000..4266ec9 --- /dev/null +++ b/project/gradle.scala @@ -0,0 +1 @@ +object Build extends android.GradleBuild \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..47ef9f8 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.hanhuy.sbt" % "android-protify" % "1.1.12") +addSbtPlugin("com.hanhuy.sbt" % "android-gradle-build" % "1.1.7") \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..d6b4c44 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':XYZReader'