├── .gitignore ├── README.md ├── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── yodel-sample ├── AndroidManifest.xml ├── build.gradle ├── libs └── YahooSearch-0.1.0-with-volley.aar ├── proguard-project-debug.txt ├── proguard-project.txt ├── res ├── animator │ ├── fade_hide.xml │ └── fade_show.xml ├── drawable-v21 │ └── list_selector_background.xml ├── drawable-xhdpi │ ├── ic_gallery.png │ ├── ic_like.png │ ├── ic_reblog.png │ ├── ic_search_blue.png │ ├── ic_search_white.png │ └── ic_video.png ├── drawable-xxhdpi │ └── bottom_shadow.9.png ├── drawable │ ├── list_selector_background.xml │ └── toolbar_gradient_bg.xml ├── layout-w580dp │ ├── list_item_ad_card.xml │ └── list_item_post_card.xml ├── layout │ ├── activity_image_gallery.xml │ ├── activity_main.xml │ ├── activity_post_detail.xml │ ├── fragment_full_image.xml │ ├── fragment_main.xml │ ├── fragment_post_detail.xml │ ├── list_item_ad_card.xml │ ├── list_item_post_card.xml │ ├── list_item_post_search.xml │ ├── view_caption_pager_indicator.xml │ ├── view_more_web_footer.xml │ ├── view_search_tool_button.xml │ ├── view_toolbar.xml │ ├── view_transparent_toolbar.xml │ └── view_ysearch_header.xml ├── menu │ └── main.xml ├── mipmap-xxhdpi │ └── ic_launcher.png ├── values-v21 │ └── styles.xml ├── values-w820dp │ └── dimens.xml ├── values │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── placeholder_strings.xml │ ├── strings.xml │ └── styles.xml └── xml │ └── searchable.xml └── src └── com └── yahoo └── mobile └── client └── android └── yodel ├── NativeAdFetcher.java ├── NativeTestAppApplication.java ├── feed └── TumblrFeedManager.java ├── ui ├── FullImageFragment.java ├── ImageGalleryActivity.java ├── MainActivity.java ├── PostDetailActivity.java ├── PostDetailFragment.java ├── PostListFragment.java ├── PostSearchActivity.java └── widgets │ ├── AspectRatioImageView.java │ ├── CaptionViewPagerIndicator.java │ ├── CustomSearchViewHolder.java │ ├── SearchToolButton.java │ └── adapters │ ├── BaseAdAdapter.java │ ├── GalleryPagerAdapter.java │ ├── PostDetailPagerAdapter.java │ ├── PostListAdapter.java │ └── PostSearchListAdapter.java └── utils ├── AnalyticsHelper.java ├── DateTimeUtil.java ├── ImageLoader.java ├── MemoryCache.java └── PostDataLoader.java /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 5 | 6 | ## Directory-based project format 7 | .idea/ 8 | # if you remove the above rule, at least ignore user-specific stuff: 9 | # .idea/workspace.xml 10 | # .idea/tasks.xml 11 | # and these sensitive or high-churn files: 12 | # .idea/dataSources.ids 13 | # .idea/dataSources.xml 14 | # .idea/sqlDataSources.xml 15 | # .idea/dynamic.xml 16 | 17 | ## File-based project format 18 | *.ipr 19 | *.iml 20 | *.iws 21 | 22 | ## Additional for IntelliJ 23 | out/ 24 | 25 | # generated by mpeltonen/sbt-idea plugin 26 | .idea_modules/ 27 | 28 | # generated by JIRA plugin 29 | atlassian-ide-plugin.xml 30 | 31 | # built application files 32 | *.apk 33 | *.ap_ 34 | 35 | # files for the dex VM 36 | *.dex 37 | 38 | # Java class files 39 | *.class 40 | 41 | # generated files 42 | build/ 43 | 44 | # Local configuration file (sdk path, etc) 45 | local.properties 46 | 47 | # Gradle 48 | .gradle 49 | 50 | # Flurry build outputs 51 | build_jenkins/ 52 | build_ant 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yahoo Mobile Developer Conference Sample App - Android 2 | 3 | This project showcases some of the new mobile development tools released by Yahoo during the 2015 4 | [Mobile Developer Conference](http://yahoomobiledevcon.tumblr.com/) 5 | 6 | ## Features 7 | 8 | - Showcases integration of Flurry native ads, along with best practices for integrating native 9 | ads in your Android apps 10 | 11 | ## Requirements for working with the source: 12 | 13 | - Android Studio 14 | - Android SDK Platform Tools r21 or later 15 | - Android SDK Build Tools r21 or later 16 | - Runtime of Android 4.1 (API 16) or later 17 | - Tumblr Developer consumer key and secret 18 | 19 | ## Gemini native ads 20 | To see best practices for integrating native ads into your app, look through the following classes: 21 | 22 | - [GeminiAdFetcher](yodel-sample/src/com/yahoo/mobile/client/android/yodel/GeminiAdFetcher.java): This 23 | shows best practices for fetching and storing native ads to be used in an Android app 24 | - [BaseAdAdapter](yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/adapters/BaseAdAdapter.java): 25 | This shows best practices for integrating native ads into a list/stream of data 26 | 27 | For more info on getting started with Flurry for Android, see 28 | [here](https://developer.yahoo.com/flurry/docs/analytics/gettingstarted/android/). 29 | 30 | ## Copyright 31 | 32 | Copyright 2015 Yahoo Inc. All rights reserved. 33 | 34 | Licensed under the Apache License, Version 2.0 (the "License"); 35 | you may not use this file except in compliance with the License. 36 | You may obtain a copy of the License at 37 | 38 | http://www.apache.org/licenses/LICENSE-2.0 39 | 40 | Unless required by applicable law or agreed to in writing, software 41 | distributed under the License is distributed on an "AS IS" BASIS, 42 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 43 | See the License for the specific language governing permissions and 44 | limitations under the License. 45 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:1.5.0' 7 | } 8 | } 9 | 10 | allprojects { 11 | repositories { 12 | jcenter() 13 | } 14 | gradle.projectsEvaluated { 15 | tasks.withType(JavaCompile) { 16 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Dec 01 12:08:07 PST 2014 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':yodel-sample' -------------------------------------------------------------------------------- /yodel-sample/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | 43 | 46 | 47 | 52 | 55 | 56 | 60 | 63 | 64 | 65 | 68 | 69 | 70 | 73 | 74 | 75 | 78 | 79 | 80 | 83 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /yodel-sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | repositories { 4 | mavenCentral() 5 | flatDir { 6 | dirs 'libs' 7 | } 8 | 9 | repositories { 10 | jcenter() 11 | } 12 | } 13 | 14 | dependencies { 15 | 16 | compile 'com.yahoo.mobile.client.share.search:YahooSearch-0.1.0-with-volley@aar' 17 | compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3' // Used by YSearch 18 | compile 'com.tumblr:jumblr:0.0.11' 19 | 20 | // Required for Flurry Analytics integration 21 | compile 'com.flurry.android:analytics:8.0.1@aar' 22 | // Optional - If you want to use the Ads SDK 23 | compile 'com.flurry.android:ads:8.0.1@aar' 24 | 25 | compile 'com.android.support:cardview-v7:25.3.1' 26 | compile 'com.android.support:support-v4:25.3.1' 27 | compile 'com.google.android.gms:play-services-ads:11.0.4' 28 | compile 'com.android.support:appcompat-v7:25.3.1' 29 | compile 'com.google.code.gson:gson:2.7' 30 | } 31 | 32 | android { 33 | compileSdkVersion 25 34 | buildToolsVersion '25.0.0' 35 | 36 | defaultConfig { 37 | applicationId "com.flurry.sample.gemini" 38 | minSdkVersion 16 39 | targetSdkVersion 23 40 | versionCode 1 41 | versionName "2.0" 42 | } 43 | 44 | compileOptions { 45 | sourceCompatibility JavaVersion.VERSION_1_7 46 | targetCompatibility JavaVersion.VERSION_1_7 47 | } 48 | 49 | sourceSets { 50 | main { 51 | manifest.srcFile 'AndroidManifest.xml' 52 | java.srcDirs = ['src'] 53 | resources.srcDirs = ['src'] 54 | aidl.srcDirs = ['src'] 55 | renderscript.srcDirs = ['src'] 56 | res.srcDirs = ['res'] 57 | assets.srcDirs = ['assets'] 58 | } 59 | 60 | androidTest { 61 | manifest.srcFile 'tests/AndroidManifest.xml' 62 | java.srcDirs = ['tests/src'] 63 | resources.srcDirs = ['tests/src'] 64 | aidl.srcDirs = ['tests/src'] 65 | renderscript.srcDirs = ['tests/src'] 66 | res.srcDirs = ['tests/res'] 67 | assets.srcDirs = ['tests/assets'] 68 | } 69 | } 70 | 71 | packagingOptions { 72 | exclude 'META-INF/DEPENDENCIES.txt' 73 | exclude 'META-INF/LICENSE.txt' 74 | exclude 'META-INF/NOTICE.txt' 75 | exclude 'META-INF/ASL2.0' 76 | } 77 | 78 | buildTypes { 79 | debug { 80 | minifyEnabled false 81 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt', 'proguard-project-debug.txt' 82 | } 83 | 84 | release { 85 | minifyEnabled true 86 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt' 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /yodel-sample/libs/YahooSearch-0.1.0-with-volley.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/libs/YahooSearch-0.1.0-with-volley.aar -------------------------------------------------------------------------------- /yodel-sample/proguard-project-debug.txt: -------------------------------------------------------------------------------- 1 | # Project-specific ProGuard settings for debug 2 | 3 | -dontobfuscate -------------------------------------------------------------------------------- /yodel-sample/proguard-project.txt: -------------------------------------------------------------------------------- 1 | # Project-specific ProGuard settings 2 | 3 | # Keep the source file and line number table so we get decipherable stack traces --> 4 | -renamesourcefileattribute SourceFile 5 | -keepattributes Exceptions, SourceFile, LineNumberTable, Signature 6 | 7 | # Preserve Flurry classes 8 | -keep class com.flurry.** { *; } 9 | -dontwarn com.flurry.** 10 | -keepattributes *Annotation*,EnclosingMethod 11 | -keepclasseswithmembers class * { 12 | public (android.content.Context, android.util.AttributeSet, int); 13 | } 14 | 15 | # Appcompat 16 | 17 | # support-v4 18 | -dontwarn android.support.v4.** 19 | -keep class android.support.v4.app.** { *; } 20 | -keep interface android.support.v4.app.** { *; } 21 | -keep class android.support.v4.widget.** { *; } 22 | -keep interface android.support.v4.widget.** { *; } 23 | 24 | # support-v7 25 | -dontwarn android.support.v7.** 26 | -keep class android.support.v7.app.** { *; } 27 | -keep interface android.support.v7.app.** { *; } 28 | -keep class android.support.v7.appcompat.** { *; } 29 | -keep class android.support.v7.widget.SearchView { *; } 30 | 31 | # Preserve GMS classes 32 | -dontwarn com.google.android.gms.** 33 | -dontwarn com.android.volley.toolbox.** 34 | -dontwarn com.yahoo.data.** 35 | 36 | -keep class * extends java.util.ListResourceBundle { 37 | protected Object[][] getContents(); 38 | } 39 | 40 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { 41 | public static final *** NULL; 42 | } 43 | 44 | -keepnames @com.google.android.gms.common.annotation.KeepName class * 45 | -keepclassmembernames class * { 46 | @com.google.android.gms.common.annotation.KeepName *; 47 | } 48 | 49 | -keepnames class * implements android.os.Parcelable { 50 | public static final ** CREATOR; 51 | } 52 | 53 | -keep class com.google.android.gms.ads.** { *; } 54 | -dontwarn com.google.android.gms.ads.** 55 | 56 | -dontwarn com.google.android.gms.R* 57 | 58 | # Preserve InMobi Ads classes 59 | -keep class com.inmobi.** { *; } 60 | -dontwarn com.inmobi.** 61 | 62 | # Preserve Millennial Ads classes 63 | -keep class com.millennialmedia.** { *; } 64 | -dontwarn com.millennialmedia.** 65 | 66 | # Preserve Facebbok Audience Network Ads classes 67 | -keep class com.facebook.** { *; } 68 | -dontwarn com.facebook.** 69 | 70 | # Ignore notes about missing unused classes dynamically referenced by com.millennialmedia and google.gson 71 | -dontnote sun.misc.Unsafe 72 | 73 | # Ignore notes about missing unused classes *.ILicensingService 74 | -dontnote com.google.vending.licensing.ILicensingService 75 | -dontnote com.android.vending.licensing.ILicensingService 76 | 77 | # Preserve YSearch SDK classes 78 | -keep class com.yahoo.mobile.client.share.search.** { *; } 79 | -keep interface com.yahoo.mobile.client.share.search.** { *; } 80 | -keep class * implements com.yahoo.mobile.client.share.search.interfaces.IFactory 81 | -dontwarn com.yahoo.mobile.client.share.search.** 82 | 83 | # If your project uses WebView with JS, uncomment the following 84 | # and specify the fully qualified class name to the JavaScript interface 85 | # class: 86 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 87 | # public *; 88 | #} 89 | -------------------------------------------------------------------------------- /yodel-sample/res/animator/fade_hide.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /yodel-sample/res/animator/fade_show.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /yodel-sample/res/drawable-v21/list_selector_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | -------------------------------------------------------------------------------- /yodel-sample/res/drawable-xhdpi/ic_gallery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/res/drawable-xhdpi/ic_gallery.png -------------------------------------------------------------------------------- /yodel-sample/res/drawable-xhdpi/ic_like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/res/drawable-xhdpi/ic_like.png -------------------------------------------------------------------------------- /yodel-sample/res/drawable-xhdpi/ic_reblog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/res/drawable-xhdpi/ic_reblog.png -------------------------------------------------------------------------------- /yodel-sample/res/drawable-xhdpi/ic_search_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/res/drawable-xhdpi/ic_search_blue.png -------------------------------------------------------------------------------- /yodel-sample/res/drawable-xhdpi/ic_search_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/res/drawable-xhdpi/ic_search_white.png -------------------------------------------------------------------------------- /yodel-sample/res/drawable-xhdpi/ic_video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/res/drawable-xhdpi/ic_video.png -------------------------------------------------------------------------------- /yodel-sample/res/drawable-xxhdpi/bottom_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/res/drawable-xxhdpi/bottom_shadow.9.png -------------------------------------------------------------------------------- /yodel-sample/res/drawable/list_selector_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /yodel-sample/res/drawable/toolbar_gradient_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | -------------------------------------------------------------------------------- /yodel-sample/res/layout-w580dp/list_item_ad_card.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 27 | 37 | 38 | 48 | 49 | 59 | 60 | 67 | 68 | 80 | 81 | 92 | 93 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /yodel-sample/res/layout-w580dp/list_item_post_card.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 22 | 27 | 37 | 38 | 48 | 49 | 59 | 60 | 68 | 69 | 81 | 82 | 93 | 94 | 100 | 101 | 111 | 112 | 120 | 121 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/activity_image_gallery.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 11 | 12 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/activity_post_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 17 | 18 | 23 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/fragment_full_image.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/fragment_main.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/fragment_post_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 17 | 25 | 32 | 39 | 40 | 50 | 51 | 62 | 73 | 74 | 81 | 82 | 92 | 93 | 103 | 104 | 112 | 113 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/list_item_ad_card.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 26 | 27 | 37 | 38 | 45 | 46 | 58 | 59 | 72 | 73 | 82 | 83 | 95 | 96 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/list_item_post_card.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | 26 | 27 | 37 | 38 | 46 | 47 | 59 | 60 | 73 | 74 | 86 | 87 | 93 | 94 | 105 | 106 | 115 | 116 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/list_item_post_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 19 | 29 | 30 | 44 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/view_caption_pager_indicator.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 22 | 23 | 32 | 33 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/view_more_web_footer.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 15 | 16 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/view_search_tool_button.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 22 | 30 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/view_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/view_transparent_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /yodel-sample/res/layout/view_ysearch_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 17 | 18 | 30 | 31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /yodel-sample/res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /yodel-sample/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flurry/YodelAndroidApp/175349dc0351cd0daf46a4175242470f0b23c3b9/yodel-sample/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /yodel-sample/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | -------------------------------------------------------------------------------- /yodel-sample/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 64dp 9 | 10 | 11 | -------------------------------------------------------------------------------- /yodel-sample/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /yodel-sample/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFF 4 | #EEE 5 | #B2B2B2 6 | #EDEDF1 7 | 8 | #5A00C8 9 | #4B048B 10 | #D84B048B 11 | #D8ED2023 12 | #FF8B12 13 | #0D9AF8 14 | #93004F 15 | -------------------------------------------------------------------------------- /yodel-sample/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16dp 5 | 16dp 6 | -16dp 7 | -16dp 8 | 9 | 8dp 10 | 16dp 11 | 12 | 48dp 13 | 14 | 12sp 15 | 14sp 16 | 18sp 17 | 20sp 18 | 19 | 20 | -------------------------------------------------------------------------------- /yodel-sample/res/values/placeholder_strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Yahoo 4 | Lorem ipsum dolor sit amet 5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit.Suspendisse varius purus eu erat mollis, eget scelerisque ante vulputate. Vivamus rutrum. 6 | 2 days ago 7 | 3 8 | #lorem #ipsum #dolor #sit #amet #consectetur #adipiscing 9 | -------------------------------------------------------------------------------- /yodel-sample/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Yodel Sample 5 | 6 | Sponsored 7 | 8 | Fetching posts 9 | Network not available 10 | Retry 11 | Cancel 12 | 13 | Search 14 | 15 | Learn more: %1$s 17 | ]]> 18 | 19 | Search for content 20 | Search the web 21 | Posts tagged \"<b>%1$s</b>\" 22 | MORE FROM WEB 23 | %1$s of %2$s 24 | Just Now 25 | 26 | 27 | A minute ago 28 | %d minutes ago 29 | 30 | 31 | An hour ago 32 | %d hours ago 33 | 34 | 35 | Yesterday 36 | %d days ago 37 | 38 | 39 | -------------------------------------------------------------------------------- /yodel-sample/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 13 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /yodel-sample/res/xml/searchable.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/NativeAdFetcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel; 18 | 19 | import android.content.Context; 20 | import android.util.Log; 21 | 22 | import com.flurry.android.ads.FlurryAdErrorType; 23 | import com.flurry.android.ads.FlurryAdNative; 24 | import com.flurry.android.ads.FlurryAdNativeListener; 25 | import com.flurry.android.ads.FlurryAdTargeting; 26 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 27 | 28 | import java.lang.ref.WeakReference; 29 | import java.util.ArrayList; 30 | import java.util.HashMap; 31 | import java.util.List; 32 | import java.util.Map; 33 | 34 | /** 35 | *

Handles fetching, caching, and destroying of native gemini ads.

36 | * 37 | */ 38 | public class NativeAdFetcher { 39 | private static final String LOG_TAG = NativeAdFetcher.class.getSimpleName(); 40 | /** 41 | * Maximum number of ads to prefetch. 42 | */ 43 | private static final int PREFETCHED_ADS_SIZE = 5; 44 | /** 45 | * Maximum number of times to try fetch an ad after failed attempts. 46 | */ 47 | private static final int MAX_FETCH_ATTEMPT = 4; 48 | private List mAdNativeListeners = new ArrayList<>(); 49 | private List mPrefetchedAdList = new ArrayList<>(); 50 | private List mFetchingAdsList = new ArrayList<>(); 51 | private Map adMapAtIndex = new HashMap<>(); 52 | private int mNoOfFetchedAds; 53 | private int mFetchFailCount; 54 | private WeakReference mContext = new WeakReference<>(null); 55 | private FlurryAdNativeListener mAdNativeListener = new FlurryAdNativeListener() { 56 | @Override 57 | public synchronized void onFetched(FlurryAdNative adNative) { 58 | Log.i(LOG_TAG, "onFetched"); 59 | if (canUseThisAd(adNative)) { 60 | mPrefetchedAdList.add(adNative); 61 | mNoOfFetchedAds++; 62 | } 63 | if (mFetchingAdsList.contains(adNative)) { 64 | mFetchingAdsList.remove(adNative); 65 | } 66 | mFetchFailCount = 0; 67 | ensurePrefetchAmount(); 68 | 69 | notifyObserversOfAdSizeChange(); 70 | } 71 | 72 | @Override 73 | public void onShowFullscreen(FlurryAdNative adNative) { } // Do nothing 74 | 75 | @Override 76 | public void onCloseFullscreen(FlurryAdNative adNative) { 77 | AnalyticsHelper.logEvent(AnalyticsHelper.EVENT_AD_CLOSEBUTTON_CLICK, null, false); 78 | } 79 | 80 | @Override 81 | public void onAppExit(FlurryAdNative adNative) { } 82 | 83 | @Override 84 | public void onClicked(FlurryAdNative adNative) { 85 | AnalyticsHelper.logEvent(AnalyticsHelper.EVENT_STREAM_AD_CLICK, null, false); 86 | } 87 | 88 | @Override 89 | public void onImpressionLogged(FlurryAdNative adNative) { } 90 | 91 | @Override 92 | public void onExpanded(FlurryAdNative flurryAdNative) { } 93 | 94 | @Override 95 | public void onCollapsed(FlurryAdNative flurryAdNative) { } 96 | 97 | @Override 98 | public void onError(FlurryAdNative adNative, FlurryAdErrorType adErrorType, int errorCode) { 99 | if (adErrorType.equals(FlurryAdErrorType.FETCH)) { 100 | Log.i(LOG_TAG, "onFetchFailed " + errorCode); 101 | 102 | if (mFetchingAdsList.contains(adNative)) { 103 | adNative.destroy(); // destroy the native ad, as we are not going to render it. 104 | mFetchingAdsList.remove(adNative); // Remove from the tracking list 105 | } 106 | mFetchFailCount++; 107 | ensurePrefetchAmount(); 108 | } 109 | } 110 | }; 111 | 112 | /** 113 | * Adds an {@link AdNativeListener} that would be notified for any changes to the native ads 114 | * list. 115 | * 116 | * @param listener the listener to be notified 117 | */ 118 | public synchronized void addListener(AdNativeListener listener) { 119 | mAdNativeListeners.add(listener); 120 | } 121 | 122 | /** 123 | * Gets native ad at a particular index in the fetched ads list. 124 | * 125 | * @see #getFetchedAdsCount() 126 | * @param index the index of ad in the fetched ads list 127 | * @return the native ad in the list 128 | */ 129 | public synchronized FlurryAdNative getAdForIndex(final int index) { 130 | FlurryAdNative adNative = adMapAtIndex.get(index); 131 | 132 | if (adNative == null && mPrefetchedAdList.size() > 0) { 133 | adNative = mPrefetchedAdList.remove(0); 134 | 135 | if (adNative != null) { 136 | adMapAtIndex.put(index, adNative); 137 | } 138 | } 139 | 140 | ensurePrefetchAmount(); // Make sure we have enough pre-fetched ads 141 | 142 | return adNative; 143 | } 144 | 145 | /** 146 | * Gets the number of ads that have been fetched so far. 147 | * 148 | * @return the number of ads that have been fetched 149 | */ 150 | public synchronized int getFetchedAdsCount() { 151 | return mNoOfFetchedAds; 152 | } 153 | 154 | /** 155 | * Fetches a new native ad. 156 | * @param context the current context. 157 | * 158 | * @see #destroyAllAds() 159 | */ 160 | public synchronized void prefetchAds(Context context) { 161 | mContext = new WeakReference<>(context); 162 | fetchAd(); 163 | } 164 | 165 | /** 166 | * Destroys ads that have been fetched, that are still being fetched and removes all resource 167 | * references that this instance still has. This should only be called when the Activity that 168 | * is showing ads is closing, preferably from the {@link android.app.Activity#onDestroy()}. 169 | *

170 | * The converse of this call is {@link #prefetchAds(android.content.Context)}. 171 | */ 172 | public synchronized void destroyAllAds() { 173 | for (FlurryAdNative flurryAdNative : adMapAtIndex.values()) { 174 | flurryAdNative.destroy(); 175 | } 176 | 177 | mFetchFailCount = 0; 178 | adMapAtIndex.clear(); 179 | 180 | for (FlurryAdNative ad : mPrefetchedAdList) { 181 | ad.destroy(); 182 | } 183 | mPrefetchedAdList.clear(); 184 | mNoOfFetchedAds = 0; 185 | 186 | for (FlurryAdNative ad : mFetchingAdsList) { 187 | ad.destroy(); 188 | } 189 | mFetchingAdsList.clear(); 190 | 191 | Log.i(LOG_TAG, "destroyAllAds adList " + adMapAtIndex.size() + " prefetched " + 192 | mPrefetchedAdList.size() + " fetched " + mFetchingAdsList.size()); 193 | 194 | mContext.clear(); 195 | 196 | notifyObserversOfAdSizeChange(); 197 | } 198 | 199 | /** 200 | * Notifies all registered {@link AdNativeListener} of a change in ad data count. 201 | */ 202 | private void notifyObserversOfAdSizeChange() { 203 | for (AdNativeListener listener : mAdNativeListeners) { 204 | listener.onAdCountChanged(); 205 | } 206 | } 207 | 208 | /** 209 | * Fetches a new native ad. 210 | */ 211 | private synchronized void fetchAd() { 212 | Context context = mContext.get(); 213 | 214 | if (context != null) { 215 | Log.i(LOG_TAG, "Fetching Ad now"); 216 | FlurryAdNative nativeAd = new FlurryAdNative( 217 | context, NativeTestAppApplication.FLURRY_ADSPACE); 218 | nativeAd.setListener(mAdNativeListener); 219 | mFetchingAdsList.add(nativeAd); 220 | nativeAd.fetchAd(); 221 | } else { 222 | mFetchFailCount++; 223 | Log.i(LOG_TAG, "Context is null, not fetching Ad"); 224 | } 225 | } 226 | 227 | /** 228 | * Ensures that the necessary amount of prefetched native ads are available. 229 | */ 230 | private synchronized void ensurePrefetchAmount() { 231 | if (mPrefetchedAdList.size() < PREFETCHED_ADS_SIZE && 232 | (mFetchFailCount < MAX_FETCH_ATTEMPT)) { 233 | fetchAd(); 234 | } 235 | } 236 | 237 | /** 238 | * Determines if the native ad can be used. 239 | * 240 | * @param adNative the native ad object 241 | * @return true if the ad object can be used, false otherwise 242 | */ 243 | private boolean canUseThisAd(FlurryAdNative adNative) { 244 | return adNative != null && adNative.isReady() && !adNative.isExpired(); 245 | } 246 | 247 | /** 248 | * Listener that is notified when changes to the list of fetched native ads are made. 249 | */ 250 | public interface AdNativeListener { 251 | /** 252 | * Triggered when the number of ads have changed. Adapters that implement this class 253 | * should notify their data views that the dataset has changed. 254 | */ 255 | void onAdCountChanged(); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/NativeTestAppApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel; 18 | 19 | import android.app.Application; 20 | import android.net.http.HttpResponseCache; 21 | import android.os.Handler; 22 | import android.os.HandlerThread; 23 | import android.os.Looper; 24 | import android.util.Log; 25 | 26 | import com.flurry.android.FlurryAgent; 27 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 28 | import com.yahoo.mobile.client.share.search.settings.SearchSDKSettings; 29 | 30 | import java.io.File; 31 | import java.io.IOException; 32 | 33 | public class NativeTestAppApplication extends Application { 34 | private static String LOG_TAG = NativeTestAppApplication.class.getSimpleName(); 35 | 36 | private static final String FLURRY_APIKEY = "JQVT87W7TGN5W7SWY2FH"; 37 | public static final String FLURRY_ADSPACE = "StaticVideoNativeTest"; 38 | public static final String YAHOO_SEARCH_APIKEY = "your_api_key"; 39 | 40 | private static NativeTestAppApplication sApplication; 41 | 42 | private Handler mMainThreadHandler; 43 | 44 | @Override 45 | public void onCreate() { 46 | super.onCreate(); 47 | 48 | // Init Search SDK 49 | SearchSDKSettings.initializeSearchSDKSettings( 50 | new SearchSDKSettings.Builder(YAHOO_SEARCH_APIKEY) 51 | .setVoiceSearchEnabled(true)); 52 | 53 | sApplication = this; 54 | 55 | // create handlers 56 | mMainThreadHandler = new Handler(Looper.getMainLooper()); 57 | HandlerThread backgroundHandlerThread = new HandlerThread(LOG_TAG); 58 | backgroundHandlerThread.start(); 59 | 60 | // http response cache 61 | File httpCacheDir = new File(getCacheDir(), "http"); 62 | long httpCacheSize = 100 * 1024 * 1024; // 100 MiB 63 | 64 | try { 65 | HttpResponseCache.install(httpCacheDir, httpCacheSize); 66 | } catch (IOException e) { 67 | AnalyticsHelper.logError(LOG_TAG, "HTTP response cache installation failed", e); 68 | } 69 | 70 | // Init Flurry 71 | new FlurryAgent.Builder() 72 | .withLogEnabled(true) 73 | .withCaptureUncaughtExceptions(true) 74 | .withContinueSessionMillis(10) 75 | .withLogEnabled(true) 76 | .withLogLevel(Log.VERBOSE) 77 | 78 | .build(this, FLURRY_APIKEY); 79 | } 80 | 81 | public static NativeTestAppApplication getInstance() { 82 | return sApplication; 83 | } 84 | 85 | public Handler getMainThreadHandler() { 86 | return mMainThreadHandler; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/feed/TumblrFeedManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.feed; 18 | 19 | import com.google.gson.Gson; 20 | import com.google.gson.reflect.TypeToken; 21 | import com.tumblr.jumblr.JumblrClient; 22 | import com.tumblr.jumblr.types.Blog; 23 | import com.tumblr.jumblr.types.LinkPost; 24 | import com.tumblr.jumblr.types.PhotoPost; 25 | import com.tumblr.jumblr.types.Post; 26 | import com.tumblr.jumblr.types.QuotePost; 27 | import com.tumblr.jumblr.types.TextPost; 28 | import com.tumblr.jumblr.types.VideoPost; 29 | 30 | import java.util.ArrayList; 31 | import java.util.HashMap; 32 | import java.util.List; 33 | 34 | /** 35 | * Class that manages fetching of Tumblr blog posts. 36 | */ 37 | public final class TumblrFeedManager { 38 | private final static String TUMBLR_CONSUMER_KEY = "xz2gUnYX7axNd9XsVFIjUyhVElCPxU5pQVpfSV1qZgYxaiz29f"; 39 | private final static String TUMBLR_CONSUMER_SECRET = "DqFvBW162BYUF92jCQgrLBzVDngxKy8TAZ3tDBspSE7mz4tqTw"; 40 | // Only a limited set of the possible Tumblr post types will be supported 41 | public final static String POST_TYPE_TEXT = "text"; 42 | public final static String POST_TYPE_QUOTE = "quote"; 43 | public final static String POST_TYPE_PHOTO = "photo"; 44 | public final static String POST_TYPE_VIDEO = "video"; 45 | public final static String POST_TYPE_LINK = "link"; 46 | 47 | private static JumblrClient sTumblrClient; 48 | private final static Blog BLOG_FEED_1; 49 | private final static Blog BLOG_FEED_2; 50 | private final static Blog BLOG_FEED_3; 51 | 52 | private final static int MAX_POSTS_TO_FETCH = 20; 53 | 54 | static { 55 | sTumblrClient = new JumblrClient(TUMBLR_CONSUMER_KEY, 56 | TUMBLR_CONSUMER_SECRET); 57 | 58 | BLOG_FEED_1 = sTumblrClient.blogInfo("archaicwonder.tumblr.com"); 59 | BLOG_FEED_2 = sTumblrClient.blogInfo("yahoo.tumblr.com"); 60 | BLOG_FEED_3 = sTumblrClient.blogInfo("natgeotravel.tumblr.com"); 61 | } 62 | 63 | public static List getRecentPosts() { 64 | HashMap optionsMap = new HashMap<>(1); 65 | optionsMap.put("filter", "text"); 66 | optionsMap.put("limit", "20"); 67 | optionsMap.put("reblog_info", "true"); 68 | List recentPosts = new ArrayList<>(MAX_POSTS_TO_FETCH); 69 | List blog1Posts = BLOG_FEED_1.posts(optionsMap); 70 | List blog2Posts = BLOG_FEED_2.posts(optionsMap); 71 | List blog3Posts = BLOG_FEED_3.posts(optionsMap); 72 | int blog1Counter, blog2Counter, blog3Counter; 73 | blog1Counter = blog2Counter = blog3Counter = 0; 74 | 75 | 76 | while (recentPosts.size() < 20) { 77 | Post nextBlogPost; 78 | if (blog1Posts.get(blog1Counter).getTimestamp() > 79 | blog2Posts.get(blog2Counter).getTimestamp() && 80 | blog1Posts.get(blog1Counter).getTimestamp() > 81 | blog3Posts.get(blog3Counter).getTimestamp()) { 82 | // If the next post from blog1 is the latest, add it to list 83 | nextBlogPost = blog1Posts.get(blog1Counter++); 84 | /* 85 | * Set the post name to a human-readable title instead of the third-level domain name 86 | * it is currently set from. 87 | */ 88 | nextBlogPost.setBlogName(BLOG_FEED_1.getTitle()); 89 | } else if (blog2Posts.get(blog2Counter).getTimestamp() > 90 | blog3Posts.get(blog3Counter).getTimestamp()) { 91 | // Else, if the next post from blog2 is the latest, add it to list 92 | nextBlogPost = blog2Posts.get(blog2Counter++); 93 | nextBlogPost.setBlogName(BLOG_FEED_2.getTitle()); 94 | } else { 95 | // Else, add the next post from blog3 to the list. 96 | nextBlogPost = blog3Posts.get(blog3Counter++); 97 | nextBlogPost.setBlogName(BLOG_FEED_3.getTitle()); 98 | } 99 | 100 | /* 101 | Remove JumblrClient since we are not doing any write operations. 102 | This makes serialization easier. 103 | */ 104 | nextBlogPost.setClient(null); 105 | recentPosts.add(nextBlogPost); 106 | } 107 | return recentPosts; 108 | } 109 | 110 | public static List getPostsWithTag(String tag) { 111 | HashMap optionsMap = new HashMap<>(1); 112 | optionsMap.put("filter", "text"); 113 | optionsMap.put("type", "photo"); // Limit to only photo posts 114 | optionsMap.put("limit", "5"); 115 | 116 | return sTumblrClient.tagged(tag, optionsMap); 117 | } 118 | 119 | public static Post deserializePostJson(String postType, String postJsonString) { 120 | Post post; 121 | switch (postType) { 122 | case POST_TYPE_PHOTO: 123 | post = new Gson().fromJson(postJsonString, 124 | new TypeToken() { 125 | }.getType()); 126 | break; 127 | case POST_TYPE_LINK: 128 | post = new Gson().fromJson(postJsonString, 129 | new TypeToken() { 130 | }.getType()); 131 | break; 132 | case POST_TYPE_VIDEO: 133 | post = new Gson().fromJson(postJsonString, 134 | new TypeToken() { 135 | }.getType()); 136 | break; 137 | case POST_TYPE_TEXT: 138 | post = new Gson().fromJson(postJsonString, 139 | new TypeToken() { 140 | }.getType()); 141 | break; 142 | case POST_TYPE_QUOTE: 143 | post = new Gson().fromJson(postJsonString, 144 | new TypeToken() { 145 | }.getType()); 146 | break; 147 | default: 148 | post = new Gson().fromJson(postJsonString, 149 | new TypeToken() { 150 | }.getType()); 151 | } 152 | return post; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/FullImageFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui; 18 | 19 | import android.os.Bundle; 20 | import android.support.annotation.Nullable; 21 | import android.support.v4.app.Fragment; 22 | import android.view.LayoutInflater; 23 | import android.view.View; 24 | import android.view.ViewGroup; 25 | import android.widget.ImageView; 26 | 27 | import com.yahoo.mobile.client.android.yodel.R; 28 | import com.yahoo.mobile.client.android.yodel.utils.ImageLoader; 29 | 30 | public class FullImageFragment extends Fragment { 31 | 32 | private final static String EXTRA_PHOTO_URL = "com.yahoo.mobile.sample.extra.photourl"; 33 | 34 | public static FullImageFragment newInstance(String photoUrl) { 35 | FullImageFragment fragment = new FullImageFragment(); 36 | Bundle bundle = new Bundle(); 37 | bundle.putString(EXTRA_PHOTO_URL, photoUrl); 38 | fragment.setArguments(bundle); 39 | return fragment; 40 | } 41 | 42 | @Override 43 | public View onCreateView(LayoutInflater inflater, 44 | @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { 45 | View rootView = inflater.inflate(R.layout.fragment_full_image, container, false); 46 | 47 | ImageView imageView = (ImageView)rootView.findViewById(R.id.full_image); 48 | 49 | String photoUrl = getArguments().getString(EXTRA_PHOTO_URL); 50 | ImageLoader.getInstance().displayImage(photoUrl, imageView); 51 | 52 | return rootView; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/ImageGalleryActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui; 18 | 19 | import android.animation.Animator; 20 | import android.animation.AnimatorInflater; 21 | import android.annotation.SuppressLint; 22 | import android.os.Build; 23 | import android.os.Bundle; 24 | import android.os.Handler; 25 | import android.os.Message; 26 | import android.support.annotation.Nullable; 27 | import android.support.v4.view.ViewPager; 28 | import android.support.v7.app.ActionBarActivity; 29 | import android.support.v7.widget.Toolbar; 30 | import android.view.GestureDetector; 31 | import android.view.MotionEvent; 32 | import android.view.View; 33 | import android.view.ViewTreeObserver; 34 | 35 | import com.yahoo.mobile.client.android.yodel.R; 36 | import com.yahoo.mobile.client.android.yodel.ui.widgets.CaptionViewPagerIndicator; 37 | import com.yahoo.mobile.client.android.yodel.ui.widgets.adapters.GalleryPagerAdapter; 38 | import com.google.gson.Gson; 39 | import com.google.gson.reflect.TypeToken; 40 | import com.tumblr.jumblr.types.Photo; 41 | 42 | import java.util.List; 43 | 44 | public class ImageGalleryActivity extends ActionBarActivity { 45 | public static final String EXTRA_PHOTO_LIST = "com.yahoo.mobile.sample.extra.photolist"; 46 | 47 | private final static int HIDE_DELAY = 3000; // 3 seconds 48 | private GalleryPagerAdapter mGalleryPagerAdapter; 49 | private final Handler mHideHandler = new Handler() { 50 | @Override 51 | public void handleMessage(Message msg) { 52 | hideSystemUi(); 53 | } 54 | }; 55 | 56 | private CaptionViewPagerIndicator mCaptionPagerIndicator; 57 | 58 | @Override 59 | public void onCreate(@Nullable Bundle savedInstanceState) { 60 | super.onCreate(savedInstanceState); 61 | setContentView(R.layout.activity_image_gallery); 62 | setTitle(""); 63 | 64 | Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar_actionbar); 65 | setSupportActionBar(toolbar); 66 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 67 | 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 69 | getWindow().setStatusBarColor(getResources().getColor(android.R.color.black)); 70 | } 71 | 72 | final List photos = new Gson().fromJson(getIntent().getStringExtra(EXTRA_PHOTO_LIST), 73 | new TypeToken>() { 74 | }.getType()); 75 | mGalleryPagerAdapter = new GalleryPagerAdapter(getSupportFragmentManager(), photos); 76 | mCaptionPagerIndicator = (CaptionViewPagerIndicator)findViewById(R.id.caption_pager_indicator); 77 | mCaptionPagerIndicator.setCurrentItem(0, mGalleryPagerAdapter.getCount()); 78 | mCaptionPagerIndicator.setCaption(photos.get(0).getCaption()); 79 | 80 | final ViewPager galleryPager = (ViewPager) findViewById(R.id.image_view_pager); 81 | galleryPager.setAdapter(mGalleryPagerAdapter); 82 | galleryPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() { 83 | @Override 84 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 85 | } 86 | 87 | @Override 88 | public void onPageSelected(int position) { 89 | mCaptionPagerIndicator.setCurrentItem(position, mGalleryPagerAdapter.getCount()); 90 | mCaptionPagerIndicator.setCaption(photos.get(position).getCaption()); 91 | } 92 | 93 | @Override 94 | public void onPageScrollStateChanged(int state) { 95 | } 96 | }); 97 | // Delay any activity transition until the ViewPager is ready to be drawn 98 | supportPostponeEnterTransition(); 99 | galleryPager.getViewTreeObserver().addOnPreDrawListener( 100 | new ViewTreeObserver.OnPreDrawListener() { 101 | public boolean onPreDraw() { 102 | galleryPager.getViewTreeObserver().removeOnPreDrawListener(this); 103 | supportStartPostponedEnterTransition(); 104 | return true; 105 | } 106 | }); 107 | final GestureDetector clickDetector = new GestureDetector(this, 108 | new GestureDetector.SimpleOnGestureListener() { 109 | @Override 110 | public boolean onSingleTapUp(MotionEvent e) { 111 | toggleSystemUiVisibility(); 112 | return true; 113 | } 114 | }); 115 | 116 | galleryPager.setOnTouchListener(new View.OnTouchListener() { 117 | @Override 118 | public boolean onTouch(View v, MotionEvent event) { 119 | return clickDetector.onTouchEvent(event); 120 | } 121 | }); 122 | } 123 | 124 | @Override 125 | protected void onStart() { 126 | super.onStart(); 127 | 128 | startUiDelayedHide(); 129 | } 130 | 131 | @Override 132 | protected void onPause() { 133 | removePendingHides(); 134 | 135 | super.onPause(); 136 | } 137 | 138 | @SuppressLint("NewApi") 139 | public void hideSystemUi() { 140 | 141 | getWindow().getDecorView().setSystemUiVisibility( 142 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 143 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 144 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 145 | | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar 146 | | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar 147 | | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY); 148 | 149 | Animator anim = AnimatorInflater.loadAnimator(this, R.animator.fade_hide); 150 | anim.setTarget(mCaptionPagerIndicator); 151 | anim.start(); 152 | 153 | getSupportActionBar().hide(); 154 | } 155 | 156 | @SuppressLint("NewApi") 157 | public void showSystemUi() { 158 | getWindow().getDecorView().setSystemUiVisibility( 159 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE 160 | | View.SYSTEM_UI_FLAG_FULLSCREEN // keep status bar hidden 161 | | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION 162 | | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); 163 | 164 | Animator anim = AnimatorInflater.loadAnimator(this, R.animator.fade_show); 165 | anim.setTarget(mCaptionPagerIndicator); 166 | anim.start(); 167 | 168 | getSupportActionBar().show(); 169 | } 170 | 171 | protected void toggleSystemUiVisibility() { 172 | boolean visible = isSystemUiVisible(); 173 | if (visible) { 174 | hideSystemUi(); 175 | } else { 176 | showSystemUi(); 177 | } 178 | } 179 | 180 | protected void startUiDelayedHide() { 181 | getWindow().getDecorView().setOnSystemUiVisibilityChangeListener( 182 | new View.OnSystemUiVisibilityChangeListener() { 183 | @Override 184 | public void onSystemUiVisibilityChange(int flags) { 185 | boolean visible = (flags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; 186 | if (visible) { 187 | delayedHide(HIDE_DELAY); 188 | } 189 | } 190 | }); 191 | // If system UI is visible, start hiding it after a short delay 192 | int visibilityFlags = getWindow().getDecorView().getSystemUiVisibility(); 193 | if ((visibilityFlags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) { 194 | delayedHide(HIDE_DELAY / 6); 195 | } 196 | } 197 | 198 | protected void removePendingHides() { 199 | mHideHandler.removeMessages(0); 200 | getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(null); 201 | resetSystemUi(); 202 | } 203 | 204 | public void resetSystemUi() { 205 | getWindow().getDecorView().setSystemUiVisibility(0); 206 | } 207 | 208 | protected boolean isSystemUiVisible() { 209 | return (getWindow().getDecorView().getSystemUiVisibility() 210 | & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; 211 | } 212 | 213 | private void delayedHide(int delayMillis) { 214 | // If less than KitKat, don't worry about setting visibility timer. 215 | // Not that it won't work, but my current implementation is strange on 216 | // API < 18 217 | if (android.os.Build.VERSION.SDK_INT < 18) { return; } 218 | 219 | mHideHandler.removeMessages(0); 220 | mHideHandler.sendEmptyMessageDelayed(0, delayMillis); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui; 18 | 19 | import android.app.SearchManager; 20 | import android.content.ComponentName; 21 | import android.content.Intent; 22 | import android.net.http.HttpResponseCache; 23 | import android.os.Bundle; 24 | import android.support.v7.app.ActionBarActivity; 25 | import android.support.v7.widget.SearchView; 26 | import android.support.v7.widget.Toolbar; 27 | import android.view.Menu; 28 | import android.view.View; 29 | 30 | import com.yahoo.mobile.client.android.yodel.R; 31 | import com.tumblr.jumblr.types.Post; 32 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 33 | 34 | import java.util.HashMap; 35 | 36 | public class MainActivity extends ActionBarActivity implements PostListFragment.Callbacks { 37 | 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_main); 42 | 43 | Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar_actionbar); 44 | setSupportActionBar(toolbar); 45 | 46 | if (savedInstanceState == null) { 47 | getSupportFragmentManager().beginTransaction() 48 | .replace(R.id.content_frame, PostListFragment.newInstance()) 49 | .commit(); 50 | } 51 | } 52 | 53 | @Override 54 | protected void onStart() { 55 | super.onStart(); 56 | 57 | // Uncomment the line below if targeting any API level less than API 14 58 | // FlurryAgent.onStartSession(this); 59 | } 60 | 61 | @Override 62 | protected void onStop() { 63 | super.onStop(); 64 | 65 | // HTTP response cache 66 | HttpResponseCache cache = HttpResponseCache.getInstalled(); 67 | if (cache != null) { 68 | cache.flush(); 69 | } 70 | 71 | // Uncomment the line below if targeting any API level less than API 14 72 | // FlurryAgent.onEndSession(this); 73 | } 74 | 75 | @Override 76 | public boolean onCreateOptionsMenu(Menu menu) { 77 | getMenuInflater().inflate(R.menu.main, menu); 78 | 79 | SearchManager searchManager = (SearchManager)getSystemService(SEARCH_SERVICE); 80 | SearchView searchView = (SearchView)menu.findItem(R.id.action_search).getActionView(); 81 | if (searchView != null) { 82 | searchView.setSearchableInfo(searchManager.getSearchableInfo( 83 | new ComponentName(this, PostSearchActivity.class))); 84 | searchView.setQueryRefinementEnabled(true); 85 | searchView.setOnSearchClickListener(new View.OnClickListener() { 86 | @Override 87 | public void onClick(View v) { 88 | AnalyticsHelper.logEvent( 89 | AnalyticsHelper.EVENT_STREAM_SEARCH_CLICK, null, false); 90 | } 91 | }); 92 | } 93 | 94 | return super.onCreateOptionsMenu(menu); 95 | } 96 | 97 | @Override 98 | public void onPostSelected(Post post, int positionId, View clickedView) { 99 | // Log the event 100 | HashMap eventParams = new HashMap<>(2); 101 | eventParams.put(AnalyticsHelper.PARAM_ARTICLE_ORIGIN, post.getBlogName()); 102 | eventParams.put(AnalyticsHelper.PARAM_ARTICLE_TYPE, post.getType()); 103 | AnalyticsHelper.logEvent(AnalyticsHelper.EVENT_STREAM_ARTICLE_CLICK, eventParams, false); 104 | 105 | Intent intent = new Intent(this, PostDetailActivity.class); 106 | intent.putExtra(PostDetailActivity.EXTRA_CURRENT_PAGE_INDEX, positionId); 107 | startActivity(intent); 108 | } 109 | 110 | 111 | } 112 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/PostDetailActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui; 18 | 19 | import android.app.ActivityOptions; 20 | import android.app.ProgressDialog; 21 | import android.content.Intent; 22 | import android.os.Build; 23 | import android.os.Bundle; 24 | import android.support.v4.app.LoaderManager; 25 | import android.support.v4.content.Loader; 26 | import android.support.v4.view.ViewPager; 27 | import android.support.v7.app.ActionBarActivity; 28 | import android.support.v7.widget.Toolbar; 29 | import android.view.MenuItem; 30 | import android.view.View; 31 | 32 | import com.yahoo.mobile.client.android.yodel.R; 33 | import com.yahoo.mobile.client.android.yodel.feed.TumblrFeedManager; 34 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 35 | import com.yahoo.mobile.client.android.yodel.utils.PostDataLoader; 36 | import com.yahoo.mobile.client.android.yodel.ui.widgets.adapters.PostDetailPagerAdapter; 37 | import com.google.gson.Gson; 38 | import com.tumblr.jumblr.types.Photo; 39 | import com.tumblr.jumblr.types.Post; 40 | 41 | import java.util.List; 42 | 43 | /** 44 | * @author ugo 45 | */ 46 | public class PostDetailActivity extends ActionBarActivity 47 | implements LoaderManager.LoaderCallbacks>, PostDetailFragment.Callback { 48 | 49 | private ViewPager mPager; 50 | private PostDetailPagerAdapter mDetailPageAdapter; 51 | private ProgressDialog mProgressDialog; 52 | 53 | private int mCurrentIndexPosition; 54 | private ViewPager.OnPageChangeListener mPageChangeListener = 55 | new ViewPager.OnPageChangeListener() { 56 | @Override 57 | public void onPageScrolled(int position, float positionOffset, 58 | int positionOffsetPixels) { } 59 | 60 | @Override 61 | public void onPageSelected(int position) { 62 | AnalyticsHelper.logEvent(AnalyticsHelper.EVENT_CAR_CONTENT_SWIPE, null, false); 63 | } 64 | 65 | @Override 66 | public void onPageScrollStateChanged(int state) { } 67 | }; 68 | 69 | final static String EXTRA_CURRENT_PAGE_INDEX = "com.yahoo.mdc.extra.currentindex"; 70 | final static String EXTRA_SINGLE_POST = "com.yahoo.mdc.extra.singlepost"; 71 | final static String EXTRA_SINGLE_POST_TYPE = "com.yahoo.mdc.extra.singleposttype"; 72 | private final static String STATE_CURRENT_PAGE_INDEX = "com.yahoo.mdc.state.currentindex"; 73 | private final static int LOADER_ID_LOAD_POSTS = 0x1; 74 | 75 | @Override 76 | protected void onCreate(Bundle savedInstanceState) { 77 | super.onCreate(savedInstanceState); 78 | setContentView(R.layout.activity_post_detail); 79 | 80 | Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar_actionbar); 81 | setSupportActionBar(toolbar); 82 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 83 | mPager = (ViewPager)findViewById(R.id.post_view_pager); 84 | mPager.setPageTransformer(true, new ParallaxPageTransformer(new int[] { R.id.post_image })); 85 | mPager.setOnPageChangeListener(mPageChangeListener); 86 | 87 | if (savedInstanceState != null) { 88 | mCurrentIndexPosition = savedInstanceState.getInt(STATE_CURRENT_PAGE_INDEX); 89 | } 90 | else { 91 | 92 | if (getIntent().hasExtra(EXTRA_SINGLE_POST)) { 93 | String postType = getIntent().getStringExtra(EXTRA_SINGLE_POST_TYPE); 94 | String postJsonString = getIntent().getStringExtra(EXTRA_SINGLE_POST); 95 | Post post; 96 | post = TumblrFeedManager.deserializePostJson(postType, postJsonString); 97 | mPager.setVisibility(View.GONE); 98 | findViewById(R.id.content_frame).setVisibility(View.VISIBLE); 99 | getSupportFragmentManager().beginTransaction() 100 | .replace(R.id.content_frame, PostDetailFragment.newInstance(post)) 101 | .commit(); 102 | } else { 103 | // We want to load multiple posts in the viewpager 104 | mDetailPageAdapter = new PostDetailPagerAdapter(this, getSupportFragmentManager()); 105 | mPager.setAdapter(mDetailPageAdapter); 106 | 107 | mCurrentIndexPosition = getIntent().getIntExtra(EXTRA_CURRENT_PAGE_INDEX, 0); 108 | 109 | mProgressDialog = new ProgressDialog(PostDetailActivity.this); 110 | mProgressDialog.setTitle(R.string.fetching_posts); 111 | mProgressDialog.setIndeterminate(false); 112 | 113 | getSupportLoaderManager().initLoader(LOADER_ID_LOAD_POSTS, null, this); 114 | } 115 | } 116 | } 117 | 118 | @Override 119 | protected void onStart() { 120 | super.onStart(); 121 | 122 | // Uncomment the line below if targeting any API level less than API 14 123 | // FlurryAgent.onStartSession(this); 124 | } 125 | 126 | @Override 127 | protected void onStop() { 128 | super.onStop(); 129 | 130 | // Uncomment the line below if targeting any API level less than API 14 131 | // FlurryAgent.onEndSession(this); 132 | } 133 | 134 | @Override 135 | protected void onDestroy() { 136 | super.onDestroy(); 137 | 138 | mDetailPageAdapter.destroy(); 139 | } 140 | 141 | @Override 142 | public boolean onOptionsItemSelected(MenuItem item) { 143 | switch (item.getItemId()) { 144 | // Respond to the action bar's Up/Home button 145 | case android.R.id.home: 146 | if (getIntent().hasExtra(EXTRA_SINGLE_POST)) { 147 | /* 148 | This activity was not started from the declared parent 149 | Do not go up to the declared parent. 150 | */ 151 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 152 | finishAfterTransition(); 153 | } else { 154 | finish(); 155 | } 156 | return true; 157 | } 158 | 159 | } 160 | return super.onOptionsItemSelected(item); 161 | } 162 | 163 | @Override 164 | public Loader> onCreateLoader(int id, Bundle args) { 165 | switch (id) { 166 | case LOADER_ID_LOAD_POSTS: 167 | if (!mProgressDialog.isShowing()) { 168 | mProgressDialog.show(); 169 | } 170 | return new PostDataLoader(this); 171 | } 172 | return null; 173 | } 174 | 175 | @Override 176 | public void onLoadFinished(Loader> loader, List data) { 177 | switch (loader.getId()) { 178 | case LOADER_ID_LOAD_POSTS: 179 | if (mProgressDialog.isShowing()) { 180 | mProgressDialog.dismiss(); 181 | } 182 | 183 | mDetailPageAdapter.setBlogPosts(data); 184 | mPager.setCurrentItem(mCurrentIndexPosition, false); 185 | break; 186 | } 187 | } 188 | 189 | @Override 190 | public void onLoaderReset(Loader> loader) { 191 | if (loader.getId() == LOADER_ID_LOAD_POSTS) { 192 | mDetailPageAdapter.setBlogPosts(null); 193 | } 194 | } 195 | 196 | @Override 197 | public void onPostImagesSelected(List imagesToShow, View clickedImageView) { 198 | Intent intent = new Intent(this, ImageGalleryActivity.class); 199 | // Because Photo objects are not parcelable, we serialize to JSON to pass between activities 200 | String photoListJson = new Gson().toJson(imagesToShow); 201 | intent.putExtra(ImageGalleryActivity.EXTRA_PHOTO_LIST, photoListJson); 202 | 203 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 204 | ActivityOptions options = ActivityOptions 205 | .makeSceneTransitionAnimation(this, clickedImageView, "post_image"); 206 | startActivity(intent, options.toBundle()); 207 | } else { 208 | startActivity(intent); 209 | } 210 | } 211 | 212 | /** 213 | * Page transformer that applies a parallax effect to specified views in a ViewPager. 214 | * 215 | */ 216 | class ParallaxPageTransformer implements ViewPager.PageTransformer { 217 | private static final float PARALLAX_COEFFICIENT = -0.3f; 218 | private final int[] PARALLAX_LAYERS; 219 | 220 | ParallaxPageTransformer(int[] parallaxLayers) { 221 | this.PARALLAX_LAYERS = parallaxLayers; 222 | } 223 | 224 | @Override 225 | public void transformPage(View page, float position) { 226 | if (PARALLAX_LAYERS != null) { 227 | float coefficient = page.getWidth() * PARALLAX_COEFFICIENT; 228 | 229 | for (int viewId : PARALLAX_LAYERS) { 230 | View v = page.findViewById(viewId); 231 | if (v != null) { 232 | v.setTranslationX(coefficient * position); 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/PostDetailFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui; 18 | 19 | import android.app.Activity; 20 | import android.os.Bundle; 21 | import android.support.annotation.Nullable; 22 | import android.support.v4.app.Fragment; 23 | import android.text.TextUtils; 24 | import android.view.LayoutInflater; 25 | import android.view.MenuItem; 26 | import android.view.View; 27 | import android.view.ViewGroup; 28 | import android.widget.ImageButton; 29 | import android.widget.ImageView; 30 | import android.widget.TextView; 31 | 32 | import com.flurry.android.ads.FlurryAdNative; 33 | import com.flurry.android.ads.FlurryAdNativeAsset; 34 | import com.yahoo.mobile.client.android.yodel.R; 35 | import com.yahoo.mobile.client.android.yodel.feed.TumblrFeedManager; 36 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 37 | import com.yahoo.mobile.client.android.yodel.utils.DateTimeUtil; 38 | import com.yahoo.mobile.client.android.yodel.utils.ImageLoader; 39 | import com.yahoo.mobile.client.android.yodel.ui.widgets.SearchToolButton; 40 | import com.tumblr.jumblr.types.LinkPost; 41 | import com.tumblr.jumblr.types.Photo; 42 | import com.tumblr.jumblr.types.PhotoPost; 43 | import com.tumblr.jumblr.types.Post; 44 | import com.tumblr.jumblr.types.QuotePost; 45 | import com.tumblr.jumblr.types.TextPost; 46 | import com.tumblr.jumblr.types.VideoPost; 47 | 48 | import java.util.HashMap; 49 | import java.util.List; 50 | 51 | /** 52 | * @author ugo 53 | */ 54 | public class PostDetailFragment extends Fragment { 55 | 56 | private Post mPost; 57 | private FlurryAdNative mAdNative; 58 | private ImageLoader mImageLoader; 59 | private Callback mCallbackHandler; 60 | 61 | private TextView mPublisherTextView; 62 | private TextView mTitleTextView; 63 | private TextView mBodyTextView; 64 | private TextView mTagsTextView; 65 | private TextView mDateTextView; 66 | private ImageView mSponsoredImage; 67 | private ImageView mPostImageView; 68 | private ImageView mImagePlayButton; 69 | private SearchToolButton mSearchToolButton; 70 | private View mMediaContainer; 71 | private View mDividerLine; 72 | 73 | public static final String AD_ASSET_HEADLINE = "headline"; 74 | public static final String AD_ASSET_SUMMARY = "summary"; 75 | public static final String AD_ASSET_SOURCE = "source"; 76 | public static final String AD_ASSET_SEC_HQ_BRANDING_LOGO = "secHqBrandingLogo"; 77 | public static final String AD_ASSET_SEC_HQ_IMAGE = "secHqImage"; 78 | 79 | public static PostDetailFragment newInstance(Post post) { 80 | PostDetailFragment fragment = new PostDetailFragment(); 81 | fragment.mPost = post; 82 | return fragment; 83 | } 84 | 85 | public static PostDetailFragment newInstance(FlurryAdNative adNative) { 86 | PostDetailFragment fragment = new PostDetailFragment(); 87 | fragment.mAdNative = adNative; 88 | return fragment; 89 | } 90 | 91 | @Override 92 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 93 | Bundle savedInstanceState) { 94 | View rootView = inflater.inflate(R.layout.fragment_post_detail, container, false); 95 | 96 | mPublisherTextView = (TextView)rootView.findViewById(R.id.post_publisher); 97 | mTitleTextView = (TextView)rootView.findViewById(R.id.post_title); 98 | mBodyTextView = (TextView)rootView.findViewById(R.id.post_body); 99 | mTagsTextView = (TextView)rootView.findViewById(R.id.post_tags); 100 | mDateTextView = (TextView)rootView.findViewById(R.id.date_text); 101 | mSponsoredImage = (ImageView)rootView.findViewById(R.id.sponsored_image); 102 | mPostImageView = (ImageView)rootView.findViewById(R.id.post_image); 103 | mImagePlayButton = (ImageButton)rootView.findViewById(R.id.image_play_button); 104 | mSearchToolButton = (SearchToolButton)rootView.findViewById(R.id.search_tool_button); 105 | mMediaContainer = rootView.findViewById(R.id.media_container); 106 | mDividerLine = rootView.findViewById(R.id.divider_line); 107 | if (mAdNative != null) { 108 | mAdNative.setTrackingView(rootView); 109 | } 110 | 111 | return rootView; 112 | } 113 | 114 | @Override 115 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 116 | super.onActivityCreated(savedInstanceState); 117 | 118 | mImageLoader = ImageLoader.getInstance(); 119 | if (mPost != null) { 120 | loadData(); 121 | } else if (mAdNative != null) { 122 | loadAd(); 123 | } 124 | } 125 | 126 | @Override 127 | public void onAttach(Activity activity) { 128 | super.onAttach(activity); 129 | 130 | if (!(activity instanceof Callback)) { 131 | throw new IllegalStateException("Activity " 132 | + activity.getClass().getSimpleName() 133 | + " must implement PostDetailFragment.Callbacks"); 134 | } else { 135 | mCallbackHandler = (Callback)activity; 136 | } 137 | } 138 | 139 | @Override 140 | public boolean onOptionsItemSelected(MenuItem item) { 141 | switch (item.getItemId()) { 142 | // Respond to the action bar's Up/Home button 143 | case android.R.id.home: 144 | // Log the event 145 | HashMap eventParams = new HashMap<>(2); 146 | eventParams.put( 147 | AnalyticsHelper.PARAM_ARTICLE_ORIGIN, mPost.getBlogName()); 148 | eventParams.put(AnalyticsHelper.PARAM_ARTICLE_TYPE, mPost.getType()); 149 | AnalyticsHelper.logEvent( 150 | AnalyticsHelper.EVENT_CAR_MOREIMG_CLICK, eventParams, false); 151 | 152 | } 153 | return super.onOptionsItemSelected(item); 154 | } 155 | 156 | private void loadAd() { 157 | if (mAdNative != null && mAdNative.isReady()) { 158 | mAdNative.getAsset(AD_ASSET_SOURCE).loadAssetIntoView(mPublisherTextView); 159 | mAdNative.getAsset(AD_ASSET_HEADLINE).loadAssetIntoView(mTitleTextView); 160 | mAdNative.getAsset(AD_ASSET_SUMMARY).loadAssetIntoView(mBodyTextView); 161 | 162 | FlurryAdNativeAsset adNativeAsset = mAdNative.getAsset(AD_ASSET_SEC_HQ_BRANDING_LOGO); 163 | mImageLoader.displayImage(adNativeAsset.getValue(), mSponsoredImage); 164 | adNativeAsset = mAdNative.getAsset(AD_ASSET_SEC_HQ_IMAGE); 165 | mImageLoader.displayImage(adNativeAsset.getValue(), mPostImageView); 166 | /* 167 | The above lines could be replaced with the following. In that case, you would let the 168 | Flurry SDK handle ad image management for your app. However, this would prevent on-device 169 | caching. 170 | 171 | mAdNative.getAsset(AD_ASSET_SEC_HQ_BRANDING_LOGO).loadAssetIntoView(mSponsoredImage); 172 | mAdNative.getAsset(AD_ASSET_SEC_HQ_IMAGE).loadAssetIntoView(mPostImageView); 173 | */ 174 | 175 | mDateTextView.setText(R.string.sponsored_text); 176 | mPublisherTextView.setTextColor(getResources().getColor(R.color.sponsored_text_color)); 177 | mDividerLine.setBackgroundResource(R.color.sponsored_text_color); 178 | 179 | mSearchToolButton.setVisibility(View.GONE); 180 | } 181 | } 182 | 183 | private void loadData() { 184 | if (mPost != null) { 185 | String type = mPost.getType(); 186 | String blogName, title, body, photoUrl, postDate; 187 | int photoCount; 188 | blogName = mPost.getBlogName(); 189 | title = body = photoUrl = null; 190 | 191 | StringBuilder tags = new StringBuilder(); 192 | for (String tag : mPost.getTags()) { 193 | tags.append("#").append(tag).append(" "); 194 | } 195 | postDate = DateTimeUtil.getFriendlyDateString(mPost.getTimestamp() * 1000, getActivity()); 196 | 197 | switch (type) { 198 | case TumblrFeedManager.POST_TYPE_TEXT: 199 | title = ((TextPost)mPost).getTitle(); 200 | body = ((TextPost)mPost).getBody(); 201 | break; 202 | case TumblrFeedManager.POST_TYPE_QUOTE: 203 | body = ((QuotePost)mPost).getText() + "\n - " + ((QuotePost)mPost).getSource(); 204 | break; 205 | case TumblrFeedManager.POST_TYPE_PHOTO: 206 | body = ((PhotoPost)mPost).getCaption(); 207 | // It can't be a photo post without photos. Either way... 208 | photoCount = ((PhotoPost)mPost).getPhotos().size(); 209 | if (photoCount > 0) { 210 | photoUrl = ((PhotoPost) mPost).getPhotos().get(0).getOriginalSize().getUrl(); 211 | } 212 | break; 213 | case TumblrFeedManager.POST_TYPE_VIDEO: 214 | body = ((VideoPost)mPost).getCaption(); 215 | photoUrl = ((VideoPost)mPost).getThumbnailUrl(); 216 | break; 217 | case TumblrFeedManager.POST_TYPE_LINK: 218 | title = ((LinkPost)mPost).getTitle(); 219 | body = ((LinkPost)mPost).getDescription() + "\n" + ((LinkPost)mPost).getLinkUrl(); 220 | break; 221 | } 222 | 223 | mPublisherTextView.setText(blogName); 224 | if (title != null) { 225 | mTitleTextView.setVisibility(View.VISIBLE); 226 | mTitleTextView.setText(title); 227 | } else { 228 | mTitleTextView.setVisibility(View.GONE); 229 | } 230 | if (body != null) { 231 | mBodyTextView.setVisibility(View.VISIBLE); 232 | mBodyTextView.setText(body); 233 | } else { 234 | mBodyTextView.setVisibility(View.GONE); 235 | } 236 | if (!TextUtils.isEmpty(tags)) { 237 | mTagsTextView.setVisibility(View.VISIBLE); 238 | mTagsTextView.setText(tags); 239 | } 240 | mDateTextView.setText(postDate); 241 | if (photoUrl != null) { 242 | mMediaContainer.setVisibility(View.VISIBLE); 243 | mPostImageView.setVisibility(View.VISIBLE); 244 | mImagePlayButton.setVisibility(View.VISIBLE); 245 | mImageLoader.displayImage(photoUrl, mPostImageView); 246 | if (type.equals(TumblrFeedManager.POST_TYPE_PHOTO)) { 247 | mImagePlayButton.setImageResource(R.drawable.ic_gallery); 248 | mImagePlayButton.setOnClickListener(new View.OnClickListener() { 249 | @Override 250 | public void onClick(View v) { 251 | HashMap eventParams = new HashMap<>(2); 252 | eventParams.put( 253 | AnalyticsHelper.PARAM_ARTICLE_ORIGIN, mPost.getBlogName()); 254 | eventParams.put(AnalyticsHelper.PARAM_ARTICLE_TYPE, mPost.getType()); 255 | AnalyticsHelper.logEvent( 256 | AnalyticsHelper.EVENT_CAR_MOREIMG_CLICK, eventParams, false); 257 | 258 | mCallbackHandler.onPostImagesSelected( 259 | ((PhotoPost) mPost).getPhotos(), mPostImageView); 260 | 261 | } 262 | }); 263 | } else { 264 | mImagePlayButton.setImageResource(R.drawable.ic_video); 265 | } 266 | } else { 267 | mMediaContainer.setVisibility(View.GONE); 268 | mPostImageView.setVisibility(View.GONE); 269 | mImagePlayButton.setVisibility(View.GONE); 270 | } 271 | 272 | if (mPost.getTags().size() > 0) { 273 | mSearchToolButton.setSearchTerm(mPost.getTags().get(0)); 274 | } else { 275 | mSearchToolButton.setVisibility(View.GONE); 276 | } 277 | } 278 | } 279 | 280 | public interface Callback { 281 | void onPostImagesSelected(List photosToShow, View clickedImageView); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/PostListFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui; 18 | 19 | import android.app.Activity; 20 | import android.app.AlertDialog; 21 | import android.app.ProgressDialog; 22 | import android.content.DialogInterface; 23 | import android.content.Intent; 24 | import android.content.res.TypedArray; 25 | import android.os.Bundle; 26 | import android.support.annotation.Nullable; 27 | import android.support.v4.app.ListFragment; 28 | import android.support.v4.app.LoaderManager; 29 | import android.support.v4.content.Loader; 30 | import android.support.v4.widget.SwipeRefreshLayout; 31 | import android.util.Log; 32 | import android.view.LayoutInflater; 33 | import android.view.View; 34 | import android.view.ViewGroup; 35 | import android.widget.ListView; 36 | 37 | import com.yahoo.mobile.client.android.yodel.R; 38 | import com.yahoo.mobile.client.android.yodel.ui.widgets.adapters.PostSearchListAdapter; 39 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 40 | import com.yahoo.mobile.client.android.yodel.utils.PostDataLoader; 41 | import com.yahoo.mobile.client.android.yodel.ui.widgets.adapters.PostListAdapter; 42 | import com.tumblr.jumblr.types.Post; 43 | import com.yahoo.mobile.client.share.search.ui.activity.SearchActivity; 44 | 45 | import java.util.HashMap; 46 | import java.util.List; 47 | 48 | /** 49 | * The list fragment containing Tumblr blog posts. 50 | */ 51 | public class PostListFragment extends ListFragment 52 | implements LoaderManager.LoaderCallbacks> { 53 | 54 | private final static String LOG_TAG = PostListFragment.class.getSimpleName(); 55 | 56 | private List mBlogPostList = null; 57 | private PostListAdapter mPostListAdapter = null; 58 | private PostSearchListAdapter mPostSearchListAdapter = null; 59 | private SwipeRefreshLayout mSwipeView = null; 60 | private AlertDialog mAlertDialog = null; 61 | private Callbacks mCallbackHandler; 62 | private String mTagQuery; 63 | 64 | private ProgressDialog mProgressDialog; 65 | 66 | private final static int LOADER_ID_LOAD_RECENT_POSTS = 0x1; 67 | private final static int LOADER_ID_LOAD_POSTS_WITH_TAGS = 0x2; 68 | private final static String EXTRA_TAG_QUERY = "com.yahoo.mobile.sample.extra.tagquery"; 69 | 70 | static PostListFragment newInstance() { 71 | return new PostListFragment(); 72 | } 73 | 74 | static PostListFragment newInstance(String tagQuery) { 75 | PostListFragment fragment = new PostListFragment(); 76 | Bundle bundle = new Bundle(); 77 | bundle.putString(EXTRA_TAG_QUERY, tagQuery); 78 | fragment.setArguments(bundle); 79 | 80 | return fragment; 81 | } 82 | 83 | @Override 84 | public void onCreate(Bundle savedInstanceState) { 85 | super.onCreate(savedInstanceState); 86 | 87 | if (savedInstanceState == null) { 88 | Bundle args = getArguments(); 89 | if (args != null) { 90 | mTagQuery = args.getString(EXTRA_TAG_QUERY); 91 | } 92 | } 93 | } 94 | 95 | @Override 96 | public View onCreateView(LayoutInflater inflater, ViewGroup container, 97 | Bundle savedInstanceState) { 98 | View rootView = inflater.inflate(R.layout.fragment_main, container, false); 99 | 100 | mSwipeView = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe); 101 | 102 | mSwipeView.setColorSchemeResources(R.color.accent_color, R.color.red_transparent, 103 | R.color.purple_transparent, R.color.y_blue); 104 | mSwipeView.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 105 | @Override 106 | public void onRefresh() { 107 | // Log a timed event 108 | AnalyticsHelper.logEvent(AnalyticsHelper.EVENT_STREAM_PULL_REFRESH, null, true); 109 | 110 | mSwipeView.setRefreshing(true); 111 | refreshPosts(); 112 | } 113 | }); 114 | // Create a reusable progress dialog 115 | createProgressDialog(); 116 | 117 | return rootView; 118 | } 119 | 120 | @Override 121 | public void onActivityCreated(@Nullable Bundle savedInstanceState) { 122 | super.onActivityCreated(savedInstanceState); 123 | 124 | if (mTagQuery == null) { 125 | mPostListAdapter = new PostListAdapter(getActivity()); 126 | setListAdapter(mPostListAdapter); 127 | } else { 128 | View footer = getActivity().getLayoutInflater() 129 | .inflate(R.layout.view_more_web_footer, getListView(), false); 130 | mPostSearchListAdapter = new PostSearchListAdapter(getActivity()); 131 | getListView().addFooterView(footer); 132 | setListAdapter(mPostSearchListAdapter); 133 | } 134 | 135 | // If blog post list wasn't persisted during config change or is just empty... 136 | if (mBlogPostList == null || mBlogPostList.size() > 0) { 137 | refreshPosts(); 138 | } 139 | 140 | /* 141 | Account for the overlay Toolbar. Warning: makes it hard to re-use this fragment 142 | (which is the whole point of fragments, isn't it?) 143 | */ 144 | final TypedArray styledAttributes = getActivity().getTheme().obtainStyledAttributes( 145 | new int[] { android.R.attr.actionBarSize }); 146 | int actionBarSize = (int) styledAttributes.getDimension(0, 0); 147 | styledAttributes.recycle(); 148 | int mediumViewMargin = getResources().getDimensionPixelSize(R.dimen.view_margin_medium); 149 | int lvTopPadding = mediumViewMargin + 150 | actionBarSize; 151 | getListView().setPadding(0, lvTopPadding, 0, mediumViewMargin); 152 | mSwipeView.setProgressViewOffset(true, mediumViewMargin, lvTopPadding); 153 | } 154 | 155 | @Override 156 | public void onAttach(Activity activity) { 157 | super.onAttach(activity); 158 | 159 | if (!(activity instanceof Callbacks)) { 160 | throw new IllegalStateException("Activity " 161 | + activity.getClass().getSimpleName() 162 | + " must implement PostListFragment.Callbacks"); 163 | } else { 164 | mCallbackHandler = (Callbacks)activity; 165 | } 166 | } 167 | 168 | @Override 169 | public void onDetach() { 170 | super.onDetach(); 171 | mCallbackHandler = null; 172 | } 173 | 174 | @Override 175 | public void onPause() { 176 | super.onPause(); 177 | 178 | if (mProgressDialog != null && mProgressDialog.isShowing()) { 179 | mProgressDialog.dismiss(); 180 | } 181 | } 182 | 183 | @Override 184 | public void onDestroyView() { 185 | super.onDestroyView(); 186 | 187 | setListAdapter(null); 188 | 189 | if (mPostListAdapter != null) { 190 | mPostListAdapter.destroy(); 191 | mPostListAdapter = null; 192 | } 193 | 194 | if (mAlertDialog != null && mAlertDialog.isShowing()) { 195 | mAlertDialog.dismiss(); 196 | } 197 | } 198 | 199 | @Override 200 | public void onListItemClick(ListView listView, View clickedView, int position, long id) { 201 | if (mCallbackHandler != null) { 202 | if (mTagQuery == null) { 203 | mCallbackHandler.onPostSelected( 204 | (Post) mPostListAdapter.getItem(position), position, clickedView); 205 | } else { 206 | if (position + 1 > mPostSearchListAdapter.getCount()) { 207 | // The search footer was clicked. Log the event and open the Search SDK 208 | HashMap eventParam = new HashMap<>(1); 209 | eventParam.put(AnalyticsHelper.PARAM_SEARCH_TERM, mTagQuery); 210 | AnalyticsHelper.logEvent( 211 | AnalyticsHelper.EVENT_SEARCH_MOREONWEB_CLICK, eventParam, false); 212 | 213 | Intent i = new Intent(getActivity(), SearchActivity.class); 214 | i.putExtra(SearchActivity.QUERY_STRING, mTagQuery); 215 | i.putExtra(SearchActivity.HEADER_RESOURCE_KEY, R.layout.view_ysearch_header); 216 | getActivity().startActivity(i); 217 | } else { 218 | mCallbackHandler.onPostSelected( 219 | (Post) mPostSearchListAdapter.getItem(position), position, clickedView); 220 | } 221 | } 222 | } 223 | } 224 | 225 | @Override 226 | public Loader> onCreateLoader(int id, Bundle args) { 227 | showProgressDialog(); 228 | switch (id) { 229 | case LOADER_ID_LOAD_RECENT_POSTS: 230 | return new PostDataLoader(getActivity()); 231 | case LOADER_ID_LOAD_POSTS_WITH_TAGS: 232 | return new PostDataLoader(getActivity(), mTagQuery); 233 | } 234 | return null; 235 | } 236 | 237 | @Override 238 | public void onLoadFinished(Loader> loader, List data) { 239 | hideProgressDialog(); 240 | mBlogPostList = data; 241 | boolean contentLoaded = false; 242 | 243 | switch (loader.getId()) { 244 | case LOADER_ID_LOAD_RECENT_POSTS: 245 | if (data != null && data.size() > 0) { 246 | contentLoaded = true; 247 | mPostListAdapter.setBlogPosts(mBlogPostList); 248 | } else if (data == null) { 249 | // Network is unavailable 250 | showAlertDialog(); 251 | } 252 | break; 253 | case LOADER_ID_LOAD_POSTS_WITH_TAGS: 254 | if (data != null && data.size() > 0) { 255 | contentLoaded = true; 256 | mPostSearchListAdapter.setBlogPosts(mBlogPostList); 257 | } else if (data == null) { 258 | // Network is unavailable 259 | showAlertDialog(); 260 | } 261 | break; 262 | } 263 | if (mSwipeView.isRefreshing()) { 264 | HashMap eventParams = new HashMap<>(1); 265 | eventParams.put(AnalyticsHelper.PARAM_CONTENT_LOADED, String.valueOf(contentLoaded)); 266 | AnalyticsHelper.endTimedEvent(AnalyticsHelper.EVENT_STREAM_PULL_REFRESH, eventParams); 267 | mSwipeView.setRefreshing(false); 268 | } 269 | } 270 | 271 | @Override 272 | public void onLoaderReset(Loader> loader) { 273 | switch (loader.getId()) { 274 | case LOADER_ID_LOAD_RECENT_POSTS: 275 | if (mPostListAdapter != null) { 276 | mPostListAdapter.setBlogPosts(null); 277 | } 278 | break; 279 | case LOADER_ID_LOAD_POSTS_WITH_TAGS: 280 | if (mPostSearchListAdapter != null) { 281 | mPostSearchListAdapter.setBlogPosts(null); 282 | } 283 | } 284 | } 285 | 286 | private void refreshPosts() { 287 | Log.d(LOG_TAG, "Refreshing post list..."); 288 | if (mTagQuery == null) { 289 | getActivity().getSupportLoaderManager() 290 | .restartLoader(LOADER_ID_LOAD_RECENT_POSTS, null, this); 291 | } else { 292 | getActivity().getSupportLoaderManager() 293 | .restartLoader(LOADER_ID_LOAD_POSTS_WITH_TAGS, null, this); 294 | } 295 | } 296 | 297 | public void createProgressDialog() { 298 | mProgressDialog = new ProgressDialog(getActivity()); 299 | mProgressDialog.setTitle(R.string.fetching_posts); 300 | mProgressDialog.setIndeterminate(false); 301 | mProgressDialog.setCancelable(false); 302 | } 303 | 304 | public void showProgressDialog(){ 305 | if (mProgressDialog != null && !mProgressDialog.isShowing()) { 306 | mProgressDialog.show(); 307 | } 308 | } 309 | 310 | public void hideProgressDialog(){ 311 | if (mProgressDialog != null && mProgressDialog.isShowing()) { 312 | mProgressDialog.hide(); 313 | } 314 | } 315 | 316 | public void showAlertDialog(){ 317 | MainActivity activity = ((MainActivity) getActivity()); 318 | if (activity == null) { 319 | return; 320 | } 321 | AlertDialog.Builder alertDialog = new AlertDialog.Builder(activity); 322 | alertDialog.setMessage(R.string.network_not_available); 323 | alertDialog.setCancelable(true); 324 | alertDialog.setPositiveButton(R.string.retry_button, 325 | new DialogInterface.OnClickListener() { 326 | public void onClick(DialogInterface dialog, int id) { 327 | refreshPosts(); 328 | dialog.dismiss(); 329 | } 330 | }); 331 | alertDialog.setNegativeButton(R.string.cancel_button, 332 | new DialogInterface.OnClickListener() { 333 | public void onClick(DialogInterface dialog, int id) { 334 | dialog.dismiss(); 335 | } 336 | }); 337 | 338 | mAlertDialog = alertDialog.create(); 339 | mAlertDialog.show(); 340 | } 341 | 342 | public interface Callbacks { 343 | void onPostSelected(Post post, int positionId, View clickedView); 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/PostSearchActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui; 18 | 19 | import android.app.ActivityOptions; 20 | import android.app.SearchManager; 21 | import android.content.Intent; 22 | import android.os.Build; 23 | import android.os.Bundle; 24 | import android.support.v7.app.ActionBarActivity; 25 | import android.support.v7.widget.Toolbar; 26 | import android.text.Html; 27 | import android.view.View; 28 | 29 | import com.google.gson.Gson; 30 | import com.yahoo.mobile.client.android.yodel.R; 31 | 32 | import com.tumblr.jumblr.types.Post; 33 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 34 | 35 | import java.util.HashMap; 36 | 37 | public class PostSearchActivity extends ActionBarActivity implements PostListFragment.Callbacks { 38 | @Override 39 | protected void onCreate(Bundle savedInstanceState) { 40 | super.onCreate(savedInstanceState); 41 | setContentView(R.layout.activity_main); 42 | 43 | Toolbar toolbar = (Toolbar)findViewById(R.id.toolbar_actionbar); 44 | setSupportActionBar(toolbar); 45 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 46 | 47 | handleIntent(getIntent()); 48 | } 49 | 50 | private void handleIntent(Intent intent) { 51 | if (Intent.ACTION_SEARCH.equals(intent.getAction())) { 52 | String searchQuery = intent.getStringExtra(SearchManager.QUERY); 53 | 54 | HashMap eventParam = new HashMap<>(1); 55 | eventParam.put(AnalyticsHelper.PARAM_SEARCH_TERM, searchQuery); 56 | AnalyticsHelper.logEvent(AnalyticsHelper.EVENT_SEARCH_STARTED, eventParam, false); 57 | 58 | doTumblrTagSearch(searchQuery); 59 | setTitle(Html.fromHtml(getString(R.string.title_search_query, searchQuery))); 60 | } 61 | } 62 | 63 | private void doTumblrTagSearch(String searchQuery) { 64 | getSupportFragmentManager().beginTransaction() 65 | .replace(R.id.content_frame, PostListFragment.newInstance(searchQuery)) 66 | .commit(); 67 | } 68 | 69 | @Override 70 | public void onPostSelected(Post post, int positionId, View clickedView) { 71 | // Log the event 72 | HashMap eventParams = new HashMap<>(2); 73 | eventParams.put(AnalyticsHelper.PARAM_ARTICLE_ORIGIN, post.getBlogName()); 74 | eventParams.put(AnalyticsHelper.PARAM_ARTICLE_TYPE, post.getType()); 75 | AnalyticsHelper.logEvent(AnalyticsHelper.EVENT_SEARCH_RESULT_CLICK, eventParams, false); 76 | 77 | /* 78 | To avoid a StackOverflowError from a circular reference during serialization, set the 79 | client of the post to null. 80 | */ 81 | post.setClient(null); 82 | 83 | String postJson = new Gson().toJson(post); 84 | Intent intent = new Intent(this, PostDetailActivity.class); 85 | intent.putExtra(PostDetailActivity.EXTRA_SINGLE_POST, postJson); 86 | intent.putExtra(PostDetailActivity.EXTRA_SINGLE_POST_TYPE, post.getType()); 87 | 88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 89 | View sharedImageElement = clickedView.findViewById(R.id.post_image); 90 | ActivityOptions options = ActivityOptions 91 | .makeSceneTransitionAnimation(this, sharedImageElement, "post_image"); 92 | startActivity(intent, options.toBundle()); 93 | } else { 94 | startActivity(intent); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/AspectRatioImageView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.util.AttributeSet; 22 | import android.widget.ImageView; 23 | 24 | import com.yahoo.mobile.client.android.yodel.R; 25 | 26 | 27 | /** 28 | * ImageView that respects a proportional relationship between its width and its height, regardless 29 | * of drawable size and regardless of screen size. 30 | */ 31 | public class AspectRatioImageView extends ImageView { 32 | 33 | private float mAspectRatioWidth; 34 | private float mAspectRationHeight; 35 | private boolean mWidthAsBase; 36 | 37 | public AspectRatioImageView(Context context, AttributeSet attrs) { 38 | super(context, attrs); 39 | 40 | TypedArray a = context.getTheme().obtainStyledAttributes( 41 | attrs, R.styleable.AspectRatioImageView, 42 | 0, 0); 43 | 44 | if (a != null) { 45 | // Defaults to a 4:3 ratio 46 | setAspectRatioWidth(a.getInt(R.styleable.AspectRatioImageView_aspectRatioWidth, 4)); 47 | setAspectRationHeight(a.getInt(R.styleable.AspectRatioImageView_aspectRatioHeight, 3)); 48 | 49 | setWidthAsBase(a.getBoolean(R.styleable.AspectRatioImageView_widthAsBase, true)); 50 | 51 | a.recycle(); 52 | } 53 | } 54 | 55 | @Override 56 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 57 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 58 | 59 | if (isWidthAsBase()) { 60 | int newHeight = (int)(getMeasuredWidth() * getAspectRationHeight() / getAspectRatioWidth()); 61 | setMeasuredDimension(getMeasuredWidth(), newHeight); 62 | } else { 63 | int newWidth = (int)(getMeasuredHeight() * getAspectRatioWidth() / getAspectRationHeight()); 64 | setMeasuredDimension(newWidth, getMeasuredHeight()); 65 | } 66 | } 67 | 68 | public float getAspectRatioWidth() { 69 | return mAspectRatioWidth; 70 | } 71 | 72 | public void setAspectRatioWidth(int aspectRatioWidth) { 73 | mAspectRatioWidth = aspectRatioWidth; 74 | } 75 | 76 | public float getAspectRationHeight() { 77 | return mAspectRationHeight; 78 | } 79 | 80 | public void setAspectRationHeight(int aspectRationHeight) { 81 | mAspectRationHeight = aspectRationHeight; 82 | } 83 | 84 | public boolean isWidthAsBase() { 85 | return mWidthAsBase; 86 | } 87 | 88 | public void setWidthAsBase(boolean widthAsBase) { 89 | mWidthAsBase = widthAsBase; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/CaptionViewPagerIndicator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.widget.LinearLayout; 24 | import android.widget.TextView; 25 | 26 | import com.yahoo.mobile.client.android.yodel.R; 27 | 28 | public class CaptionViewPagerIndicator extends LinearLayout { 29 | 30 | private TextView mCaptionTextView; 31 | private TextView mPageIndicatorTextView; 32 | 33 | public CaptionViewPagerIndicator(Context context) { 34 | this(context, null); 35 | } 36 | 37 | public CaptionViewPagerIndicator(Context context, AttributeSet attrs) { 38 | this(context, attrs, 0); 39 | } 40 | 41 | 42 | public CaptionViewPagerIndicator(Context context, AttributeSet attrs, int defStyleAttr) { 43 | super(context, attrs, defStyleAttr); 44 | 45 | LayoutInflater layoutInflater = (LayoutInflater)context 46 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 47 | View root = layoutInflater.inflate(R.layout.view_caption_pager_indicator, this); 48 | mCaptionTextView = (TextView)root.findViewById(R.id.image_caption_text); 49 | mPageIndicatorTextView = (TextView)root.findViewById(R.id.page_indicator_text); 50 | } 51 | 52 | public void setCurrentItem(int itemIndex, int total) { 53 | mPageIndicatorTextView.setText(getResources().getString(R.string.view_pager_indicator, 54 | itemIndex + 1, total)); 55 | } 56 | 57 | public void setCaption(CharSequence caption) { 58 | mCaptionTextView.setText(caption); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/CustomSearchViewHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.view.View; 22 | import android.widget.EditText; 23 | import android.widget.LinearLayout; 24 | 25 | import com.yahoo.mobile.client.android.yodel.R; 26 | import com.yahoo.mobile.client.share.search.interfaces.ISearchController; 27 | import com.yahoo.mobile.client.share.search.interfaces.ISearchViewHolder; 28 | 29 | /** 30 | *

A custom search view holder to be used as header in Yahoo Search SDK.

31 | *

See Yahoo Search SDK documentation for more.

32 | */ 33 | public class CustomSearchViewHolder extends LinearLayout implements ISearchViewHolder { 34 | 35 | public CustomSearchViewHolder(Context context) { 36 | super(context); 37 | } 38 | 39 | public CustomSearchViewHolder(Context context, AttributeSet attrs) { 40 | super(context, attrs); 41 | } 42 | 43 | public CustomSearchViewHolder(Context context, AttributeSet attrs, int defStyleAttr) { 44 | super(context, attrs, defStyleAttr); 45 | } 46 | 47 | @Override 48 | public void setSearchController(ISearchController iSearchController) { 49 | 50 | } 51 | 52 | @Override 53 | public EditText getSearchEditText() { 54 | return (EditText)findViewById(R.id.search_bar_edit_text); 55 | } 56 | 57 | @Override 58 | public View getVoiceSearchButton() { 59 | return null; 60 | } 61 | 62 | @Override 63 | public View getClearTextButton() { 64 | return findViewById(R.id.search_bar_clear_icon); 65 | } 66 | 67 | @Override 68 | public int getSearchViewHeightOffset() { 69 | return 0; 70 | } 71 | 72 | @Override 73 | public void onVoiceSearchAvailabilityChanged(boolean b) { 74 | 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/SearchToolButton.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets; 18 | 19 | import android.annotation.TargetApi; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.os.Build; 23 | import android.text.Html; 24 | import android.util.AttributeSet; 25 | import android.view.LayoutInflater; 26 | import android.view.View; 27 | import android.widget.LinearLayout; 28 | import android.widget.TextView; 29 | 30 | import com.yahoo.mobile.client.android.yodel.R; 31 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 32 | import com.yahoo.mobile.client.share.search.ui.activity.SearchActivity; 33 | 34 | import java.util.HashMap; 35 | 36 | public class SearchToolButton extends LinearLayout implements View.OnClickListener { 37 | 38 | private TextView mSearchTermTextView; 39 | private View mContainer; 40 | private CharSequence mSearchTerm; 41 | 42 | public SearchToolButton(Context context, AttributeSet attrs) { 43 | super(context, attrs); 44 | 45 | LayoutInflater layoutInflater = (LayoutInflater)context 46 | .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 47 | View root = layoutInflater.inflate(R.layout.view_search_tool_button, this); 48 | mSearchTermTextView = (TextView)root.findViewById(R.id.search_term_text); 49 | mContainer = root.findViewById(R.id.search_button_container); 50 | 51 | this.setOnClickListener(this); 52 | } 53 | 54 | public void setSearchTerm(CharSequence searchTerm) { 55 | mSearchTerm = searchTerm; 56 | mSearchTermTextView.setText(Html.fromHtml(getContext().getResources() 57 | .getString(R.string.search_learn_more_prompt, searchTerm))); 58 | } 59 | 60 | public CharSequence getSearchTerm() { 61 | return mSearchTerm; 62 | } 63 | 64 | @Override 65 | public void onClick(View v) { 66 | HashMap eventParam = new HashMap<>(1); 67 | eventParam.put(AnalyticsHelper.PARAM_SEARCH_TERM, getSearchTerm().toString()); 68 | AnalyticsHelper.logEvent(AnalyticsHelper.EVENT_CAR_LEARNMORE_CLICK, eventParam, false); 69 | 70 | Intent i = new Intent(getContext(), 71 | SearchActivity.class); 72 | i.putExtra(SearchActivity.QUERY_STRING, getSearchTerm()); 73 | i.putExtra(SearchActivity.HEADER_RESOURCE_KEY, R.layout.view_ysearch_header); 74 | getContext().startActivity(i); 75 | } 76 | 77 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 78 | @Override 79 | public void drawableHotspotChanged(float x, float y) { 80 | super.drawableHotspotChanged(x, y); 81 | 82 | if (mContainer.getBackground() != null) { 83 | mContainer.getBackground().setHotspot(x, y); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/adapters/BaseAdAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets.adapters; 18 | 19 | import android.content.Context; 20 | import android.view.View; 21 | 22 | import com.flurry.android.ads.FlurryAdNative; 23 | import com.flurry.android.ads.FlurryAdNativeAsset; 24 | import com.yahoo.mobile.client.android.yodel.NativeAdFetcher; 25 | 26 | import java.util.List; 27 | 28 | /** 29 | * Adapter that has common functionality for any adapters that need to show ads in-between 30 | * other data. 31 | */ 32 | abstract class BaseAdAdapter implements NativeAdFetcher.AdNativeListener { 33 | 34 | private final static int DEFAULT_NO_OF_DATA_BETWEEN_ADS = 3; 35 | private int mNoOfDataBetweenAds; 36 | private volatile List mData = null; 37 | private NativeAdFetcher mAdFetcher; 38 | 39 | protected BaseAdAdapter(Context context) { 40 | setNoOfDataBetweenAds(DEFAULT_NO_OF_DATA_BETWEEN_ADS); // Default 41 | this.mAdFetcher = new NativeAdFetcher(); 42 | mAdFetcher.addListener(this); 43 | // Start prefetching ads 44 | mAdFetcher.prefetchAds(context); 45 | } 46 | 47 | 48 | @Override 49 | public abstract void onAdCountChanged(); 50 | 51 | /** 52 | *

Gets the count of all data, including interspersed ads.

53 | * 54 | *

If data size is 10 and an ad is to be showed after every 5 items, this method 55 | * will return 12.

56 | * 57 | * @see BaseAdAdapter#setNoOfDataBetweenAds(int) 58 | * @see BaseAdAdapter#getNoOfDataBetweenAds() 59 | * @return the total number of items this adapter can show, including ads. 60 | */ 61 | protected int getCount() { 62 | if (mData != null) { 63 | /* 64 | No of currently fetched ads, as long as it isn't more than no of max ads that can 65 | fit dataset. 66 | */ 67 | int noOfAds = Math.min(mAdFetcher.getFetchedAdsCount(), 68 | mData.size() / getNoOfDataBetweenAds()); 69 | return mData.size() > 0 ? mData.size() + noOfAds : 0; 70 | } else { 71 | return 0; 72 | } 73 | } 74 | 75 | /** 76 | * Gets the item in a given position in the dataset. If an ad is to be returned, 77 | * a {@link com.flurry.android.ads.FlurryAdNative} object is returned. 78 | * 79 | * @param position the adapter position 80 | * @return the object or ad contained in this adapter position 81 | */ 82 | protected Object getItem(int position) { 83 | if (canShowAdAtPosition(position)) { 84 | return mAdFetcher.getAdForIndex(getAdIndex(position)); 85 | } else { 86 | return mData.get(getOriginalContentPosition(position)); 87 | } 88 | } 89 | 90 | /** 91 | * Translates an adapter position to an actual position within the underlying dataset. 92 | * 93 | * @param position the adapter position 94 | * @return the original position that the adapter position would have been without ads 95 | */ 96 | protected int getOriginalContentPosition(int position) { 97 | int noOfFetchedAds = mAdFetcher.getFetchedAdsCount(); 98 | // No of spaces for ads in the dataset, according to ad placement rules 99 | int adSpacesCount = position / (getNoOfDataBetweenAds() + 1); 100 | return position - Math.min(adSpacesCount, noOfFetchedAds); 101 | } 102 | 103 | /** 104 | * Determines if an ad can be shown at the given position. Checks if the position is for 105 | * an ad, using the preconfigured ad positioning rules; and if a native ad object is 106 | * available to place in that position. 107 | * 108 | * @param position the adapter position 109 | * @return true if ads can 110 | */ 111 | protected boolean canShowAdAtPosition(int position) { 112 | 113 | // Is this a valid position for an ad? 114 | boolean isAdPosition = isAdPosition(position); 115 | // Is an ad for this position available? 116 | boolean isAdAvailable = isAdAvailable(position); 117 | 118 | return isAdPosition && isAdAvailable; 119 | } 120 | 121 | /** 122 | * Destroys all currently fetched ads 123 | */ 124 | protected void destroy() { 125 | mAdFetcher.destroyAllAds(); 126 | } 127 | 128 | /** 129 | * Loads an ad asset into a given view. 130 | * 131 | * @param adNative the {@link com.flurry.android.ads.FlurryAdNative} containing the ad asset 132 | * @param assetName the name of the asset 133 | * @param view the {@link View} to load the asset into 134 | */ 135 | protected void loadAdAssetInView(FlurryAdNative adNative, String assetName, View view) { 136 | FlurryAdNativeAsset adNativeAsset = adNative.getAsset(assetName); 137 | if (adNativeAsset != null) { 138 | adNativeAsset.loadAssetIntoView(view); 139 | } else { 140 | view.setVisibility(View.GONE); 141 | } 142 | } 143 | 144 | /** 145 | * Sets the underlying dataset. 146 | * 147 | * @param data the dataset 148 | */ 149 | protected void setData(List data) { 150 | mData = data; 151 | } 152 | 153 | /** 154 | * Gets the number of data items to show between ads. 155 | * 156 | * @return the number of data items to show between ads 157 | */ 158 | protected int getNoOfDataBetweenAds() { 159 | return mNoOfDataBetweenAds; 160 | } 161 | 162 | /** 163 | * Sets the number of data items to show between ads. 164 | * 165 | * @param noOfDataBetweenAds the number of data items to show between ads 166 | */ 167 | protected void setNoOfDataBetweenAds(int noOfDataBetweenAds) { 168 | mNoOfDataBetweenAds = noOfDataBetweenAds; 169 | } 170 | 171 | /** 172 | * Gets the ad index for this adapter position within the list of currently fetched ads. 173 | * 174 | * @param position the adapter position 175 | * @return the index of the ad within the list of fetched ads 176 | */ 177 | private int getAdIndex(int position) { 178 | return (position / getNoOfDataBetweenAds()) - 1; 179 | } 180 | 181 | /** 182 | * Checks if adapter position is an ad position. 183 | * 184 | * @param position the adapter position 185 | * @return {@code true} if an ad position, {@code false} otherwise 186 | */ 187 | private boolean isAdPosition(int position) { 188 | return (position + 1) % (getNoOfDataBetweenAds() + 1) == 0; 189 | } 190 | 191 | /** 192 | * Checks if an ad is available for this position. 193 | * 194 | * @param position the adapter position 195 | * @return {@code true} if an ad is available, {@code false} otherwise 196 | */ 197 | private boolean isAdAvailable(int position) { 198 | int adIndex = getAdIndex(position); 199 | return position >= getNoOfDataBetweenAds() 200 | && adIndex >= 0 201 | && mAdFetcher.getFetchedAdsCount() > adIndex; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/adapters/GalleryPagerAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets.adapters; 18 | 19 | import android.support.v4.app.Fragment; 20 | import android.support.v4.app.FragmentManager; 21 | import android.support.v4.app.FragmentStatePagerAdapter; 22 | 23 | import com.yahoo.mobile.client.android.yodel.ui.FullImageFragment; 24 | import com.tumblr.jumblr.types.Photo; 25 | 26 | import java.util.List; 27 | 28 | public class GalleryPagerAdapter extends FragmentStatePagerAdapter { 29 | 30 | private List mPhotos; 31 | 32 | public GalleryPagerAdapter(FragmentManager fm, List photos) { 33 | super(fm); 34 | 35 | this.mPhotos = photos; 36 | } 37 | 38 | @Override 39 | public Fragment getItem(int position) { 40 | return FullImageFragment.newInstance(mPhotos.get(position).getOriginalSize().getUrl()); 41 | } 42 | 43 | @Override 44 | public int getCount() { 45 | return mPhotos != null ? mPhotos.size() : 0; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/adapters/PostDetailPagerAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets.adapters; 18 | 19 | import android.content.Context; 20 | import android.support.v4.app.Fragment; 21 | import android.support.v4.app.FragmentManager; 22 | import android.support.v4.app.FragmentStatePagerAdapter; 23 | 24 | import com.flurry.android.ads.FlurryAdNative; 25 | import com.yahoo.mobile.client.android.yodel.ui.PostDetailFragment; 26 | import com.tumblr.jumblr.types.Post; 27 | 28 | import java.util.List; 29 | 30 | public class PostDetailPagerAdapter extends FragmentStatePagerAdapter { 31 | private BaseAdAdapter mAdAdapter; 32 | 33 | public PostDetailPagerAdapter(Context context, FragmentManager fm) { 34 | super(fm); 35 | mAdAdapter = new BaseAdAdapter(context) { 36 | @Override 37 | public void onAdCountChanged() { 38 | notifyDataSetChanged(); 39 | } 40 | }; 41 | } 42 | 43 | @Override 44 | public Fragment getItem(int position) { 45 | Object listItemObject = mAdAdapter.getItem(position); 46 | if (listItemObject instanceof FlurryAdNative) { 47 | return PostDetailFragment.newInstance((FlurryAdNative)listItemObject); 48 | } else { 49 | return PostDetailFragment.newInstance((Post)listItemObject); 50 | } 51 | } 52 | 53 | @Override 54 | public int getCount() { 55 | return mAdAdapter.getCount(); 56 | } 57 | 58 | public void setBlogPosts(List blogPosts) { 59 | mAdAdapter.setData(blogPosts); 60 | notifyDataSetChanged(); 61 | } 62 | 63 | public void destroy() { 64 | mAdAdapter.destroy(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/adapters/PostListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets.adapters; 18 | 19 | import android.content.Context; 20 | import android.util.Log; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.BaseAdapter; 25 | import android.widget.ImageView; 26 | import android.widget.TextView; 27 | 28 | import com.flurry.android.ads.FlurryAdNative; 29 | import com.flurry.android.ads.FlurryAdNativeAsset; 30 | import com.yahoo.mobile.client.android.yodel.R; 31 | import com.yahoo.mobile.client.android.yodel.feed.TumblrFeedManager; 32 | import com.yahoo.mobile.client.android.yodel.utils.AnalyticsHelper; 33 | import com.yahoo.mobile.client.android.yodel.utils.DateTimeUtil; 34 | import com.yahoo.mobile.client.android.yodel.utils.ImageLoader; 35 | import com.tumblr.jumblr.types.LinkPost; 36 | import com.tumblr.jumblr.types.PhotoPost; 37 | import com.tumblr.jumblr.types.Post; 38 | import com.tumblr.jumblr.types.QuotePost; 39 | import com.tumblr.jumblr.types.TextPost; 40 | import com.tumblr.jumblr.types.VideoPost; 41 | 42 | import java.util.List; 43 | 44 | public class PostListAdapter extends BaseAdapter { 45 | 46 | private static final String LOG_TAG = PostListAdapter.class.getName(); 47 | 48 | private BaseAdAdapter mAdAdapter; 49 | private Context mContext; 50 | private LayoutInflater mInflater; 51 | private ImageLoader mImageLoader; 52 | 53 | private static final int VIEW_TYPE_COUNT = 2; 54 | private static final int VIEW_TYPE_NORMAL = 0; 55 | private static final int VIEW_TYPE_AD = 1; 56 | 57 | public static final String AD_ASSET_HEADLINE = "headline"; 58 | public static final String AD_ASSET_SUMMARY = "summary"; 59 | public static final String AD_ASSET_SOURCE = "source"; 60 | public static final String AD_ASSET_SEC_HQ_BRANDING_LOGO = "secHqBrandingLogo"; 61 | public static final String AD_ASSET_SEC_HQ_IMAGE = "secHqImage"; 62 | public static final String AD_ASSET_VIDEO = "videoUrl"; 63 | 64 | public PostListAdapter(Context context) { 65 | mInflater = LayoutInflater.from(context); 66 | mImageLoader = ImageLoader.getInstance(); 67 | this.mContext = context; 68 | this.mAdAdapter = new BaseAdAdapter(mContext) { 69 | @Override 70 | public void onAdCountChanged() { 71 | notifyDataSetChanged(); 72 | } 73 | }; 74 | } 75 | 76 | @Override 77 | public View getView(final int position, View convertView, ViewGroup parent) { 78 | 79 | switch (getItemViewType(position)) { 80 | case VIEW_TYPE_AD: 81 | AdViewHolder adHolder; 82 | FlurryAdNative adNative = (FlurryAdNative)getItem(position); 83 | 84 | if (convertView == null) { 85 | adHolder = new AdViewHolder(); 86 | 87 | convertView = mInflater.inflate(R.layout.list_item_ad_card, parent, false); 88 | 89 | adHolder.adImage = (ImageView) convertView.findViewById(R.id.ad_image); 90 | adHolder.adVideo = (ViewGroup) convertView.findViewById(R.id.ad_video); 91 | adHolder.adTitle = (TextView) convertView.findViewById(R.id.ad_title); 92 | adHolder.adSummary = (TextView) convertView.findViewById(R.id.ad_summary); 93 | adHolder.publisher = (TextView) convertView.findViewById(R.id.ad_publisher); 94 | adHolder.sponsoredImage = (ImageView) convertView.findViewById(R.id.sponsored_image); 95 | 96 | convertView.setTag(adHolder); 97 | } else { 98 | adHolder = (AdViewHolder) convertView.getTag(); 99 | adHolder.adNative.removeTrackingView(); // Remove the old tracking view 100 | } 101 | 102 | adHolder.adNative = adNative; 103 | // Set this convertView as the tracking view so it could open the ad when tapped. 104 | adNative.setTrackingView(convertView); 105 | 106 | loadAdInView(adHolder, adNative); 107 | break; 108 | case VIEW_TYPE_NORMAL: 109 | PostViewHolder postHolder; 110 | 111 | if (convertView == null) { 112 | postHolder = new PostViewHolder(); 113 | 114 | convertView = mInflater.inflate(R.layout.list_item_post_card, parent, false); 115 | 116 | postHolder.postImage = (ImageView) convertView.findViewById(R.id.post_image); 117 | postHolder.postTitle = (TextView) convertView.findViewById(R.id.post_title); 118 | postHolder.postSummary = (TextView) convertView.findViewById(R.id.post_summary); 119 | postHolder.postDate = (TextView) convertView.findViewById(R.id.date_text); 120 | postHolder.publisher = (TextView) convertView.findViewById(R.id.post_publisher); 121 | postHolder.retumbleIcon = (ImageView) convertView.findViewById(R.id.icon_reblog); 122 | postHolder.likeIcon = (ImageView) convertView.findViewById(R.id.icon_like); 123 | 124 | convertView.setTag(postHolder); 125 | } else { 126 | postHolder = (PostViewHolder) convertView.getTag(); 127 | } 128 | 129 | loadPostInView(postHolder, (Post) getItem(position)); 130 | break; 131 | } 132 | 133 | return convertView; 134 | } 135 | 136 | @Override 137 | public int getCount() { 138 | return mAdAdapter.getCount(); 139 | } 140 | 141 | @Override 142 | public long getItemId(int position) { 143 | return position; 144 | } 145 | 146 | @Override 147 | public Object getItem(int position) { 148 | return mAdAdapter.getItem(position); 149 | } 150 | 151 | @Override 152 | public int getViewTypeCount() { 153 | return VIEW_TYPE_COUNT; 154 | } 155 | 156 | @Override 157 | public int getItemViewType(int position) { 158 | if (mAdAdapter.canShowAdAtPosition(position)) { 159 | return VIEW_TYPE_AD; 160 | } else { 161 | return VIEW_TYPE_NORMAL; 162 | } 163 | } 164 | 165 | public void setBlogPosts(List blogPosts) { 166 | mAdAdapter.setData(blogPosts); 167 | notifyDataSetChanged(); 168 | } 169 | 170 | public void destroy() { 171 | notifyDataSetInvalidated(); 172 | mAdAdapter.destroy(); 173 | } 174 | 175 | private void loadPostInView(PostViewHolder viewHolder, Post post) { 176 | viewHolder.postTitle.setText(post.getSourceTitle()); 177 | 178 | viewHolder.postImage.setImageBitmap(null); 179 | 180 | String type = post.getType(); 181 | String blogName, title, body, photoUrl, postDate; 182 | blogName = post.getBlogName(); 183 | title = body = photoUrl = null; 184 | postDate = DateTimeUtil.getFriendlyDateString(post.getTimestamp() * 1000, mContext); 185 | 186 | switch (type) { 187 | case TumblrFeedManager.POST_TYPE_TEXT: 188 | title = ((TextPost)post).getTitle(); 189 | body = ((TextPost)post).getBody(); 190 | break; 191 | case TumblrFeedManager.POST_TYPE_QUOTE: 192 | body = ((QuotePost)post).getText() + "\n - " + ((QuotePost)post).getSource(); 193 | break; 194 | case TumblrFeedManager.POST_TYPE_PHOTO: 195 | body = ((PhotoPost)post).getCaption(); 196 | // It can't be a photo post without photos. Either way... 197 | if (((PhotoPost)post).getPhotos().size() > 0) { 198 | photoUrl = ((PhotoPost) post).getPhotos().get(0).getOriginalSize().getUrl(); 199 | } 200 | break; 201 | case TumblrFeedManager.POST_TYPE_VIDEO: 202 | body = ((VideoPost)post).getCaption(); 203 | photoUrl = ((VideoPost)post).getThumbnailUrl(); 204 | break; 205 | case TumblrFeedManager.POST_TYPE_LINK: 206 | title = ((LinkPost)post).getTitle(); 207 | body = ((LinkPost)post).getDescription() + "\n" + ((LinkPost)post).getLinkUrl(); 208 | break; 209 | } 210 | viewHolder.publisher.setText(blogName); 211 | viewHolder.publisher.setTextColor(mContext.getResources().getColor(R.color.y_blue)); 212 | 213 | if (title != null) { 214 | viewHolder.postTitle.setVisibility(View.VISIBLE); 215 | viewHolder.postTitle.setText(title); 216 | } else { 217 | viewHolder.postTitle.setVisibility(View.GONE); 218 | } 219 | if (body != null) { 220 | viewHolder.postSummary.setVisibility(View.VISIBLE); 221 | viewHolder.postSummary.setText(body); 222 | } else { 223 | viewHolder.postSummary.setVisibility(View.VISIBLE); 224 | } 225 | if (photoUrl != null) { 226 | viewHolder.postImage.setVisibility(View.VISIBLE); 227 | mImageLoader.displayImage(photoUrl, viewHolder.postImage); 228 | } else { 229 | viewHolder.postImage.setVisibility(View.GONE); 230 | } 231 | 232 | viewHolder.postDate.setText(postDate); 233 | 234 | } 235 | 236 | public void loadAdInView(AdViewHolder viewHolder, FlurryAdNative adNative) { 237 | try { 238 | mAdAdapter.loadAdAssetInView(adNative, AD_ASSET_HEADLINE, viewHolder.adTitle); 239 | mAdAdapter.loadAdAssetInView(adNative, AD_ASSET_SUMMARY, viewHolder.adSummary); 240 | mAdAdapter.loadAdAssetInView(adNative, AD_ASSET_SOURCE, viewHolder.publisher); 241 | 242 | FlurryAdNativeAsset adNativeAsset = adNative.getAsset(AD_ASSET_SEC_HQ_BRANDING_LOGO); 243 | mImageLoader.displayImage(adNativeAsset.getValue(), viewHolder.sponsoredImage); 244 | if (adNative.isVideoAd()) { 245 | mAdAdapter.loadAdAssetInView(adNative, AD_ASSET_VIDEO, viewHolder.adVideo); 246 | } else { 247 | adNativeAsset = adNative.getAsset(AD_ASSET_SEC_HQ_IMAGE); 248 | mImageLoader.displayImage(adNativeAsset.getValue(), viewHolder.adImage); 249 | } 250 | 251 | } catch (Exception e) { 252 | Log.i(LOG_TAG, "Exception in fetching an Ad"); 253 | AnalyticsHelper.logError(LOG_TAG, "Exception in fetching an ad", e); 254 | } 255 | } 256 | 257 | public static class PostViewHolder 258 | { 259 | ImageView postImage; 260 | TextView postTitle; 261 | TextView postSummary; 262 | TextView postDate; 263 | TextView publisher; 264 | ImageView retumbleIcon; 265 | ImageView likeIcon; 266 | } 267 | 268 | public static class AdViewHolder 269 | { 270 | ImageView adImage; 271 | ViewGroup adVideo; 272 | TextView adTitle; 273 | TextView adSummary; 274 | TextView publisher; 275 | ImageView sponsoredImage; 276 | FlurryAdNative adNative; 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/ui/widgets/adapters/PostSearchListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.ui.widgets.adapters; 18 | 19 | import android.content.Context; 20 | import android.view.LayoutInflater; 21 | import android.view.View; 22 | import android.view.ViewGroup; 23 | import android.widget.BaseAdapter; 24 | import android.widget.ImageView; 25 | import android.widget.TextView; 26 | 27 | import com.yahoo.mobile.client.android.yodel.R; 28 | import com.yahoo.mobile.client.android.yodel.utils.ImageLoader; 29 | import com.tumblr.jumblr.types.PhotoPost; 30 | import com.tumblr.jumblr.types.Post; 31 | 32 | import java.util.List; 33 | 34 | public class PostSearchListAdapter extends BaseAdapter { 35 | 36 | private List mPosts; 37 | private Context mContext; 38 | 39 | public PostSearchListAdapter(Context context) { 40 | super(); 41 | this.mContext = context; 42 | } 43 | @Override 44 | public View getView(int position, View convertView, ViewGroup parent) { 45 | ViewHolder holder; 46 | 47 | if (convertView == null) { 48 | holder = new ViewHolder(); 49 | 50 | convertView = LayoutInflater 51 | .from(mContext).inflate(R.layout.list_item_post_search, parent, false); 52 | 53 | holder.postImage = (ImageView) convertView.findViewById(R.id.post_image); 54 | holder.publisher = (TextView) convertView.findViewById(R.id.post_publisher); 55 | holder.postSummary = (TextView) convertView.findViewById(R.id.post_summary); 56 | 57 | convertView.setTag(holder); 58 | } else { 59 | holder = (ViewHolder) convertView.getTag(); 60 | } 61 | 62 | Post post = mPosts.get(position); 63 | String type = post.getType(); 64 | String blogName, caption, photoUrl; 65 | blogName = post.getBlogName(); 66 | 67 | // We are only concerned with photo posts for this scenario 68 | if ("photo".equals(type)) { 69 | caption = ((PhotoPost)post).getCaption(); 70 | // It can't be a photo post without photos. Either way... 71 | photoUrl = ((PhotoPost) post).getPhotos().get(0).getOriginalSize().getUrl(); 72 | 73 | holder.postSummary.setText(caption); 74 | ImageLoader.getInstance().displayImage(photoUrl, holder.postImage); 75 | } 76 | 77 | holder.publisher.setText(blogName); 78 | 79 | return convertView; 80 | } 81 | 82 | @Override 83 | public int getCount() { 84 | return mPosts != null ? mPosts.size() : 0; 85 | } 86 | 87 | @Override 88 | public Object getItem(int position) { 89 | return mPosts.get(position); 90 | } 91 | 92 | @Override 93 | public long getItemId(int position) { 94 | return position; 95 | } 96 | 97 | public void setBlogPosts(List blogPosts) { 98 | mPosts = blogPosts; 99 | notifyDataSetChanged(); 100 | } 101 | 102 | public static class ViewHolder 103 | { 104 | ImageView postImage; 105 | TextView postSummary; 106 | TextView publisher; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/utils/AnalyticsHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.utils; 18 | 19 | import com.flurry.android.FlurryAgent; 20 | 21 | import java.util.Map; 22 | 23 | /** 24 | * Helps with logging custom events and errors to Flurry 25 | */ 26 | public class AnalyticsHelper { 27 | public static final String EVENT_STREAM_ARTICLE_CLICK = "stream_article_click"; 28 | public static final String EVENT_STREAM_AD_CLICK = "stream_ad_click"; 29 | public static final String EVENT_STREAM_SEARCH_CLICK = "stream_search_click"; 30 | public static final String EVENT_STREAM_PULL_REFRESH = "stream_pullto_refresh"; 31 | public static final String EVENT_CAR_MOREIMG_CLICK = "carousel_moreimages_click"; 32 | public static final String EVENT_CAR_LEARNMORE_CLICK = "carousel_learnmore_click"; 33 | public static final String EVENT_CAR_CONTENT_SWIPE = "carousel_content_swipe"; 34 | public static final String EVENT_AD_CLOSEBUTTON_CLICK = "ad_closebutton_click"; 35 | public static final String EVENT_SEARCH_STARTED = "search_term_started"; 36 | public static final String EVENT_SEARCH_MOREONWEB_CLICK = "search_moreonweb_click"; 37 | public static final String EVENT_SEARCH_RESULT_CLICK = "search_result_click"; 38 | 39 | public static final String PARAM_ARTICLE_ORIGIN = "article_origin"; 40 | public static final String PARAM_ARTICLE_TYPE = "article_type"; 41 | public static final String PARAM_CONTENT_LOADED = "content_loaded"; 42 | public static final String PARAM_SEARCH_TERM = "search_term"; 43 | 44 | /** 45 | * Logs an event for analytics. 46 | * 47 | * @param eventName name of the event 48 | * @param eventParams event parameters (can be null) 49 | * @param timed true if the event should be timed, false otherwise 50 | */ 51 | public static void logEvent(String eventName, Map eventParams, boolean timed) { 52 | FlurryAgent.logEvent(eventName, eventParams, timed); 53 | } 54 | 55 | /** 56 | * Ends a timed event that was previously started. 57 | * 58 | * @param eventName name of the event 59 | * @param eventParams event parameters (can be null) 60 | */ 61 | public static void endTimedEvent(String eventName, Map eventParams) { 62 | FlurryAgent.endTimedEvent(eventName, eventParams); 63 | } 64 | 65 | /** 66 | * Logs an error. 67 | * 68 | * @param errorId error ID 69 | * @param errorDescription error description 70 | * @param throwable a {@link Throwable} that describes the error 71 | */ 72 | public static void logError(String errorId, String errorDescription, Throwable throwable) { 73 | FlurryAgent.onError(errorId, errorDescription, throwable); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/utils/DateTimeUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.utils; 18 | 19 | import android.content.Context; 20 | import android.text.format.DateFormat; 21 | 22 | import com.yahoo.mobile.client.android.yodel.R; 23 | 24 | import java.util.Date; 25 | 26 | public final class DateTimeUtil { 27 | 28 | /** 29 | * Gets the current time in epoch time milliseconds. 30 | * @return the current time in epoch time milliseconds. 31 | */ 32 | public static long getNow() { 33 | return new Date().getTime(); 34 | } 35 | 36 | /** 37 | * Gets the local time as a Date object. 38 | * 39 | * @param unixTimeStampMillis the epoch time in milliseconds. 40 | * @return the {@link java.util.Date} representing the local time. 41 | */ 42 | public static Date getLocalDateFromTicks(long unixTimeStampMillis) { 43 | return new Date(unixTimeStampMillis); 44 | } 45 | 46 | /** 47 | *

Gets a simple representation of the time at time in which something occurred relative 48 | * to now.

49 | * 50 | *

E.g. "1 day ago", "3 hours ago", "Just now"

51 | * @param unixTimeStampMillis the epoch time in the past in milliseconds. 52 | * @param context the current application context. 53 | * @return a string representation of the time. 54 | */ 55 | public static String getFriendlyDateString(long unixTimeStampMillis, Context context) { 56 | // Don't trust this method in prod. Currently, 1.4 days ago is "yesterday" 57 | long timeDiffMs = getNow() - unixTimeStampMillis; 58 | long timeDiffMinutes = timeDiffMs / (1000 * 60); 59 | long timeDiffHours = timeDiffMinutes / 60; 60 | long timeDiffDays = timeDiffHours / 24; 61 | 62 | if (timeDiffDays > 7) { 63 | return DateFormat.getMediumDateFormat(context) 64 | .format(getLocalDateFromTicks(unixTimeStampMillis)); 65 | } else if (timeDiffDays > 0) { 66 | return context.getResources() 67 | .getQuantityString(R.plurals.friendly_date_day_ago, 68 | (int)timeDiffDays, (int)timeDiffDays); 69 | } else if (timeDiffHours > 0) { 70 | return context.getResources() 71 | .getQuantityString(R.plurals.friendly_date_hour_ago, 72 | (int)timeDiffHours, (int)timeDiffHours); 73 | } else if (timeDiffMinutes > 0) { 74 | return context.getResources() 75 | .getQuantityString(R.plurals.friendly_date_minute_ago, 76 | (int)timeDiffMinutes, (int)timeDiffMinutes); 77 | } else { 78 | return context.getResources().getString(R.string.friendly_date_now); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/utils/ImageLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.utils; 18 | 19 | import android.graphics.Bitmap; 20 | import android.graphics.BitmapFactory; 21 | import android.os.Build; 22 | import android.text.TextUtils; 23 | import android.util.DisplayMetrics; 24 | import android.widget.ImageView; 25 | 26 | import com.yahoo.mobile.client.android.yodel.NativeTestAppApplication; 27 | 28 | import java.io.InputStream; 29 | import java.lang.ref.WeakReference; 30 | import java.net.HttpURLConnection; 31 | import java.net.URL; 32 | import java.net.URLConnection; 33 | import java.util.concurrent.ExecutorService; 34 | import java.util.concurrent.Executors; 35 | 36 | public class ImageLoader { 37 | private final MemoryCache memoryCache = new MemoryCache(); 38 | private final ExecutorService executorService; 39 | 40 | private static ImageLoader sInstance = null; 41 | 42 | private ImageLoader() { 43 | executorService = Executors.newFixedThreadPool(2); 44 | } 45 | 46 | public static ImageLoader getInstance() { 47 | synchronized (ImageLoader.class) { 48 | if (sInstance == null) { 49 | sInstance = new ImageLoader(); 50 | } 51 | } 52 | 53 | return sInstance; 54 | } 55 | 56 | public void displayImage(String url, ImageView imageView) { 57 | if (TextUtils.isEmpty(url) || imageView == null) { 58 | return; 59 | } 60 | 61 | imageView.setTag(url); 62 | Bitmap bitmap = memoryCache.get(url); 63 | if (bitmap != null) { 64 | setImageViewBitmap(imageView, bitmap); 65 | } else { 66 | executorService.submit(new PhotosLoader(url, imageView)); 67 | } 68 | } 69 | 70 | private Bitmap getBitmap(String url) { 71 | if (url == null) { 72 | return null; 73 | } 74 | 75 | URLConnection conn = null; 76 | InputStream is = null; 77 | 78 | try { 79 | final URL imageUrl = new URL(url); 80 | conn = imageUrl.openConnection(); 81 | conn.setConnectTimeout(10000); 82 | conn.setReadTimeout(30000); 83 | is = conn.getInputStream(); 84 | return BitmapFactory.decodeStream(is, null, null); 85 | } catch (Exception ex) { 86 | return null; 87 | } finally { 88 | if (is != null) { 89 | try { 90 | is.close(); 91 | } catch (Exception e) { 92 | // Deliberately empty 93 | } 94 | } 95 | if (conn != null) { 96 | if (conn instanceof HttpURLConnection) { 97 | ((HttpURLConnection) conn).disconnect(); 98 | } 99 | } 100 | } 101 | } 102 | 103 | private class PhotosLoader implements Runnable { 104 | private String mUrl; 105 | private WeakReference mImageView; 106 | 107 | public PhotosLoader(final String url, final ImageView imageView) { 108 | mUrl = url; 109 | mImageView = new WeakReference<>(imageView); 110 | } 111 | 112 | @Override 113 | public void run() { 114 | ImageView imageView = mImageView.get(); 115 | if (imageView == null || !mUrl.equals(imageView.getTag())) { 116 | return; 117 | } 118 | 119 | final Bitmap bmp = getBitmap(mUrl); 120 | if (bmp != null) { 121 | memoryCache.put(mUrl, bmp); 122 | 123 | final ImageView imageViewToLoad = mImageView.get(); 124 | if (!mUrl.equals(imageView.getTag())) { 125 | return; 126 | } 127 | 128 | NativeTestAppApplication.getInstance().getMainThreadHandler().post(new Runnable() { 129 | @Override 130 | public void run() { 131 | setImageViewBitmap(imageViewToLoad, bmp); 132 | } 133 | }); 134 | } 135 | } 136 | } 137 | 138 | private void setImageViewBitmap(ImageView image, Bitmap bitmap) { 139 | String deviceName = getDeviceName(); 140 | if (!TextUtils.isEmpty(deviceName) && deviceName.toUpperCase().contains("SAMSUNG")) { 141 | bitmap.setDensity(DisplayMetrics.DENSITY_HIGH); 142 | } 143 | image.setImageBitmap(bitmap); 144 | } 145 | 146 | private String getDeviceName() { 147 | String manufacturer = Build.MANUFACTURER; 148 | String model = Build.MODEL; 149 | if (model.startsWith(manufacturer)) { 150 | return model; 151 | } else { 152 | return manufacturer + " " + model; 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/utils/MemoryCache.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.utils; 18 | 19 | import android.graphics.Bitmap; 20 | import android.support.v4.util.LruCache; 21 | import android.text.TextUtils; 22 | import android.util.Log; 23 | 24 | public class MemoryCache { 25 | private static String LOG_TAG = MemoryCache.class.getSimpleName(); 26 | 27 | private final LruCache mCache; 28 | 29 | public MemoryCache() { 30 | int cacheSize = 10 * 1024 * 1024 / 1024; // 10 mB 31 | Log.i(LOG_TAG, "Image cache size: " + cacheSize + "kB"); 32 | 33 | mCache = new LruCache(cacheSize) { 34 | @Override 35 | protected int sizeOf(String key, Bitmap value) { 36 | return value.getByteCount() / 1024; 37 | } 38 | }; 39 | } 40 | 41 | public Bitmap get(String key) { 42 | if (TextUtils.isEmpty(key)) { 43 | return null; 44 | } 45 | 46 | return mCache.get(key); 47 | } 48 | 49 | public Bitmap put(String key, Bitmap value) { 50 | if (TextUtils.isEmpty(key) || value == null) { 51 | return null; 52 | } 53 | 54 | return mCache.put(key, value); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /yodel-sample/src/com/yahoo/mobile/client/android/yodel/utils/PostDataLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Yahoo Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.yahoo.mobile.client.android.yodel.utils; 18 | 19 | 20 | import android.content.Context; 21 | import android.support.v4.content.AsyncTaskLoader; 22 | 23 | import com.yahoo.mobile.client.android.yodel.feed.TumblrFeedManager; 24 | import com.tumblr.jumblr.types.Post; 25 | 26 | import java.util.List; 27 | 28 | public class PostDataLoader extends AsyncTaskLoader> { 29 | 30 | private List mPostsCache; 31 | private final String mTagQuery; 32 | 33 | public PostDataLoader(Context context) { 34 | this(context, null); 35 | } 36 | 37 | public PostDataLoader(Context context, String tagQuery) { 38 | super(context); 39 | this.mTagQuery = tagQuery; 40 | } 41 | 42 | @Override 43 | public List loadInBackground() { 44 | if (mTagQuery == null) { 45 | return TumblrFeedManager.getRecentPosts(); 46 | } else { 47 | return TumblrFeedManager.getPostsWithTag(mTagQuery); 48 | } 49 | } 50 | 51 | /** 52 | * Derived classes must call through to the super class's implementation of this method, kthnx 53 | */ 54 | @Override 55 | protected void onStartLoading() { 56 | if (mPostsCache != null) { 57 | deliverResult(mPostsCache); 58 | } else { 59 | forceLoad(); 60 | } 61 | } 62 | 63 | @Override 64 | public void deliverResult(List data) { 65 | if (isReset()) { 66 | mPostsCache = null; 67 | return; 68 | } 69 | 70 | mPostsCache = data; 71 | super.deliverResult(data); 72 | } 73 | } 74 | --------------------------------------------------------------------------------