From 66c1e22624f03e10abfc90785b8d733b8d608f8a Mon Sep 17 00:00:00 2001 From: Fabio Pires Prado Date: Sat, 15 Dec 2018 02:40:20 -0200 Subject: [PATCH 1/2] Meu Projeto --- .gitignore | 9 + app/.gitignore | 1 + app/build.gradle | 34 + app/proguard-rules.pro | 25 + .../ExampleInstrumentedTest.java | 26 + app/src/main/AndroidManifest.xml | 36 ++ .../moviesdatabase/Activity/MovieDetails.java | 148 +++++ .../moviesdatabase/Activity/MoviesList.java | 233 +++++++ .../moviesdatabase/Activity/SplashScreen.java | 53 ++ .../Adapter/MovieListAdapter.java | 100 +++ .../Interface/ResultsMovie.java | 14 + .../com/moviesdatabase/Pojo/Movies.java | 34 + .../WebRequests/CustomVolleyRequestQueue.java | 68 ++ .../WebRequests/MovieDetailsRequest.java | 106 +++ .../WebRequests/SearchMovies.java | 116 ++++ app/src/main/res/anim/alpha.xml | 8 + app/src/main/res/anim/translate.xml | 16 + app/src/main/res/drawable/card_movie.png | Bin 0 -> 2001 bytes app/src/main/res/drawable/fundocinza.png | Bin 0 -> 57645 bytes app/src/main/res/drawable/logo_imdb.jpg | Bin 0 -> 121559 bytes app/src/main/res/drawable/movie_selected.xml | 14 + app/src/main/res/drawable/search_menu.png | Bin 0 -> 1581 bytes app/src/main/res/layout/actionbar_detail.xml | 17 + app/src/main/res/layout/activity_main.xml | 27 + .../res/layout/activity_movie_details.xml | 436 +++++++++++++ .../main/res/layout/activity_movies_list.xml | 54 ++ app/src/main/res/layout/card_movie.xml | 146 +++++ app/src/main/res/menu/search_menu.xml | 17 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes app/src/main/res/mipmap-hdpi/imdb.png | Bin 0 -> 654 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes app/src/main/res/mipmap-mdpi/imdb.png | Bin 0 -> 654 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes app/src/main/res/mipmap-xhdpi/imdb.png | Bin 0 -> 654 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes app/src/main/res/mipmap-xxhdpi/imdb.png | Bin 0 -> 654 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10486 bytes app/src/main/res/mipmap-xxxhdpi/imdb.png | Bin 0 -> 654 bytes app/src/main/res/values/colors.xml | 8 + app/src/main/res/values/strings.xml | 7 + app/src/main/res/values/styles.xml | 32 + .../com/moviesdatabase/ExampleUnitTest.java | 17 + build.gradle | 23 + gradle.properties | 17 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 53636 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 160 +++++ gradlew.bat | 90 +++ settings.gradle | 1 + volley/.gitignore | 9 + volley/Android.mk | 33 + volley/bintray.gradle | 87 +++ volley/build.gradle | 38 ++ volley/build.xml | 92 +++ volley/custom_rules.xml | 10 + volley/pom.xml | 168 +++++ volley/proguard-project.txt | 20 + volley/proguard.cfg | 40 ++ volley/rules.gradle | 12 + volley/src/main/AndroidManifest.xml | 11 + .../com/android/volley/AuthFailureError.java | 58 ++ .../main/java/com/android/volley/Cache.java | 100 +++ .../com/android/volley/CacheDispatcher.java | 158 +++++ .../java/com/android/volley/ClientError.java | 35 + .../android/volley/DefaultRetryPolicy.java | 105 +++ .../com/android/volley/ExecutorDelivery.java | 118 ++++ .../main/java/com/android/volley/Network.java | 30 + .../com/android/volley/NetworkDispatcher.java | 152 +++++ .../java/com/android/volley/NetworkError.java | 35 + .../com/android/volley/NetworkResponse.java | 73 +++ .../com/android/volley/NoConnectionError.java | 31 + .../java/com/android/volley/ParseError.java | 33 + .../main/java/com/android/volley/Request.java | 609 ++++++++++++++++++ .../java/com/android/volley/RequestQueue.java | 317 +++++++++ .../java/com/android/volley/Response.java | 85 +++ .../com/android/volley/ResponseDelivery.java | 35 + .../java/com/android/volley/RetryPolicy.java | 41 ++ .../java/com/android/volley/ServerError.java | 32 + .../java/com/android/volley/TimeoutError.java | 23 + .../java/com/android/volley/VolleyError.java | 57 ++ .../java/com/android/volley/VolleyLog.java | 181 ++++++ .../volley/toolbox/AndroidAuthenticator.java | 114 ++++ .../android/volley/toolbox/Authenticator.java | 36 ++ .../android/volley/toolbox/BasicNetwork.java | 277 ++++++++ .../android/volley/toolbox/ByteArrayPool.java | 135 ++++ .../volley/toolbox/ClearCacheRequest.java | 70 ++ .../volley/toolbox/DiskBasedCache.java | 575 +++++++++++++++++ .../volley/toolbox/HttpClientStack.java | 194 ++++++ .../volley/toolbox/HttpHeaderParser.java | 168 +++++ .../com/android/volley/toolbox/HttpStack.java | 45 ++ .../com/android/volley/toolbox/HurlStack.java | 269 ++++++++ .../android/volley/toolbox/ImageLoader.java | 507 +++++++++++++++ .../android/volley/toolbox/ImageRequest.java | 244 +++++++ .../volley/toolbox/JsonArrayRequest.java | 73 +++ .../volley/toolbox/JsonObjectRequest.java | 76 +++ .../android/volley/toolbox/JsonRequest.java | 105 +++ .../volley/toolbox/NetworkImageView.java | 220 +++++++ .../com/android/volley/toolbox/NoCache.java | 49 ++ .../toolbox/PoolingByteArrayOutputStream.java | 93 +++ .../android/volley/toolbox/RequestFuture.java | 153 +++++ .../android/volley/toolbox/StringRequest.java | 73 +++ .../com/android/volley/toolbox/Volley.java | 80 +++ .../android/volley/CacheDispatcherTest.java | 115 ++++ .../android/volley/NetworkDispatcherTest.java | 100 +++ .../volley/RequestQueueIntegrationTest.java | 207 ++++++ .../com/android/volley/RequestQueueTest.java | 72 +++ .../java/com/android/volley/RequestTest.java | 97 +++ .../android/volley/ResponseDeliveryTest.java | 68 ++ .../com/android/volley/mock/MockCache.java | 65 ++ .../android/volley/mock/MockHttpClient.java | 114 ++++ .../android/volley/mock/MockHttpStack.java | 82 +++ .../volley/mock/MockHttpURLConnection.java | 77 +++ .../com/android/volley/mock/MockNetwork.java | 58 ++ .../com/android/volley/mock/MockRequest.java | 101 +++ .../volley/mock/MockResponseDelivery.java | 51 ++ .../volley/mock/ShadowSystemClock.java | 11 + .../com/android/volley/mock/TestRequest.java | 179 +++++ .../android/volley/mock/WaitableQueue.java | 72 +++ .../toolbox/AndroidAuthenticatorTest.java | 107 +++ .../volley/toolbox/BasicNetworkTest.java | 273 ++++++++ .../volley/toolbox/ByteArrayPoolTest.java | 76 +++ .../com/android/volley/toolbox/CacheTest.java | 39 ++ .../volley/toolbox/DiskBasedCacheTest.java | 136 ++++ .../volley/toolbox/HttpClientStackTest.java | 144 +++++ .../volley/toolbox/HttpHeaderParserTest.java | 292 +++++++++ .../android/volley/toolbox/HurlStackTest.java | 155 +++++ .../volley/toolbox/ImageLoaderTest.java | 101 +++ .../volley/toolbox/ImageRequestTest.java | 176 +++++ .../toolbox/JsonRequestCharsetTest.java | 119 ++++ .../volley/toolbox/JsonRequestTest.java | 49 ++ .../volley/toolbox/NetworkImageViewTest.java | 69 ++ .../PoolingByteArrayOutputStreamTest.java | 78 +++ .../volley/toolbox/RequestFutureTest.java | 35 + .../volley/toolbox/RequestQueueTest.java | 46 ++ .../android/volley/toolbox/RequestTest.java | 69 ++ .../android/volley/toolbox/ResponseTest.java | 53 ++ .../volley/toolbox/StringRequestTest.java | 37 ++ .../android/volley/utils/CacheTestUtils.java | 40 ++ .../utils/ImmediateResponseDelivery.java | 23 + .../org.robolectric.Config.properties | 1 + 140 files changed, 12070 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/android_challenge/com/moviesdatabase/ExampleInstrumentedTest.java create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/Activity/MovieDetails.java create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/Activity/MoviesList.java create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/Activity/SplashScreen.java create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/Adapter/MovieListAdapter.java create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/Interface/ResultsMovie.java create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/Pojo/Movies.java create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/CustomVolleyRequestQueue.java create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/MovieDetailsRequest.java create mode 100644 app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/SearchMovies.java create mode 100644 app/src/main/res/anim/alpha.xml create mode 100644 app/src/main/res/anim/translate.xml create mode 100644 app/src/main/res/drawable/card_movie.png create mode 100644 app/src/main/res/drawable/fundocinza.png create mode 100644 app/src/main/res/drawable/logo_imdb.jpg create mode 100644 app/src/main/res/drawable/movie_selected.xml create mode 100644 app/src/main/res/drawable/search_menu.png create mode 100644 app/src/main/res/layout/actionbar_detail.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_movie_details.xml create mode 100644 app/src/main/res/layout/activity_movies_list.xml create mode 100644 app/src/main/res/layout/card_movie.xml create mode 100644 app/src/main/res/menu/search_menu.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/imdb.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/imdb.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/imdb.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/imdb.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/imdb.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/test/java/android_challenge/com/moviesdatabase/ExampleUnitTest.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 volley/.gitignore create mode 100644 volley/Android.mk create mode 100644 volley/bintray.gradle create mode 100644 volley/build.gradle create mode 100644 volley/build.xml create mode 100644 volley/custom_rules.xml create mode 100644 volley/pom.xml create mode 100644 volley/proguard-project.txt create mode 100644 volley/proguard.cfg create mode 100644 volley/rules.gradle create mode 100644 volley/src/main/AndroidManifest.xml create mode 100644 volley/src/main/java/com/android/volley/AuthFailureError.java create mode 100644 volley/src/main/java/com/android/volley/Cache.java create mode 100644 volley/src/main/java/com/android/volley/CacheDispatcher.java create mode 100644 volley/src/main/java/com/android/volley/ClientError.java create mode 100644 volley/src/main/java/com/android/volley/DefaultRetryPolicy.java create mode 100644 volley/src/main/java/com/android/volley/ExecutorDelivery.java create mode 100644 volley/src/main/java/com/android/volley/Network.java create mode 100644 volley/src/main/java/com/android/volley/NetworkDispatcher.java create mode 100644 volley/src/main/java/com/android/volley/NetworkError.java create mode 100644 volley/src/main/java/com/android/volley/NetworkResponse.java create mode 100644 volley/src/main/java/com/android/volley/NoConnectionError.java create mode 100644 volley/src/main/java/com/android/volley/ParseError.java create mode 100644 volley/src/main/java/com/android/volley/Request.java create mode 100644 volley/src/main/java/com/android/volley/RequestQueue.java create mode 100644 volley/src/main/java/com/android/volley/Response.java create mode 100644 volley/src/main/java/com/android/volley/ResponseDelivery.java create mode 100644 volley/src/main/java/com/android/volley/RetryPolicy.java create mode 100644 volley/src/main/java/com/android/volley/ServerError.java create mode 100644 volley/src/main/java/com/android/volley/TimeoutError.java create mode 100644 volley/src/main/java/com/android/volley/VolleyError.java create mode 100644 volley/src/main/java/com/android/volley/VolleyLog.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/Authenticator.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/BasicNetwork.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/ByteArrayPool.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/DiskBasedCache.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/HttpClientStack.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/HttpStack.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/HurlStack.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/ImageLoader.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/ImageRequest.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/JsonRequest.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/NetworkImageView.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/NoCache.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/RequestFuture.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/StringRequest.java create mode 100644 volley/src/main/java/com/android/volley/toolbox/Volley.java create mode 100644 volley/src/test/java/com/android/volley/CacheDispatcherTest.java create mode 100644 volley/src/test/java/com/android/volley/NetworkDispatcherTest.java create mode 100644 volley/src/test/java/com/android/volley/RequestQueueIntegrationTest.java create mode 100644 volley/src/test/java/com/android/volley/RequestQueueTest.java create mode 100644 volley/src/test/java/com/android/volley/RequestTest.java create mode 100644 volley/src/test/java/com/android/volley/ResponseDeliveryTest.java create mode 100644 volley/src/test/java/com/android/volley/mock/MockCache.java create mode 100644 volley/src/test/java/com/android/volley/mock/MockHttpClient.java create mode 100644 volley/src/test/java/com/android/volley/mock/MockHttpStack.java create mode 100644 volley/src/test/java/com/android/volley/mock/MockHttpURLConnection.java create mode 100644 volley/src/test/java/com/android/volley/mock/MockNetwork.java create mode 100644 volley/src/test/java/com/android/volley/mock/MockRequest.java create mode 100644 volley/src/test/java/com/android/volley/mock/MockResponseDelivery.java create mode 100644 volley/src/test/java/com/android/volley/mock/ShadowSystemClock.java create mode 100644 volley/src/test/java/com/android/volley/mock/TestRequest.java create mode 100644 volley/src/test/java/com/android/volley/mock/WaitableQueue.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/CacheTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/HurlStackTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/ImageRequestTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/JsonRequestTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/RequestFutureTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/RequestQueueTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/RequestTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/ResponseTest.java create mode 100644 volley/src/test/java/com/android/volley/toolbox/StringRequestTest.java create mode 100644 volley/src/test/java/com/android/volley/utils/CacheTestUtils.java create mode 100644 volley/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java create mode 100644 volley/src/test/resources/org.robolectric.Config.properties diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39fb081 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..065a7d5 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 25 + buildToolsVersion "25.0.2" + defaultConfig { + applicationId "android_challenge.com.moviesdatabase" + minSdkVersion 11 + targetSdkVersion 25 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + compile 'com.android.support:recyclerview-v7:25.1.0' + compile 'com.android.support:cardview-v7:25.1.0' + compile 'com.android.support:design:25.1.0' + compile 'com.android.support:appcompat-v7:25.1.0' + compile 'com.android.support.constraint:constraint-layout:1.0.2' + compile 'com.android.volley:volley:1.0.0' + testCompile 'junit:junit:4.12' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..6af4248 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in C:\Users\Fabio_2\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/androidTest/java/android_challenge/com/moviesdatabase/ExampleInstrumentedTest.java b/app/src/androidTest/java/android_challenge/com/moviesdatabase/ExampleInstrumentedTest.java new file mode 100644 index 0000000..1fb953f --- /dev/null +++ b/app/src/androidTest/java/android_challenge/com/moviesdatabase/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package android_challenge.com.moviesdatabase; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("android_challenge.com.moviesdatabase", appContext.getPackageName()); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2aadf95 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/Activity/MovieDetails.java b/app/src/main/java/android_challenge/com/moviesdatabase/Activity/MovieDetails.java new file mode 100644 index 0000000..ee1f154 --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/Activity/MovieDetails.java @@ -0,0 +1,148 @@ +package android_challenge.com.moviesdatabase.Activity; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.widget.TextView; +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; +import org.json.JSONException; +import org.json.JSONObject; +import android_challenge.com.moviesdatabase.Interface.ResultsMovie; +import android_challenge.com.moviesdatabase.R; +import android_challenge.com.moviesdatabase.WebRequests.CustomVolleyRequestQueue; +import android_challenge.com.moviesdatabase.WebRequests.MovieDetailsRequest; + +public class MovieDetails extends AppCompatActivity { + + private String imdbID; + + private TextView title; + private TextView type; + private TextView year; + private TextView released; + private TextView rated; + private TextView runtime; + private TextView director; + private TextView genre; + private TextView actors; + private TextView plot; + private TextView awards; + private TextView language; + private TextView country; + private TextView metascore; + private TextView imdbrating; + private TextView imdbvotes; + private TextView writer; + private NetworkImageView mNetworkImageView; + private ImageLoader mImageLoader; + + ResultsMovie initResultsmovieCallback1 = null; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_movie_details); + + getSupportActionBar().setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); + getSupportActionBar().setCustomView(R.layout.actionbar_detail); + + title=(TextView)findViewById(getResources().getIdentifier("action_bar_title", "id", getPackageName())); + + Bundle extras = getIntent().getExtras(); + if (extras == null) { + imdbID = null; + } else { + imdbID = extras.getString("ID"); + } + + type = (TextView) findViewById(R.id.type_movie); + year = (TextView) findViewById(R.id.year_movie); + released = (TextView) findViewById(R.id.released_movie); + rated = (TextView) findViewById(R.id.rated_movie); + runtime = (TextView) findViewById(R.id.runtime_movie); + director = (TextView) findViewById(R.id.director_movie); + genre = (TextView) findViewById(R.id.genre_movie); + actors = (TextView) findViewById(R.id.actors_movie); + plot = (TextView) findViewById(R.id.plot_movie); + awards = (TextView) findViewById(R.id.awards_movie); + language = (TextView) findViewById(R.id.language_movie); + country = (TextView) findViewById(R.id.country_movie); + metascore = (TextView) findViewById(R.id.metascore_movie); + imdbrating = (TextView) findViewById(R.id.imdbrating_movie); + imdbvotes = (TextView) findViewById(R.id.imdbvotes_movie); + writer = (TextView) findViewById(R.id.writer_movie); + mNetworkImageView = (NetworkImageView)findViewById(R.id.poster_movie); + makeRequest(); + } + + public void makeRequest() { + + ConnectivityManager connMgr = (ConnectivityManager) + getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + + if (networkInfo != null && networkInfo.isConnected()) { + initResultsVolleyArrayCallback(); + MovieDetailsRequest moviedetails = new MovieDetailsRequest(initResultsmovieCallback1); + moviedetails.searchByID(this, imdbID); + } else { + new AlertDialog.Builder(MovieDetails.this) + .setTitle("Sem Internet!") + .setMessage("Não tem nenhuma conexão de rede disponível!") + .setPositiveButton(R.string.box_msg_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + } + + + void initResultsVolleyArrayCallback() { + + initResultsmovieCallback1 = new ResultsMovie() { + + @Override + public void notifySuccess(JSONObject movieInformations) { + try { + title.setText(movieInformations.getString("Title")); + type.setText(movieInformations.getString("Type")); + year.setText(movieInformations.getString("Year")); + released.setText(movieInformations.getString("Released")); + rated.setText(movieInformations.getString("Rated")); + runtime.setText(movieInformations.getString("Runtime")); + director.setText(movieInformations.getString("Director")); + genre.setText(movieInformations.getString("Genre")); + actors.setText(movieInformations.getString("Actors")); + plot.setText(movieInformations.getString("Plot")); + awards.setText(movieInformations.getString("Awards")); + language.setText(movieInformations.getString("Language")); + country.setText(movieInformations.getString("Country")); + metascore.setText(movieInformations.getString("Metascore")); + imdbrating.setText(movieInformations.getString("imdbRating")); + imdbvotes.setText(movieInformations.getString("imdbVotes")); + writer.setText(movieInformations.getString("Writer")); + mImageLoader = CustomVolleyRequestQueue.getInstance(MovieDetails.this).getImageLoader(); + mImageLoader.get(movieInformations.getString("Poster"), ImageLoader.getImageListener(mNetworkImageView, + R.mipmap.ic_launcher, android.R.drawable.ic_dialog_alert)); + mNetworkImageView.setImageUrl(movieInformations.getString("Poster"), mImageLoader); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + @Override + public void notifyError(String error) { + + } + }; + } +} diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/Activity/MoviesList.java b/app/src/main/java/android_challenge/com/moviesdatabase/Activity/MoviesList.java new file mode 100644 index 0000000..5136b57 --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/Activity/MoviesList.java @@ -0,0 +1,233 @@ +package android_challenge.com.moviesdatabase.Activity; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.TextView; +import java.util.ArrayList; +import java.util.List; +import android_challenge.com.moviesdatabase.Adapter.MovieListAdapter; +import android_challenge.com.moviesdatabase.Pojo.Movies; +import android_challenge.com.moviesdatabase.R; +import android_challenge.com.moviesdatabase.WebRequests.SearchMovies; + +public class MoviesList extends AppCompatActivity implements MovieListAdapter.OnDataSelected, SearchView.OnQueryTextListener { + + private RecyclerView.Adapter adapter; + private RecyclerView recyclerView; + private Button seemoremovies; + private LinearLayoutManager linearLayoutManager; + public List moviesList = new ArrayList<>(); + private TextView notfound; + private int page = 1; + private int scrolled = 1; + private int idscrooled; + private LinearLayout linearLayout; + private MenuItem item; + private SearchView searchView; + private String text; + private String title = "Star"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_movies_list); + + notfound = (TextView) findViewById(R.id.notfound); + recyclerView = (RecyclerView) findViewById(R.id.recycle_view_movie); + recyclerView.setHasFixedSize(true); + linearLayoutManager = new LinearLayoutManager(this); + linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); + recyclerView.setLayoutManager(linearLayoutManager); + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(final RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + if (moviesList.size() == linearLayoutManager.findLastCompletelyVisibleItemPosition() + scrolled) { + scrolled = -1; + idscrooled = linearLayoutManager.findLastCompletelyVisibleItemPosition(); + seemoremovies = new Button(MoviesList.this); + seemoremovies.setText("Ver mais"); + seemoremovies.setBackgroundColor(getResources().getColor(R.color.cardview_light_background)); + seemoremovies.setTextColor(getResources().getColor(R.color.colorPrimaryDark)); + seemoremovies.setTextSize(16); + linearLayout = (LinearLayout) findViewById(R.id.see_more_products); + linearLayout.addView(seemoremovies); + + seemoremovies.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + page = page + 1; + + ConnectivityManager connMgr = (ConnectivityManager) + getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + + if (networkInfo != null && networkInfo.isConnected()) { + SearchMovies searchMovies = new SearchMovies(); + searchMovies.getMovies(MoviesList.this, title, page, moviesList, adapter, notfound); + linearLayout.removeView(seemoremovies); + scrolled = 1; + } else { + new AlertDialog.Builder(MoviesList.this) + .setTitle("Sem Internet!") + .setMessage("Não tem nenhuma conexão de rede disponível!") + .setPositiveButton(R.string.box_msg_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + } + }); + } + if (idscrooled != 0) { + if (idscrooled != linearLayoutManager.findLastVisibleItemPosition()) { + linearLayout.removeView(seemoremovies); + scrolled = 1; + } + } + } + }); + + adapter = new MovieListAdapter(this, (MovieListAdapter.OnDataSelected) this, moviesList); + recyclerView.setAdapter(adapter); + + makerequest(); + + } + public void makerequest() { + + ConnectivityManager connMgr = (ConnectivityManager) + getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + + if (networkInfo != null && networkInfo.isConnected()) { + SearchMovies moviesRequests = new SearchMovies(); + moviesRequests.getMovies(MoviesList.this, this.title, this.page, moviesList, adapter, notfound); + } else { + new AlertDialog.Builder(MoviesList.this) + .setTitle("Sem Internet!") + .setMessage("Não tem nenhuma conexão de rede disponível!") + .setPositiveButton(R.string.box_msg_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + } + + + @Override + public void onDataSelected(View view, int position) { + + Movies selectedItem; + selectedItem = moviesList.get(position); + String imdbID = selectedItem.imdbID; + + Intent intent = new Intent(this, MovieDetails.class); + intent.putExtra("ID", imdbID); + startActivity(intent); + + } + + public boolean onCreateOptionsMenu(final Menu menu) { + super.onCreateOptionsMenu(menu); + getMenuInflater().inflate(R.menu.search_menu, menu); + + item = menu.findItem(R.id.action_search); + item.setVisible(true); + searchView = (SearchView) MenuItemCompat.getActionView(item); + searchView.setIconifiedByDefault(true); + searchView.setQueryHint("Pesquisar filme"); + searchView.setOnQueryTextListener(MoviesList.this); + + searchView.setOnSearchClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + setItemsVisibility(menu, item, false); + + } + }); + + searchView.setOnCloseListener(new SearchView.OnCloseListener() { + @Override + public boolean onClose() { + setItemsVisibility(menu, item, true); + return false; + } + }); + + return super.onCreateOptionsMenu(menu); + } + + private void setItemsVisibility(Menu menu, MenuItem exception, boolean visible) { + for (int i=0; i(); + adapter = new MovieListAdapter(this,(MovieListAdapter.OnDataSelected)this,moviesList); + recyclerView.setAdapter(adapter); + page = 1; + title = text.toString().trim(); + notfound.setText(""); + + ConnectivityManager connMgr = (ConnectivityManager) + getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo networkInfo = connMgr.getActiveNetworkInfo(); + + if (networkInfo != null && networkInfo.isConnected()) { + SearchMovies searchMovies = new SearchMovies(); + searchMovies.getMovies(MoviesList.this, title, page, moviesList, adapter, notfound); + searchView.clearFocus(); + } else{ + new AlertDialog.Builder(MoviesList.this) + .setTitle("Sem Internet!") + .setMessage("Não tem nenhuma conexão de rede disponível!") + .setPositiveButton(R.string.box_msg_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + @Override + public void onBackPressed() { + } + +} diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/Activity/SplashScreen.java b/app/src/main/java/android_challenge/com/moviesdatabase/Activity/SplashScreen.java new file mode 100644 index 0000000..58e435c --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/Activity/SplashScreen.java @@ -0,0 +1,53 @@ +package android_challenge.com.moviesdatabase.Activity; + +import android.content.Intent; +import android.os.Handler; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android_challenge.com.moviesdatabase.R; + +public class SplashScreen extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_main); + StartAnimations(); + } + + + private void StartAnimations(){ + Animation anim = AnimationUtils.loadAnimation(this, R.anim.alpha); + anim.reset(); + LinearLayout l = (LinearLayout) findViewById(R.id.splashscreen); + if (l != null){ + l.clearAnimation(); + l.startAnimation(anim); + } + + anim = AnimationUtils.loadAnimation(this, R.anim.translate); + anim.reset(); + ImageView iv = (ImageView) findViewById(R.id.imageview_logo); + if (iv != null){ + iv.clearAnimation(); + iv.startAnimation(anim); + } + + int SPLASH_DISPLAY_LENGTH = 6000; + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + Intent intent = new Intent (SplashScreen.this, MoviesList.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + startActivity(intent); + SplashScreen.this.finish(); + } + }, SPLASH_DISPLAY_LENGTH); + } + +} diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/Adapter/MovieListAdapter.java b/app/src/main/java/android_challenge/com/moviesdatabase/Adapter/MovieListAdapter.java new file mode 100644 index 0000000..dc005c8 --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/Adapter/MovieListAdapter.java @@ -0,0 +1,100 @@ +package android_challenge.com.moviesdatabase.Adapter; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import com.android.volley.Response; +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; +import org.json.JSONArray; +import java.util.List; +import android_challenge.com.moviesdatabase.Pojo.Movies; +import android_challenge.com.moviesdatabase.R; +import android_challenge.com.moviesdatabase.WebRequests.CustomVolleyRequestQueue; + +/** + * Created by Fabio_2 on 10/12/2018. + */ + +public class MovieListAdapter extends RecyclerView.Adapter implements Response.Listener { + + private List moviesList; + private Context context; + private MovieListAdapter.OnDataSelected onDataSelected; + private ImageLoader mImageLoader; + + + public MovieListAdapter(Context context, MovieListAdapter.OnDataSelected onDataSelected, List moviesList) { + this.context = context; + this.onDataSelected = onDataSelected; + this.moviesList = moviesList; + } + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_movie, parent, false); + ViewHolder viewHolder = new ViewHolder(view); + return viewHolder; + } + + @Override + public void onBindViewHolder(MovieListAdapter.ViewHolder holder, int position) { + Movies movies = moviesList.get(position); + + holder.textViewTitleMovie.setText(movies.getTitle()); + holder.textViewTypeMovie.setText(movies.getType()); + holder.textViewYearMovie.setText(String.valueOf(movies.getYear())); + + mImageLoader = CustomVolleyRequestQueue.getInstance(context).getImageLoader(); + mImageLoader.get(movies.getPoster(),ImageLoader.getImageListener(holder.mNetworkImageView,R.mipmap.ic_launcher, android.R.drawable.ic_dialog_alert)); + holder.mNetworkImageView.setImageUrl(movies.getPoster(),mImageLoader); + } + + @Override + public int getItemCount() { + return moviesList.size(); + } + + + @Override + public void onResponse(JSONArray response) { + } + + public interface OnDataSelected { + void onDataSelected(View view, int position); + } + + public class ViewHolder extends RecyclerView.ViewHolder{ + + public TextView textViewTitleMovie; + public TextView textViewTypeMovie; + public TextView textViewYearMovie; + public NetworkImageView mNetworkImageView; + + public ViewHolder(View view) { + super(view); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + treatOnDataSelectedIfNecessary(v,getAdapterPosition()); + } + }); + + textViewTitleMovie = (TextView)view.findViewById(R.id.title_movie); + textViewTypeMovie = (TextView) view.findViewById(R.id.type_movie); + textViewYearMovie = (TextView)view.findViewById(R.id.year_movie); + mNetworkImageView = (NetworkImageView)view.findViewById(R.id.poster_movie); + + } + + private void treatOnDataSelectedIfNecessary(View view, int position) { + if(onDataSelected != null) { + onDataSelected.onDataSelected(view, position); + + } + } + + } +} diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/Interface/ResultsMovie.java b/app/src/main/java/android_challenge/com/moviesdatabase/Interface/ResultsMovie.java new file mode 100644 index 0000000..6d6d8a5 --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/Interface/ResultsMovie.java @@ -0,0 +1,14 @@ +package android_challenge.com.moviesdatabase.Interface; + +import org.json.JSONObject; + +/** + * Created by Fabio_2 on 12/12/2018. + */ + +public interface ResultsMovie { + + void notifySuccess(JSONObject movieInformations); + void notifyError(String error); + +} diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/Pojo/Movies.java b/app/src/main/java/android_challenge/com/moviesdatabase/Pojo/Movies.java new file mode 100644 index 0000000..5e3577c --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/Pojo/Movies.java @@ -0,0 +1,34 @@ +package android_challenge.com.moviesdatabase.Pojo; + +/** + * Created by Fabio_2 on 10/12/2018. + */ + +public class Movies { + + public String title; + public int year; + public String imdbID; + public String type; + public String poster; + + public Movies(String title, int year, String imdbID, String type, String poster){ + this.title = title; + this.year = year; + this.imdbID = imdbID; + this.type = type; + this.poster = poster; + + } + + public String getTitle(){ return this.title; } + + public int getYear(){ return this.year; } + + public String getImdbID(){ return this.imdbID; } + + public String getType(){ return this.type; } + + public String getPoster(){ return this.poster; } + +} diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/CustomVolleyRequestQueue.java b/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/CustomVolleyRequestQueue.java new file mode 100644 index 0000000..9b696cf --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/CustomVolleyRequestQueue.java @@ -0,0 +1,68 @@ +package android_challenge.com.moviesdatabase.WebRequests; + +import android.content.Context; +import android.graphics.Bitmap; +import android.util.LruCache; +import com.android.volley.Cache; +import com.android.volley.Network; +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.BasicNetwork; +import com.android.volley.toolbox.DiskBasedCache; +import com.android.volley.toolbox.HurlStack; +import com.android.volley.toolbox.ImageLoader; + +/** + * Created by Fabio_2 on 10/12/2018. + */ + +public class CustomVolleyRequestQueue { + + private static CustomVolleyRequestQueue mInstance; + private static Context mCtx; + private RequestQueue mRequestQueue; + private ImageLoader mImageLoader; + + + private CustomVolleyRequestQueue(Context context) { + mCtx = context; + mRequestQueue = getRequestQueue(); + + mImageLoader = new ImageLoader(mRequestQueue, + new ImageLoader.ImageCache() { + private final LruCache + cache = new LruCache(20); + + @Override + public Bitmap getBitmap(String url) { + return cache.get(url); + } + + @Override + public void putBitmap(String url, Bitmap bitmap) { + cache.put(url, bitmap); + } + }); + } + + public static synchronized CustomVolleyRequestQueue getInstance(Context context) { + if (mInstance == null) { + mInstance = new CustomVolleyRequestQueue(context); + } + return mInstance; + } + + public RequestQueue getRequestQueue() { + if (mRequestQueue == null) { + Cache cache = new DiskBasedCache(mCtx.getCacheDir(), 10 * 1024 * 1024); + Network network = new BasicNetwork(new HurlStack()); + mRequestQueue = new RequestQueue(cache, network); + mRequestQueue.start(); + } + return mRequestQueue; + } + + public ImageLoader getImageLoader() { + return mImageLoader; + } + +} diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/MovieDetailsRequest.java b/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/MovieDetailsRequest.java new file mode 100644 index 0000000..f078c55 --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/MovieDetailsRequest.java @@ -0,0 +1,106 @@ +package android_challenge.com.moviesdatabase.WebRequests; + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import com.android.volley.DefaultRetryPolicy; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import org.json.JSONObject; +import android_challenge.com.moviesdatabase.Interface.ResultsMovie; +import android_challenge.com.moviesdatabase.R; + +/** + * Created by Fabio_2 on 12/12/2018. + */ + +public class MovieDetailsRequest { + + private String _url = "http://www.omdbapi.com/?"; + private String _api_key = "58b7ee65"; + + public MovieDetailsRequest(){ + + } + + private ResultsMovie mResultsMovieCallBack = null; + + public MovieDetailsRequest(ResultsMovie mResultsMovieCallBack1){ + this.mResultsMovieCallBack = mResultsMovieCallBack1; + } + + public void searchByID(final Context context, final String id) { + + String url = ""; + url = _url + "i=" + id + "&apikey=" + _api_key; + + final ProgressDialog dialog; + + dialog = new ProgressDialog(context); + dialog.setMessage("Por favor aguarde..."); + dialog.setCanceledOnTouchOutside(false); + dialog.setCancelable(false); + dialog.show(); + + + final JsonObjectRequest req = new JsonObjectRequest(Request.Method.GET, url, new JSONObject(), + new Response.Listener() { + + @Override + public void onResponse(JSONObject response) { + dialog.dismiss(); + try { + + if (mResultsMovieCallBack != null) + mResultsMovieCallBack.notifySuccess(response); + + } catch (Exception e) { + e.printStackTrace(); + } + + } + + + }, new Response.ErrorListener() { + + + @Override + public void onErrorResponse(VolleyError error) { + dialog.dismiss(); + + if (error.getClass().equals(TimeoutError.class)) { + dialog.dismiss(); + new AlertDialog.Builder(context) + .setTitle("Problema na conexão!") + .setMessage("Internet instável! Favor tentar novamente!") + .setPositiveButton(R.string.msg_no_internet, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + searchByID(context, id); + } + }) + .setNegativeButton(R.string.canceled, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (mResultsMovieCallBack != null) { + mResultsMovieCallBack.notifyError("return"); + } + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + } + }); + int socketTimeout = 10000; + RetryPolicy policy = new DefaultRetryPolicy(socketTimeout, 1, 1); + req.setRetryPolicy(policy); + Volley.newRequestQueue(context).add(req); + } +} diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/SearchMovies.java b/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/SearchMovies.java new file mode 100644 index 0000000..6e1f8b7 --- /dev/null +++ b/app/src/main/java/android_challenge/com/moviesdatabase/WebRequests/SearchMovies.java @@ -0,0 +1,116 @@ +package android_challenge.com.moviesdatabase.WebRequests; + + +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.support.v7.widget.RecyclerView; +import android.widget.TextView; +import com.android.volley.DefaultRetryPolicy; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.toolbox.JsonObjectRequest; +import com.android.volley.toolbox.Volley; +import org.json.JSONArray; +import org.json.JSONObject; +import java.util.List; +import android_challenge.com.moviesdatabase.Pojo.Movies; +import android_challenge.com.moviesdatabase.R; + +/** + * Created by Fabio_2 on 09/12/2018. + */ + +public class SearchMovies { + + private String _url = "http://www.omdbapi.com/?"; + private String _api_key = "58b7ee65"; + + public SearchMovies() { + + } + + public void getMovies(final Context context, final String title, final int page, final List moviesList, + final RecyclerView.Adapter adapter, final TextView notfound) { + + String url = "s=" + title + "&page=" + String.valueOf(page) + "&apikey=" + this._api_key; + this._url = this._url + url; + + + final ProgressDialog dialog; + if(page == 1){ + dialog = new ProgressDialog(context); + dialog.setMessage("Buscando Filmes..."); + } else { + dialog = new ProgressDialog(context, R.style.MyThemeDialog); + dialog.setProgressStyle(android.R.style.Widget_ProgressBar_Small); + } + dialog.setCanceledOnTouchOutside(false); + dialog.setCancelable(false); + dialog.show(); + + final JsonObjectRequest req = new JsonObjectRequest(Request.Method.GET, this._url, new JSONObject(), + new Response.Listener() { + + @Override + public void onResponse(JSONObject response) { + dialog.dismiss(); + try { + + if (!response.toString().contains("Search")) + notfound.setText("Nenhum filme encontrado!"); + else { + JSONArray jsonArray = response.getJSONArray("Search"); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + String title = jsonObject.getString("Title"); + int year = Integer.parseInt(jsonObject.getString("Year")); + String imdbID = jsonObject.getString("imdbID"); + String type = jsonObject.getString("Type"); + String poster = jsonObject.getString("Poster"); + Movies movies = new Movies(title, year, imdbID, type, poster); + moviesList.add(movies); + adapter.notifyDataSetChanged(); + } + } + }catch (Exception e){ + + } + + } + }, new Response.ErrorListener() { + + @Override + public void onErrorResponse(VolleyError error) { + dialog.dismiss(); + if (error.getClass().equals(TimeoutError.class)) { + dialog.dismiss(); + new AlertDialog.Builder(context) + .setTitle("Problema na conexão!") + .setMessage("Internet instável! Favor tentar novamente!") + .setPositiveButton(R.string.msg_no_internet, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + getMovies(context, title, page, moviesList, adapter, notfound); + } + }) + .setNegativeButton(R.string.canceled, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + } + }); + int socketTimeout = 10000; + RetryPolicy policy = new DefaultRetryPolicy(socketTimeout, 1, 1); + req.setRetryPolicy(policy); + Volley.newRequestQueue(context).add(req); + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/alpha.xml b/app/src/main/res/anim/alpha.xml new file mode 100644 index 0000000..8fbf28f --- /dev/null +++ b/app/src/main/res/anim/alpha.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/translate.xml b/app/src/main/res/anim/translate.xml new file mode 100644 index 0000000..f6cf643 --- /dev/null +++ b/app/src/main/res/anim/translate.xml @@ -0,0 +1,16 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/card_movie.png b/app/src/main/res/drawable/card_movie.png new file mode 100644 index 0000000000000000000000000000000000000000..c10739c2840dfd25156b66902ae4d6a3966ea173 GIT binary patch literal 2001 zcmW-iYgm%m7RNVU!pj(X@DdTm(NgOftr4{}3{t~ODV6!ypjdOfAf_kB3%+KjvnwBEz=rMUo~5cEn3Zsmy<+pT{CC~K{Sqh^&*;u>tV2ItHFymRqbTp+2iEGK`g_2`S>0p_0>a?aH2 zKy#4(hA)@Xm5x}GuN^>fqOhLNQ-kuzFsgA%i1JywR4A$eB}XZCj;?H}Cc-(&%r0`n2t)Au(Pyw3!ASye_4iL*f3~6xQ17k% zo3Qzm*rQJ7;RvREt{bY-1CAa(EFcKLJI0WH9p$KLV`<2P%&e^2HsIZk1V!vig1aM3q(V1mH2zxj`6n>f{S3;e z+!F24qck`0wEA^AJ{v*K!2P|S%co%N<-BL-o={L6kU%Oki|+Av5pr+B2ijlVLzN&;cZ*`ZagJ0kWIORF24BQ zm+T%yN;(}0b<~|3H#k~~-|*sV&8T~FRtuZ^n3ZFXl}gy)8n<#o)tP6hq|KYt|9^J( zM=hL$&B-`vH9iWAOx@6`-8zzZd_Pc zXxZg;%Le#4A3ze87%)tQ0GL5}{9>XKcFR(K*p2mSZg{pp& zN`6rLdefr4&~q1(5sA$rYj6yQ-1kqNzNje7NB_9qQqNE&P54hObZKdFnp!metNg=Z z9g8Pf?0YT@KIp@|lj4(j>p+)z@y6-Q1BVW&ryQ=R&Y_4QNI+znYG!s;(9l4LN7?%- zNXrOCrV$k$HbO;|m#o+Pfo?c|KHCZW-Hl9-tQ?bjm?pKsNC=$FH5nRYzc>AKL;VMrH|>!xe5=G zeuqYpm=Pnntmk$Z%qu%u`fy2#gAlck!SF4ZdVM_}JRy?Jn@))XTzVoLspD4a`(EVy z3{MEvN(G6UF?eY3Ue?ISNTMY*k<0qsZ~CQk0R$J8T&^Hx2hGmC=`3p5(+b;8vlnh` zYik>s8t6#laQfnaUkIY;mF!dtgGD^FAPgC*g=+hEUxpV{3=J8wvJ`XDd*_XyIvg3= z2VwDe_Vt>FRK)EZ zpO5PZg$4)-N8AEBc^3fzkI!`?lDVESe@LY@ymJmeDzM(W@lt{SUb}7J7b3am21Msc z1J%olzJ{hRT>Sno9b!>p2y1qBaZ&N`nl$B$e#z-=zx8EkoOJS2XU||%XV-7H7Ekzn+SHpFJ=AyU z4T=5PH1w!)#5DhD3jemTEna%3Lv&~P)|o{Nx|sSW9^?_oYse^~bMkld;E;~t6V@d; Q@HY%1x5tD@LJl1NAK^-*KL7v# literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/fundocinza.png b/app/src/main/res/drawable/fundocinza.png new file mode 100644 index 0000000000000000000000000000000000000000..7ba367aae8fc1e86763b2951b0b47b43428ce053 GIT binary patch literal 57645 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 X#PMb4Cq9AydHapQ|J~T{q9SFYQX-ACgdiaz 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 + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/search_menu.png b/app/src/main/res/drawable/search_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..05e8436a8a5bd8eebca7623066ca07b17abb43f6 GIT binary patch literal 1581 zcmV+|2GaS7P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!T$Z3V;H}%@5rDUDi$?(7x-C-U06#xJ%FN89lP6D7e0)4bM@Q4ag9pji*O%7U z*J*iqnclp4L*wJ)^!)jAnx3BKzxm%GMS!TND7tdx3gzYHu?IHG>({U8_U+r$-``I^ ze*ECGd&{l_2n-CQ($Z48bmQ{79cS(k(!&E>G0vh zd}jANeE5(W8XD;9*ROm=Jx&RblaoV@jg8FaWivA~L*?b=^x?w?KBE?g1vq!^9JREx z(7t`PHxStK=;$bOa!X4~^ySMJ3JndVh=>Tv$jG1*CrTMV=cp3zt?RFUy7sB4sG!rQPxFxm z_V?=QYPx&(E^TgZ@>z3y`t*q&Ja|B1VPTYzkicg(u;PY?hn4yRQ;{H+Y++#`A8Fvt z*VNR|(9jT{ah#t&e=>)P9Y7EK41`ySDFKR#ipbj=i)AOU_a{%D@R9RatyZe9uV*`e z9+*d3S{fh8$4~%&e}Br&&E+GFckkXYo0rA6Z{KX%yTxMRJ^2_40K10+)YIA7$(FB7 zUc7k0as)kyJM3+{yTR~Vk=W?@{{1@*4i55>Tm}XPcuylJC`jleWnd@(vN}C@OIWfh zc>44ybFO+s_aOsA0b*iecu!+$YKr&dv$nRz0xCU7tyN$s0G6K~)p0t)XFZOj*^Xc+ zfGF(!{P{EQsX_GpDiSYXC;%coJxFX+5)u-^dm5^Ij-ddej4V7nocGj#tWl51EM#CP z!0hZS?`hadUSx3e=uws%=z070E$_+2PyiG#^&m-6wF=IhIm3H95#5Ij34R*xfl#}nql1neJH}@;K7Ra2RaI5OSVR`4OJb<- z;w(Uv8z56Vb?Ov5=`s7ZP+)=0H#Id;WMm|t-3f{$6B84Bqy~qo!ok78tO#N&F@e8$ z{P;0TX`elNCaesjsCxG7St=+fV0Lc{d{1R%B_FB7;d22z04l(uXl*;F4$sfevj`5c zJ;G<~8ltMQ2$&OUjc|f5U%nJNVi`Cr0628$5M8}`m5v`j&S&HSUy_xT#bS-^w6(R- zjT<-kNFJtrz;^IP@87@ALRMLY3e*8$^X={Jl#-J2FEfKEaE4=JV|+#qP6+_8G-0Q& zUcI8&*jQowZU&qsV5@_-{~hk0KYw1hE2t2`X#swN>TqvwFS{v(vN-KAb5xGsgI(if z2u^eRRtFQqt|CIV2u}F`d!TTUoSaN?adB+RJaFIuE3MzQvg$TPg0hQp09RZWA`hyH}_@e~?szvzY1pq2Va0vl`DiK^p0H8tyml6QT z7Qy8N0A@tMf21NkH^L + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..4396300 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_movie_details.xml b/app/src/main/res/layout/activity_movie_details.xml new file mode 100644 index 0000000..c0763e5 --- /dev/null +++ b/app/src/main/res/layout/activity_movie_details.xml @@ -0,0 +1,436 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_movies_list.xml b/app/src/main/res/layout/activity_movies_list.xml new file mode 100644 index 0000000..5707325 --- /dev/null +++ b/app/src/main/res/layout/activity_movies_list.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/card_movie.xml b/app/src/main/res/layout/card_movie.xml new file mode 100644 index 0000000..43a904e --- /dev/null +++ b/app/src/main/res/layout/card_movie.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/search_menu.xml b/app/src/main/res/menu/search_menu.xml new file mode 100644 index 0000000..ce0d57b --- /dev/null +++ b/app/src/main/res/menu/search_menu.xml @@ -0,0 +1,17 @@ + +

+ + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c133a0cbd379f5af6dbf1a899a0293ca5eccfad0 GIT binary patch literal 2206 zcmZ{mc|6mPAICqNwJrBe)OTwlM~;a|Uz&T!k&1?zWA4p4M`7F7`J;wP4IvS^nJ7{y zAtgDMD>=W8<&GtU-}>|S$M5}kyxz~p>-~Pb{(irc?QF~icx8A201&Xin%Hxx@kekd zw>yHjlemC*8(JFz05gs6x7#7EM|xoGtpVVs0szqB0bqwaqAdVG7&rLc6#(=y0YEA! z=jFw}xeKVfmAMI*+}bv7qH=LK2#X5^06wul0s+}M(f|O@&WMyG9frlGyLb z&Eix=47rL84J+tEWcy_XTyc*xw9uOQy`qmHCjAeJ?d=dUhm;P}^F=LH42AEMIh6X8 z*I7Q1jK%gVlL|8w?%##)xSIY`Y+9$SC8!X*_A*S0SWOKNUtza(FZHahoC2|6f=*oD zxJ8-RZk!+YpG+J}Uqnq$y%y>O^@e5M3SSw^29PMwt%8lX^9FT=O@VX$FCLBdlj#<{ zJWWH<#iU!^E7axvK+`u;$*sGq1SmGYc&{g03Md&$r@btQSUIjl&yJXA&=79FdJ+D< z4K^ORdM{M0b2{wRROvjz1@Rb>5dFb@gfkYiIOAKM(NR3*1JpeR_Hk3>WGvU&>}D^HXZ02JUnM z@1s_HhX#rG7;|FkSh2#agJ_2fREo)L`ws+6{?IeWV(>Dy8A(6)IjpSH-n_uO=810y z#4?ez9NnERv6k)N13sXmx)=sv=$$i_QK`hp%I2cyi*J=ihBWZLwpx9Z#|s;+XI!0s zLjYRVt!1KO;mnb7ZL~XoefWU02f{jcY`2wZ4QK+q7gc4iz%d0)5$tPUg~$jVI6vFO zK^wG7t=**T40km@TNUK+WTx<1mL|6Tn6+kB+E$Gpt8SauF9E-CR9Uui_EHn_nmBqS z>o#G}58nHFtICqJPx<_?UZ;z0_(0&UqMnTftMKW@%AxYpa!g0fxGe060^xkRtYguj ze&fPtC!?RgE}FsE0*^2lnE>42K#jp^nJDyzp{JV*jU?{+%KzW37-q|d3i&%eooE6C8Z2t2 z9bBL;^fzVhdLxCQh1+Ms5P)ilz9MYFKdqYN%*u^ch(Fq~QJASr5V_=szAKA4Xm5M} z(Kka%r!noMtz6ZUbjBrJ?Hy&c+mHB{OFQ}=41Irej{0N90`E*~_F1&7Du+zF{Dky) z+KN|-mmIT`Thcij!{3=ibyIn830G zN{kI3d`NgUEJ|2If}J!?@w~FV+v?~tlo8ps3Nl`3^kI)WfZ0|ms6U8HEvD9HIDWkz6`T_QSewYZyzkRh)!g~R>!jaR9;K|#82kfE5^;R!~}H4C?q{1AG?O$5kGp)G$f%VML%aPD?{ zG6)*KodSZRXbl8OD=ETxQLJz)KMI7xjArKUNh3@0f|T|75?Yy=pD7056ja0W)O;Td zCEJ=7q?d|$3rZb+8Cvt6mybV-#1B2}Jai^DOjM2<90tpql|M5tmheg){2NyZR}x3w zL6u}F+C-PIzZ56q0x$;mVJXM1V0;F}y9F29ob51f;;+)t&7l30gloMMHPTuod530FC}j^4#qOJV%5!&e!H9#!N&XQvs5{R zD_FOomd-uk@?_JiWP%&nQ_myBlM6so1Ffa1aaL7B`!ZTXPg_S%TUS*>M^8iJRj1*~ e{{%>Z1YfTk|3C04d;8A^0$7;Zm{b|L#{L(;l>}-4 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-mdpi/imdb.png b/app/src/main/res/mipmap-mdpi/imdb.png new file mode 100644 index 0000000000000000000000000000000000000000..d3604898f20454d254de4e523e6d16476eb1ff0f GIT binary patch literal 654 zcmV;90&)F`P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0wYO8K~!i%?U>E0 zO;H$!H`hnqiIM>mif*EmB1y{R8c9ryZYDl6Vt`B)`43De69&v~DFcym>r(D5N;06t zR743Sp5J=+I<39$$~mq>d)52Y(|*sp*V$+N+IydM_Ng4laU92S9OqA1i2Lvg-od*a z^*SEIQJljne1ty>@*Qr+9!mBMbA!9?M_iAAGngAxijx?a#oT~W{Eo}8`cgM?zQ6;C z>OE}r4x#!_lU{TNwXfYjif8c_enX8pfpPzhx;Lm>F}owGNvEi%4{7rruUXmPHtIdU zV*)j1b$Df*!#IaQ+=JXH}MCi^J+V^AeZs5 zl?`%1bj(Xu_IQJ7kTs}vEz<7{4K0YS?{O;|%s0pm)V&hX8BFKZc4$F#pfM{O%s0p$ z)LDpZ!gOA3|2oK`daJL$rB+sp>#Zmz$cB2GlHSP01knLjTG``Zy%j|T*==QDIi?dR zD#$4-1G});AXlvnoWy=Xj$nEpX}$P>X%L;|(^dxlOOVG_1{A7akTzN`G$swAa7(NV z=(!`#Dd$jve8L6cm4(U$S(~^Hx8Pn>@c5%K22lt-1s+F@)8#saaZWiykgrLjRJURE zJZY4ueo(H!>O$Qp)%zIGcmG?=4eGkOzrtk|*@&+(H@K_n8D>AuLI0*`al3~v@l{8i oMlGoN3(IjF$8j9T`R7#CFEe90V$?Qc(*OVf07*qoM6N<$f`M5h=>Px# literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f0e7b91d006d22352c9ff2f134e504e3c1d GIT binary patch literal 4842 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/imdb.png b/app/src/main/res/mipmap-xhdpi/imdb.png new file mode 100644 index 0000000000000000000000000000000000000000..d3604898f20454d254de4e523e6d16476eb1ff0f GIT binary patch literal 654 zcmV;90&)F`P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0wYO8K~!i%?U>E0 zO;H$!H`hnqiIM>mif*EmB1y{R8c9ryZYDl6Vt`B)`43De69&v~DFcym>r(D5N;06t zR743Sp5J=+I<39$$~mq>d)52Y(|*sp*V$+N+IydM_Ng4laU92S9OqA1i2Lvg-od*a z^*SEIQJljne1ty>@*Qr+9!mBMbA!9?M_iAAGngAxijx?a#oT~W{Eo}8`cgM?zQ6;C z>OE}r4x#!_lU{TNwXfYjif8c_enX8pfpPzhx;Lm>F}owGNvEi%4{7rruUXmPHtIdU zV*)j1b$Df*!#IaQ+=JXH}MCi^J+V^AeZs5 zl?`%1bj(Xu_IQJ7kTs}vEz<7{4K0YS?{O;|%s0pm)V&hX8BFKZc4$F#pfM{O%s0p$ z)LDpZ!gOA3|2oK`daJL$rB+sp>#Zmz$cB2GlHSP01knLjTG``Zy%j|T*==QDIi?dR zD#$4-1G});AXlvnoWy=Xj$nEpX}$P>X%L;|(^dxlOOVG_1{A7akTzN`G$swAa7(NV z=(!`#Dd$jve8L6cm4(U$S(~^Hx8Pn>@c5%K22lt-1s+F@)8#saaZWiykgrLjRJURE zJZY4ueo(H!>O$Qp)%zIGcmG?=4eGkOzrtk|*@&+(H@K_n8D>AuLI0*`al3~v@l{8i oMlGoN3(IjF$8j9T`R7#CFEe90V$?Qc(*OVf07*qoM6N<$f`M5h=>Px# literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/imdb.png b/app/src/main/res/mipmap-xxhdpi/imdb.png new file mode 100644 index 0000000000000000000000000000000000000000..d3604898f20454d254de4e523e6d16476eb1ff0f GIT binary patch literal 654 zcmV;90&)F`P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0wYO8K~!i%?U>E0 zO;H$!H`hnqiIM>mif*EmB1y{R8c9ryZYDl6Vt`B)`43De69&v~DFcym>r(D5N;06t zR743Sp5J=+I<39$$~mq>d)52Y(|*sp*V$+N+IydM_Ng4laU92S9OqA1i2Lvg-od*a z^*SEIQJljne1ty>@*Qr+9!mBMbA!9?M_iAAGngAxijx?a#oT~W{Eo}8`cgM?zQ6;C z>OE}r4x#!_lU{TNwXfYjif8c_enX8pfpPzhx;Lm>F}owGNvEi%4{7rruUXmPHtIdU zV*)j1b$Df*!#IaQ+=JXH}MCi^J+V^AeZs5 zl?`%1bj(Xu_IQJ7kTs}vEz<7{4K0YS?{O;|%s0pm)V&hX8BFKZc4$F#pfM{O%s0p$ z)LDpZ!gOA3|2oK`daJL$rB+sp>#Zmz$cB2GlHSP01knLjTG``Zy%j|T*==QDIi?dR zD#$4-1G});AXlvnoWy=Xj$nEpX}$P>X%L;|(^dxlOOVG_1{A7akTzN`G$swAa7(NV z=(!`#Dd$jve8L6cm4(U$S(~^Hx8Pn>@c5%K22lt-1s+F@)8#saaZWiykgrLjRJURE zJZY4ueo(H!>O$Qp)%zIGcmG?=4eGkOzrtk|*@&+(H@K_n8D>AuLI0*`al3~v@l{8i oMlGoN3(IjF$8j9T`R7#CFEe90V$?Qc(*OVf07*qoM6N<$f`M5h=>Px# literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..aee44e138434630332d88b1680f33c4b24c70ab3 GIT binary patch literal 10486 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..b001dfd --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #3F51B5 + #303F9F + #FF4081 + #F9E832 + #212121 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..86af93b --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + IMDb Movie Listing + Ok + Tentar Novamente? + Cancelar + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..166e3fd --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/app/src/test/java/android_challenge/com/moviesdatabase/ExampleUnitTest.java b/app/src/test/java/android_challenge/com/moviesdatabase/ExampleUnitTest.java new file mode 100644 index 0000000..abdc913 --- /dev/null +++ b/app/src/test/java/android_challenge/com/moviesdatabase/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package android_challenge.com.moviesdatabase; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..b78a0b8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,23 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:2.3.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..aac7c9b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13372aef5e24af05341d49695ee84e5f9b594659 GIT binary patch literal 53636 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ⅅ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 H-+%o-(+5aQ literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..851c774 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Dec 09 23:29:21 BRST 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..9d82f78 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..046dc4d --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app', ':volley' diff --git a/volley/.gitignore b/volley/.gitignore new file mode 100644 index 0000000..8889923 --- /dev/null +++ b/volley/.gitignore @@ -0,0 +1,9 @@ +bin +gen +.gradle +build +.settings +target +*.iml +.idea +local.properties diff --git a/volley/Android.mk b/volley/Android.mk new file mode 100644 index 0000000..dbe5194 --- /dev/null +++ b/volley/Android.mk @@ -0,0 +1,33 @@ +# +# Copyright (C) 2011 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := volley +LOCAL_SDK_VERSION := 17 +LOCAL_SRC_FILES := $(call all-java-files-under, src/main/java) + +include $(BUILD_STATIC_JAVA_LIBRARY) + +# Include this library in the build server's output directory +# TODO: Not yet. +#$(call dist-for-goals, dist_files, $(LOCAL_BUILT_MODULE):volley.jar) + +# Include build files in subdirectories +include $(call all-makefiles-under,$(LOCAL_PATH)) + diff --git a/volley/bintray.gradle b/volley/bintray.gradle new file mode 100644 index 0000000..f07693e --- /dev/null +++ b/volley/bintray.gradle @@ -0,0 +1,87 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2" + } +} + +// apply the plugin with its class name rather than its Id to work around gradle limitation of +// not being able to find the plugin by Id despite the dependencies being added right above. Gradle +// is currently not capable of loading plugins by Id if the dependency is anywhere else than +// in the main project build.gradle. This file is "imported" into the project's build.gradle +// through a "apply from:". +apply plugin: com.jfrog.bintray.gradle.BintrayPlugin +apply plugin: 'maven-publish' + +project.ext.group = 'com.android.volley' +project.ext.archivesBaseName = 'volley' +project.ext.version = '1.0.0' +project.ext.pomDesc = 'Volley Android library' + +task sourcesJar(type: Jar) { + classifier = 'sources' + from android.sourceSets.main.java.srcDirs +} + +task javadoc(type: Javadoc) { + source = android.sourceSets.main.java.srcDirs + classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives javadocJar + archives sourcesJar +} + +publishing { + publications { + library(MavenPublication) { + groupId project.ext.group + artifactId project.ext.archivesBaseName + version project.ext.version + + // Release AAR, Sources, and JavaDoc + artifact "$buildDir/outputs/aar/volley-release.aar" + artifact sourcesJar + artifact javadocJar + } + } +} + +bintray { + user = System.env.BINTRAY_USER + key = System.env.BINTRAY_USER_KEY + + publications = [ 'library' ] + + publish = project.hasProperty("release") + pkg { + userOrg = 'android' + repo = 'android-utils' + group = project.ext.group + name = project.ext.group + '.' + project.ext.archivesBaseName + desc = project.ext.pomDesc + licenses = [ 'Apache-2.0' ] + websiteUrl = 'https://tools.android.com' + issueTrackerUrl = 'https://code.google.com/p/android/' + vcsUrl = 'https://android.googlesource.com/platform/frameworks/volley.git' + labels = ['android', 'volley', 'network'] + publicDownloadNumbers = true + + version { + name = project.ext.version + desc = project.ext.pomDesc + ' version ' + project.ext.version + gpg { + sign = true + passphrase = System.env.GPG_PASSPHRASE + } + } + } +} diff --git a/volley/build.gradle b/volley/build.gradle new file mode 100644 index 0000000..966b588 --- /dev/null +++ b/volley/build.gradle @@ -0,0 +1,38 @@ +// NOTE: The only changes that belong in this file are the definitions +// of tool versions (gradle plugin, compile SDK, build tools), so that +// Volley can be built via gradle as a standalone project. +// +// Any other changes to the build config belong in rules.gradle, which +// is used by projects that depend on Volley but define their own +// tools versions across all dependencies to ensure a consistent build. +// +// Most users should just add this line to settings.gradle: +// include(":volley") +// +// If you have a more complicated Gradle setup you can choose to use +// this instead: +// include(":volley") +// project(':volley').buildFileName = 'rules.gradle' + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + } +} + +apply plugin: 'com.android.library' + +repositories { + jcenter() +} + +android { + compileSdkVersion 22 + buildToolsVersion = '25.0.0' +} + +apply from: 'rules.gradle' +apply from: 'bintray.gradle' diff --git a/volley/build.xml b/volley/build.xml new file mode 100644 index 0000000..219c63c --- /dev/null +++ b/volley/build.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/volley/custom_rules.xml b/volley/custom_rules.xml new file mode 100644 index 0000000..1b94e5d --- /dev/null +++ b/volley/custom_rules.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/volley/pom.xml b/volley/pom.xml new file mode 100644 index 0000000..7c37e0f --- /dev/null +++ b/volley/pom.xml @@ -0,0 +1,168 @@ + + 4.0.0 + + com.android.volley + volley + 1.0-SNAPSHOT + jar + + volley + http://android.com + + + UTF-8 + + 1.6 + + + + + com.google.android + android + 4.1.1.4 + + + junit + junit + 4.10 + test + + + org.robolectric + robolectric + 3.0 + test + + + org.mockito + mockito-core + 1.9.5 + test + + + + + + + + com.jayway.maven.plugins.android.generation2 + android-maven-plugin + 3.8.1 + + + 19 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.0 + + ${java.version} + ${java.version} + + + + + + + + + debug + + true + + performDebugBuild + true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18.1 + + + default-test + + ${surefireArgLine} + + + + + + org.jacoco + jacoco-maven-plugin + + 0.7.2.201409121644 + + + pre-unit-test + + prepare-agent + + + ${project.build.directory}/surefire-reports/jacoco-ut.exec + surefireArgLine + + + + jacoco-report + post-integration-test + + report + check + + + ${project.build.directory}/surefire-reports/jacoco-ut.exec + ${project.build.directory}/jacoco-report + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.40 + + + + + + + + + + + + + + + + + diff --git a/volley/proguard-project.txt b/volley/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/volley/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/volley/proguard.cfg b/volley/proguard.cfg new file mode 100644 index 0000000..b1cdf17 --- /dev/null +++ b/volley/proguard.cfg @@ -0,0 +1,40 @@ +-optimizationpasses 5 +-dontusemixedcaseclassnames +-dontskipnonpubliclibraryclasses +-dontpreverify +-verbose +-optimizations !code/simplification/arithmetic,!field/*,!class/merging/* + +-keep public class * extends android.app.Activity +-keep public class * extends android.app.Application +-keep public class * extends android.app.Service +-keep public class * extends android.content.BroadcastReceiver +-keep public class * extends android.content.ContentProvider +-keep public class * extends android.app.backup.BackupAgentHelper +-keep public class * extends android.preference.Preference +-keep public class com.android.vending.licensing.ILicensingService + +-keepclasseswithmembernames class * { + native ; +} + +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet); +} + +-keepclasseswithmembers class * { + public (android.content.Context, android.util.AttributeSet, int); +} + +-keepclassmembers class * extends android.app.Activity { + public void *(android.view.View); +} + +-keepclassmembers enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keep class * implements android.os.Parcelable { + public static final android.os.Parcelable$Creator *; +} diff --git a/volley/rules.gradle b/volley/rules.gradle new file mode 100644 index 0000000..04dd681 --- /dev/null +++ b/volley/rules.gradle @@ -0,0 +1,12 @@ +// See build.gradle for an explanation of what this file is. + +apply plugin: 'com.android.library' + +// Check if the android plugin version supports unit testing. +if (configurations.findByName("testCompile")) { + dependencies { + testCompile "junit:junit:4.10" + testCompile "org.mockito:mockito-core:1.9.5" + testCompile "org.robolectric:robolectric:3.0" + } +} diff --git a/volley/src/main/AndroidManifest.xml b/volley/src/main/AndroidManifest.xml new file mode 100644 index 0000000..16eec15 --- /dev/null +++ b/volley/src/main/AndroidManifest.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/volley/src/main/java/com/android/volley/AuthFailureError.java b/volley/src/main/java/com/android/volley/AuthFailureError.java new file mode 100644 index 0000000..87c811d --- /dev/null +++ b/volley/src/main/java/com/android/volley/AuthFailureError.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.content.Intent; + +/** + * Error indicating that there was an authentication failure when performing a Request. + */ +@SuppressWarnings("serial") +public class AuthFailureError extends VolleyError { + /** An intent that can be used to resolve this exception. (Brings up the password dialog.) */ + private Intent mResolutionIntent; + + public AuthFailureError() { } + + public AuthFailureError(Intent intent) { + mResolutionIntent = intent; + } + + public AuthFailureError(NetworkResponse response) { + super(response); + } + + public AuthFailureError(String message) { + super(message); + } + + public AuthFailureError(String message, Exception reason) { + super(message, reason); + } + + public Intent getResolutionIntent() { + return mResolutionIntent; + } + + @Override + public String getMessage() { + if (mResolutionIntent != null) { + return "User needs to (re)enter credentials."; + } + return super.getMessage(); + } +} diff --git a/volley/src/main/java/com/android/volley/Cache.java b/volley/src/main/java/com/android/volley/Cache.java new file mode 100644 index 0000000..f1ec757 --- /dev/null +++ b/volley/src/main/java/com/android/volley/Cache.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import java.util.Collections; +import java.util.Map; + +/** + * An interface for a cache keyed by a String with a byte array as data. + */ +public interface Cache { + /** + * Retrieves an entry from the cache. + * @param key Cache key + * @return An {@link Entry} or null in the event of a cache miss + */ + public Entry get(String key); + + /** + * Adds or replaces an entry to the cache. + * @param key Cache key + * @param entry Data to store and metadata for cache coherency, TTL, etc. + */ + public void put(String key, Entry entry); + + /** + * Performs any potentially long-running actions needed to initialize the cache; + * will be called from a worker thread. + */ + public void initialize(); + + /** + * Invalidates an entry in the cache. + * @param key Cache key + * @param fullExpire True to fully expire the entry, false to soft expire + */ + public void invalidate(String key, boolean fullExpire); + + /** + * Removes an entry from the cache. + * @param key Cache key + */ + public void remove(String key); + + /** + * Empties the cache. + */ + public void clear(); + + /** + * Data and metadata for an entry returned by the cache. + */ + public static class Entry { + /** The data returned from cache. */ + public byte[] data; + + /** ETag for cache coherency. */ + public String etag; + + /** Date of this response as reported by the server. */ + public long serverDate; + + /** The last modified date for the requested object. */ + public long lastModified; + + /** TTL for this record. */ + public long ttl; + + /** Soft TTL for this record. */ + public long softTtl; + + /** Immutable response headers as received from server; must be non-null. */ + public Map responseHeaders = Collections.emptyMap(); + + /** True if the entry is expired. */ + public boolean isExpired() { + return this.ttl < System.currentTimeMillis(); + } + + /** True if a refresh is needed from the original data source. */ + public boolean refreshNeeded() { + return this.softTtl < System.currentTimeMillis(); + } + } + +} diff --git a/volley/src/main/java/com/android/volley/CacheDispatcher.java b/volley/src/main/java/com/android/volley/CacheDispatcher.java new file mode 100644 index 0000000..18d219b --- /dev/null +++ b/volley/src/main/java/com/android/volley/CacheDispatcher.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.Process; + +import java.util.concurrent.BlockingQueue; + +/** + * Provides a thread for performing cache triage on a queue of requests. + * + * Requests added to the specified cache queue are resolved from cache. + * Any deliverable response is posted back to the caller via a + * {@link ResponseDelivery}. Cache misses and responses that require + * refresh are enqueued on the specified network queue for processing + * by a {@link NetworkDispatcher}. + */ +public class CacheDispatcher extends Thread { + + private static final boolean DEBUG = VolleyLog.DEBUG; + + /** The queue of requests coming in for triage. */ + private final BlockingQueue> mCacheQueue; + + /** The queue of requests going out to the network. */ + private final BlockingQueue> mNetworkQueue; + + /** The cache to read from. */ + private final Cache mCache; + + /** For posting responses. */ + private final ResponseDelivery mDelivery; + + /** Used for telling us to die. */ + private volatile boolean mQuit = false; + + /** + * Creates a new cache triage dispatcher thread. You must call {@link #start()} + * in order to begin processing. + * + * @param cacheQueue Queue of incoming requests for triage + * @param networkQueue Queue to post requests that require network to + * @param cache Cache interface to use for resolution + * @param delivery Delivery interface to use for posting responses + */ + public CacheDispatcher( + BlockingQueue> cacheQueue, BlockingQueue> networkQueue, + Cache cache, ResponseDelivery delivery) { + mCacheQueue = cacheQueue; + mNetworkQueue = networkQueue; + mCache = cache; + mDelivery = delivery; + } + + /** + * Forces this dispatcher to quit immediately. If any requests are still in + * the queue, they are not guaranteed to be processed. + */ + public void quit() { + mQuit = true; + interrupt(); + } + + @Override + public void run() { + if (DEBUG) VolleyLog.v("start new dispatcher"); + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + + // Make a blocking call to initialize the cache. + mCache.initialize(); + + while (true) { + try { + // Get a request from the cache triage queue, blocking until + // at least one is available. + final Request request = mCacheQueue.take(); + request.addMarker("cache-queue-take"); + + // If the request has been canceled, don't bother dispatching it. + if (request.isCanceled()) { + request.finish("cache-discard-canceled"); + continue; + } + + // Attempt to retrieve this item from cache. + Cache.Entry entry = mCache.get(request.getCacheKey()); + if (entry == null) { + request.addMarker("cache-miss"); + // Cache miss; send off to the network dispatcher. + mNetworkQueue.put(request); + continue; + } + + // If it is completely expired, just send it to the network. + if (entry.isExpired()) { + request.addMarker("cache-hit-expired"); + request.setCacheEntry(entry); + mNetworkQueue.put(request); + continue; + } + + // We have a cache hit; parse its data for delivery back to the request. + request.addMarker("cache-hit"); + Response response = request.parseNetworkResponse( + new NetworkResponse(entry.data, entry.responseHeaders)); + request.addMarker("cache-hit-parsed"); + + if (!entry.refreshNeeded()) { + // Completely unexpired cache hit. Just deliver the response. + mDelivery.postResponse(request, response); + } else { + // Soft-expired cache hit. We can deliver the cached response, + // but we need to also send the request to the network for + // refreshing. + request.addMarker("cache-hit-refresh-needed"); + request.setCacheEntry(entry); + + // Mark the response as intermediate. + response.intermediate = true; + + // Post the intermediate response back to the user and have + // the delivery then forward the request along to the network. + mDelivery.postResponse(request, response, new Runnable() { + @Override + public void run() { + try { + mNetworkQueue.put(request); + } catch (InterruptedException e) { + // Not much we can do about this. + } + } + }); + } + + } catch (InterruptedException e) { + // We may have been interrupted because it was time to quit. + if (mQuit) { + return; + } + continue; + } + } + } +} diff --git a/volley/src/main/java/com/android/volley/ClientError.java b/volley/src/main/java/com/android/volley/ClientError.java new file mode 100644 index 0000000..a8c8141 --- /dev/null +++ b/volley/src/main/java/com/android/volley/ClientError.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Indicates that the server responded with an error response indicating that the client has erred. + * + * For backwards compatibility, extends ServerError which used to be thrown for all server errors, + * including 4xx error codes indicating a client error. + */ +@SuppressWarnings("serial") +public class ClientError extends ServerError { + public ClientError(NetworkResponse networkResponse) { + super(networkResponse); + } + + public ClientError() { + super(); + } +} + diff --git a/volley/src/main/java/com/android/volley/DefaultRetryPolicy.java b/volley/src/main/java/com/android/volley/DefaultRetryPolicy.java new file mode 100644 index 0000000..d8abab0 --- /dev/null +++ b/volley/src/main/java/com/android/volley/DefaultRetryPolicy.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Default retry policy for requests. + */ +public class DefaultRetryPolicy implements RetryPolicy { + /** The current timeout in milliseconds. */ + private int mCurrentTimeoutMs; + + /** The current retry count. */ + private int mCurrentRetryCount; + + /** The maximum number of attempts. */ + private final int mMaxNumRetries; + + /** The backoff multiplier for the policy. */ + private final float mBackoffMultiplier; + + /** The default socket timeout in milliseconds */ + public static final int DEFAULT_TIMEOUT_MS = 2500; + + /** The default number of retries */ + public static final int DEFAULT_MAX_RETRIES = 1; + + /** The default backoff multiplier */ + public static final float DEFAULT_BACKOFF_MULT = 1f; + + /** + * Constructs a new retry policy using the default timeouts. + */ + public DefaultRetryPolicy() { + this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT); + } + + /** + * Constructs a new retry policy. + * @param initialTimeoutMs The initial timeout for the policy. + * @param maxNumRetries The maximum number of retries. + * @param backoffMultiplier Backoff multiplier for the policy. + */ + public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) { + mCurrentTimeoutMs = initialTimeoutMs; + mMaxNumRetries = maxNumRetries; + mBackoffMultiplier = backoffMultiplier; + } + + /** + * Returns the current timeout. + */ + @Override + public int getCurrentTimeout() { + return mCurrentTimeoutMs; + } + + /** + * Returns the current retry count. + */ + @Override + public int getCurrentRetryCount() { + return mCurrentRetryCount; + } + + /** + * Returns the backoff multiplier for the policy. + */ + public float getBackoffMultiplier() { + return mBackoffMultiplier; + } + + /** + * Prepares for the next retry by applying a backoff to the timeout. + * @param error The error code of the last attempt. + */ + @Override + public void retry(VolleyError error) throws VolleyError { + mCurrentRetryCount++; + mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier); + if (!hasAttemptRemaining()) { + throw error; + } + } + + /** + * Returns true if this policy has attempts remaining, false otherwise. + */ + protected boolean hasAttemptRemaining() { + return mCurrentRetryCount <= mMaxNumRetries; + } +} diff --git a/volley/src/main/java/com/android/volley/ExecutorDelivery.java b/volley/src/main/java/com/android/volley/ExecutorDelivery.java new file mode 100644 index 0000000..1babfcd --- /dev/null +++ b/volley/src/main/java/com/android/volley/ExecutorDelivery.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.Handler; + +import java.util.concurrent.Executor; + +/** + * Delivers responses and errors. + */ +public class ExecutorDelivery implements ResponseDelivery { + /** Used for posting responses, typically to the main thread. */ + private final Executor mResponsePoster; + + /** + * Creates a new response delivery interface. + * @param handler {@link Handler} to post responses on + */ + public ExecutorDelivery(final Handler handler) { + // Make an Executor that just wraps the handler. + mResponsePoster = new Executor() { + @Override + public void execute(Runnable command) { + handler.post(command); + } + }; + } + + /** + * Creates a new response delivery interface, mockable version + * for testing. + * @param executor For running delivery tasks + */ + public ExecutorDelivery(Executor executor) { + mResponsePoster = executor; + } + + @Override + public void postResponse(Request request, Response response) { + postResponse(request, response, null); + } + + @Override + public void postResponse(Request request, Response response, Runnable runnable) { + request.markDelivered(); + request.addMarker("post-response"); + mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable)); + } + + @Override + public void postError(Request request, VolleyError error) { + request.addMarker("post-error"); + Response response = Response.error(error); + mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null)); + } + + /** + * A Runnable used for delivering network responses to a listener on the + * main thread. + */ + @SuppressWarnings("rawtypes") + private class ResponseDeliveryRunnable implements Runnable { + private final Request mRequest; + private final Response mResponse; + private final Runnable mRunnable; + + public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) { + mRequest = request; + mResponse = response; + mRunnable = runnable; + } + + @SuppressWarnings("unchecked") + @Override + public void run() { + // If this request has canceled, finish it and don't deliver. + if (mRequest.isCanceled()) { + mRequest.finish("canceled-at-delivery"); + return; + } + + // Deliver a normal response or error, depending. + if (mResponse.isSuccess()) { + mRequest.deliverResponse(mResponse.result); + } else { + mRequest.deliverError(mResponse.error); + } + + // If this is an intermediate response, add a marker, otherwise we're done + // and the request can be finished. + if (mResponse.intermediate) { + mRequest.addMarker("intermediate-response"); + } else { + mRequest.finish("done"); + } + + // If we have been provided a post-delivery runnable, run it. + if (mRunnable != null) { + mRunnable.run(); + } + } + } +} diff --git a/volley/src/main/java/com/android/volley/Network.java b/volley/src/main/java/com/android/volley/Network.java new file mode 100644 index 0000000..ab45830 --- /dev/null +++ b/volley/src/main/java/com/android/volley/Network.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * An interface for performing requests. + */ +public interface Network { + /** + * Performs the specified request. + * @param request Request to process + * @return A {@link NetworkResponse} with data and caching metadata; will never be null + * @throws VolleyError on errors + */ + public NetworkResponse performRequest(Request request) throws VolleyError; +} diff --git a/volley/src/main/java/com/android/volley/NetworkDispatcher.java b/volley/src/main/java/com/android/volley/NetworkDispatcher.java new file mode 100644 index 0000000..beb7861 --- /dev/null +++ b/volley/src/main/java/com/android/volley/NetworkDispatcher.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.annotation.TargetApi; +import android.net.TrafficStats; +import android.os.Build; +import android.os.Process; +import android.os.SystemClock; + +import java.util.concurrent.BlockingQueue; + +/** + * Provides a thread for performing network dispatch from a queue of requests. + * + * Requests added to the specified queue are processed from the network via a + * specified {@link Network} interface. Responses are committed to cache, if + * eligible, using a specified {@link Cache} interface. Valid responses and + * errors are posted back to the caller via a {@link ResponseDelivery}. + */ +public class NetworkDispatcher extends Thread { + /** The queue of requests to service. */ + private final BlockingQueue> mQueue; + /** The network interface for processing requests. */ + private final Network mNetwork; + /** The cache to write to. */ + private final Cache mCache; + /** For posting responses and errors. */ + private final ResponseDelivery mDelivery; + /** Used for telling us to die. */ + private volatile boolean mQuit = false; + + /** + * Creates a new network dispatcher thread. You must call {@link #start()} + * in order to begin processing. + * + * @param queue Queue of incoming requests for triage + * @param network Network interface to use for performing requests + * @param cache Cache interface to use for writing responses to cache + * @param delivery Delivery interface to use for posting responses + */ + public NetworkDispatcher(BlockingQueue> queue, + Network network, Cache cache, + ResponseDelivery delivery) { + mQueue = queue; + mNetwork = network; + mCache = cache; + mDelivery = delivery; + } + + /** + * Forces this dispatcher to quit immediately. If any requests are still in + * the queue, they are not guaranteed to be processed. + */ + public void quit() { + mQuit = true; + interrupt(); + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + private void addTrafficStatsTag(Request request) { + // Tag the request (if API >= 14) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + TrafficStats.setThreadStatsTag(request.getTrafficStatsTag()); + } + } + + @Override + public void run() { + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + while (true) { + long startTimeMs = SystemClock.elapsedRealtime(); + Request request; + try { + // Take a request from the queue. + request = mQueue.take(); + } catch (InterruptedException e) { + // We may have been interrupted because it was time to quit. + if (mQuit) { + return; + } + continue; + } + + try { + request.addMarker("network-queue-take"); + + // If the request was cancelled already, do not perform the + // network request. + if (request.isCanceled()) { + request.finish("network-discard-cancelled"); + continue; + } + + addTrafficStatsTag(request); + + // Perform the network request. + NetworkResponse networkResponse = mNetwork.performRequest(request); + request.addMarker("network-http-complete"); + + // If the server returned 304 AND we delivered a response already, + // we're done -- don't deliver a second identical response. + if (networkResponse.notModified && request.hasHadResponseDelivered()) { + request.finish("not-modified"); + continue; + } + + // Parse the response here on the worker thread. + Response response = request.parseNetworkResponse(networkResponse); + request.addMarker("network-parse-complete"); + + // Write to cache if applicable. + // TODO: Only update cache metadata instead of entire record for 304s. + if (request.shouldCache() && response.cacheEntry != null) { + mCache.put(request.getCacheKey(), response.cacheEntry); + request.addMarker("network-cache-written"); + } + + // Post the response back. + request.markDelivered(); + mDelivery.postResponse(request, response); + } catch (VolleyError volleyError) { + volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); + parseAndDeliverNetworkError(request, volleyError); + } catch (Exception e) { + VolleyLog.e(e, "Unhandled exception %s", e.toString()); + VolleyError volleyError = new VolleyError(e); + volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs); + mDelivery.postError(request, volleyError); + } + } + } + + private void parseAndDeliverNetworkError(Request request, VolleyError error) { + error = request.parseNetworkError(error); + mDelivery.postError(request, error); + } +} diff --git a/volley/src/main/java/com/android/volley/NetworkError.java b/volley/src/main/java/com/android/volley/NetworkError.java new file mode 100644 index 0000000..40b41c5 --- /dev/null +++ b/volley/src/main/java/com/android/volley/NetworkError.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Indicates that there was a network error when performing a Volley request. + */ +@SuppressWarnings("serial") +public class NetworkError extends VolleyError { + public NetworkError() { + super(); + } + + public NetworkError(Throwable cause) { + super(cause); + } + + public NetworkError(NetworkResponse networkResponse) { + super(networkResponse); + } +} diff --git a/volley/src/main/java/com/android/volley/NetworkResponse.java b/volley/src/main/java/com/android/volley/NetworkResponse.java new file mode 100644 index 0000000..a787fa7 --- /dev/null +++ b/volley/src/main/java/com/android/volley/NetworkResponse.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import org.apache.http.HttpStatus; + +import java.util.Collections; +import java.util.Map; + +/** + * Data and headers returned from {@link Network#performRequest(Request)}. + */ +public class NetworkResponse { + /** + * Creates a new network response. + * @param statusCode the HTTP status code + * @param data Response body + * @param headers Headers returned with this response, or null for none + * @param notModified True if the server returned a 304 and the data was already in cache + * @param networkTimeMs Round-trip network time to receive network response + */ + public NetworkResponse(int statusCode, byte[] data, Map headers, + boolean notModified, long networkTimeMs) { + this.statusCode = statusCode; + this.data = data; + this.headers = headers; + this.notModified = notModified; + this.networkTimeMs = networkTimeMs; + } + + public NetworkResponse(int statusCode, byte[] data, Map headers, + boolean notModified) { + this(statusCode, data, headers, notModified, 0); + } + + public NetworkResponse(byte[] data) { + this(HttpStatus.SC_OK, data, Collections.emptyMap(), false, 0); + } + + public NetworkResponse(byte[] data, Map headers) { + this(HttpStatus.SC_OK, data, headers, false, 0); + } + + /** The HTTP status code. */ + public final int statusCode; + + /** Raw data from this response. */ + public final byte[] data; + + /** Response headers. */ + public final Map headers; + + /** True if the server returned a 304 (Not Modified). */ + public final boolean notModified; + + /** Network roundtrip time in milliseconds. */ + public final long networkTimeMs; +} + diff --git a/volley/src/main/java/com/android/volley/NoConnectionError.java b/volley/src/main/java/com/android/volley/NoConnectionError.java new file mode 100644 index 0000000..fc23156 --- /dev/null +++ b/volley/src/main/java/com/android/volley/NoConnectionError.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Error indicating that no connection could be established when performing a Volley request. + */ +@SuppressWarnings("serial") +public class NoConnectionError extends NetworkError { + public NoConnectionError() { + super(); + } + + public NoConnectionError(Throwable reason) { + super(reason); + } +} diff --git a/volley/src/main/java/com/android/volley/ParseError.java b/volley/src/main/java/com/android/volley/ParseError.java new file mode 100644 index 0000000..959d8fb --- /dev/null +++ b/volley/src/main/java/com/android/volley/ParseError.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Indicates that the server's response could not be parsed. + */ +@SuppressWarnings("serial") +public class ParseError extends VolleyError { + public ParseError() { } + + public ParseError(NetworkResponse networkResponse) { + super(networkResponse); + } + + public ParseError(Throwable cause) { + super(cause); + } +} diff --git a/volley/src/main/java/com/android/volley/Request.java b/volley/src/main/java/com/android/volley/Request.java new file mode 100644 index 0000000..8200f6e --- /dev/null +++ b/volley/src/main/java/com/android/volley/Request.java @@ -0,0 +1,609 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.net.TrafficStats; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.text.TextUtils; + +import com.android.volley.VolleyLog.MarkerLog; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Collections; +import java.util.Map; + +/** + * Base class for all network requests. + * + * @param The type of parsed response this request expects. + */ +public abstract class Request implements Comparable> { + + /** + * Default encoding for POST or PUT parameters. See {@link #getParamsEncoding()}. + */ + private static final String DEFAULT_PARAMS_ENCODING = "UTF-8"; + + /** + * Supported request methods. + */ + public interface Method { + int DEPRECATED_GET_OR_POST = -1; + int GET = 0; + int POST = 1; + int PUT = 2; + int DELETE = 3; + int HEAD = 4; + int OPTIONS = 5; + int TRACE = 6; + int PATCH = 7; + } + + /** An event log tracing the lifetime of this request; for debugging. */ + private final MarkerLog mEventLog = MarkerLog.ENABLED ? new MarkerLog() : null; + + /** + * Request method of this request. Currently supports GET, POST, PUT, DELETE, HEAD, OPTIONS, + * TRACE, and PATCH. + */ + private final int mMethod; + + /** URL of this request. */ + private final String mUrl; + + /** Default tag for {@link TrafficStats}. */ + private final int mDefaultTrafficStatsTag; + + /** Listener interface for errors. */ + private final Response.ErrorListener mErrorListener; + + /** Sequence number of this request, used to enforce FIFO ordering. */ + private Integer mSequence; + + /** The request queue this request is associated with. */ + private RequestQueue mRequestQueue; + + /** Whether or not responses to this request should be cached. */ + private boolean mShouldCache = true; + + /** Whether or not this request has been canceled. */ + private boolean mCanceled = false; + + /** Whether or not a response has been delivered for this request yet. */ + private boolean mResponseDelivered = false; + + /** Whether the request should be retried in the event of an HTTP 5xx (server) error. */ + private boolean mShouldRetryServerErrors = false; + + /** The retry policy for this request. */ + private RetryPolicy mRetryPolicy; + + /** + * When a request can be retrieved from cache but must be refreshed from + * the network, the cache entry will be stored here so that in the event of + * a "Not Modified" response, we can be sure it hasn't been evicted from cache. + */ + private Cache.Entry mCacheEntry = null; + + /** An opaque token tagging this request; used for bulk cancellation. */ + private Object mTag; + + /** + * Creates a new request with the given URL and error listener. Note that + * the normal response listener is not provided here as delivery of responses + * is provided by subclasses, who have a better idea of how to deliver an + * already-parsed response. + * + * @deprecated Use {@link #Request(int, String, com.android.volley.Response.ErrorListener)}. + */ + @Deprecated + public Request(String url, Response.ErrorListener listener) { + this(Method.DEPRECATED_GET_OR_POST, url, listener); + } + + /** + * Creates a new request with the given method (one of the values from {@link Method}), + * URL, and error listener. Note that the normal response listener is not provided here as + * delivery of responses is provided by subclasses, who have a better idea of how to deliver + * an already-parsed response. + */ + public Request(int method, String url, Response.ErrorListener listener) { + mMethod = method; + mUrl = url; + mErrorListener = listener; + setRetryPolicy(new DefaultRetryPolicy()); + + mDefaultTrafficStatsTag = findDefaultTrafficStatsTag(url); + } + + /** + * Return the method for this request. Can be one of the values in {@link Method}. + */ + public int getMethod() { + return mMethod; + } + + /** + * Set a tag on this request. Can be used to cancel all requests with this + * tag by {@link RequestQueue#cancelAll(Object)}. + * + * @return This Request object to allow for chaining. + */ + public Request setTag(Object tag) { + mTag = tag; + return this; + } + + /** + * Returns this request's tag. + * @see Request#setTag(Object) + */ + public Object getTag() { + return mTag; + } + + /** + * @return this request's {@link com.android.volley.Response.ErrorListener}. + */ + public Response.ErrorListener getErrorListener() { + return mErrorListener; + } + + /** + * @return A tag for use with {@link TrafficStats#setThreadStatsTag(int)} + */ + public int getTrafficStatsTag() { + return mDefaultTrafficStatsTag; + } + + /** + * @return The hashcode of the URL's host component, or 0 if there is none. + */ + private static int findDefaultTrafficStatsTag(String url) { + if (!TextUtils.isEmpty(url)) { + Uri uri = Uri.parse(url); + if (uri != null) { + String host = uri.getHost(); + if (host != null) { + return host.hashCode(); + } + } + } + return 0; + } + + /** + * Sets the retry policy for this request. + * + * @return This Request object to allow for chaining. + */ + public Request setRetryPolicy(RetryPolicy retryPolicy) { + mRetryPolicy = retryPolicy; + return this; + } + + /** + * Adds an event to this request's event log; for debugging. + */ + public void addMarker(String tag) { + if (MarkerLog.ENABLED) { + mEventLog.add(tag, Thread.currentThread().getId()); + } + } + + /** + * Notifies the request queue that this request has finished (successfully or with error). + * + *

Also dumps all events from this request's event log; for debugging.

+ */ + void finish(final String tag) { + if (mRequestQueue != null) { + mRequestQueue.finish(this); + } + if (MarkerLog.ENABLED) { + final long threadId = Thread.currentThread().getId(); + if (Looper.myLooper() != Looper.getMainLooper()) { + // If we finish marking off of the main thread, we need to + // actually do it on the main thread to ensure correct ordering. + Handler mainThread = new Handler(Looper.getMainLooper()); + mainThread.post(new Runnable() { + @Override + public void run() { + mEventLog.add(tag, threadId); + mEventLog.finish(this.toString()); + } + }); + return; + } + + mEventLog.add(tag, threadId); + mEventLog.finish(this.toString()); + } + } + + /** + * Associates this request with the given queue. The request queue will be notified when this + * request has finished. + * + * @return This Request object to allow for chaining. + */ + public Request setRequestQueue(RequestQueue requestQueue) { + mRequestQueue = requestQueue; + return this; + } + + /** + * Sets the sequence number of this request. Used by {@link RequestQueue}. + * + * @return This Request object to allow for chaining. + */ + public final Request setSequence(int sequence) { + mSequence = sequence; + return this; + } + + /** + * Returns the sequence number of this request. + */ + public final int getSequence() { + if (mSequence == null) { + throw new IllegalStateException("getSequence called before setSequence"); + } + return mSequence; + } + + /** + * Returns the URL of this request. + */ + public String getUrl() { + return mUrl; + } + + /** + * Returns the cache key for this request. By default, this is the URL. + */ + public String getCacheKey() { + return getUrl(); + } + + /** + * Annotates this request with an entry retrieved for it from cache. + * Used for cache coherency support. + * + * @return This Request object to allow for chaining. + */ + public Request setCacheEntry(Cache.Entry entry) { + mCacheEntry = entry; + return this; + } + + /** + * Returns the annotated cache entry, or null if there isn't one. + */ + public Cache.Entry getCacheEntry() { + return mCacheEntry; + } + + /** + * Mark this request as canceled. No callback will be delivered. + */ + public void cancel() { + mCanceled = true; + } + + /** + * Returns true if this request has been canceled. + */ + public boolean isCanceled() { + return mCanceled; + } + + /** + * Returns a list of extra HTTP headers to go along with this request. Can + * throw {@link AuthFailureError} as authentication may be required to + * provide these values. + * @throws AuthFailureError In the event of auth failure + */ + public Map getHeaders() throws AuthFailureError { + return Collections.emptyMap(); + } + + /** + * Returns a Map of POST parameters to be used for this request, or null if + * a simple GET should be used. Can throw {@link AuthFailureError} as + * authentication may be required to provide these values. + * + *

Note that only one of getPostParams() and getPostBody() can return a non-null + * value.

+ * @throws AuthFailureError In the event of auth failure + * + * @deprecated Use {@link #getParams()} instead. + */ + @Deprecated + protected Map getPostParams() throws AuthFailureError { + return getParams(); + } + + /** + * Returns which encoding should be used when converting POST parameters returned by + * {@link #getPostParams()} into a raw POST body. + * + *

This controls both encodings: + *

    + *
  1. The string encoding used when converting parameter names and values into bytes prior + * to URL encoding them.
  2. + *
  3. The string encoding used when converting the URL encoded parameters into a raw + * byte array.
  4. + *
+ * + * @deprecated Use {@link #getParamsEncoding()} instead. + */ + @Deprecated + protected String getPostParamsEncoding() { + return getParamsEncoding(); + } + + /** + * @deprecated Use {@link #getBodyContentType()} instead. + */ + @Deprecated + public String getPostBodyContentType() { + return getBodyContentType(); + } + + /** + * Returns the raw POST body to be sent. + * + * @throws AuthFailureError In the event of auth failure + * + * @deprecated Use {@link #getBody()} instead. + */ + @Deprecated + public byte[] getPostBody() throws AuthFailureError { + // Note: For compatibility with legacy clients of volley, this implementation must remain + // here instead of simply calling the getBody() function because this function must + // call getPostParams() and getPostParamsEncoding() since legacy clients would have + // overridden these two member functions for POST requests. + Map postParams = getPostParams(); + if (postParams != null && postParams.size() > 0) { + return encodeParameters(postParams, getPostParamsEncoding()); + } + return null; + } + + /** + * Returns a Map of parameters to be used for a POST or PUT request. Can throw + * {@link AuthFailureError} as authentication may be required to provide these values. + * + *

Note that you can directly override {@link #getBody()} for custom data.

+ * + * @throws AuthFailureError in the event of auth failure + */ + protected Map getParams() throws AuthFailureError { + return null; + } + + /** + * Returns which encoding should be used when converting POST or PUT parameters returned by + * {@link #getParams()} into a raw POST or PUT body. + * + *

This controls both encodings: + *

    + *
  1. The string encoding used when converting parameter names and values into bytes prior + * to URL encoding them.
  2. + *
  3. The string encoding used when converting the URL encoded parameters into a raw + * byte array.
  4. + *
+ */ + protected String getParamsEncoding() { + return DEFAULT_PARAMS_ENCODING; + } + + /** + * Returns the content type of the POST or PUT body. + */ + public String getBodyContentType() { + return "application/x-www-form-urlencoded; charset=" + getParamsEncoding(); + } + + /** + * Returns the raw POST or PUT body to be sent. + * + *

By default, the body consists of the request parameters in + * application/x-www-form-urlencoded format. When overriding this method, consider overriding + * {@link #getBodyContentType()} as well to match the new body format. + * + * @throws AuthFailureError in the event of auth failure + */ + public byte[] getBody() throws AuthFailureError { + Map params = getParams(); + if (params != null && params.size() > 0) { + return encodeParameters(params, getParamsEncoding()); + } + return null; + } + + /** + * Converts params into an application/x-www-form-urlencoded encoded string. + */ + private byte[] encodeParameters(Map params, String paramsEncoding) { + StringBuilder encodedParams = new StringBuilder(); + try { + for (Map.Entry entry : params.entrySet()) { + encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding)); + encodedParams.append('='); + encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding)); + encodedParams.append('&'); + } + return encodedParams.toString().getBytes(paramsEncoding); + } catch (UnsupportedEncodingException uee) { + throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee); + } + } + + /** + * Set whether or not responses to this request should be cached. + * + * @return This Request object to allow for chaining. + */ + public final Request setShouldCache(boolean shouldCache) { + mShouldCache = shouldCache; + return this; + } + + /** + * Returns true if responses to this request should be cached. + */ + public final boolean shouldCache() { + return mShouldCache; + } + + /** + * Sets whether or not the request should be retried in the event of an HTTP 5xx (server) error. + * + * @return This Request object to allow for chaining. + */ + public final Request setShouldRetryServerErrors(boolean shouldRetryServerErrors) { + mShouldRetryServerErrors = shouldRetryServerErrors; + return this; + } + + /** + * Returns true if this request should be retried in the event of an HTTP 5xx (server) error. + */ + public final boolean shouldRetryServerErrors() { + return mShouldRetryServerErrors; + } + + /** + * Priority values. Requests will be processed from higher priorities to + * lower priorities, in FIFO order. + */ + public enum Priority { + LOW, + NORMAL, + HIGH, + IMMEDIATE + } + + /** + * Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default. + */ + public Priority getPriority() { + return Priority.NORMAL; + } + + /** + * Returns the socket timeout in milliseconds per retry attempt. (This value can be changed + * per retry attempt if a backoff is specified via backoffTimeout()). If there are no retry + * attempts remaining, this will cause delivery of a {@link TimeoutError} error. + */ + public final int getTimeoutMs() { + return mRetryPolicy.getCurrentTimeout(); + } + + /** + * Returns the retry policy that should be used for this request. + */ + public RetryPolicy getRetryPolicy() { + return mRetryPolicy; + } + + /** + * Mark this request as having a response delivered on it. This can be used + * later in the request's lifetime for suppressing identical responses. + */ + public void markDelivered() { + mResponseDelivered = true; + } + + /** + * Returns true if this request has had a response delivered for it. + */ + public boolean hasHadResponseDelivered() { + return mResponseDelivered; + } + + /** + * Subclasses must implement this to parse the raw network response + * and return an appropriate response type. This method will be + * called from a worker thread. The response will not be delivered + * if you return null. + * @param response Response from the network + * @return The parsed response, or null in the case of an error + */ + abstract protected Response parseNetworkResponse(NetworkResponse response); + + /** + * Subclasses can override this method to parse 'networkError' and return a more specific error. + * + *

The default implementation just returns the passed 'networkError'.

+ * + * @param volleyError the error retrieved from the network + * @return an NetworkError augmented with additional information + */ + protected VolleyError parseNetworkError(VolleyError volleyError) { + return volleyError; + } + + /** + * Subclasses must implement this to perform delivery of the parsed + * response to their listeners. The given response is guaranteed to + * be non-null; responses that fail to parse are not delivered. + * @param response The parsed response returned by + * {@link #parseNetworkResponse(NetworkResponse)} + */ + abstract protected void deliverResponse(T response); + + /** + * Delivers error message to the ErrorListener that the Request was + * initialized with. + * + * @param error Error details + */ + public void deliverError(VolleyError error) { + if (mErrorListener != null) { + mErrorListener.onErrorResponse(error); + } + } + + /** + * Our comparator sorts from high to low priority, and secondarily by + * sequence number to provide FIFO ordering. + */ + @Override + public int compareTo(Request other) { + Priority left = this.getPriority(); + Priority right = other.getPriority(); + + // High-priority requests are "lesser" so they are sorted to the front. + // Equal priorities are sorted by sequence number to provide FIFO ordering. + return left == right ? + this.mSequence - other.mSequence : + right.ordinal() - left.ordinal(); + } + + @Override + public String toString() { + String trafficStatsTag = "0x" + Integer.toHexString(getTrafficStatsTag()); + return (mCanceled ? "[X] " : "[ ] ") + getUrl() + " " + trafficStatsTag + " " + + getPriority() + " " + mSequence; + } +} diff --git a/volley/src/main/java/com/android/volley/RequestQueue.java b/volley/src/main/java/com/android/volley/RequestQueue.java new file mode 100644 index 0000000..4324590 --- /dev/null +++ b/volley/src/main/java/com/android/volley/RequestQueue.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.Handler; +import android.os.Looper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * A request dispatch queue with a thread pool of dispatchers. + * + * Calling {@link #add(Request)} will enqueue the given Request for dispatch, + * resolving from either cache or network on a worker thread, and then delivering + * a parsed response on the main thread. + */ +public class RequestQueue { + + /** Callback interface for completed requests. */ + public static interface RequestFinishedListener { + /** Called when a request has finished processing. */ + public void onRequestFinished(Request request); + } + + /** Used for generating monotonically-increasing sequence numbers for requests. */ + private AtomicInteger mSequenceGenerator = new AtomicInteger(); + + /** + * Staging area for requests that already have a duplicate request in flight. + * + *
    + *
  • containsKey(cacheKey) indicates that there is a request in flight for the given cache + * key.
  • + *
  • get(cacheKey) returns waiting requests for the given cache key. The in flight request + * is not contained in that list. Is null if no requests are staged.
  • + *
+ */ + private final Map>> mWaitingRequests = + new HashMap>>(); + + /** + * The set of all requests currently being processed by this RequestQueue. A Request + * will be in this set if it is waiting in any queue or currently being processed by + * any dispatcher. + */ + private final Set> mCurrentRequests = new HashSet>(); + + /** The cache triage queue. */ + private final PriorityBlockingQueue> mCacheQueue = + new PriorityBlockingQueue>(); + + /** The queue of requests that are actually going out to the network. */ + private final PriorityBlockingQueue> mNetworkQueue = + new PriorityBlockingQueue>(); + + /** Number of network request dispatcher threads to start. */ + private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4; + + /** Cache interface for retrieving and storing responses. */ + private final Cache mCache; + + /** Network interface for performing requests. */ + private final Network mNetwork; + + /** Response delivery mechanism. */ + private final ResponseDelivery mDelivery; + + /** The network dispatchers. */ + private NetworkDispatcher[] mDispatchers; + + /** The cache dispatcher. */ + private CacheDispatcher mCacheDispatcher; + + private List mFinishedListeners = + new ArrayList(); + + /** + * Creates the worker pool. Processing will not begin until {@link #start()} is called. + * + * @param cache A Cache to use for persisting responses to disk + * @param network A Network interface for performing HTTP requests + * @param threadPoolSize Number of network dispatcher threads to create + * @param delivery A ResponseDelivery interface for posting responses and errors + */ + public RequestQueue(Cache cache, Network network, int threadPoolSize, + ResponseDelivery delivery) { + mCache = cache; + mNetwork = network; + mDispatchers = new NetworkDispatcher[threadPoolSize]; + mDelivery = delivery; + } + + /** + * Creates the worker pool. Processing will not begin until {@link #start()} is called. + * + * @param cache A Cache to use for persisting responses to disk + * @param network A Network interface for performing HTTP requests + * @param threadPoolSize Number of network dispatcher threads to create + */ + public RequestQueue(Cache cache, Network network, int threadPoolSize) { + this(cache, network, threadPoolSize, + new ExecutorDelivery(new Handler(Looper.getMainLooper()))); + } + + /** + * Creates the worker pool. Processing will not begin until {@link #start()} is called. + * + * @param cache A Cache to use for persisting responses to disk + * @param network A Network interface for performing HTTP requests + */ + public RequestQueue(Cache cache, Network network) { + this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE); + } + + /** + * Starts the dispatchers in this queue. + */ + public void start() { + stop(); // Make sure any currently running dispatchers are stopped. + // Create the cache dispatcher and start it. + mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); + mCacheDispatcher.start(); + + // Create network dispatchers (and corresponding threads) up to the pool size. + for (int i = 0; i < mDispatchers.length; i++) { + NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, + mCache, mDelivery); + mDispatchers[i] = networkDispatcher; + networkDispatcher.start(); + } + } + + /** + * Stops the cache and network dispatchers. + */ + public void stop() { + if (mCacheDispatcher != null) { + mCacheDispatcher.quit(); + } + for (int i = 0; i < mDispatchers.length; i++) { + if (mDispatchers[i] != null) { + mDispatchers[i].quit(); + } + } + } + + /** + * Gets a sequence number. + */ + public int getSequenceNumber() { + return mSequenceGenerator.incrementAndGet(); + } + + /** + * Gets the {@link Cache} instance being used. + */ + public Cache getCache() { + return mCache; + } + + /** + * A simple predicate or filter interface for Requests, for use by + * {@link RequestQueue#cancelAll(RequestFilter)}. + */ + public interface RequestFilter { + public boolean apply(Request request); + } + + /** + * Cancels all requests in this queue for which the given filter applies. + * @param filter The filtering function to use + */ + public void cancelAll(RequestFilter filter) { + synchronized (mCurrentRequests) { + for (Request request : mCurrentRequests) { + if (filter.apply(request)) { + request.cancel(); + } + } + } + } + + /** + * Cancels all requests in this queue with the given tag. Tag must be non-null + * and equality is by identity. + */ + public void cancelAll(final Object tag) { + if (tag == null) { + throw new IllegalArgumentException("Cannot cancelAll with a null tag"); + } + cancelAll(new RequestFilter() { + @Override + public boolean apply(Request request) { + return request.getTag() == tag; + } + }); + } + + /** + * Adds a Request to the dispatch queue. + * @param request The request to service + * @return The passed-in request + */ + public Request add(Request request) { + // Tag the request as belonging to this queue and add it to the set of current requests. + request.setRequestQueue(this); + synchronized (mCurrentRequests) { + mCurrentRequests.add(request); + } + + // Process requests in the order they are added. + request.setSequence(getSequenceNumber()); + request.addMarker("add-to-queue"); + + // If the request is uncacheable, skip the cache queue and go straight to the network. + if (!request.shouldCache()) { + mNetworkQueue.add(request); + return request; + } + + // Insert request into stage if there's already a request with the same cache key in flight. + synchronized (mWaitingRequests) { + String cacheKey = request.getCacheKey(); + if (mWaitingRequests.containsKey(cacheKey)) { + // There is already a request in flight. Queue up. + Queue> stagedRequests = mWaitingRequests.get(cacheKey); + if (stagedRequests == null) { + stagedRequests = new LinkedList>(); + } + stagedRequests.add(request); + mWaitingRequests.put(cacheKey, stagedRequests); + if (VolleyLog.DEBUG) { + VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey); + } + } else { + // Insert 'null' queue for this cacheKey, indicating there is now a request in + // flight. + mWaitingRequests.put(cacheKey, null); + mCacheQueue.add(request); + } + return request; + } + } + + /** + * Called from {@link Request#finish(String)}, indicating that processing of the given request + * has finished. + * + *

Releases waiting requests for request.getCacheKey() if + * request.shouldCache().

+ */ + void finish(Request request) { + // Remove from the set of requests currently being processed. + synchronized (mCurrentRequests) { + mCurrentRequests.remove(request); + } + synchronized (mFinishedListeners) { + for (RequestFinishedListener listener : mFinishedListeners) { + listener.onRequestFinished(request); + } + } + + if (request.shouldCache()) { + synchronized (mWaitingRequests) { + String cacheKey = request.getCacheKey(); + Queue> waitingRequests = mWaitingRequests.remove(cacheKey); + if (waitingRequests != null) { + if (VolleyLog.DEBUG) { + VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.", + waitingRequests.size(), cacheKey); + } + // Process all queued up requests. They won't be considered as in flight, but + // that's not a problem as the cache has been primed by 'request'. + mCacheQueue.addAll(waitingRequests); + } + } + } + } + + public void addRequestFinishedListener(RequestFinishedListener listener) { + synchronized (mFinishedListeners) { + mFinishedListeners.add(listener); + } + } + + /** + * Remove a RequestFinishedListener. Has no effect if listener was not previously added. + */ + public void removeRequestFinishedListener(RequestFinishedListener listener) { + synchronized (mFinishedListeners) { + mFinishedListeners.remove(listener); + } + } +} diff --git a/volley/src/main/java/com/android/volley/Response.java b/volley/src/main/java/com/android/volley/Response.java new file mode 100644 index 0000000..1165595 --- /dev/null +++ b/volley/src/main/java/com/android/volley/Response.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Encapsulates a parsed response for delivery. + * + * @param Parsed type of this response + */ +public class Response { + + /** Callback interface for delivering parsed responses. */ + public interface Listener { + /** Called when a response is received. */ + public void onResponse(T response); + } + + /** Callback interface for delivering error responses. */ + public interface ErrorListener { + /** + * Callback method that an error has been occurred with the + * provided error code and optional user-readable message. + */ + public void onErrorResponse(VolleyError error); + } + + /** Returns a successful response containing the parsed result. */ + public static Response success(T result, Cache.Entry cacheEntry) { + return new Response(result, cacheEntry); + } + + /** + * Returns a failed response containing the given error code and an optional + * localized message displayed to the user. + */ + public static Response error(VolleyError error) { + return new Response(error); + } + + /** Parsed response, or null in the case of error. */ + public final T result; + + /** Cache metadata for this response, or null in the case of error. */ + public final Cache.Entry cacheEntry; + + /** Detailed error information if errorCode != OK. */ + public final VolleyError error; + + /** True if this response was a soft-expired one and a second one MAY be coming. */ + public boolean intermediate = false; + + /** + * Returns whether this response is considered successful. + */ + public boolean isSuccess() { + return error == null; + } + + + private Response(T result, Cache.Entry cacheEntry) { + this.result = result; + this.cacheEntry = cacheEntry; + this.error = null; + } + + private Response(VolleyError error) { + this.result = null; + this.cacheEntry = null; + this.error = error; + } +} diff --git a/volley/src/main/java/com/android/volley/ResponseDelivery.java b/volley/src/main/java/com/android/volley/ResponseDelivery.java new file mode 100644 index 0000000..87706af --- /dev/null +++ b/volley/src/main/java/com/android/volley/ResponseDelivery.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +public interface ResponseDelivery { + /** + * Parses a response from the network or cache and delivers it. + */ + public void postResponse(Request request, Response response); + + /** + * Parses a response from the network or cache and delivers it. The provided + * Runnable will be executed after delivery. + */ + public void postResponse(Request request, Response response, Runnable runnable); + + /** + * Posts an error for the given request. + */ + public void postError(Request request, VolleyError error); +} diff --git a/volley/src/main/java/com/android/volley/RetryPolicy.java b/volley/src/main/java/com/android/volley/RetryPolicy.java new file mode 100644 index 0000000..0dd198b --- /dev/null +++ b/volley/src/main/java/com/android/volley/RetryPolicy.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Retry policy for a request. + */ +public interface RetryPolicy { + + /** + * Returns the current timeout (used for logging). + */ + public int getCurrentTimeout(); + + /** + * Returns the current retry count (used for logging). + */ + public int getCurrentRetryCount(); + + /** + * Prepares for the next retry by applying a backoff to the timeout. + * @param error The error code of the last attempt. + * @throws VolleyError In the event that the retry could not be performed (for example if we + * ran out of attempts), the passed in error is thrown. + */ + public void retry(VolleyError error) throws VolleyError; +} diff --git a/volley/src/main/java/com/android/volley/ServerError.java b/volley/src/main/java/com/android/volley/ServerError.java new file mode 100644 index 0000000..7b33c33 --- /dev/null +++ b/volley/src/main/java/com/android/volley/ServerError.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Indicates that the server responded with an error response. + */ +@SuppressWarnings("serial") +public class ServerError extends VolleyError { + public ServerError(NetworkResponse networkResponse) { + super(networkResponse); + } + + public ServerError() { + super(); + } +} + diff --git a/volley/src/main/java/com/android/volley/TimeoutError.java b/volley/src/main/java/com/android/volley/TimeoutError.java new file mode 100644 index 0000000..0b5d6ac --- /dev/null +++ b/volley/src/main/java/com/android/volley/TimeoutError.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Indicates that the connection or the socket timed out. + */ +@SuppressWarnings("serial") +public class TimeoutError extends VolleyError { } diff --git a/volley/src/main/java/com/android/volley/VolleyError.java b/volley/src/main/java/com/android/volley/VolleyError.java new file mode 100644 index 0000000..1471d40 --- /dev/null +++ b/volley/src/main/java/com/android/volley/VolleyError.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +/** + * Exception style class encapsulating Volley errors + */ +@SuppressWarnings("serial") +public class VolleyError extends Exception { + public final NetworkResponse networkResponse; + private long networkTimeMs; + + public VolleyError() { + networkResponse = null; + } + + public VolleyError(NetworkResponse response) { + networkResponse = response; + } + + public VolleyError(String exceptionMessage) { + super(exceptionMessage); + networkResponse = null; + } + + public VolleyError(String exceptionMessage, Throwable reason) { + super(exceptionMessage, reason); + networkResponse = null; + } + + public VolleyError(Throwable cause) { + super(cause); + networkResponse = null; + } + + /* package */ void setNetworkTimeMs(long networkTimeMs) { + this.networkTimeMs = networkTimeMs; + } + + public long getNetworkTimeMs() { + return networkTimeMs; + } +} diff --git a/volley/src/main/java/com/android/volley/VolleyLog.java b/volley/src/main/java/com/android/volley/VolleyLog.java new file mode 100644 index 0000000..ffe9eb8 --- /dev/null +++ b/volley/src/main/java/com/android/volley/VolleyLog.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import android.os.SystemClock; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Logging helper class. + *

+ * to see Volley logs call:
+ * {@code /platform-tools/adb shell setprop log.tag.Volley VERBOSE} + */ +public class VolleyLog { + public static String TAG = "Volley"; + + public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE); + + /** + * Customize the log tag for your application, so that other apps + * using Volley don't mix their logs with yours. + *
+ * Enable the log property for your tag before starting your app: + *
+ * {@code adb shell setprop log.tag.<tag>} + */ + public static void setTag(String tag) { + d("Changing log tag to %s", tag); + TAG = tag; + + // Reinitialize the DEBUG "constant" + DEBUG = Log.isLoggable(TAG, Log.VERBOSE); + } + + public static void v(String format, Object... args) { + if (DEBUG) { + Log.v(TAG, buildMessage(format, args)); + } + } + + public static void d(String format, Object... args) { + Log.d(TAG, buildMessage(format, args)); + } + + public static void e(String format, Object... args) { + Log.e(TAG, buildMessage(format, args)); + } + + public static void e(Throwable tr, String format, Object... args) { + Log.e(TAG, buildMessage(format, args), tr); + } + + public static void wtf(String format, Object... args) { + Log.wtf(TAG, buildMessage(format, args)); + } + + public static void wtf(Throwable tr, String format, Object... args) { + Log.wtf(TAG, buildMessage(format, args), tr); + } + + /** + * Formats the caller's provided message and prepends useful info like + * calling thread ID and method name. + */ + private static String buildMessage(String format, Object... args) { + String msg = (args == null) ? format : String.format(Locale.US, format, args); + StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace(); + + String caller = ""; + // Walk up the stack looking for the first caller outside of VolleyLog. + // It will be at least two frames up, so start there. + for (int i = 2; i < trace.length; i++) { + Class clazz = trace[i].getClass(); + if (!clazz.equals(VolleyLog.class)) { + String callingClass = trace[i].getClassName(); + callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1); + callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1); + + caller = callingClass + "." + trace[i].getMethodName(); + break; + } + } + return String.format(Locale.US, "[%d] %s: %s", + Thread.currentThread().getId(), caller, msg); + } + + /** + * A simple event log with records containing a name, thread ID, and timestamp. + */ + static class MarkerLog { + public static final boolean ENABLED = VolleyLog.DEBUG; + + /** Minimum duration from first marker to last in an marker log to warrant logging. */ + private static final long MIN_DURATION_FOR_LOGGING_MS = 0; + + private static class Marker { + public final String name; + public final long thread; + public final long time; + + public Marker(String name, long thread, long time) { + this.name = name; + this.thread = thread; + this.time = time; + } + } + + private final List mMarkers = new ArrayList(); + private boolean mFinished = false; + + /** Adds a marker to this log with the specified name. */ + public synchronized void add(String name, long threadId) { + if (mFinished) { + throw new IllegalStateException("Marker added to finished log"); + } + + mMarkers.add(new Marker(name, threadId, SystemClock.elapsedRealtime())); + } + + /** + * Closes the log, dumping it to logcat if the time difference between + * the first and last markers is greater than {@link #MIN_DURATION_FOR_LOGGING_MS}. + * @param header Header string to print above the marker log. + */ + public synchronized void finish(String header) { + mFinished = true; + + long duration = getTotalDuration(); + if (duration <= MIN_DURATION_FOR_LOGGING_MS) { + return; + } + + long prevTime = mMarkers.get(0).time; + d("(%-4d ms) %s", duration, header); + for (Marker marker : mMarkers) { + long thisTime = marker.time; + d("(+%-4d) [%2d] %s", (thisTime - prevTime), marker.thread, marker.name); + prevTime = thisTime; + } + } + + @Override + protected void finalize() throws Throwable { + // Catch requests that have been collected (and hence end-of-lifed) + // but had no debugging output printed for them. + if (!mFinished) { + finish("Request on the loose"); + e("Marker log finalized without finish() - uncaught exit point for request"); + } + } + + /** Returns the time difference between the first and last events in this log. */ + private long getTotalDuration() { + if (mMarkers.size() == 0) { + return 0; + } + + long first = mMarkers.get(0).time; + long last = mMarkers.get(mMarkers.size() - 1).time; + return last - first; + } + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java b/volley/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java new file mode 100644 index 0000000..18f8597 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/AndroidAuthenticator.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +/** + * An Authenticator that uses {@link AccountManager} to get auth + * tokens of a specified type for a specified account. + */ +public class AndroidAuthenticator implements Authenticator { + private final AccountManager mAccountManager; + private final Account mAccount; + private final String mAuthTokenType; + private final boolean mNotifyAuthFailure; + + /** + * Creates a new authenticator. + * @param context Context for accessing AccountManager + * @param account Account to authenticate as + * @param authTokenType Auth token type passed to AccountManager + */ + public AndroidAuthenticator(Context context, Account account, String authTokenType) { + this(context, account, authTokenType, false); + } + + /** + * Creates a new authenticator. + * @param context Context for accessing AccountManager + * @param account Account to authenticate as + * @param authTokenType Auth token type passed to AccountManager + * @param notifyAuthFailure Whether to raise a notification upon auth failure + */ + public AndroidAuthenticator(Context context, Account account, String authTokenType, + boolean notifyAuthFailure) { + this(AccountManager.get(context), account, authTokenType, notifyAuthFailure); + } + + // Visible for testing. Allows injection of a mock AccountManager. + AndroidAuthenticator(AccountManager accountManager, Account account, + String authTokenType, boolean notifyAuthFailure) { + mAccountManager = accountManager; + mAccount = account; + mAuthTokenType = authTokenType; + mNotifyAuthFailure = notifyAuthFailure; + } + + /** + * Returns the Account being used by this authenticator. + */ + public Account getAccount() { + return mAccount; + } + + /** + * Returns the Auth Token Type used by this authenticator. + */ + public String getAuthTokenType() { + return mAuthTokenType; + } + + // TODO: Figure out what to do about notifyAuthFailure + @SuppressWarnings("deprecation") + @Override + public String getAuthToken() throws AuthFailureError { + AccountManagerFuture future = mAccountManager.getAuthToken(mAccount, + mAuthTokenType, mNotifyAuthFailure, null, null); + Bundle result; + try { + result = future.getResult(); + } catch (Exception e) { + throw new AuthFailureError("Error while retrieving auth token", e); + } + String authToken = null; + if (future.isDone() && !future.isCancelled()) { + if (result.containsKey(AccountManager.KEY_INTENT)) { + Intent intent = result.getParcelable(AccountManager.KEY_INTENT); + throw new AuthFailureError(intent); + } + authToken = result.getString(AccountManager.KEY_AUTHTOKEN); + } + if (authToken == null) { + throw new AuthFailureError("Got null auth token for type: " + mAuthTokenType); + } + + return authToken; + } + + @Override + public void invalidateAuthToken(String authToken) { + mAccountManager.invalidateAuthToken(mAccount.type, authToken); + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/Authenticator.java b/volley/src/main/java/com/android/volley/toolbox/Authenticator.java new file mode 100644 index 0000000..d9f5e3c --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/Authenticator.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; + +/** + * An interface for interacting with auth tokens. + */ +public interface Authenticator { + /** + * Synchronously retrieves an auth token. + * + * @throws AuthFailureError If authentication did not succeed + */ + public String getAuthToken() throws AuthFailureError; + + /** + * Invalidates the provided auth token. + */ + public void invalidateAuthToken(String authToken); +} diff --git a/volley/src/main/java/com/android/volley/toolbox/BasicNetwork.java b/volley/src/main/java/com/android/volley/toolbox/BasicNetwork.java new file mode 100644 index 0000000..37c35ec --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/BasicNetwork.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.SystemClock; + +import com.android.volley.AuthFailureError; +import com.android.volley.Cache; +import com.android.volley.Cache.Entry; +import com.android.volley.ClientError; +import com.android.volley.Network; +import com.android.volley.NetworkError; +import com.android.volley.NetworkResponse; +import com.android.volley.NoConnectionError; +import com.android.volley.Request; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.VolleyLog; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.impl.cookie.DateUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * A network performing Volley requests over an {@link HttpStack}. + */ +public class BasicNetwork implements Network { + protected static final boolean DEBUG = VolleyLog.DEBUG; + + private static int SLOW_REQUEST_THRESHOLD_MS = 3000; + + private static int DEFAULT_POOL_SIZE = 4096; + + protected final HttpStack mHttpStack; + + protected final ByteArrayPool mPool; + + /** + * @param httpStack HTTP stack to be used + */ + public BasicNetwork(HttpStack httpStack) { + // If a pool isn't passed in, then build a small default pool that will give us a lot of + // benefit and not use too much memory. + this(httpStack, new ByteArrayPool(DEFAULT_POOL_SIZE)); + } + + /** + * @param httpStack HTTP stack to be used + * @param pool a buffer pool that improves GC performance in copy operations + */ + public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) { + mHttpStack = httpStack; + mPool = pool; + } + + @Override + public NetworkResponse performRequest(Request request) throws VolleyError { + long requestStart = SystemClock.elapsedRealtime(); + while (true) { + HttpResponse httpResponse = null; + byte[] responseContents = null; + Map responseHeaders = Collections.emptyMap(); + try { + // Gather headers. + Map headers = new HashMap(); + addCacheHeaders(headers, request.getCacheEntry()); + httpResponse = mHttpStack.performRequest(request, headers); + StatusLine statusLine = httpResponse.getStatusLine(); + int statusCode = statusLine.getStatusCode(); + + responseHeaders = convertHeaders(httpResponse.getAllHeaders()); + // Handle cache validation. + if (statusCode == HttpStatus.SC_NOT_MODIFIED) { + + Entry entry = request.getCacheEntry(); + if (entry == null) { + return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null, + responseHeaders, true, + SystemClock.elapsedRealtime() - requestStart); + } + + // A HTTP 304 response does not have all header fields. We + // have to use the header fields from the cache entry plus + // the new ones from the response. + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5 + entry.responseHeaders.putAll(responseHeaders); + return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data, + entry.responseHeaders, true, + SystemClock.elapsedRealtime() - requestStart); + } + + // Some responses such as 204s do not have content. We must check. + if (httpResponse.getEntity() != null) { + responseContents = entityToBytes(httpResponse.getEntity()); + } else { + // Add 0 byte response as a way of honestly representing a + // no-content request. + responseContents = new byte[0]; + } + + // if the request is slow, log it. + long requestLifetime = SystemClock.elapsedRealtime() - requestStart; + logSlowRequests(requestLifetime, request, responseContents, statusLine); + + if (statusCode < 200 || statusCode > 299) { + throw new IOException(); + } + return new NetworkResponse(statusCode, responseContents, responseHeaders, false, + SystemClock.elapsedRealtime() - requestStart); + } catch (SocketTimeoutException e) { + attemptRetryOnException("socket", request, new TimeoutError()); + } catch (ConnectTimeoutException e) { + attemptRetryOnException("connection", request, new TimeoutError()); + } catch (MalformedURLException e) { + throw new RuntimeException("Bad URL " + request.getUrl(), e); + } catch (IOException e) { + int statusCode; + if (httpResponse != null) { + statusCode = httpResponse.getStatusLine().getStatusCode(); + } else { + throw new NoConnectionError(e); + } + VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl()); + NetworkResponse networkResponse; + if (responseContents != null) { + networkResponse = new NetworkResponse(statusCode, responseContents, + responseHeaders, false, SystemClock.elapsedRealtime() - requestStart); + if (statusCode == HttpStatus.SC_UNAUTHORIZED || + statusCode == HttpStatus.SC_FORBIDDEN) { + attemptRetryOnException("auth", + request, new AuthFailureError(networkResponse)); + } else if (statusCode >= 400 && statusCode <= 499) { + // Don't retry other client errors. + throw new ClientError(networkResponse); + } else if (statusCode >= 500 && statusCode <= 599) { + if (request.shouldRetryServerErrors()) { + attemptRetryOnException("server", + request, new ServerError(networkResponse)); + } else { + throw new ServerError(networkResponse); + } + } else { + // 3xx? No reason to retry. + throw new ServerError(networkResponse); + } + } else { + attemptRetryOnException("network", request, new NetworkError()); + } + } + } + } + + /** + * Logs requests that took over SLOW_REQUEST_THRESHOLD_MS to complete. + */ + private void logSlowRequests(long requestLifetime, Request request, + byte[] responseContents, StatusLine statusLine) { + if (DEBUG || requestLifetime > SLOW_REQUEST_THRESHOLD_MS) { + VolleyLog.d("HTTP response for request=<%s> [lifetime=%d], [size=%s], " + + "[rc=%d], [retryCount=%s]", request, requestLifetime, + responseContents != null ? responseContents.length : "null", + statusLine.getStatusCode(), request.getRetryPolicy().getCurrentRetryCount()); + } + } + + /** + * Attempts to prepare the request for a retry. If there are no more attempts remaining in the + * request's retry policy, a timeout exception is thrown. + * @param request The request to use. + */ + private static void attemptRetryOnException(String logPrefix, Request request, + VolleyError exception) throws VolleyError { + RetryPolicy retryPolicy = request.getRetryPolicy(); + int oldTimeout = request.getTimeoutMs(); + + try { + retryPolicy.retry(exception); + } catch (VolleyError e) { + request.addMarker( + String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout)); + throw e; + } + request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout)); + } + + private void addCacheHeaders(Map headers, Cache.Entry entry) { + // If there's no cache entry, we're done. + if (entry == null) { + return; + } + + if (entry.etag != null) { + headers.put("If-None-Match", entry.etag); + } + + if (entry.lastModified > 0) { + Date refTime = new Date(entry.lastModified); + headers.put("If-Modified-Since", DateUtils.formatDate(refTime)); + } + } + + protected void logError(String what, String url, long start) { + long now = SystemClock.elapsedRealtime(); + VolleyLog.v("HTTP ERROR(%s) %d ms to fetch %s", what, (now - start), url); + } + + /** Reads the contents of HttpEntity into a byte[]. */ + private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError { + PoolingByteArrayOutputStream bytes = + new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength()); + byte[] buffer = null; + try { + InputStream in = entity.getContent(); + if (in == null) { + throw new ServerError(); + } + buffer = mPool.getBuf(1024); + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + return bytes.toByteArray(); + } finally { + try { + // Close the InputStream and release the resources by "consuming the content". + entity.consumeContent(); + } catch (IOException e) { + // This can happen if there was an exception above that left the entity in + // an invalid state. + VolleyLog.v("Error occured when calling consumingContent"); + } + mPool.returnBuf(buffer); + bytes.close(); + } + } + + /** + * Converts Headers[] to Map. + */ + protected static Map convertHeaders(Header[] headers) { + Map result = new TreeMap(String.CASE_INSENSITIVE_ORDER); + for (int i = 0; i < headers.length; i++) { + result.put(headers[i].getName(), headers[i].getValue()); + } + return result; + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/ByteArrayPool.java b/volley/src/main/java/com/android/volley/toolbox/ByteArrayPool.java new file mode 100644 index 0000000..af95076 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/ByteArrayPool.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.List; + +/** + * ByteArrayPool is a source and repository of byte[] objects. Its purpose is to + * supply those buffers to consumers who need to use them for a short period of time and then + * dispose of them. Simply creating and disposing such buffers in the conventional manner can + * considerable heap churn and garbage collection delays on Android, which lacks good management of + * short-lived heap objects. It may be advantageous to trade off some memory in the form of a + * permanently allocated pool of buffers in order to gain heap performance improvements; that is + * what this class does. + *

+ * A good candidate user for this class is something like an I/O system that uses large temporary + * byte[] buffers to copy data around. In these use cases, often the consumer wants + * the buffer to be a certain minimum size to ensure good performance (e.g. when copying data chunks + * off of a stream), but doesn't mind if the buffer is larger than the minimum. Taking this into + * account and also to maximize the odds of being able to reuse a recycled buffer, this class is + * free to return buffers larger than the requested size. The caller needs to be able to gracefully + * deal with getting buffers any size over the minimum. + *

+ * If there is not a suitably-sized buffer in its recycling pool when a buffer is requested, this + * class will allocate a new buffer and return it. + *

+ * This class has no special ownership of buffers it creates; the caller is free to take a buffer + * it receives from this pool, use it permanently, and never return it to the pool; additionally, + * it is not harmful to return to this pool a buffer that was allocated elsewhere, provided there + * are no other lingering references to it. + *

+ * This class ensures that the total size of the buffers in its recycling pool never exceeds a + * certain byte limit. When a buffer is returned that would cause the pool to exceed the limit, + * least-recently-used buffers are disposed. + */ +public class ByteArrayPool { + /** The buffer pool, arranged both by last use and by buffer size */ + private List mBuffersByLastUse = new LinkedList(); + private List mBuffersBySize = new ArrayList(64); + + /** The total size of the buffers in the pool */ + private int mCurrentSize = 0; + + /** + * The maximum aggregate size of the buffers in the pool. Old buffers are discarded to stay + * under this limit. + */ + private final int mSizeLimit; + + /** Compares buffers by size */ + protected static final Comparator BUF_COMPARATOR = new Comparator() { + @Override + public int compare(byte[] lhs, byte[] rhs) { + return lhs.length - rhs.length; + } + }; + + /** + * @param sizeLimit the maximum size of the pool, in bytes + */ + public ByteArrayPool(int sizeLimit) { + mSizeLimit = sizeLimit; + } + + /** + * Returns a buffer from the pool if one is available in the requested size, or allocates a new + * one if a pooled one is not available. + * + * @param len the minimum size, in bytes, of the requested buffer. The returned buffer may be + * larger. + * @return a byte[] buffer is always returned. + */ + public synchronized byte[] getBuf(int len) { + for (int i = 0; i < mBuffersBySize.size(); i++) { + byte[] buf = mBuffersBySize.get(i); + if (buf.length >= len) { + mCurrentSize -= buf.length; + mBuffersBySize.remove(i); + mBuffersByLastUse.remove(buf); + return buf; + } + } + return new byte[len]; + } + + /** + * Returns a buffer to the pool, throwing away old buffers if the pool would exceed its allotted + * size. + * + * @param buf the buffer to return to the pool. + */ + public synchronized void returnBuf(byte[] buf) { + if (buf == null || buf.length > mSizeLimit) { + return; + } + mBuffersByLastUse.add(buf); + int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR); + if (pos < 0) { + pos = -pos - 1; + } + mBuffersBySize.add(pos, buf); + mCurrentSize += buf.length; + trim(); + } + + /** + * Removes buffers from the pool until it is under its size limit. + */ + private synchronized void trim() { + while (mCurrentSize > mSizeLimit) { + byte[] buf = mBuffersByLastUse.remove(0); + mBuffersBySize.remove(buf); + mCurrentSize -= buf.length; + } + } + +} diff --git a/volley/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java b/volley/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java new file mode 100644 index 0000000..a3478bf --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/ClearCacheRequest.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; + +import android.os.Handler; +import android.os.Looper; + +/** + * A synthetic request used for clearing the cache. + */ +public class ClearCacheRequest extends Request { + private final Cache mCache; + private final Runnable mCallback; + + /** + * Creates a synthetic request for clearing the cache. + * @param cache Cache to clear + * @param callback Callback to make on the main thread once the cache is clear, + * or null for none + */ + public ClearCacheRequest(Cache cache, Runnable callback) { + super(Method.GET, null, null); + mCache = cache; + mCallback = callback; + } + + @Override + public boolean isCanceled() { + // This is a little bit of a hack, but hey, why not. + mCache.clear(); + if (mCallback != null) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.postAtFrontOfQueue(mCallback); + } + return true; + } + + @Override + public Priority getPriority() { + return Priority.IMMEDIATE; + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(Object response) { + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/DiskBasedCache.java b/volley/src/main/java/com/android/volley/toolbox/DiskBasedCache.java new file mode 100644 index 0000000..f724d72 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/DiskBasedCache.java @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.os.SystemClock; + +import com.android.volley.Cache; +import com.android.volley.VolleyLog; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Cache implementation that caches files directly onto the hard disk in the specified + * directory. The default disk usage size is 5MB, but is configurable. + */ +public class DiskBasedCache implements Cache { + + /** Map of the Key, CacheHeader pairs */ + private final Map mEntries = + new LinkedHashMap(16, .75f, true); + + /** Total amount of space currently used by the cache in bytes. */ + private long mTotalSize = 0; + + /** The root directory to use for the cache. */ + private final File mRootDirectory; + + /** The maximum size of the cache in bytes. */ + private final int mMaxCacheSizeInBytes; + + /** Default maximum disk usage in bytes. */ + private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; + + /** High water mark percentage for the cache */ + private static final float HYSTERESIS_FACTOR = 0.9f; + + /** Magic number for current version of cache file format. */ + private static final int CACHE_MAGIC = 0x20150306; + + /** + * Constructs an instance of the DiskBasedCache at the specified directory. + * @param rootDirectory The root directory of the cache. + * @param maxCacheSizeInBytes The maximum size of the cache in bytes. + */ + public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) { + mRootDirectory = rootDirectory; + mMaxCacheSizeInBytes = maxCacheSizeInBytes; + } + + /** + * Constructs an instance of the DiskBasedCache at the specified directory using + * the default maximum cache size of 5MB. + * @param rootDirectory The root directory of the cache. + */ + public DiskBasedCache(File rootDirectory) { + this(rootDirectory, DEFAULT_DISK_USAGE_BYTES); + } + + /** + * Clears the cache. Deletes all cached files from disk. + */ + @Override + public synchronized void clear() { + File[] files = mRootDirectory.listFiles(); + if (files != null) { + for (File file : files) { + file.delete(); + } + } + mEntries.clear(); + mTotalSize = 0; + VolleyLog.d("Cache cleared."); + } + + /** + * Returns the cache entry with the specified key if it exists, null otherwise. + */ + @Override + public synchronized Entry get(String key) { + CacheHeader entry = mEntries.get(key); + // if the entry does not exist, return. + if (entry == null) { + return null; + } + + File file = getFileForKey(key); + CountingInputStream cis = null; + try { + cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file))); + CacheHeader.readHeader(cis); // eat header + byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead)); + return entry.toCacheEntry(data); + } catch (IOException e) { + VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); + remove(key); + return null; + } catch (NegativeArraySizeException e) { + VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); + remove(key); + return null; + } finally { + if (cis != null) { + try { + cis.close(); + } catch (IOException ioe) { + return null; + } + } + } + } + + /** + * Initializes the DiskBasedCache by scanning for all files currently in the + * specified root directory. Creates the root directory if necessary. + */ + @Override + public synchronized void initialize() { + if (!mRootDirectory.exists()) { + if (!mRootDirectory.mkdirs()) { + VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath()); + } + return; + } + + File[] files = mRootDirectory.listFiles(); + if (files == null) { + return; + } + for (File file : files) { + BufferedInputStream fis = null; + try { + fis = new BufferedInputStream(new FileInputStream(file)); + CacheHeader entry = CacheHeader.readHeader(fis); + entry.size = file.length(); + putEntry(entry.key, entry); + } catch (IOException e) { + if (file != null) { + file.delete(); + } + } finally { + try { + if (fis != null) { + fis.close(); + } + } catch (IOException ignored) { } + } + } + } + + /** + * Invalidates an entry in the cache. + * @param key Cache key + * @param fullExpire True to fully expire the entry, false to soft expire + */ + @Override + public synchronized void invalidate(String key, boolean fullExpire) { + Entry entry = get(key); + if (entry != null) { + entry.softTtl = 0; + if (fullExpire) { + entry.ttl = 0; + } + put(key, entry); + } + + } + + /** + * Puts the entry with the specified key into the cache. + */ + @Override + public synchronized void put(String key, Entry entry) { + pruneIfNeeded(entry.data.length); + File file = getFileForKey(key); + try { + BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file)); + CacheHeader e = new CacheHeader(key, entry); + boolean success = e.writeHeader(fos); + if (!success) { + fos.close(); + VolleyLog.d("Failed to write header for %s", file.getAbsolutePath()); + throw new IOException(); + } + fos.write(entry.data); + fos.close(); + putEntry(key, e); + return; + } catch (IOException e) { + } + boolean deleted = file.delete(); + if (!deleted) { + VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); + } + } + + /** + * Removes the specified key from the cache if it exists. + */ + @Override + public synchronized void remove(String key) { + boolean deleted = getFileForKey(key).delete(); + removeEntry(key); + if (!deleted) { + VolleyLog.d("Could not delete cache entry for key=%s, filename=%s", + key, getFilenameForKey(key)); + } + } + + /** + * Creates a pseudo-unique filename for the specified cache key. + * @param key The key to generate a file name for. + * @return A pseudo-unique filename. + */ + private String getFilenameForKey(String key) { + int firstHalfLength = key.length() / 2; + String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode()); + localFilename += String.valueOf(key.substring(firstHalfLength).hashCode()); + return localFilename; + } + + /** + * Returns a file object for the given cache key. + */ + public File getFileForKey(String key) { + return new File(mRootDirectory, getFilenameForKey(key)); + } + + /** + * Prunes the cache to fit the amount of bytes specified. + * @param neededSpace The amount of bytes we are trying to fit into the cache. + */ + private void pruneIfNeeded(int neededSpace) { + if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) { + return; + } + if (VolleyLog.DEBUG) { + VolleyLog.v("Pruning old cache entries."); + } + + long before = mTotalSize; + int prunedFiles = 0; + long startTime = SystemClock.elapsedRealtime(); + + Iterator> iterator = mEntries.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + CacheHeader e = entry.getValue(); + boolean deleted = getFileForKey(e.key).delete(); + if (deleted) { + mTotalSize -= e.size; + } else { + VolleyLog.d("Could not delete cache entry for key=%s, filename=%s", + e.key, getFilenameForKey(e.key)); + } + iterator.remove(); + prunedFiles++; + + if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { + break; + } + } + + if (VolleyLog.DEBUG) { + VolleyLog.v("pruned %d files, %d bytes, %d ms", + prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime); + } + } + + /** + * Puts the entry with the specified key into the cache. + * @param key The key to identify the entry by. + * @param entry The entry to cache. + */ + private void putEntry(String key, CacheHeader entry) { + if (!mEntries.containsKey(key)) { + mTotalSize += entry.size; + } else { + CacheHeader oldEntry = mEntries.get(key); + mTotalSize += (entry.size - oldEntry.size); + } + mEntries.put(key, entry); + } + + /** + * Removes the entry identified by 'key' from the cache. + */ + private void removeEntry(String key) { + CacheHeader entry = mEntries.get(key); + if (entry != null) { + mTotalSize -= entry.size; + mEntries.remove(key); + } + } + + /** + * Reads the contents of an InputStream into a byte[]. + * */ + private static byte[] streamToBytes(InputStream in, int length) throws IOException { + byte[] bytes = new byte[length]; + int count; + int pos = 0; + while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) { + pos += count; + } + if (pos != length) { + throw new IOException("Expected " + length + " bytes, read " + pos + " bytes"); + } + return bytes; + } + + /** + * Handles holding onto the cache headers for an entry. + */ + // Visible for testing. + static class CacheHeader { + /** The size of the data identified by this CacheHeader. (This is not + * serialized to disk. */ + public long size; + + /** The key that identifies the cache entry. */ + public String key; + + /** ETag for cache coherence. */ + public String etag; + + /** Date of this response as reported by the server. */ + public long serverDate; + + /** The last modified date for the requested object. */ + public long lastModified; + + /** TTL for this record. */ + public long ttl; + + /** Soft TTL for this record. */ + public long softTtl; + + /** Headers from the response resulting in this cache entry. */ + public Map responseHeaders; + + private CacheHeader() { } + + /** + * Instantiates a new CacheHeader object + * @param key The key that identifies the cache entry + * @param entry The cache entry. + */ + public CacheHeader(String key, Entry entry) { + this.key = key; + this.size = entry.data.length; + this.etag = entry.etag; + this.serverDate = entry.serverDate; + this.lastModified = entry.lastModified; + this.ttl = entry.ttl; + this.softTtl = entry.softTtl; + this.responseHeaders = entry.responseHeaders; + } + + /** + * Reads the header off of an InputStream and returns a CacheHeader object. + * @param is The InputStream to read from. + * @throws IOException + */ + public static CacheHeader readHeader(InputStream is) throws IOException { + CacheHeader entry = new CacheHeader(); + int magic = readInt(is); + if (magic != CACHE_MAGIC) { + // don't bother deleting, it'll get pruned eventually + throw new IOException(); + } + entry.key = readString(is); + entry.etag = readString(is); + if (entry.etag.equals("")) { + entry.etag = null; + } + entry.serverDate = readLong(is); + entry.lastModified = readLong(is); + entry.ttl = readLong(is); + entry.softTtl = readLong(is); + entry.responseHeaders = readStringStringMap(is); + + return entry; + } + + /** + * Creates a cache entry for the specified data. + */ + public Entry toCacheEntry(byte[] data) { + Entry e = new Entry(); + e.data = data; + e.etag = etag; + e.serverDate = serverDate; + e.lastModified = lastModified; + e.ttl = ttl; + e.softTtl = softTtl; + e.responseHeaders = responseHeaders; + return e; + } + + + /** + * Writes the contents of this CacheHeader to the specified OutputStream. + */ + public boolean writeHeader(OutputStream os) { + try { + writeInt(os, CACHE_MAGIC); + writeString(os, key); + writeString(os, etag == null ? "" : etag); + writeLong(os, serverDate); + writeLong(os, lastModified); + writeLong(os, ttl); + writeLong(os, softTtl); + writeStringStringMap(responseHeaders, os); + os.flush(); + return true; + } catch (IOException e) { + VolleyLog.d("%s", e.toString()); + return false; + } + } + + } + + private static class CountingInputStream extends FilterInputStream { + private int bytesRead = 0; + + private CountingInputStream(InputStream in) { + super(in); + } + + @Override + public int read() throws IOException { + int result = super.read(); + if (result != -1) { + bytesRead++; + } + return result; + } + + @Override + public int read(byte[] buffer, int offset, int count) throws IOException { + int result = super.read(buffer, offset, count); + if (result != -1) { + bytesRead += result; + } + return result; + } + } + + /* + * Homebrewed simple serialization system used for reading and writing cache + * headers on disk. Once upon a time, this used the standard Java + * Object{Input,Output}Stream, but the default implementation relies heavily + * on reflection (even for standard types) and generates a ton of garbage. + */ + + /** + * Simple wrapper around {@link InputStream#read()} that throws EOFException + * instead of returning -1. + */ + private static int read(InputStream is) throws IOException { + int b = is.read(); + if (b == -1) { + throw new EOFException(); + } + return b; + } + + static void writeInt(OutputStream os, int n) throws IOException { + os.write((n >> 0) & 0xff); + os.write((n >> 8) & 0xff); + os.write((n >> 16) & 0xff); + os.write((n >> 24) & 0xff); + } + + static int readInt(InputStream is) throws IOException { + int n = 0; + n |= (read(is) << 0); + n |= (read(is) << 8); + n |= (read(is) << 16); + n |= (read(is) << 24); + return n; + } + + static void writeLong(OutputStream os, long n) throws IOException { + os.write((byte)(n >>> 0)); + os.write((byte)(n >>> 8)); + os.write((byte)(n >>> 16)); + os.write((byte)(n >>> 24)); + os.write((byte)(n >>> 32)); + os.write((byte)(n >>> 40)); + os.write((byte)(n >>> 48)); + os.write((byte)(n >>> 56)); + } + + static long readLong(InputStream is) throws IOException { + long n = 0; + n |= ((read(is) & 0xFFL) << 0); + n |= ((read(is) & 0xFFL) << 8); + n |= ((read(is) & 0xFFL) << 16); + n |= ((read(is) & 0xFFL) << 24); + n |= ((read(is) & 0xFFL) << 32); + n |= ((read(is) & 0xFFL) << 40); + n |= ((read(is) & 0xFFL) << 48); + n |= ((read(is) & 0xFFL) << 56); + return n; + } + + static void writeString(OutputStream os, String s) throws IOException { + byte[] b = s.getBytes("UTF-8"); + writeLong(os, b.length); + os.write(b, 0, b.length); + } + + static String readString(InputStream is) throws IOException { + int n = (int) readLong(is); + byte[] b = streamToBytes(is, n); + return new String(b, "UTF-8"); + } + + static void writeStringStringMap(Map map, OutputStream os) throws IOException { + if (map != null) { + writeInt(os, map.size()); + for (Map.Entry entry : map.entrySet()) { + writeString(os, entry.getKey()); + writeString(os, entry.getValue()); + } + } else { + writeInt(os, 0); + } + } + + static Map readStringStringMap(InputStream is) throws IOException { + int size = readInt(is); + Map result = (size == 0) + ? Collections.emptyMap() + : new HashMap(size); + for (int i = 0; i < size; i++) { + String key = readString(is).intern(); + String value = readString(is).intern(); + result.put(key, value); + } + return result; + } + + +} diff --git a/volley/src/main/java/com/android/volley/toolbox/HttpClientStack.java b/volley/src/main/java/com/android/volley/toolbox/HttpClientStack.java new file mode 100644 index 0000000..377110e --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/HttpClientStack.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.Request.Method; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * An HttpStack that performs request over an {@link HttpClient}. + */ +public class HttpClientStack implements HttpStack { + protected final HttpClient mClient; + + private final static String HEADER_CONTENT_TYPE = "Content-Type"; + + public HttpClientStack(HttpClient client) { + mClient = client; + } + + private static void addHeaders(HttpUriRequest httpRequest, Map headers) { + for (String key : headers.keySet()) { + httpRequest.setHeader(key, headers.get(key)); + } + } + + @SuppressWarnings("unused") + private static List getPostParameterPairs(Map postParams) { + List result = new ArrayList(postParams.size()); + for (String key : postParams.keySet()) { + result.add(new BasicNameValuePair(key, postParams.get(key))); + } + return result; + } + + @Override + public HttpResponse performRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + HttpUriRequest httpRequest = createHttpRequest(request, additionalHeaders); + addHeaders(httpRequest, additionalHeaders); + addHeaders(httpRequest, request.getHeaders()); + onPrepareRequest(httpRequest); + HttpParams httpParams = httpRequest.getParams(); + int timeoutMs = request.getTimeoutMs(); + // TODO: Reevaluate this connection timeout based on more wide-scale + // data collection and possibly different for wifi vs. 3G. + HttpConnectionParams.setConnectionTimeout(httpParams, 5000); + HttpConnectionParams.setSoTimeout(httpParams, timeoutMs); + return mClient.execute(httpRequest); + } + + /** + * Creates the appropriate subclass of HttpUriRequest for passed in request. + */ + @SuppressWarnings("deprecation") + /* protected */ static HttpUriRequest createHttpRequest(Request request, + Map additionalHeaders) throws AuthFailureError { + switch (request.getMethod()) { + case Method.DEPRECATED_GET_OR_POST: { + // This is the deprecated way that needs to be handled for backwards compatibility. + // If the request's post body is null, then the assumption is that the request is + // GET. Otherwise, it is assumed that the request is a POST. + byte[] postBody = request.getPostBody(); + if (postBody != null) { + HttpPost postRequest = new HttpPost(request.getUrl()); + postRequest.addHeader(HEADER_CONTENT_TYPE, request.getPostBodyContentType()); + HttpEntity entity; + entity = new ByteArrayEntity(postBody); + postRequest.setEntity(entity); + return postRequest; + } else { + return new HttpGet(request.getUrl()); + } + } + case Method.GET: + return new HttpGet(request.getUrl()); + case Method.DELETE: + return new HttpDelete(request.getUrl()); + case Method.POST: { + HttpPost postRequest = new HttpPost(request.getUrl()); + postRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); + setEntityIfNonEmptyBody(postRequest, request); + return postRequest; + } + case Method.PUT: { + HttpPut putRequest = new HttpPut(request.getUrl()); + putRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); + setEntityIfNonEmptyBody(putRequest, request); + return putRequest; + } + case Method.HEAD: + return new HttpHead(request.getUrl()); + case Method.OPTIONS: + return new HttpOptions(request.getUrl()); + case Method.TRACE: + return new HttpTrace(request.getUrl()); + case Method.PATCH: { + HttpPatch patchRequest = new HttpPatch(request.getUrl()); + patchRequest.addHeader(HEADER_CONTENT_TYPE, request.getBodyContentType()); + setEntityIfNonEmptyBody(patchRequest, request); + return patchRequest; + } + default: + throw new IllegalStateException("Unknown request method."); + } + } + + private static void setEntityIfNonEmptyBody(HttpEntityEnclosingRequestBase httpRequest, + Request request) throws AuthFailureError { + byte[] body = request.getBody(); + if (body != null) { + HttpEntity entity = new ByteArrayEntity(body); + httpRequest.setEntity(entity); + } + } + + /** + * Called before the request is executed using the underlying HttpClient. + * + *

Overwrite in subclasses to augment the request.

+ */ + protected void onPrepareRequest(HttpUriRequest request) throws IOException { + // Nothing. + } + + /** + * The HttpPatch class does not exist in the Android framework, so this has been defined here. + */ + public static final class HttpPatch extends HttpEntityEnclosingRequestBase { + + public final static String METHOD_NAME = "PATCH"; + + public HttpPatch() { + super(); + } + + public HttpPatch(final URI uri) { + super(); + setURI(uri); + } + + /** + * @throws IllegalArgumentException if the uri is invalid. + */ + public HttpPatch(final String uri) { + super(); + setURI(URI.create(uri)); + } + + @Override + public String getMethod() { + return METHOD_NAME; + } + + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java b/volley/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java new file mode 100644 index 0000000..c3b48d8 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/HttpHeaderParser.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; + +import org.apache.http.impl.cookie.DateParseException; +import org.apache.http.impl.cookie.DateUtils; +import org.apache.http.protocol.HTTP; + +import java.util.Map; + +/** + * Utility methods for parsing HTTP headers. + */ +public class HttpHeaderParser { + + /** + * Extracts a {@link Cache.Entry} from a {@link NetworkResponse}. + * + * @param response The network response to parse headers from + * @return a cache entry for the given response, or null if the response is not cacheable. + */ + public static Cache.Entry parseCacheHeaders(NetworkResponse response) { + long now = System.currentTimeMillis(); + + Map headers = response.headers; + + long serverDate = 0; + long lastModified = 0; + long serverExpires = 0; + long softExpire = 0; + long finalExpire = 0; + long maxAge = 0; + long staleWhileRevalidate = 0; + boolean hasCacheControl = false; + boolean mustRevalidate = false; + + String serverEtag = null; + String headerValue; + + headerValue = headers.get("Date"); + if (headerValue != null) { + serverDate = parseDateAsEpoch(headerValue); + } + + headerValue = headers.get("Cache-Control"); + if (headerValue != null) { + hasCacheControl = true; + String[] tokens = headerValue.split(","); + for (int i = 0; i < tokens.length; i++) { + String token = tokens[i].trim(); + if (token.equals("no-cache") || token.equals("no-store")) { + return null; + } else if (token.startsWith("max-age=")) { + try { + maxAge = Long.parseLong(token.substring(8)); + } catch (Exception e) { + } + } else if (token.startsWith("stale-while-revalidate=")) { + try { + staleWhileRevalidate = Long.parseLong(token.substring(23)); + } catch (Exception e) { + } + } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) { + mustRevalidate = true; + } + } + } + + headerValue = headers.get("Expires"); + if (headerValue != null) { + serverExpires = parseDateAsEpoch(headerValue); + } + + headerValue = headers.get("Last-Modified"); + if (headerValue != null) { + lastModified = parseDateAsEpoch(headerValue); + } + + serverEtag = headers.get("ETag"); + + // Cache-Control takes precedence over an Expires header, even if both exist and Expires + // is more restrictive. + if (hasCacheControl) { + softExpire = now + maxAge * 1000; + finalExpire = mustRevalidate + ? softExpire + : softExpire + staleWhileRevalidate * 1000; + } else if (serverDate > 0 && serverExpires >= serverDate) { + // Default semantic for Expire header in HTTP specification is softExpire. + softExpire = now + (serverExpires - serverDate); + finalExpire = softExpire; + } + + Cache.Entry entry = new Cache.Entry(); + entry.data = response.data; + entry.etag = serverEtag; + entry.softTtl = softExpire; + entry.ttl = finalExpire; + entry.serverDate = serverDate; + entry.lastModified = lastModified; + entry.responseHeaders = headers; + + return entry; + } + + /** + * Parse date in RFC1123 format, and return its value as epoch + */ + public static long parseDateAsEpoch(String dateStr) { + try { + // Parse date in RFC1123 format if this header contains one + return DateUtils.parseDate(dateStr).getTime(); + } catch (DateParseException e) { + // Date in invalid format, fallback to 0 + return 0; + } + } + + /** + * Retrieve a charset from headers + * + * @param headers An {@link java.util.Map} of headers + * @param defaultCharset Charset to return if none can be found + * @return Returns the charset specified in the Content-Type of this header, + * or the defaultCharset if none can be found. + */ + public static String parseCharset(Map headers, String defaultCharset) { + String contentType = headers.get(HTTP.CONTENT_TYPE); + if (contentType != null) { + String[] params = contentType.split(";"); + for (int i = 1; i < params.length; i++) { + String[] pair = params[i].trim().split("="); + if (pair.length == 2) { + if (pair[0].equals("charset")) { + return pair[1]; + } + } + } + } + + return defaultCharset; + } + + /** + * Returns the charset specified in the Content-Type of this header, + * or the HTTP default (ISO-8859-1) if none can be found. + */ + public static String parseCharset(Map headers) { + return parseCharset(headers, HTTP.DEFAULT_CONTENT_CHARSET); + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/HttpStack.java b/volley/src/main/java/com/android/volley/toolbox/HttpStack.java new file mode 100644 index 0000000..a52fd06 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/HttpStack.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; + +import org.apache.http.HttpResponse; + +import java.io.IOException; +import java.util.Map; + +/** + * An HTTP stack abstraction. + */ +public interface HttpStack { + /** + * Performs an HTTP request with the given parameters. + * + *

A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise, + * and the Content-Type header is set to request.getPostBodyContentType().

+ * + * @param request the request to perform + * @param additionalHeaders additional headers to be sent together with + * {@link Request#getHeaders()} + * @return the HTTP response + */ + public HttpResponse performRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError; + +} diff --git a/volley/src/main/java/com/android/volley/toolbox/HurlStack.java b/volley/src/main/java/com/android/volley/toolbox/HurlStack.java new file mode 100644 index 0000000..c53d5e0 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/HurlStack.java @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.Request.Method; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.entity.BasicHttpEntity; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicHttpResponse; +import org.apache.http.message.BasicStatusLine; + +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +/** + * An {@link HttpStack} based on {@link HttpURLConnection}. + */ +public class HurlStack implements HttpStack { + + private static final String HEADER_CONTENT_TYPE = "Content-Type"; + + /** + * An interface for transforming URLs before use. + */ + public interface UrlRewriter { + /** + * Returns a URL to use instead of the provided one, or null to indicate + * this URL should not be used at all. + */ + public String rewriteUrl(String originalUrl); + } + + private final UrlRewriter mUrlRewriter; + private final SSLSocketFactory mSslSocketFactory; + + public HurlStack() { + this(null); + } + + /** + * @param urlRewriter Rewriter to use for request URLs + */ + public HurlStack(UrlRewriter urlRewriter) { + this(urlRewriter, null); + } + + /** + * @param urlRewriter Rewriter to use for request URLs + * @param sslSocketFactory SSL factory to use for HTTPS connections + */ + public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { + mUrlRewriter = urlRewriter; + mSslSocketFactory = sslSocketFactory; + } + + @Override + public HttpResponse performRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + String url = request.getUrl(); + HashMap map = new HashMap(); + map.putAll(request.getHeaders()); + map.putAll(additionalHeaders); + if (mUrlRewriter != null) { + String rewritten = mUrlRewriter.rewriteUrl(url); + if (rewritten == null) { + throw new IOException("URL blocked by rewriter: " + url); + } + url = rewritten; + } + URL parsedUrl = new URL(url); + HttpURLConnection connection = openConnection(parsedUrl, request); + for (String headerName : map.keySet()) { + connection.addRequestProperty(headerName, map.get(headerName)); + } + setConnectionParametersForRequest(connection, request); + // Initialize HttpResponse with data from the HttpURLConnection. + ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); + int responseCode = connection.getResponseCode(); + if (responseCode == -1) { + // -1 is returned by getResponseCode() if the response code could not be retrieved. + // Signal to the caller that something was wrong with the connection. + throw new IOException("Could not retrieve response code from HttpUrlConnection."); + } + StatusLine responseStatus = new BasicStatusLine(protocolVersion, + connection.getResponseCode(), connection.getResponseMessage()); + BasicHttpResponse response = new BasicHttpResponse(responseStatus); + if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) { + response.setEntity(entityFromConnection(connection)); + } + for (Entry> header : connection.getHeaderFields().entrySet()) { + if (header.getKey() != null) { + Header h = new BasicHeader(header.getKey(), header.getValue().get(0)); + response.addHeader(h); + } + } + return response; + } + + /** + * Checks if a response message contains a body. + * @see RFC 7230 section 3.3 + * @param requestMethod request method + * @param responseCode response status code + * @return whether the response has a body + */ + private static boolean hasResponseBody(int requestMethod, int responseCode) { + return requestMethod != Request.Method.HEAD + && !(HttpStatus.SC_CONTINUE <= responseCode && responseCode < HttpStatus.SC_OK) + && responseCode != HttpStatus.SC_NO_CONTENT + && responseCode != HttpStatus.SC_NOT_MODIFIED; + } + + /** + * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}. + * @param connection + * @return an HttpEntity populated with data from connection. + */ + private static HttpEntity entityFromConnection(HttpURLConnection connection) { + BasicHttpEntity entity = new BasicHttpEntity(); + InputStream inputStream; + try { + inputStream = connection.getInputStream(); + } catch (IOException ioe) { + inputStream = connection.getErrorStream(); + } + entity.setContent(inputStream); + entity.setContentLength(connection.getContentLength()); + entity.setContentEncoding(connection.getContentEncoding()); + entity.setContentType(connection.getContentType()); + return entity; + } + + /** + * Create an {@link HttpURLConnection} for the specified {@code url}. + */ + protected HttpURLConnection createConnection(URL url) throws IOException { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + // Workaround for the M release HttpURLConnection not observing the + // HttpURLConnection.setFollowRedirects() property. + // https://code.google.com/p/android/issues/detail?id=194495 + connection.setInstanceFollowRedirects(HttpURLConnection.getFollowRedirects()); + + return connection; + } + + /** + * Opens an {@link HttpURLConnection} with parameters. + * @param url + * @return an open connection + * @throws IOException + */ + private HttpURLConnection openConnection(URL url, Request request) throws IOException { + HttpURLConnection connection = createConnection(url); + + int timeoutMs = request.getTimeoutMs(); + connection.setConnectTimeout(timeoutMs); + connection.setReadTimeout(timeoutMs); + connection.setUseCaches(false); + connection.setDoInput(true); + + // use caller-provided custom SslSocketFactory, if any, for HTTPS + if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) { + ((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory); + } + + return connection; + } + + @SuppressWarnings("deprecation") + /* package */ static void setConnectionParametersForRequest(HttpURLConnection connection, + Request request) throws IOException, AuthFailureError { + switch (request.getMethod()) { + case Method.DEPRECATED_GET_OR_POST: + // This is the deprecated way that needs to be handled for backwards compatibility. + // If the request's post body is null, then the assumption is that the request is + // GET. Otherwise, it is assumed that the request is a POST. + byte[] postBody = request.getPostBody(); + if (postBody != null) { + // Prepare output. There is no need to set Content-Length explicitly, + // since this is handled by HttpURLConnection using the size of the prepared + // output stream. + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.addRequestProperty(HEADER_CONTENT_TYPE, + request.getPostBodyContentType()); + DataOutputStream out = new DataOutputStream(connection.getOutputStream()); + out.write(postBody); + out.close(); + } + break; + case Method.GET: + // Not necessary to set the request method because connection defaults to GET but + // being explicit here. + connection.setRequestMethod("GET"); + break; + case Method.DELETE: + connection.setRequestMethod("DELETE"); + break; + case Method.POST: + connection.setRequestMethod("POST"); + addBodyIfExists(connection, request); + break; + case Method.PUT: + connection.setRequestMethod("PUT"); + addBodyIfExists(connection, request); + break; + case Method.HEAD: + connection.setRequestMethod("HEAD"); + break; + case Method.OPTIONS: + connection.setRequestMethod("OPTIONS"); + break; + case Method.TRACE: + connection.setRequestMethod("TRACE"); + break; + case Method.PATCH: + connection.setRequestMethod("PATCH"); + addBodyIfExists(connection, request); + break; + default: + throw new IllegalStateException("Unknown method type."); + } + } + + private static void addBodyIfExists(HttpURLConnection connection, Request request) + throws IOException, AuthFailureError { + byte[] body = request.getBody(); + if (body != null) { + connection.setDoOutput(true); + connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType()); + DataOutputStream out = new DataOutputStream(connection.getOutputStream()); + out.write(body); + out.close(); + } + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/ImageLoader.java b/volley/src/main/java/com/android/volley/toolbox/ImageLoader.java new file mode 100644 index 0000000..d5305e3 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/ImageLoader.java @@ -0,0 +1,507 @@ +/** + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.volley.toolbox; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.os.Handler; +import android.os.Looper; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; +import com.android.volley.VolleyError; + +import java.util.HashMap; +import java.util.LinkedList; + +/** + * Helper that handles loading and caching images from remote URLs. + * + * The simple way to use this class is to call {@link ImageLoader#get(String, ImageListener)} + * and to pass in the default image listener provided by + * {@link ImageLoader#getImageListener(ImageView, int, int)}. Note that all function calls to + * this class must be made from the main thead, and all responses will be delivered to the main + * thread as well. + */ +public class ImageLoader { + /** RequestQueue for dispatching ImageRequests onto. */ + private final RequestQueue mRequestQueue; + + /** Amount of time to wait after first response arrives before delivering all responses. */ + private int mBatchResponseDelayMs = 100; + + /** The cache implementation to be used as an L1 cache before calling into volley. */ + private final ImageCache mCache; + + /** + * HashMap of Cache keys -> BatchedImageRequest used to track in-flight requests so + * that we can coalesce multiple requests to the same URL into a single network request. + */ + private final HashMap mInFlightRequests = + new HashMap(); + + /** HashMap of the currently pending responses (waiting to be delivered). */ + private final HashMap mBatchedResponses = + new HashMap(); + + /** Handler to the main thread. */ + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + /** Runnable for in-flight response delivery. */ + private Runnable mRunnable; + + /** + * Simple cache adapter interface. If provided to the ImageLoader, it + * will be used as an L1 cache before dispatch to Volley. Implementations + * must not block. Implementation with an LruCache is recommended. + */ + public interface ImageCache { + public Bitmap getBitmap(String url); + public void putBitmap(String url, Bitmap bitmap); + } + + /** + * Constructs a new ImageLoader. + * @param queue The RequestQueue to use for making image requests. + * @param imageCache The cache to use as an L1 cache. + */ + public ImageLoader(RequestQueue queue, ImageCache imageCache) { + mRequestQueue = queue; + mCache = imageCache; + } + + /** + * The default implementation of ImageListener which handles basic functionality + * of showing a default image until the network response is received, at which point + * it will switch to either the actual image or the error image. + * @param view The imageView that the listener is associated with. + * @param defaultImageResId Default image resource ID to use, or 0 if it doesn't exist. + * @param errorImageResId Error image resource ID to use, or 0 if it doesn't exist. + */ + public static ImageListener getImageListener(final ImageView view, + final int defaultImageResId, final int errorImageResId) { + return new ImageListener() { + @Override + public void onErrorResponse(VolleyError error) { + if (errorImageResId != 0) { + view.setImageResource(errorImageResId); + } + } + + @Override + public void onResponse(ImageContainer response, boolean isImmediate) { + if (response.getBitmap() != null) { + view.setImageBitmap(response.getBitmap()); + } else if (defaultImageResId != 0) { + view.setImageResource(defaultImageResId); + } + } + }; + } + + /** + * Interface for the response handlers on image requests. + * + * The call flow is this: + * 1. Upon being attached to a request, onResponse(response, true) will + * be invoked to reflect any cached data that was already available. If the + * data was available, response.getBitmap() will be non-null. + * + * 2. After a network response returns, only one of the following cases will happen: + * - onResponse(response, false) will be called if the image was loaded. + * or + * - onErrorResponse will be called if there was an error loading the image. + */ + public interface ImageListener extends ErrorListener { + /** + * Listens for non-error changes to the loading of the image request. + * + * @param response Holds all information pertaining to the request, as well + * as the bitmap (if it is loaded). + * @param isImmediate True if this was called during ImageLoader.get() variants. + * This can be used to differentiate between a cached image loading and a network + * image loading in order to, for example, run an animation to fade in network loaded + * images. + */ + public void onResponse(ImageContainer response, boolean isImmediate); + } + + /** + * Checks if the item is available in the cache. + * @param requestUrl The url of the remote image + * @param maxWidth The maximum width of the returned image. + * @param maxHeight The maximum height of the returned image. + * @return True if the item exists in cache, false otherwise. + */ + public boolean isCached(String requestUrl, int maxWidth, int maxHeight) { + return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); + } + + /** + * Checks if the item is available in the cache. + * + * @param requestUrl The url of the remote image + * @param maxWidth The maximum width of the returned image. + * @param maxHeight The maximum height of the returned image. + * @param scaleType The scaleType of the imageView. + * @return True if the item exists in cache, false otherwise. + */ + public boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) { + throwIfNotOnMainThread(); + + String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); + return mCache.getBitmap(cacheKey) != null; + } + + /** + * Returns an ImageContainer for the requested URL. + * + * The ImageContainer will contain either the specified default bitmap or the loaded bitmap. + * If the default was returned, the {@link ImageLoader} will be invoked when the + * request is fulfilled. + * + * @param requestUrl The URL of the image to be loaded. + */ + public ImageContainer get(String requestUrl, final ImageListener listener) { + return get(requestUrl, listener, 0, 0); + } + + /** + * Equivalent to calling {@link #get(String, ImageListener, int, int, ScaleType)} with + * {@code Scaletype == ScaleType.CENTER_INSIDE}. + */ + public ImageContainer get(String requestUrl, ImageListener imageListener, + int maxWidth, int maxHeight) { + return get(requestUrl, imageListener, maxWidth, maxHeight, ScaleType.CENTER_INSIDE); + } + + /** + * Issues a bitmap request with the given URL if that image is not available + * in the cache, and returns a bitmap container that contains all of the data + * relating to the request (as well as the default image if the requested + * image is not available). + * @param requestUrl The url of the remote image + * @param imageListener The listener to call when the remote image is loaded + * @param maxWidth The maximum width of the returned image. + * @param maxHeight The maximum height of the returned image. + * @param scaleType The ImageViews ScaleType used to calculate the needed image size. + * @return A container object that contains all of the properties of the request, as well as + * the currently available image (default if remote is not loaded). + */ + public ImageContainer get(String requestUrl, ImageListener imageListener, + int maxWidth, int maxHeight, ScaleType scaleType) { + + // only fulfill requests that were initiated from the main thread. + throwIfNotOnMainThread(); + + final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType); + + // Try to look up the request in the cache of remote images. + Bitmap cachedBitmap = mCache.getBitmap(cacheKey); + if (cachedBitmap != null) { + // Return the cached bitmap. + ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); + imageListener.onResponse(container, true); + return container; + } + + // The bitmap did not exist in the cache, fetch it! + ImageContainer imageContainer = + new ImageContainer(null, requestUrl, cacheKey, imageListener); + + // Update the caller to let them know that they should use the default bitmap. + imageListener.onResponse(imageContainer, true); + + // Check to see if a request is already in-flight. + BatchedImageRequest request = mInFlightRequests.get(cacheKey); + if (request != null) { + // If it is, add this request to the list of listeners. + request.addContainer(imageContainer); + return imageContainer; + } + + // The request is not already in flight. Send the new request to the network and + // track it. + Request newRequest = makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, + cacheKey); + + mRequestQueue.add(newRequest); + mInFlightRequests.put(cacheKey, + new BatchedImageRequest(newRequest, imageContainer)); + return imageContainer; + } + + protected Request makeImageRequest(String requestUrl, int maxWidth, int maxHeight, + ScaleType scaleType, final String cacheKey) { + return new ImageRequest(requestUrl, new Listener() { + @Override + public void onResponse(Bitmap response) { + onGetImageSuccess(cacheKey, response); + } + }, maxWidth, maxHeight, scaleType, Config.RGB_565, new ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + onGetImageError(cacheKey, error); + } + }); + } + + /** + * Sets the amount of time to wait after the first response arrives before delivering all + * responses. Batching can be disabled entirely by passing in 0. + * @param newBatchedResponseDelayMs The time in milliseconds to wait. + */ + public void setBatchedResponseDelay(int newBatchedResponseDelayMs) { + mBatchResponseDelayMs = newBatchedResponseDelayMs; + } + + /** + * Handler for when an image was successfully loaded. + * @param cacheKey The cache key that is associated with the image request. + * @param response The bitmap that was returned from the network. + */ + protected void onGetImageSuccess(String cacheKey, Bitmap response) { + // cache the image that was fetched. + mCache.putBitmap(cacheKey, response); + + // remove the request from the list of in-flight requests. + BatchedImageRequest request = mInFlightRequests.remove(cacheKey); + + if (request != null) { + // Update the response bitmap. + request.mResponseBitmap = response; + + // Send the batched response + batchResponse(cacheKey, request); + } + } + + /** + * Handler for when an image failed to load. + * @param cacheKey The cache key that is associated with the image request. + */ + protected void onGetImageError(String cacheKey, VolleyError error) { + // Notify the requesters that something failed via a null result. + // Remove this request from the list of in-flight requests. + BatchedImageRequest request = mInFlightRequests.remove(cacheKey); + + if (request != null) { + // Set the error for this request + request.setError(error); + + // Send the batched response + batchResponse(cacheKey, request); + } + } + + /** + * Container object for all of the data surrounding an image request. + */ + public class ImageContainer { + /** + * The most relevant bitmap for the container. If the image was in cache, the + * Holder to use for the final bitmap (the one that pairs to the requested URL). + */ + private Bitmap mBitmap; + + private final ImageListener mListener; + + /** The cache key that was associated with the request */ + private final String mCacheKey; + + /** The request URL that was specified */ + private final String mRequestUrl; + + /** + * Constructs a BitmapContainer object. + * @param bitmap The final bitmap (if it exists). + * @param requestUrl The requested URL for this container. + * @param cacheKey The cache key that identifies the requested URL for this container. + */ + public ImageContainer(Bitmap bitmap, String requestUrl, + String cacheKey, ImageListener listener) { + mBitmap = bitmap; + mRequestUrl = requestUrl; + mCacheKey = cacheKey; + mListener = listener; + } + + /** + * Releases interest in the in-flight request (and cancels it if no one else is listening). + */ + public void cancelRequest() { + if (mListener == null) { + return; + } + + BatchedImageRequest request = mInFlightRequests.get(mCacheKey); + if (request != null) { + boolean canceled = request.removeContainerAndCancelIfNecessary(this); + if (canceled) { + mInFlightRequests.remove(mCacheKey); + } + } else { + // check to see if it is already batched for delivery. + request = mBatchedResponses.get(mCacheKey); + if (request != null) { + request.removeContainerAndCancelIfNecessary(this); + if (request.mContainers.size() == 0) { + mBatchedResponses.remove(mCacheKey); + } + } + } + } + + /** + * Returns the bitmap associated with the request URL if it has been loaded, null otherwise. + */ + public Bitmap getBitmap() { + return mBitmap; + } + + /** + * Returns the requested URL for this container. + */ + public String getRequestUrl() { + return mRequestUrl; + } + } + + /** + * Wrapper class used to map a Request to the set of active ImageContainer objects that are + * interested in its results. + */ + private class BatchedImageRequest { + /** The request being tracked */ + private final Request mRequest; + + /** The result of the request being tracked by this item */ + private Bitmap mResponseBitmap; + + /** Error if one occurred for this response */ + private VolleyError mError; + + /** List of all of the active ImageContainers that are interested in the request */ + private final LinkedList mContainers = new LinkedList(); + + /** + * Constructs a new BatchedImageRequest object + * @param request The request being tracked + * @param container The ImageContainer of the person who initiated the request. + */ + public BatchedImageRequest(Request request, ImageContainer container) { + mRequest = request; + mContainers.add(container); + } + + /** + * Set the error for this response + */ + public void setError(VolleyError error) { + mError = error; + } + + /** + * Get the error for this response + */ + public VolleyError getError() { + return mError; + } + + /** + * Adds another ImageContainer to the list of those interested in the results of + * the request. + */ + public void addContainer(ImageContainer container) { + mContainers.add(container); + } + + /** + * Detatches the bitmap container from the request and cancels the request if no one is + * left listening. + * @param container The container to remove from the list + * @return True if the request was canceled, false otherwise. + */ + public boolean removeContainerAndCancelIfNecessary(ImageContainer container) { + mContainers.remove(container); + if (mContainers.size() == 0) { + mRequest.cancel(); + return true; + } + return false; + } + } + + /** + * Starts the runnable for batched delivery of responses if it is not already started. + * @param cacheKey The cacheKey of the response being delivered. + * @param request The BatchedImageRequest to be delivered. + */ + private void batchResponse(String cacheKey, BatchedImageRequest request) { + mBatchedResponses.put(cacheKey, request); + // If we don't already have a batch delivery runnable in flight, make a new one. + // Note that this will be used to deliver responses to all callers in mBatchedResponses. + if (mRunnable == null) { + mRunnable = new Runnable() { + @Override + public void run() { + for (BatchedImageRequest bir : mBatchedResponses.values()) { + for (ImageContainer container : bir.mContainers) { + // If one of the callers in the batched request canceled the request + // after the response was received but before it was delivered, + // skip them. + if (container.mListener == null) { + continue; + } + if (bir.getError() == null) { + container.mBitmap = bir.mResponseBitmap; + container.mListener.onResponse(container, false); + } else { + container.mListener.onErrorResponse(bir.getError()); + } + } + } + mBatchedResponses.clear(); + mRunnable = null; + } + + }; + // Post the runnable. + mHandler.postDelayed(mRunnable, mBatchResponseDelayMs); + } + } + + private void throwIfNotOnMainThread() { + if (Looper.myLooper() != Looper.getMainLooper()) { + throw new IllegalStateException("ImageLoader must be invoked from the main thread."); + } + } + /** + * Creates a cache key for use with the L1 cache. + * @param url The URL of the request. + * @param maxWidth The max-width of the output. + * @param maxHeight The max-height of the output. + * @param scaleType The scaleType of the imageView. + */ + private static String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) { + return new StringBuilder(url.length() + 12).append("#W").append(maxWidth) + .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url) + .toString(); + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/ImageRequest.java b/volley/src/main/java/com/android/volley/toolbox/ImageRequest.java new file mode 100644 index 0000000..d663f5f --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/ImageRequest.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.widget.ImageView.ScaleType; + +import com.android.volley.DefaultRetryPolicy; +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyLog; + +/** + * A canned request for getting an image at a given URL and calling + * back with a decoded Bitmap. + */ +public class ImageRequest extends Request { + /** Socket timeout in milliseconds for image requests */ + public static final int DEFAULT_IMAGE_TIMEOUT_MS = 1000; + + /** Default number of retries for image requests */ + public static final int DEFAULT_IMAGE_MAX_RETRIES = 2; + + /** Default backoff multiplier for image requests */ + public static final float DEFAULT_IMAGE_BACKOFF_MULT = 2f; + + private final Response.Listener mListener; + private final Config mDecodeConfig; + private final int mMaxWidth; + private final int mMaxHeight; + private ScaleType mScaleType; + + /** Decoding lock so that we don't decode more than one image at a time (to avoid OOM's) */ + private static final Object sDecodeLock = new Object(); + + /** + * Creates a new image request, decoding to a maximum specified width and + * height. If both width and height are zero, the image will be decoded to + * its natural size. If one of the two is nonzero, that dimension will be + * clamped and the other one will be set to preserve the image's aspect + * ratio. If both width and height are nonzero, the image will be decoded to + * be fit in the rectangle of dimensions width x height while keeping its + * aspect ratio. + * + * @param url URL of the image + * @param listener Listener to receive the decoded bitmap + * @param maxWidth Maximum width to decode this bitmap to, or zero for none + * @param maxHeight Maximum height to decode this bitmap to, or zero for + * none + * @param scaleType The ImageViews ScaleType used to calculate the needed image size. + * @param decodeConfig Format to decode the bitmap to + * @param errorListener Error listener, or null to ignore errors + */ + public ImageRequest(String url, Response.Listener listener, int maxWidth, int maxHeight, + ScaleType scaleType, Config decodeConfig, Response.ErrorListener errorListener) { + super(Method.GET, url, errorListener); + setRetryPolicy(new DefaultRetryPolicy(DEFAULT_IMAGE_TIMEOUT_MS, DEFAULT_IMAGE_MAX_RETRIES, + DEFAULT_IMAGE_BACKOFF_MULT)); + mListener = listener; + mDecodeConfig = decodeConfig; + mMaxWidth = maxWidth; + mMaxHeight = maxHeight; + mScaleType = scaleType; + } + + /** + * For API compatibility with the pre-ScaleType variant of the constructor. Equivalent to + * the normal constructor with {@code ScaleType.CENTER_INSIDE}. + */ + @Deprecated + public ImageRequest(String url, Response.Listener listener, int maxWidth, int maxHeight, + Config decodeConfig, Response.ErrorListener errorListener) { + this(url, listener, maxWidth, maxHeight, + ScaleType.CENTER_INSIDE, decodeConfig, errorListener); + } + @Override + public Priority getPriority() { + return Priority.LOW; + } + + /** + * Scales one side of a rectangle to fit aspect ratio. + * + * @param maxPrimary Maximum size of the primary dimension (i.e. width for + * max width), or zero to maintain aspect ratio with secondary + * dimension + * @param maxSecondary Maximum size of the secondary dimension, or zero to + * maintain aspect ratio with primary dimension + * @param actualPrimary Actual size of the primary dimension + * @param actualSecondary Actual size of the secondary dimension + * @param scaleType The ScaleType used to calculate the needed image size. + */ + private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary, + int actualSecondary, ScaleType scaleType) { + + // If no dominant value at all, just return the actual. + if ((maxPrimary == 0) && (maxSecondary == 0)) { + return actualPrimary; + } + + // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio. + if (scaleType == ScaleType.FIT_XY) { + if (maxPrimary == 0) { + return actualPrimary; + } + return maxPrimary; + } + + // If primary is unspecified, scale primary to match secondary's scaling ratio. + if (maxPrimary == 0) { + double ratio = (double) maxSecondary / (double) actualSecondary; + return (int) (actualPrimary * ratio); + } + + if (maxSecondary == 0) { + return maxPrimary; + } + + double ratio = (double) actualSecondary / (double) actualPrimary; + int resized = maxPrimary; + + // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio. + if (scaleType == ScaleType.CENTER_CROP) { + if ((resized * ratio) < maxSecondary) { + resized = (int) (maxSecondary / ratio); + } + return resized; + } + + if ((resized * ratio) > maxSecondary) { + resized = (int) (maxSecondary / ratio); + } + return resized; + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + // Serialize all decode on a global lock to reduce concurrent heap usage. + synchronized (sDecodeLock) { + try { + return doParse(response); + } catch (OutOfMemoryError e) { + VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl()); + return Response.error(new ParseError(e)); + } + } + } + + /** + * The real guts of parseNetworkResponse. Broken out for readability. + */ + private Response doParse(NetworkResponse response) { + byte[] data = response.data; + BitmapFactory.Options decodeOptions = new BitmapFactory.Options(); + Bitmap bitmap = null; + if (mMaxWidth == 0 && mMaxHeight == 0) { + decodeOptions.inPreferredConfig = mDecodeConfig; + bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + } else { + // If we have to resize this image, first get the natural bounds. + decodeOptions.inJustDecodeBounds = true; + BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + int actualWidth = decodeOptions.outWidth; + int actualHeight = decodeOptions.outHeight; + + // Then compute the dimensions we would ideally like to decode to. + int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight, + actualWidth, actualHeight, mScaleType); + int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth, + actualHeight, actualWidth, mScaleType); + + // Decode to the nearest power of two scaling factor. + decodeOptions.inJustDecodeBounds = false; + // TODO(ficus): Do we need this or is it okay since API 8 doesn't support it? + // decodeOptions.inPreferQualityOverSpeed = PREFER_QUALITY_OVER_SPEED; + decodeOptions.inSampleSize = + findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight); + Bitmap tempBitmap = + BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions); + + // If necessary, scale down to the maximal acceptable size. + if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || + tempBitmap.getHeight() > desiredHeight)) { + bitmap = Bitmap.createScaledBitmap(tempBitmap, + desiredWidth, desiredHeight, true); + tempBitmap.recycle(); + } else { + bitmap = tempBitmap; + } + } + + if (bitmap == null) { + return Response.error(new ParseError(response)); + } else { + return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response)); + } + } + + @Override + protected void deliverResponse(Bitmap response) { + mListener.onResponse(response); + } + + /** + * Returns the largest power-of-two divisor for use in downscaling a bitmap + * that will not result in the scaling past the desired dimensions. + * + * @param actualWidth Actual width of the bitmap + * @param actualHeight Actual height of the bitmap + * @param desiredWidth Desired width of the bitmap + * @param desiredHeight Desired height of the bitmap + */ + // Visible for testing. + static int findBestSampleSize( + int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) { + double wr = (double) actualWidth / desiredWidth; + double hr = (double) actualHeight / desiredHeight; + double ratio = Math.min(wr, hr); + float n = 1.0f; + while ((n * 2) <= ratio) { + n *= 2; + } + + return (int) n; + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java b/volley/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java new file mode 100644 index 0000000..ba35d26 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/JsonArrayRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; + +import org.json.JSONArray; +import org.json.JSONException; + +import java.io.UnsupportedEncodingException; + +/** + * A request for retrieving a {@link JSONArray} response body at a given URL. + */ +public class JsonArrayRequest extends JsonRequest { + + /** + * Creates a new request. + * @param url URL to fetch the JSON from + * @param listener Listener to receive the JSON response + * @param errorListener Error listener, or null to ignore errors. + */ + public JsonArrayRequest(String url, Listener listener, ErrorListener errorListener) { + super(Method.GET, url, null, listener, errorListener); + } + + /** + * Creates a new request. + * @param method the HTTP method to use + * @param url URL to fetch the JSON from + * @param jsonRequest A {@link JSONArray} to post with the request. Null is allowed and + * indicates no parameters will be posted along with request. + * @param listener Listener to receive the JSON response + * @param errorListener Error listener, or null to ignore errors. + */ + public JsonArrayRequest(int method, String url, JSONArray jsonRequest, + Listener listener, ErrorListener errorListener) { + super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener, + errorListener); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + try { + String jsonString = new String(response.data, + HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET)); + return Response.success(new JSONArray(jsonString), + HttpHeaderParser.parseCacheHeaders(response)); + } catch (UnsupportedEncodingException e) { + return Response.error(new ParseError(e)); + } catch (JSONException je) { + return Response.error(new ParseError(je)); + } + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java b/volley/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java new file mode 100644 index 0000000..2991898 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/JsonObjectRequest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.NetworkResponse; +import com.android.volley.ParseError; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.UnsupportedEncodingException; + +/** + * A request for retrieving a {@link JSONObject} response body at a given URL, allowing for an + * optional {@link JSONObject} to be passed in as part of the request body. + */ +public class JsonObjectRequest extends JsonRequest { + + /** + * Creates a new request. + * @param method the HTTP method to use + * @param url URL to fetch the JSON from + * @param jsonRequest A {@link JSONObject} to post with the request. Null is allowed and + * indicates no parameters will be posted along with request. + * @param listener Listener to receive the JSON response + * @param errorListener Error listener, or null to ignore errors. + */ + public JsonObjectRequest(int method, String url, JSONObject jsonRequest, + Listener listener, ErrorListener errorListener) { + super(method, url, (jsonRequest == null) ? null : jsonRequest.toString(), listener, + errorListener); + } + + /** + * Constructor which defaults to GET if jsonRequest is + * null, POST otherwise. + * + * @see #JsonObjectRequest(int, String, JSONObject, Listener, ErrorListener) + */ + public JsonObjectRequest(String url, JSONObject jsonRequest, Listener listener, + ErrorListener errorListener) { + this(jsonRequest == null ? Method.GET : Method.POST, url, jsonRequest, + listener, errorListener); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + try { + String jsonString = new String(response.data, + HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET)); + return Response.success(new JSONObject(jsonString), + HttpHeaderParser.parseCacheHeaders(response)); + } catch (UnsupportedEncodingException e) { + return Response.error(new ParseError(e)); + } catch (JSONException je) { + return Response.error(new ParseError(je)); + } + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/JsonRequest.java b/volley/src/main/java/com/android/volley/toolbox/JsonRequest.java new file mode 100644 index 0000000..2d58f40 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/JsonRequest.java @@ -0,0 +1,105 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; +import com.android.volley.VolleyLog; + +import java.io.UnsupportedEncodingException; + +/** + * A request for retrieving a T type response body at a given URL that also + * optionally sends along a JSON body in the request specified. + * + * @param JSON type of response expected + */ +public abstract class JsonRequest extends Request { + /** Default charset for JSON request. */ + protected static final String PROTOCOL_CHARSET = "utf-8"; + + /** Content type for request. */ + private static final String PROTOCOL_CONTENT_TYPE = + String.format("application/json; charset=%s", PROTOCOL_CHARSET); + + private final Listener mListener; + private final String mRequestBody; + + /** + * Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()} + * or {@link #getPostParams()} is overridden (which defaults to POST). + * + * @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}. + */ + @Deprecated + public JsonRequest(String url, String requestBody, Listener listener, + ErrorListener errorListener) { + this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener); + } + + public JsonRequest(int method, String url, String requestBody, Listener listener, + ErrorListener errorListener) { + super(method, url, errorListener); + mListener = listener; + mRequestBody = requestBody; + } + + @Override + protected void deliverResponse(T response) { + mListener.onResponse(response); + } + + @Override + abstract protected Response parseNetworkResponse(NetworkResponse response); + + /** + * @deprecated Use {@link #getBodyContentType()}. + */ + @Deprecated + @Override + public String getPostBodyContentType() { + return getBodyContentType(); + } + + /** + * @deprecated Use {@link #getBody()}. + */ + @Deprecated + @Override + public byte[] getPostBody() { + return getBody(); + } + + @Override + public String getBodyContentType() { + return PROTOCOL_CONTENT_TYPE; + } + + @Override + public byte[] getBody() { + try { + return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET); + } catch (UnsupportedEncodingException uee) { + VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s", + mRequestBody, PROTOCOL_CHARSET); + return null; + } + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/NetworkImageView.java b/volley/src/main/java/com/android/volley/toolbox/NetworkImageView.java new file mode 100644 index 0000000..324dbc0 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/NetworkImageView.java @@ -0,0 +1,220 @@ +/** + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.volley.toolbox; + +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; +import android.widget.ImageView; + +import com.android.volley.VolleyError; +import com.android.volley.toolbox.ImageLoader.ImageContainer; +import com.android.volley.toolbox.ImageLoader.ImageListener; + +/** + * Handles fetching an image from a URL as well as the life-cycle of the + * associated request. + */ +public class NetworkImageView extends ImageView { + /** The URL of the network image to load */ + private String mUrl; + + /** + * Resource ID of the image to be used as a placeholder until the network image is loaded. + */ + private int mDefaultImageId; + + /** + * Resource ID of the image to be used if the network response fails. + */ + private int mErrorImageId; + + /** Local copy of the ImageLoader. */ + private ImageLoader mImageLoader; + + /** Current ImageContainer. (either in-flight or finished) */ + private ImageContainer mImageContainer; + + public NetworkImageView(Context context) { + this(context, null); + } + + public NetworkImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NetworkImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Sets URL of the image that should be loaded into this view. Note that calling this will + * immediately either set the cached image (if available) or the default image specified by + * {@link NetworkImageView#setDefaultImageResId(int)} on the view. + * + * NOTE: If applicable, {@link NetworkImageView#setDefaultImageResId(int)} and + * {@link NetworkImageView#setErrorImageResId(int)} should be called prior to calling + * this function. + * + * @param url The URL that should be loaded into this ImageView. + * @param imageLoader ImageLoader that will be used to make the request. + */ + public void setImageUrl(String url, ImageLoader imageLoader) { + mUrl = url; + mImageLoader = imageLoader; + // The URL has potentially changed. See if we need to load it. + loadImageIfNecessary(false); + } + + /** + * Sets the default image resource ID to be used for this view until the attempt to load it + * completes. + */ + public void setDefaultImageResId(int defaultImage) { + mDefaultImageId = defaultImage; + } + + /** + * Sets the error image resource ID to be used for this view in the event that the image + * requested fails to load. + */ + public void setErrorImageResId(int errorImage) { + mErrorImageId = errorImage; + } + + /** + * Loads the image for the view if it isn't already loaded. + * @param isInLayoutPass True if this was invoked from a layout pass, false otherwise. + */ + void loadImageIfNecessary(final boolean isInLayoutPass) { + int width = getWidth(); + int height = getHeight(); + ScaleType scaleType = getScaleType(); + + boolean wrapWidth = false, wrapHeight = false; + if (getLayoutParams() != null) { + wrapWidth = getLayoutParams().width == LayoutParams.WRAP_CONTENT; + wrapHeight = getLayoutParams().height == LayoutParams.WRAP_CONTENT; + } + + // if the view's bounds aren't known yet, and this is not a wrap-content/wrap-content + // view, hold off on loading the image. + boolean isFullyWrapContent = wrapWidth && wrapHeight; + if (width == 0 && height == 0 && !isFullyWrapContent) { + return; + } + + // if the URL to be loaded in this view is empty, cancel any old requests and clear the + // currently loaded image. + if (TextUtils.isEmpty(mUrl)) { + if (mImageContainer != null) { + mImageContainer.cancelRequest(); + mImageContainer = null; + } + setDefaultImageOrNull(); + return; + } + + // if there was an old request in this view, check if it needs to be canceled. + if (mImageContainer != null && mImageContainer.getRequestUrl() != null) { + if (mImageContainer.getRequestUrl().equals(mUrl)) { + // if the request is from the same URL, return. + return; + } else { + // if there is a pre-existing request, cancel it if it's fetching a different URL. + mImageContainer.cancelRequest(); + setDefaultImageOrNull(); + } + } + + // Calculate the max image width / height to use while ignoring WRAP_CONTENT dimens. + int maxWidth = wrapWidth ? 0 : width; + int maxHeight = wrapHeight ? 0 : height; + + // The pre-existing content of this view didn't match the current URL. Load the new image + // from the network. + ImageContainer newContainer = mImageLoader.get(mUrl, + new ImageListener() { + @Override + public void onErrorResponse(VolleyError error) { + if (mErrorImageId != 0) { + setImageResource(mErrorImageId); + } + } + + @Override + public void onResponse(final ImageContainer response, boolean isImmediate) { + // If this was an immediate response that was delivered inside of a layout + // pass do not set the image immediately as it will trigger a requestLayout + // inside of a layout. Instead, defer setting the image by posting back to + // the main thread. + if (isImmediate && isInLayoutPass) { + post(new Runnable() { + @Override + public void run() { + onResponse(response, false); + } + }); + return; + } + + if (response.getBitmap() != null) { + setImageBitmap(response.getBitmap()); + } else if (mDefaultImageId != 0) { + setImageResource(mDefaultImageId); + } + } + }, maxWidth, maxHeight, scaleType); + + // update the ImageContainer to be the new bitmap container. + mImageContainer = newContainer; + } + + private void setDefaultImageOrNull() { + if(mDefaultImageId != 0) { + setImageResource(mDefaultImageId); + } + else { + setImageBitmap(null); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + loadImageIfNecessary(true); + } + + @Override + protected void onDetachedFromWindow() { + if (mImageContainer != null) { + // If the view was bound to an image request, cancel it and clear + // out the image from the view. + mImageContainer.cancelRequest(); + setImageBitmap(null); + // also clear out the container so we can reload the image if necessary. + mImageContainer = null; + } + super.onDetachedFromWindow(); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + invalidate(); + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/NoCache.java b/volley/src/main/java/com/android/volley/toolbox/NoCache.java new file mode 100644 index 0000000..ab66254 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/NoCache.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Cache; + +/** + * A cache that doesn't. + */ +public class NoCache implements Cache { + @Override + public void clear() { + } + + @Override + public Entry get(String key) { + return null; + } + + @Override + public void put(String key, Entry entry) { + } + + @Override + public void invalidate(String key, boolean fullExpire) { + } + + @Override + public void remove(String key) { + } + + @Override + public void initialize() { + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java b/volley/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java new file mode 100644 index 0000000..9971566 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/PoolingByteArrayOutputStream.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * A variation of {@link java.io.ByteArrayOutputStream} that uses a pool of byte[] buffers instead + * of always allocating them fresh, saving on heap churn. + */ +public class PoolingByteArrayOutputStream extends ByteArrayOutputStream { + /** + * If the {@link #PoolingByteArrayOutputStream(ByteArrayPool)} constructor is called, this is + * the default size to which the underlying byte array is initialized. + */ + private static final int DEFAULT_SIZE = 256; + + private final ByteArrayPool mPool; + + /** + * Constructs a new PoolingByteArrayOutputStream with a default size. If more bytes are written + * to this instance, the underlying byte array will expand. + */ + public PoolingByteArrayOutputStream(ByteArrayPool pool) { + this(pool, DEFAULT_SIZE); + } + + /** + * Constructs a new {@code ByteArrayOutputStream} with a default size of {@code size} bytes. If + * more than {@code size} bytes are written to this instance, the underlying byte array will + * expand. + * + * @param size initial size for the underlying byte array. The value will be pinned to a default + * minimum size. + */ + public PoolingByteArrayOutputStream(ByteArrayPool pool, int size) { + mPool = pool; + buf = mPool.getBuf(Math.max(size, DEFAULT_SIZE)); + } + + @Override + public void close() throws IOException { + mPool.returnBuf(buf); + buf = null; + super.close(); + } + + @Override + public void finalize() { + mPool.returnBuf(buf); + } + + /** + * Ensures there is enough space in the buffer for the given number of additional bytes. + */ + private void expand(int i) { + /* Can the buffer handle @i more bytes, if not expand it */ + if (count + i <= buf.length) { + return; + } + byte[] newbuf = mPool.getBuf((count + i) * 2); + System.arraycopy(buf, 0, newbuf, 0, count); + mPool.returnBuf(buf); + buf = newbuf; + } + + @Override + public synchronized void write(byte[] buffer, int offset, int len) { + expand(len); + super.write(buffer, offset, len); + } + + @Override + public synchronized void write(int oneByte) { + expand(1); + super.write(oneByte); + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/RequestFuture.java b/volley/src/main/java/com/android/volley/toolbox/RequestFuture.java new file mode 100644 index 0000000..173c44c --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/RequestFuture.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.VolleyError; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * A Future that represents a Volley request. + * + * Used by providing as your response and error listeners. For example: + *
+ * RequestFuture<JSONObject> future = RequestFuture.newFuture();
+ * MyRequest request = new MyRequest(URL, future, future);
+ *
+ * // If you want to be able to cancel the request:
+ * future.setRequest(requestQueue.add(request));
+ *
+ * // Otherwise:
+ * requestQueue.add(request);
+ *
+ * try {
+ *   JSONObject response = future.get();
+ *   // do something with response
+ * } catch (InterruptedException e) {
+ *   // handle the error
+ * } catch (ExecutionException e) {
+ *   // handle the error
+ * }
+ * 
+ * + * @param The type of parsed response this future expects. + */ +public class RequestFuture implements Future, Response.Listener, + Response.ErrorListener { + private Request mRequest; + private boolean mResultReceived = false; + private T mResult; + private VolleyError mException; + + public static RequestFuture newFuture() { + return new RequestFuture(); + } + + private RequestFuture() {} + + public void setRequest(Request request) { + mRequest = request; + } + + @Override + public synchronized boolean cancel(boolean mayInterruptIfRunning) { + if (mRequest == null) { + return false; + } + + if (!isDone()) { + mRequest.cancel(); + return true; + } else { + return false; + } + } + + @Override + public T get() throws InterruptedException, ExecutionException { + try { + return doGet(null); + } catch (TimeoutException e) { + throw new AssertionError(e); + } + } + + @Override + public T get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return doGet(TimeUnit.MILLISECONDS.convert(timeout, unit)); + } + + private synchronized T doGet(Long timeoutMs) + throws InterruptedException, ExecutionException, TimeoutException { + if (mException != null) { + throw new ExecutionException(mException); + } + + if (mResultReceived) { + return mResult; + } + + if (timeoutMs == null) { + wait(0); + } else if (timeoutMs > 0) { + wait(timeoutMs); + } + + if (mException != null) { + throw new ExecutionException(mException); + } + + if (!mResultReceived) { + throw new TimeoutException(); + } + + return mResult; + } + + @Override + public boolean isCancelled() { + if (mRequest == null) { + return false; + } + return mRequest.isCanceled(); + } + + @Override + public synchronized boolean isDone() { + return mResultReceived || mException != null || isCancelled(); + } + + @Override + public synchronized void onResponse(T response) { + mResultReceived = true; + mResult = response; + notifyAll(); + } + + @Override + public synchronized void onErrorResponse(VolleyError error) { + mException = error; + notifyAll(); + } +} + diff --git a/volley/src/main/java/com/android/volley/toolbox/StringRequest.java b/volley/src/main/java/com/android/volley/toolbox/StringRequest.java new file mode 100644 index 0000000..6b3dfcf --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/StringRequest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.Response.Listener; + +import java.io.UnsupportedEncodingException; + +/** + * A canned request for retrieving the response body at a given URL as a String. + */ +public class StringRequest extends Request { + private final Listener mListener; + + /** + * Creates a new request with the given method. + * + * @param method the request {@link Method} to use + * @param url URL to fetch the string at + * @param listener Listener to receive the String response + * @param errorListener Error listener, or null to ignore errors + */ + public StringRequest(int method, String url, Listener listener, + ErrorListener errorListener) { + super(method, url, errorListener); + mListener = listener; + } + + /** + * Creates a new GET request. + * + * @param url URL to fetch the string at + * @param listener Listener to receive the String response + * @param errorListener Error listener, or null to ignore errors + */ + public StringRequest(String url, Listener listener, ErrorListener errorListener) { + this(Method.GET, url, listener, errorListener); + } + + @Override + protected void deliverResponse(String response) { + mListener.onResponse(response); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + String parsed; + try { + parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); + } catch (UnsupportedEncodingException e) { + parsed = new String(response.data); + } + return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); + } +} diff --git a/volley/src/main/java/com/android/volley/toolbox/Volley.java b/volley/src/main/java/com/android/volley/toolbox/Volley.java new file mode 100644 index 0000000..0e04e87 --- /dev/null +++ b/volley/src/main/java/com/android/volley/toolbox/Volley.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.http.AndroidHttpClient; +import android.os.Build; + +import com.android.volley.Network; +import com.android.volley.RequestQueue; + +import java.io.File; + +public class Volley { + + /** Default on-disk cache directory. */ + private static final String DEFAULT_CACHE_DIR = "volley"; + + /** + * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. + * + * @param context A {@link Context} to use for creating the cache dir. + * @param stack An {@link HttpStack} to use for the network, or null for default. + * @return A started {@link RequestQueue} instance. + */ + public static RequestQueue newRequestQueue(Context context, HttpStack stack) { + File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR); + + String userAgent = "volley/0"; + try { + String packageName = context.getPackageName(); + PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); + userAgent = packageName + "/" + info.versionCode; + } catch (NameNotFoundException e) { + } + + if (stack == null) { + if (Build.VERSION.SDK_INT >= 9) { + stack = new HurlStack(); + } else { + // Prior to Gingerbread, HttpUrlConnection was unreliable. + // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html + stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent)); + } + } + + Network network = new BasicNetwork(stack); + + RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network); + queue.start(); + + return queue; + } + + /** + * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. + * + * @param context A {@link Context} to use for creating the cache dir. + * @return A started {@link RequestQueue} instance. + */ + public static RequestQueue newRequestQueue(Context context) { + return newRequestQueue(context, null); + } +} diff --git a/volley/src/test/java/com/android/volley/CacheDispatcherTest.java b/volley/src/test/java/com/android/volley/CacheDispatcherTest.java new file mode 100644 index 0000000..42bdda0 --- /dev/null +++ b/volley/src/test/java/com/android/volley/CacheDispatcherTest.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import com.android.volley.mock.MockCache; +import com.android.volley.mock.MockRequest; +import com.android.volley.mock.MockResponseDelivery; +import com.android.volley.mock.WaitableQueue; +import com.android.volley.utils.CacheTestUtils; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +@SuppressWarnings("rawtypes") +public class CacheDispatcherTest { + private CacheDispatcher mDispatcher; + private WaitableQueue mCacheQueue; + private WaitableQueue mNetworkQueue; + private MockCache mCache; + private MockResponseDelivery mDelivery; + private MockRequest mRequest; + + private static final long TIMEOUT_MILLIS = 5000; + + @Before public void setUp() throws Exception { + mCacheQueue = new WaitableQueue(); + mNetworkQueue = new WaitableQueue(); + mCache = new MockCache(); + mDelivery = new MockResponseDelivery(); + + mRequest = new MockRequest(); + + mDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery); + mDispatcher.start(); + } + + @After public void tearDown() throws Exception { + mDispatcher.quit(); + mDispatcher.join(); + } + + // A cancelled request should not be processed at all. + @Test public void cancelledRequest() throws Exception { + mRequest.cancel(); + mCacheQueue.add(mRequest); + mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertFalse(mCache.getCalled); + assertFalse(mDelivery.wasEitherResponseCalled()); + } + + // A cache miss does not post a response and puts the request on the network queue. + @Test public void cacheMiss() throws Exception { + mCacheQueue.add(mRequest); + mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertFalse(mDelivery.wasEitherResponseCalled()); + assertTrue(mNetworkQueue.size() > 0); + Request request = mNetworkQueue.take(); + assertNull(request.getCacheEntry()); + } + + // A non-expired cache hit posts a response and does not queue to the network. + @Test public void nonExpiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, false); + mCache.setEntryToReturn(entry); + mCacheQueue.add(mRequest); + mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertTrue(mDelivery.postResponse_called); + assertFalse(mDelivery.postError_called); + } + + // A soft-expired cache hit posts a response and queues to the network. + @Test public void softExpiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, false, true); + mCache.setEntryToReturn(entry); + mCacheQueue.add(mRequest); + mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertTrue(mDelivery.postResponse_called); + assertFalse(mDelivery.postError_called); + assertTrue(mNetworkQueue.size() > 0); + Request request = mNetworkQueue.take(); + assertSame(entry, request.getCacheEntry()); + } + + // An expired cache hit does not post a response and queues to the network. + @Test public void expiredCacheHit() throws Exception { + Cache.Entry entry = CacheTestUtils.makeRandomCacheEntry(null, true, true); + mCache.setEntryToReturn(entry); + mCacheQueue.add(mRequest); + mCacheQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertFalse(mDelivery.wasEitherResponseCalled()); + assertTrue(mNetworkQueue.size() > 0); + Request request = mNetworkQueue.take(); + assertSame(entry, request.getCacheEntry()); + } +} diff --git a/volley/src/test/java/com/android/volley/NetworkDispatcherTest.java b/volley/src/test/java/com/android/volley/NetworkDispatcherTest.java new file mode 100644 index 0000000..c5763bd --- /dev/null +++ b/volley/src/test/java/com/android/volley/NetworkDispatcherTest.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import com.android.volley.mock.MockCache; +import com.android.volley.mock.MockNetwork; +import com.android.volley.mock.MockRequest; +import com.android.volley.mock.MockResponseDelivery; +import com.android.volley.mock.WaitableQueue; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Arrays; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class NetworkDispatcherTest { + private NetworkDispatcher mDispatcher; + private MockResponseDelivery mDelivery; + private WaitableQueue mNetworkQueue; + private MockNetwork mNetwork; + private MockCache mCache; + private MockRequest mRequest; + + private static final byte[] CANNED_DATA = "Ceci n'est pas une vraie reponse".getBytes(); + private static final long TIMEOUT_MILLIS = 5000; + + @Before public void setUp() throws Exception { + mDelivery = new MockResponseDelivery(); + mNetworkQueue = new WaitableQueue(); + mNetwork = new MockNetwork(); + mCache = new MockCache(); + mRequest = new MockRequest(); + mDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); + mDispatcher.start(); + } + + @After public void tearDown() throws Exception { + mDispatcher.quit(); + mDispatcher.join(); + } + + @Test public void successPostsResponse() throws Exception { + mNetwork.setDataToReturn(CANNED_DATA); + mNetwork.setNumExceptionsToThrow(0); + mNetworkQueue.add(mRequest); + mNetworkQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertFalse(mDelivery.postError_called); + assertTrue(mDelivery.postResponse_called); + Response response = mDelivery.responsePosted; + assertNotNull(response); + assertTrue(response.isSuccess()); + assertTrue(Arrays.equals((byte[])response.result, CANNED_DATA)); + } + + @Test public void exceptionPostsError() throws Exception { + mNetwork.setNumExceptionsToThrow(MockNetwork.ALWAYS_THROW_EXCEPTIONS); + mNetworkQueue.add(mRequest); + mNetworkQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertFalse(mDelivery.postResponse_called); + assertTrue(mDelivery.postError_called); + } + + @Test public void shouldCacheFalse() throws Exception { + mRequest.setShouldCache(false); + mNetworkQueue.add(mRequest); + mNetworkQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertFalse(mCache.putCalled); + } + + @Test public void shouldCacheTrue() throws Exception { + mNetwork.setDataToReturn(CANNED_DATA); + mRequest.setShouldCache(true); + mRequest.setCacheKey("bananaphone"); + mNetworkQueue.add(mRequest); + mNetworkQueue.waitUntilEmpty(TIMEOUT_MILLIS); + assertTrue(mCache.putCalled); + assertNotNull(mCache.entryPut); + assertTrue(Arrays.equals(mCache.entryPut.data, CANNED_DATA)); + assertEquals("bananaphone", mCache.keyPut); + } +} diff --git a/volley/src/test/java/com/android/volley/RequestQueueIntegrationTest.java b/volley/src/test/java/com/android/volley/RequestQueueIntegrationTest.java new file mode 100644 index 0000000..a73435c --- /dev/null +++ b/volley/src/test/java/com/android/volley/RequestQueueIntegrationTest.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import com.android.volley.Request.Priority; +import com.android.volley.RequestQueue.RequestFinishedListener; +import com.android.volley.mock.MockRequest; +import com.android.volley.mock.ShadowSystemClock; +import com.android.volley.toolbox.NoCache; +import com.android.volley.utils.ImmediateResponseDelivery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + + +/** + * Integration tests for {@link RequestQueue}, that verify its behavior in conjunction with real dispatcher, queues and + * Requests. Network is mocked out + */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowSystemClock.class}) +public class RequestQueueIntegrationTest { + + private ResponseDelivery mDelivery; + @Mock private Network mMockNetwork; + + @Before public void setUp() throws Exception { + mDelivery = new ImmediateResponseDelivery(); + initMocks(this); + } + + @Test public void add_requestProcessedInCorrectOrder() throws Exception { + // Enqueue 2 requests with different cache keys, and different priorities. The second, higher priority request + // takes 20ms. + // Assert that first request is only handled after the first one has been parsed and delivered. + MockRequest lowerPriorityReq = new MockRequest(); + MockRequest higherPriorityReq = new MockRequest(); + lowerPriorityReq.setCacheKey("1"); + higherPriorityReq.setCacheKey("2"); + lowerPriorityReq.setPriority(Priority.LOW); + higherPriorityReq.setPriority(Priority.HIGH); + + RequestFinishedListener listener = mock(RequestFinishedListener.class); + Answer delayAnswer = new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable { + Thread.sleep(20); + return mock(NetworkResponse.class); + } + }; + //delay only for higher request + when(mMockNetwork.performRequest(higherPriorityReq)).thenAnswer(delayAnswer); + when(mMockNetwork.performRequest(lowerPriorityReq)).thenReturn(mock(NetworkResponse.class)); + + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + queue.addRequestFinishedListener(listener); + queue.add(lowerPriorityReq); + queue.add(higherPriorityReq); + queue.start(); + + // you cannot do strict order verification in combination with timeouts with mockito 1.9.5 :( + // as an alternative, first verify no requests have finished, while higherPriorityReq should be processing + verifyNoMoreInteractions(listener); + // verify higherPriorityReq goes through first + verify(listener, timeout(100)).onRequestFinished(higherPriorityReq); + // verify lowerPriorityReq goes last + verify(listener, timeout(10)).onRequestFinished(lowerPriorityReq); + queue.stop(); + } + + /** + * Asserts that requests with same cache key are processed in order. + * + * Needs to be an integration test because relies on complex interations between various queues + */ + @Test public void add_dedupeByCacheKey() throws Exception { + // Enqueue 2 requests with the same cache key. The first request takes 20ms. Assert that the + // second request is only handled after the first one has been parsed and delivered. + Request req1 = new MockRequest(); + Request req2 = new MockRequest(); + RequestFinishedListener listener = mock(RequestFinishedListener.class); + Answer delayAnswer = new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable { + Thread.sleep(20); + return mock(NetworkResponse.class); + } + }; + //delay only for first + when(mMockNetwork.performRequest(req1)).thenAnswer(delayAnswer); + when(mMockNetwork.performRequest(req2)).thenReturn(mock(NetworkResponse.class)); + + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 3, mDelivery); + queue.addRequestFinishedListener(listener); + queue.add(req1); + queue.add(req2); + queue.start(); + + // you cannot do strict order verification with mockito 1.9.5 :( + // as an alternative, first verify no requests have finished, then verify req1 goes through + verifyNoMoreInteractions(listener); + verify(listener, timeout(100)).onRequestFinished(req1); + verify(listener, timeout(10)).onRequestFinished(req2); + queue.stop(); + } + + /** + * Verify RequestFinishedListeners are informed when requests are canceled + * + * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction + */ + @Test public void add_requestFinishedListenerCanceled() throws Exception { + RequestFinishedListener listener = mock(RequestFinishedListener.class); + Request request = new MockRequest(); + Answer delayAnswer = new Answer() { + @Override + public NetworkResponse answer(InvocationOnMock invocationOnMock) throws Throwable { + Thread.sleep(200); + return mock(NetworkResponse.class); + } + }; + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + when(mMockNetwork.performRequest(request)).thenAnswer(delayAnswer); + + queue.addRequestFinishedListener(listener); + queue.start(); + queue.add(request); + + request.cancel(); + verify(listener, timeout(100)).onRequestFinished(request); + queue.stop(); + } + + /** + * Verify RequestFinishedListeners are informed when requests are successfully delivered + * + * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction + */ + @Test public void add_requestFinishedListenerSuccess() throws Exception { + NetworkResponse response = mock(NetworkResponse.class); + Request request = new MockRequest(); + RequestFinishedListener listener = mock(RequestFinishedListener.class); + RequestFinishedListener listener2 = mock(RequestFinishedListener.class); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + queue.addRequestFinishedListener(listener); + queue.addRequestFinishedListener(listener2); + queue.start(); + queue.add(request); + + verify(listener, timeout(100)).onRequestFinished(request); + verify(listener2, timeout(100)).onRequestFinished(request); + + queue.stop(); + } + + /** + * Verify RequestFinishedListeners are informed when request errors + * + * Needs to be an integration test because relies on Request -> dispatcher -> RequestQueue interaction + */ + @Test public void add_requestFinishedListenerError() throws Exception { + RequestFinishedListener listener = mock(RequestFinishedListener.class); + Request request = new MockRequest(); + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 1, mDelivery); + + when(mMockNetwork.performRequest(request)).thenThrow(new VolleyError()); + + queue.addRequestFinishedListener(listener); + queue.start(); + queue.add(request); + + verify(listener, timeout(100)).onRequestFinished(request); + queue.stop(); + } + +} diff --git a/volley/src/test/java/com/android/volley/RequestQueueTest.java b/volley/src/test/java/com/android/volley/RequestQueueTest.java new file mode 100644 index 0000000..bcf3ff2 --- /dev/null +++ b/volley/src/test/java/com/android/volley/RequestQueueTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import com.android.volley.mock.ShadowSystemClock; +import com.android.volley.toolbox.NoCache; +import com.android.volley.utils.ImmediateResponseDelivery; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + +/** + * Unit tests for RequestQueue, with all dependencies mocked out + */ +@RunWith(RobolectricTestRunner.class) +@Config(shadows = {ShadowSystemClock.class}) +public class RequestQueueTest { + + private ResponseDelivery mDelivery; + @Mock private Network mMockNetwork; + + @Before public void setUp() throws Exception { + mDelivery = new ImmediateResponseDelivery(); + initMocks(this); + } + + @Test public void cancelAll_onlyCorrectTag() throws Exception { + RequestQueue queue = new RequestQueue(new NoCache(), mMockNetwork, 0, mDelivery); + Object tagA = new Object(); + Object tagB = new Object(); + Request req1 = mock(Request.class); + when(req1.getTag()).thenReturn(tagA); + Request req2 = mock(Request.class); + when(req2.getTag()).thenReturn(tagB); + Request req3 = mock(Request.class); + when(req3.getTag()).thenReturn(tagA); + Request req4 = mock(Request.class); + when(req4.getTag()).thenReturn(tagA); + + queue.add(req1); // A + queue.add(req2); // B + queue.add(req3); // A + queue.cancelAll(tagA); + queue.add(req4); // A + + verify(req1).cancel(); // A cancelled + verify(req3).cancel(); // A cancelled + verify(req2, never()).cancel(); // B not cancelled + verify(req4, never()).cancel(); // A added after cancel not cancelled + } +} diff --git a/volley/src/test/java/com/android/volley/RequestTest.java b/volley/src/test/java/com/android/volley/RequestTest.java new file mode 100644 index 0000000..d5beca5 --- /dev/null +++ b/volley/src/test/java/com/android/volley/RequestTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import com.android.volley.Request.Priority; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class RequestTest { + + @Test public void compareTo() { + int sequence = 0; + TestRequest low = new TestRequest(Priority.LOW); + low.setSequence(sequence++); + TestRequest low2 = new TestRequest(Priority.LOW); + low2.setSequence(sequence++); + TestRequest high = new TestRequest(Priority.HIGH); + high.setSequence(sequence++); + TestRequest immediate = new TestRequest(Priority.IMMEDIATE); + immediate.setSequence(sequence++); + + // "Low" should sort higher because it's really processing order. + assertTrue(low.compareTo(high) > 0); + assertTrue(high.compareTo(low) < 0); + assertTrue(low.compareTo(low2) < 0); + assertTrue(low.compareTo(immediate) > 0); + assertTrue(immediate.compareTo(high) < 0); + } + + private class TestRequest extends Request { + private Priority mPriority = Priority.NORMAL; + public TestRequest(Priority priority) { + super(Request.Method.GET, "", null); + mPriority = priority; + } + + @Override + public Priority getPriority() { + return mPriority; + } + + @Override + protected void deliverResponse(Object response) { + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + } + + @Test public void urlParsing() { + UrlParseRequest nullUrl = new UrlParseRequest(null); + assertEquals(0, nullUrl.getTrafficStatsTag()); + UrlParseRequest emptyUrl = new UrlParseRequest(""); + assertEquals(0, emptyUrl.getTrafficStatsTag()); + UrlParseRequest noHost = new UrlParseRequest("http:///"); + assertEquals(0, noHost.getTrafficStatsTag()); + UrlParseRequest badProtocol = new UrlParseRequest("bad:http://foo"); + assertEquals(0, badProtocol.getTrafficStatsTag()); + UrlParseRequest goodProtocol = new UrlParseRequest("http://foo"); + assertFalse(0 == goodProtocol.getTrafficStatsTag()); + } + + private class UrlParseRequest extends Request { + public UrlParseRequest(String url) { + super(Request.Method.GET, url, null); + } + + @Override + protected void deliverResponse(Object response) { + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + } +} diff --git a/volley/src/test/java/com/android/volley/ResponseDeliveryTest.java b/volley/src/test/java/com/android/volley/ResponseDeliveryTest.java new file mode 100644 index 0000000..9fadfc3 --- /dev/null +++ b/volley/src/test/java/com/android/volley/ResponseDeliveryTest.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley; + +import com.android.volley.mock.MockRequest; +import com.android.volley.utils.CacheTestUtils; +import com.android.volley.utils.ImmediateResponseDelivery; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class ResponseDeliveryTest { + + private ExecutorDelivery mDelivery; + private MockRequest mRequest; + private Response mSuccessResponse; + + @Before public void setUp() throws Exception { + // Make the delivery just run its posted responses immediately. + mDelivery = new ImmediateResponseDelivery(); + mRequest = new MockRequest(); + mRequest.setSequence(1); + byte[] data = new byte[16]; + Cache.Entry cacheEntry = CacheTestUtils.makeRandomCacheEntry(data); + mSuccessResponse = Response.success(data, cacheEntry); + } + + @Test public void postResponseCallsDeliverResponse() { + mDelivery.postResponse(mRequest, mSuccessResponse); + assertTrue(mRequest.deliverResponse_called); + assertFalse(mRequest.deliverError_called); + } + + @Test public void postResponseSuppressesCanceled() { + mRequest.cancel(); + mDelivery.postResponse(mRequest, mSuccessResponse); + assertFalse(mRequest.deliverResponse_called); + assertFalse(mRequest.deliverError_called); + } + + @Test public void postErrorCallsDeliverError() { + Response errorResponse = Response.error(new ServerError()); + + mDelivery.postResponse(mRequest, errorResponse); + assertTrue(mRequest.deliverError_called); + assertFalse(mRequest.deliverResponse_called); + } +} diff --git a/volley/src/test/java/com/android/volley/mock/MockCache.java b/volley/src/test/java/com/android/volley/mock/MockCache.java new file mode 100644 index 0000000..85a4607 --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/MockCache.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.Cache; + +public class MockCache implements Cache { + + public boolean clearCalled = false; + @Override + public void clear() { + clearCalled = true; + } + + public boolean getCalled = false; + private Entry mFakeEntry = null; + + public void setEntryToReturn(Entry entry) { + mFakeEntry = entry; + } + + @Override + public Entry get(String key) { + getCalled = true; + return mFakeEntry; + } + + public boolean putCalled = false; + public String keyPut = null; + public Entry entryPut = null; + + @Override + public void put(String key, Entry entry) { + putCalled = true; + keyPut = key; + entryPut = entry; + } + + @Override + public void invalidate(String key, boolean fullExpire) { + } + + @Override + public void remove(String key) { + } + + @Override + public void initialize() { + } + +} diff --git a/volley/src/test/java/com/android/volley/mock/MockHttpClient.java b/volley/src/test/java/com/android/volley/mock/MockHttpClient.java new file mode 100644 index 0000000..c2a36bc --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/MockHttpClient.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.message.BasicHttpResponse; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.params.HttpParams; +import org.apache.http.protocol.HttpContext; + + +public class MockHttpClient implements HttpClient { + private int mStatusCode = HttpStatus.SC_OK; + private HttpEntity mResponseEntity = null; + + public void setResponseData(HttpEntity entity) { + mStatusCode = HttpStatus.SC_OK; + mResponseEntity = entity; + } + + public void setErrorCode(int statusCode) { + if (statusCode == HttpStatus.SC_OK) { + throw new IllegalArgumentException("statusCode cannot be 200 for an error"); + } + mStatusCode = statusCode; + } + + public HttpUriRequest requestExecuted = null; + + // This is the only one we actually use. + @Override + public HttpResponse execute(HttpUriRequest request, HttpContext context) { + requestExecuted = request; + StatusLine statusLine = new BasicStatusLine( + new ProtocolVersion("HTTP", 1, 1), mStatusCode, ""); + HttpResponse response = new BasicHttpResponse(statusLine); + response.setEntity(mResponseEntity); + + return response; + } + + + // Unimplemented methods ahoy + + @Override + public HttpResponse execute(HttpUriRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpResponse execute(HttpHost target, HttpRequest request) { + throw new UnsupportedOperationException(); + } + + @Override + public T execute(HttpUriRequest arg0, ResponseHandler arg1) { + throw new UnsupportedOperationException(); + } + + @Override + public HttpResponse execute(HttpHost target, HttpRequest request, HttpContext context) { + throw new UnsupportedOperationException(); + } + + @Override + public T execute(HttpUriRequest arg0, ResponseHandler arg1, HttpContext arg2) { + throw new UnsupportedOperationException(); + } + + @Override + public T execute(HttpHost arg0, HttpRequest arg1, ResponseHandler arg2) { + throw new UnsupportedOperationException(); + } + + @Override + public T execute(HttpHost arg0, HttpRequest arg1, ResponseHandler arg2, + HttpContext arg3) { + throw new UnsupportedOperationException(); + } + + @Override + public ClientConnectionManager getConnectionManager() { + throw new UnsupportedOperationException(); + } + + @Override + public HttpParams getParams() { + throw new UnsupportedOperationException(); + } +} diff --git a/volley/src/test/java/com/android/volley/mock/MockHttpStack.java b/volley/src/test/java/com/android/volley/mock/MockHttpStack.java new file mode 100644 index 0000000..91872d3 --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/MockHttpStack.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.toolbox.HttpStack; + +import org.apache.http.HttpResponse; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class MockHttpStack implements HttpStack { + + private HttpResponse mResponseToReturn; + + private IOException mExceptionToThrow; + + private String mLastUrl; + + private Map mLastHeaders; + + private byte[] mLastPostBody; + + public String getLastUrl() { + return mLastUrl; + } + + public Map getLastHeaders() { + return mLastHeaders; + } + + public byte[] getLastPostBody() { + return mLastPostBody; + } + + public void setResponseToReturn(HttpResponse response) { + mResponseToReturn = response; + } + + public void setExceptionToThrow(IOException exception) { + mExceptionToThrow = exception; + } + + @Override + public HttpResponse performRequest(Request request, Map additionalHeaders) + throws IOException, AuthFailureError { + if (mExceptionToThrow != null) { + throw mExceptionToThrow; + } + mLastUrl = request.getUrl(); + mLastHeaders = new HashMap(); + if (request.getHeaders() != null) { + mLastHeaders.putAll(request.getHeaders()); + } + if (additionalHeaders != null) { + mLastHeaders.putAll(additionalHeaders); + } + try { + mLastPostBody = request.getBody(); + } catch (AuthFailureError e) { + mLastPostBody = null; + } + return mResponseToReturn; + } +} diff --git a/volley/src/test/java/com/android/volley/mock/MockHttpURLConnection.java b/volley/src/test/java/com/android/volley/mock/MockHttpURLConnection.java new file mode 100644 index 0000000..efa3a21 --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/MockHttpURLConnection.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +public class MockHttpURLConnection extends HttpURLConnection { + + private boolean mDoOutput; + private String mRequestMethod; + private OutputStream mOutputStream; + + public MockHttpURLConnection() throws MalformedURLException { + super(new URL("http://foo.com")); + mDoOutput = false; + mRequestMethod = "GET"; + mOutputStream = new ByteArrayOutputStream(); + } + + @Override + public void setDoOutput(boolean flag) { + mDoOutput = flag; + } + + @Override + public boolean getDoOutput() { + return mDoOutput; + } + + @Override + public void setRequestMethod(String method) { + mRequestMethod = method; + } + + @Override + public String getRequestMethod() { + return mRequestMethod; + } + + @Override + public OutputStream getOutputStream() { + return mOutputStream; + } + + @Override + public void disconnect() { + } + + @Override + public boolean usingProxy() { + return false; + } + + @Override + public void connect() throws IOException { + } + +} diff --git a/volley/src/test/java/com/android/volley/mock/MockNetwork.java b/volley/src/test/java/com/android/volley/mock/MockNetwork.java new file mode 100644 index 0000000..207ec63 --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/MockNetwork.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.Network; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.ServerError; +import com.android.volley.VolleyError; + +public class MockNetwork implements Network { + public final static int ALWAYS_THROW_EXCEPTIONS = -1; + + private int mNumExceptionsToThrow = 0; + private byte[] mDataToReturn = null; + + /** + * @param numExceptionsToThrow number of times to throw an exception or + * {@link #ALWAYS_THROW_EXCEPTIONS} + */ + public void setNumExceptionsToThrow(int numExceptionsToThrow) { + mNumExceptionsToThrow = numExceptionsToThrow; + } + + public void setDataToReturn(byte[] data) { + mDataToReturn = data; + } + + public Request requestHandled = null; + + @Override + public NetworkResponse performRequest(Request request) throws VolleyError { + if (mNumExceptionsToThrow > 0 || mNumExceptionsToThrow == ALWAYS_THROW_EXCEPTIONS) { + if (mNumExceptionsToThrow != ALWAYS_THROW_EXCEPTIONS) { + mNumExceptionsToThrow--; + } + throw new ServerError(); + } + + requestHandled = request; + return new NetworkResponse(mDataToReturn); + } + +} diff --git a/volley/src/test/java/com/android/volley/mock/MockRequest.java b/volley/src/test/java/com/android/volley/mock/MockRequest.java new file mode 100644 index 0000000..9815ea8 --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/MockRequest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.Response.ErrorListener; +import com.android.volley.VolleyError; +import com.android.volley.utils.CacheTestUtils; + +import java.util.HashMap; +import java.util.Map; + +public class MockRequest extends Request { + public MockRequest() { + super(Request.Method.GET, "http://foo.com", null); + } + + public MockRequest(String url, ErrorListener listener) { + super(Request.Method.GET, url, listener); + } + + private Map mPostParams = new HashMap(); + + public void setPostParams(Map postParams) { + mPostParams = postParams; + } + + @Override + public Map getPostParams() { + return mPostParams; + } + + private String mCacheKey = super.getCacheKey(); + + public void setCacheKey(String cacheKey) { + mCacheKey = cacheKey; + } + + @Override + public String getCacheKey() { + return mCacheKey; + } + + public boolean deliverResponse_called = false; + public boolean parseResponse_called = false; + + @Override + protected void deliverResponse(byte[] response) { + deliverResponse_called = true; + } + + public boolean deliverError_called = false; + + @Override + public void deliverError(VolleyError error) { + super.deliverError(error); + deliverError_called = true; + } + + public boolean cancel_called = false; + + @Override + public void cancel() { + cancel_called = true; + super.cancel(); + } + + private Priority mPriority = super.getPriority(); + + public void setPriority(Priority priority) { + mPriority = priority; + } + + @Override + public Priority getPriority() { + return mPriority; + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + parseResponse_called = true; + return Response.success(response.data, CacheTestUtils.makeRandomCacheEntry(response.data)); + } + +} diff --git a/volley/src/test/java/com/android/volley/mock/MockResponseDelivery.java b/volley/src/test/java/com/android/volley/mock/MockResponseDelivery.java new file mode 100644 index 0000000..4dbfd5c --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/MockResponseDelivery.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.ResponseDelivery; +import com.android.volley.VolleyError; + +public class MockResponseDelivery implements ResponseDelivery { + + public boolean postResponse_called = false; + public boolean postError_called = false; + + public boolean wasEitherResponseCalled() { + return postResponse_called || postError_called; + } + + public Response responsePosted = null; + @Override + public void postResponse(Request request, Response response) { + postResponse_called = true; + responsePosted = response; + } + + @Override + public void postResponse(Request request, Response response, Runnable runnable) { + postResponse_called = true; + responsePosted = response; + runnable.run(); + } + + @Override + public void postError(Request request, VolleyError error) { + postError_called = true; + } +} diff --git a/volley/src/test/java/com/android/volley/mock/ShadowSystemClock.java b/volley/src/test/java/com/android/volley/mock/ShadowSystemClock.java new file mode 100644 index 0000000..f2697cc --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/ShadowSystemClock.java @@ -0,0 +1,11 @@ +package com.android.volley.mock; + +import android.os.SystemClock; +import org.robolectric.annotation.Implements; + +@Implements(value = SystemClock.class, callThroughByDefault = true) +public class ShadowSystemClock { + public static long elapsedRealtime() { + return 0; + } +} diff --git a/volley/src/test/java/com/android/volley/mock/TestRequest.java b/volley/src/test/java/com/android/volley/mock/TestRequest.java new file mode 100644 index 0000000..dfc4dc1 --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/TestRequest.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; + +import java.util.HashMap; +import java.util.Map; + +public class TestRequest { + private static final String TEST_URL = "http://foo.com"; + + /** Base Request class for testing allowing both the deprecated and new constructor. */ + private static class Base extends Request { + @SuppressWarnings("deprecation") + public Base(String url, Response.ErrorListener listener) { + super(url, listener); + } + + public Base(int method, String url, Response.ErrorListener listener) { + super(method, url, listener); + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(byte[] response) { + } + } + + /** Test example of a GET request in the deprecated style. */ + public static class DeprecatedGet extends Base { + public DeprecatedGet() { + super(TEST_URL, null); + } + } + + /** Test example of a POST request in the deprecated style. */ + public static class DeprecatedPost extends Base { + private Map mPostParams; + + public DeprecatedPost() { + super(TEST_URL, null); + mPostParams = new HashMap(); + mPostParams.put("requestpost", "foo"); + } + + @Override + protected Map getPostParams() { + return mPostParams; + } + } + + /** Test example of a GET request in the new style. */ + public static class Get extends Base { + public Get() { + super(Method.GET, TEST_URL, null); + } + } + + /** + * Test example of a POST request in the new style. In the new style, it is possible + * to have a POST with no body. + */ + public static class Post extends Base { + public Post() { + super(Method.POST, TEST_URL, null); + } + } + + /** Test example of a POST request in the new style with a body. */ + public static class PostWithBody extends Post { + private Map mParams; + + public PostWithBody() { + mParams = new HashMap(); + mParams.put("testKey", "testValue"); + } + + @Override + public Map getParams() { + return mParams; + } + } + + /** + * Test example of a PUT request in the new style. In the new style, it is possible to have a + * PUT with no body. + */ + public static class Put extends Base { + public Put() { + super(Method.PUT, TEST_URL, null); + } + } + + /** Test example of a PUT request in the new style with a body. */ + public static class PutWithBody extends Put { + private Map mParams = new HashMap(); + + public PutWithBody() { + mParams = new HashMap(); + mParams.put("testKey", "testValue"); + } + + @Override + public Map getParams() { + return mParams; + } + } + + /** Test example of a DELETE request in the new style. */ + public static class Delete extends Base { + public Delete() { + super(Method.DELETE, TEST_URL, null); + } + } + + /** Test example of a HEAD request in the new style. */ + public static class Head extends Base { + public Head() { + super(Method.HEAD, TEST_URL, null); + } + } + + /** Test example of a OPTIONS request in the new style. */ + public static class Options extends Base { + public Options() { + super(Method.OPTIONS, TEST_URL, null); + } + } + + /** Test example of a TRACE request in the new style. */ + public static class Trace extends Base { + public Trace() { + super(Method.TRACE, TEST_URL, null); + } + } + + /** Test example of a PATCH request in the new style. */ + public static class Patch extends Base { + public Patch() { + super(Method.PATCH, TEST_URL, null); + } + } + + /** Test example of a PATCH request in the new style with a body. */ + public static class PatchWithBody extends Patch { + private Map mParams = new HashMap(); + + public PatchWithBody() { + mParams = new HashMap(); + mParams.put("testKey", "testValue"); + } + + @Override + public Map getParams() { + return mParams; + } + } +} diff --git a/volley/src/test/java/com/android/volley/mock/WaitableQueue.java b/volley/src/test/java/com/android/volley/mock/WaitableQueue.java new file mode 100644 index 0000000..079bbf5 --- /dev/null +++ b/volley/src/test/java/com/android/volley/mock/WaitableQueue.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.mock; + +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; + +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +// TODO: the name of this class sucks +@SuppressWarnings("serial") +public class WaitableQueue extends PriorityBlockingQueue> { + private final Request mStopRequest = new MagicStopRequest(); + private final Semaphore mStopEvent = new Semaphore(0); + + // TODO: this isn't really "until empty" it's "until next call to take() after empty" + public void waitUntilEmpty(long timeoutMillis) + throws TimeoutException, InterruptedException { + add(mStopRequest); + if (!mStopEvent.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS)) { + throw new TimeoutException(); + } + } + + @Override + public Request take() throws InterruptedException { + Request item = super.take(); + if (item == mStopRequest) { + mStopEvent.release(); + return take(); + } + return item; + } + + private static class MagicStopRequest extends Request { + public MagicStopRequest() { + super(Request.Method.GET, "", null); + } + + @Override + public Priority getPriority() { + return Priority.LOW; + } + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(Object response) { + } + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java b/volley/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java new file mode 100644 index 0000000..e878658 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/AndroidAuthenticatorTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import com.android.volley.AuthFailureError; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; + +import static org.mockito.Mockito.*; + +@RunWith(RobolectricTestRunner.class) +public class AndroidAuthenticatorTest { + private AccountManager mAccountManager; + private Account mAccount; + private AccountManagerFuture mFuture; + private AndroidAuthenticator mAuthenticator; + + @Before + public void setUp() { + mAccountManager = mock(AccountManager.class); + mFuture = mock(AccountManagerFuture.class); + mAccount = new Account("coolperson", "cooltype"); + mAuthenticator = new AndroidAuthenticator(mAccountManager, mAccount, "cooltype", false); + } + + @Test(expected = AuthFailureError.class) + public void failedGetAuthToken() throws Exception { + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)).thenReturn(mFuture); + when(mFuture.getResult()).thenThrow(new AuthenticatorException("sadness!")); + mAuthenticator.getAuthToken(); + } + + @Test(expected = AuthFailureError.class) + public void resultContainsIntent() throws Exception { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + bundle.putParcelable(AccountManager.KEY_INTENT, intent); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)).thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + mAuthenticator.getAuthToken(); + } + + @Test(expected = AuthFailureError.class) + public void missingAuthToken() throws Exception { + Bundle bundle = new Bundle(); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)).thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + mAuthenticator.getAuthToken(); + } + + @Test + public void invalidateAuthToken() throws Exception { + mAuthenticator.invalidateAuthToken("monkey"); + verify(mAccountManager).invalidateAuthToken("cooltype", "monkey"); + } + + @Test + public void goodToken() throws Exception { + Bundle bundle = new Bundle(); + bundle.putString(AccountManager.KEY_AUTHTOKEN, "monkey"); + when(mAccountManager.getAuthToken(mAccount, "cooltype", false, null, null)).thenReturn(mFuture); + when(mFuture.getResult()).thenReturn(bundle); + when(mFuture.isDone()).thenReturn(true); + when(mFuture.isCancelled()).thenReturn(false); + Assert.assertEquals("monkey", mAuthenticator.getAuthToken()); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + Context context = mock(Context.class); + new AndroidAuthenticator(context, mAccount, "cooltype"); + new AndroidAuthenticator(context, mAccount, "cooltype", true); + Assert.assertSame(mAccount, mAuthenticator.getAccount()); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java b/volley/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java new file mode 100644 index 0000000..c01d9b0 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/BasicNetworkTest.java @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.AuthFailureError; +import com.android.volley.NetworkResponse; +import com.android.volley.Request; +import com.android.volley.Response; +import com.android.volley.RetryPolicy; +import com.android.volley.ServerError; +import com.android.volley.TimeoutError; +import com.android.volley.VolleyError; +import com.android.volley.mock.MockHttpStack; + +import org.apache.http.ProtocolVersion; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.entity.StringEntity; +import org.apache.http.message.BasicHttpResponse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.robolectric.RobolectricTestRunner; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + +@RunWith(RobolectricTestRunner.class) +public class BasicNetworkTest { + + @Mock private Request mMockRequest; + @Mock private RetryPolicy mMockRetryPolicy; + private BasicNetwork mNetwork; + + @Before public void setUp() throws Exception { + initMocks(this); + } + + @Test public void headersAndPostParams() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), + 200, "OK"); + fakeResponse.setEntity(new StringEntity("foobar")); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + httpNetwork.performRequest(request); + assertEquals("foo", mockHttpStack.getLastHeaders().get("requestheader")); + assertEquals("requestpost=foo&", new String(mockHttpStack.getLastPostBody())); + } + + @Test public void socketTimeout() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new SocketTimeoutException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry socket timeouts + verify(mMockRetryPolicy).retry(any(TimeoutError.class)); + } + + @Test public void connectTimeout() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new ConnectTimeoutException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry connection timeouts + verify(mMockRetryPolicy).retry(any(TimeoutError.class)); + } + + @Test public void noConnection() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + mockHttpStack.setExceptionToThrow(new IOException()); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry when there is no connection + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + } + + @Test public void unauthorized() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), + 401, "Unauthorized"); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + } + + @Test public void forbidden() throws Exception { + MockHttpStack mockHttpStack = new MockHttpStack(); + BasicHttpResponse fakeResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), + 403, "Forbidden"); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry in case it's an auth failure. + verify(mMockRetryPolicy).retry(any(AuthFailureError.class)); + } + + @Test public void redirect() throws Exception { + for (int i = 300; i <= 399; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + BasicHttpResponse fakeResponse = + new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, ""); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry 300 responses. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + @Test public void otherClientError() throws Exception { + for (int i = 400; i <= 499; i++) { + if (i == 401 || i == 403) { + // covered above. + continue; + } + MockHttpStack mockHttpStack = new MockHttpStack(); + BasicHttpResponse fakeResponse = + new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, ""); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry other 400 errors. + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + @Test public void serverError_enableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + BasicHttpResponse fakeResponse = + new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, ""); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = + new BasicNetwork(mockHttpStack, new ByteArrayPool(4096)); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + request.setShouldRetryServerErrors(true); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should retry all 500 errors + verify(mMockRetryPolicy).retry(any(ServerError.class)); + reset(mMockRetryPolicy); + } + } + + @Test public void serverError_disableRetries() throws Exception { + for (int i = 500; i <= 599; i++) { + MockHttpStack mockHttpStack = new MockHttpStack(); + BasicHttpResponse fakeResponse = + new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), i, ""); + mockHttpStack.setResponseToReturn(fakeResponse); + BasicNetwork httpNetwork = new BasicNetwork(mockHttpStack); + Request request = buildRequest(); + request.setRetryPolicy(mMockRetryPolicy); + doThrow(new VolleyError()).when(mMockRetryPolicy).retry(any(VolleyError.class)); + try { + httpNetwork.performRequest(request); + } catch (VolleyError e) { + // expected + } + // should not retry any 500 error w/ HTTP 500 retries turned off (the default). + verify(mMockRetryPolicy, never()).retry(any(VolleyError.class)); + reset(mMockRetryPolicy); + } + } + + private static Request buildRequest() { + return new Request(Request.Method.GET, "http://foo", null) { + + @Override + protected Response parseNetworkResponse(NetworkResponse response) { + return null; + } + + @Override + protected void deliverResponse(String response) { + } + + @Override + public Map getHeaders() { + Map result = new HashMap(); + result.put("requestheader", "foo"); + return result; + } + + @Override + public Map getParams() { + Map result = new HashMap(); + result.put("requestpost", "foo"); + return result; + } + }; + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java b/volley/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java new file mode 100644 index 0000000..661e994 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/ByteArrayPoolTest.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +public class ByteArrayPoolTest { + @Test public void reusesBuffer() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + byte[] buf2 = pool.getBuf(16); + + pool.returnBuf(buf1); + pool.returnBuf(buf2); + + byte[] buf3 = pool.getBuf(16); + byte[] buf4 = pool.getBuf(16); + assertTrue(buf3 == buf1 || buf3 == buf2); + assertTrue(buf4 == buf1 || buf4 == buf2); + assertTrue(buf3 != buf4); + } + + @Test public void obeysSizeLimit() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + byte[] buf2 = pool.getBuf(16); + byte[] buf3 = pool.getBuf(16); + + pool.returnBuf(buf1); + pool.returnBuf(buf2); + pool.returnBuf(buf3); + + byte[] buf4 = pool.getBuf(16); + byte[] buf5 = pool.getBuf(16); + byte[] buf6 = pool.getBuf(16); + + assertTrue(buf4 == buf2 || buf4 == buf3); + assertTrue(buf5 == buf2 || buf5 == buf3); + assertTrue(buf4 != buf5); + assertTrue(buf6 != buf1 && buf6 != buf2 && buf6 != buf3); + } + + @Test public void returnsBufferWithRightSize() { + ByteArrayPool pool = new ByteArrayPool(32); + + byte[] buf1 = pool.getBuf(16); + pool.returnBuf(buf1); + + byte[] buf2 = pool.getBuf(17); + assertNotSame(buf2, buf1); + + byte[] buf3 = pool.getBuf(15); + assertSame(buf3, buf1); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/CacheTest.java b/volley/src/test/java/com/android/volley/toolbox/CacheTest.java new file mode 100644 index 0000000..dcd8a27 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/CacheTest.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Cache; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +public class CacheTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(Cache.class.getMethod("get", String.class)); + assertNotNull(Cache.class.getMethod("put", String.class, Cache.Entry.class)); + assertNotNull(Cache.class.getMethod("initialize")); + assertNotNull(Cache.class.getMethod("invalidate", String.class, boolean.class)); + assertNotNull(Cache.class.getMethod("remove", String.class)); + assertNotNull(Cache.class.getMethod("clear")); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java b/volley/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java new file mode 100644 index 0000000..0a8be77 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/DiskBasedCacheTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Cache; +import com.android.volley.toolbox.DiskBasedCache.CacheHeader; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +public class DiskBasedCacheTest { + + // Simple end-to-end serialize/deserialize test. + @Test public void cacheHeaderSerialization() throws Exception { + Cache.Entry e = new Cache.Entry(); + e.data = new byte[8]; + e.serverDate = 1234567L; + e.lastModified = 13572468L; + e.ttl = 9876543L; + e.softTtl = 8765432L; + e.etag = "etag"; + e.responseHeaders = new HashMap(); + e.responseHeaders.put("fruit", "banana"); + + CacheHeader first = new CacheHeader("my-magical-key", e); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + first.writeHeader(baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + CacheHeader second = CacheHeader.readHeader(bais); + + assertEquals(first.key, second.key); + assertEquals(first.serverDate, second.serverDate); + assertEquals(first.lastModified, second.lastModified); + assertEquals(first.ttl, second.ttl); + assertEquals(first.softTtl, second.softTtl); + assertEquals(first.etag, second.etag); + assertEquals(first.responseHeaders, second.responseHeaders); + } + + @Test public void serializeInt() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeInt(baos, 0); + DiskBasedCache.writeInt(baos, 19791214); + DiskBasedCache.writeInt(baos, -20050711); + DiskBasedCache.writeInt(baos, Integer.MIN_VALUE); + DiskBasedCache.writeInt(baos, Integer.MAX_VALUE); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + assertEquals(DiskBasedCache.readInt(bais), 0); + assertEquals(DiskBasedCache.readInt(bais), 19791214); + assertEquals(DiskBasedCache.readInt(bais), -20050711); + assertEquals(DiskBasedCache.readInt(bais), Integer.MIN_VALUE); + assertEquals(DiskBasedCache.readInt(bais), Integer.MAX_VALUE); + } + + @Test public void serializeLong() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeLong(baos, 0); + DiskBasedCache.writeLong(baos, 31337); + DiskBasedCache.writeLong(baos, -4160); + DiskBasedCache.writeLong(baos, 4295032832L); + DiskBasedCache.writeLong(baos, -4314824046L); + DiskBasedCache.writeLong(baos, Long.MIN_VALUE); + DiskBasedCache.writeLong(baos, Long.MAX_VALUE); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + assertEquals(DiskBasedCache.readLong(bais), 0); + assertEquals(DiskBasedCache.readLong(bais), 31337); + assertEquals(DiskBasedCache.readLong(bais), -4160); + assertEquals(DiskBasedCache.readLong(bais), 4295032832L); + assertEquals(DiskBasedCache.readLong(bais), -4314824046L); + assertEquals(DiskBasedCache.readLong(bais), Long.MIN_VALUE); + assertEquals(DiskBasedCache.readLong(bais), Long.MAX_VALUE); + } + + @Test public void serializeString() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DiskBasedCache.writeString(baos, ""); + DiskBasedCache.writeString(baos, "This is a string."); + DiskBasedCache.writeString(baos, "ファイカス"); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + assertEquals(DiskBasedCache.readString(bais), ""); + assertEquals(DiskBasedCache.readString(bais), "This is a string."); + assertEquals(DiskBasedCache.readString(bais), "ファイカス"); + } + + @Test public void serializeMap() throws Exception { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + Map empty = new HashMap(); + DiskBasedCache.writeStringStringMap(empty, baos); + DiskBasedCache.writeStringStringMap(null, baos); + Map twoThings = new HashMap(); + twoThings.put("first", "thing"); + twoThings.put("second", "item"); + DiskBasedCache.writeStringStringMap(twoThings, baos); + Map emptyKey = new HashMap(); + emptyKey.put("", "value"); + DiskBasedCache.writeStringStringMap(emptyKey, baos); + Map emptyValue = new HashMap(); + emptyValue.put("key", ""); + DiskBasedCache.writeStringStringMap(emptyValue, baos); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + assertEquals(DiskBasedCache.readStringStringMap(bais), empty); + assertEquals(DiskBasedCache.readStringStringMap(bais), empty); // null reads back empty + assertEquals(DiskBasedCache.readStringStringMap(bais), twoThings); + assertEquals(DiskBasedCache.readStringStringMap(bais), emptyKey); + assertEquals(DiskBasedCache.readStringStringMap(bais), emptyValue); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(DiskBasedCache.class.getConstructor(File.class, int.class)); + assertNotNull(DiskBasedCache.class.getConstructor(File.class)); + + assertNotNull(DiskBasedCache.class.getMethod("getFileForKey", String.class)); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java b/volley/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java new file mode 100644 index 0000000..0c417d4 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/HttpClientStackTest.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Request.Method; +import com.android.volley.mock.TestRequest; +import com.android.volley.toolbox.HttpClientStack.HttpPatch; + +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.client.methods.HttpUriRequest; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class HttpClientStackTest { + + @Test public void createDeprecatedGetRequest() throws Exception { + TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpGet); + } + + @Test public void createDeprecatedPostRequest() throws Exception { + TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test public void createGetRequest() throws Exception { + TestRequest.Get request = new TestRequest.Get(); + assertEquals(request.getMethod(), Method.GET); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpGet); + } + + @Test public void createPostRequest() throws Exception { + TestRequest.Post request = new TestRequest.Post(); + assertEquals(request.getMethod(), Method.POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test public void createPostRequestWithBody() throws Exception { + TestRequest.PostWithBody request = new TestRequest.PostWithBody(); + assertEquals(request.getMethod(), Method.POST); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPost); + } + + @Test public void createPutRequest() throws Exception { + TestRequest.Put request = new TestRequest.Put(); + assertEquals(request.getMethod(), Method.PUT); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPut); + } + + @Test public void createPutRequestWithBody() throws Exception { + TestRequest.PutWithBody request = new TestRequest.PutWithBody(); + assertEquals(request.getMethod(), Method.PUT); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPut); + } + + @Test public void createDeleteRequest() throws Exception { + TestRequest.Delete request = new TestRequest.Delete(); + assertEquals(request.getMethod(), Method.DELETE); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpDelete); + } + + @Test public void createHeadRequest() throws Exception { + TestRequest.Head request = new TestRequest.Head(); + assertEquals(request.getMethod(), Method.HEAD); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpHead); + } + + @Test public void createOptionsRequest() throws Exception { + TestRequest.Options request = new TestRequest.Options(); + assertEquals(request.getMethod(), Method.OPTIONS); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpOptions); + } + + @Test public void createTraceRequest() throws Exception { + TestRequest.Trace request = new TestRequest.Trace(); + assertEquals(request.getMethod(), Method.TRACE); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpTrace); + } + + @Test public void createPatchRequest() throws Exception { + TestRequest.Patch request = new TestRequest.Patch(); + assertEquals(request.getMethod(), Method.PATCH); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPatch); + } + + @Test public void createPatchRequestWithBody() throws Exception { + TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); + assertEquals(request.getMethod(), Method.PATCH); + + HttpUriRequest httpRequest = HttpClientStack.createHttpRequest(request, null); + assertTrue(httpRequest instanceof HttpPatch); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java b/volley/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java new file mode 100644 index 0000000..fd8cf51 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/HttpHeaderParserTest.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; + +import org.apache.http.Header; +import org.apache.http.message.BasicHeader; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class HttpHeaderParserTest { + + private static long ONE_MINUTE_MILLIS = 1000L * 60; + private static long ONE_HOUR_MILLIS = 1000L * 60 * 60; + private static long ONE_DAY_MILLIS = ONE_HOUR_MILLIS * 24; + private static long ONE_WEEK_MILLIS = ONE_DAY_MILLIS * 7; + + private NetworkResponse response; + private Map headers; + + @Before public void setUp() throws Exception { + headers = new HashMap(); + response = new NetworkResponse(0, null, headers, false); + } + + @Test public void parseCacheHeaders_noHeaders() { + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEquals(0, entry.serverDate); + assertEquals(0, entry.lastModified); + assertEquals(0, entry.ttl); + assertEquals(0, entry.softTtl); + } + + @Test public void parseCacheHeaders_headersSet() { + headers.put("MyCustomHeader", "42"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNotNull(entry.responseHeaders); + assertEquals(1, entry.responseHeaders.size()); + assertEquals("42", entry.responseHeaders.get("MyCustomHeader")); + } + + @Test public void parseCacheHeaders_etag() { + headers.put("ETag", "Yow!"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertEquals("Yow!", entry.etag); + } + + @Test public void parseCacheHeaders_normalExpire() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Last-Modified", rfc1123Date(now - ONE_DAY_MILLIS)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); + assertEqualsWithin(entry.lastModified, (now - ONE_DAY_MILLIS), ONE_MINUTE_MILLIS); + assertTrue(entry.softTtl >= (now + ONE_HOUR_MILLIS)); + assertTrue(entry.ttl == entry.softTtl); + } + + @Test public void parseCacheHeaders_expiresInPast() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now - ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(entry.serverDate, now, ONE_MINUTE_MILLIS); + assertEquals(0, entry.ttl); + assertEquals(0, entry.softTtl); + } + + @Test public void parseCacheHeaders_serverRelative() { + + long now = System.currentTimeMillis(); + // Set "current" date as one hour in the future + headers.put("Date", rfc1123Date(now + ONE_HOUR_MILLIS)); + // TTL four hours in the future, so should be three hours from now + headers.put("Expires", rfc1123Date(now + 4 * ONE_HOUR_MILLIS)); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertEqualsWithin(now + 3 * ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test public void parseCacheHeaders_cacheControlOverridesExpires() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "public, max-age=86400"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test public void testParseCacheHeaders_staleWhileRevalidate() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day + // - stale-while-revalidate (entry.ttl) indicates that the asset may + // continue to be served stale for up to additional 7 days + headers.put("Cache-Control", "max-age=86400, stale-while-revalidate=604800"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); + assertEqualsWithin(now + ONE_DAY_MILLIS + ONE_WEEK_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + } + + @Test public void parseCacheHeaders_cacheControlNoCache() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "no-cache"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNull(entry); + } + + @Test public void parseCacheHeaders_cacheControlMustRevalidateNoMaxAge() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "must-revalidate"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAge() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headers.put("Cache-Control", "must-revalidate, max-age=3600"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_HOUR_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + @Test public void parseCacheHeaders_cacheControlMustRevalidateWithMaxAgeAndStale() { + long now = System.currentTimeMillis(); + headers.put("Date", rfc1123Date(now)); + headers.put("Expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + + // - max-age (entry.softTtl) indicates that the asset is fresh for 1 day + // - stale-while-revalidate (entry.ttl) indicates that the asset may + // continue to be served stale for up to additional 7 days, but this is + // ignored in this case because of the must-revalidate header. + headers.put("Cache-Control", + "must-revalidate, max-age=86400, stale-while-revalidate=604800"); + + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + assertNotNull(entry); + assertNull(entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.softTtl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + } + + private void assertEqualsWithin(long expected, long value, long fudgeFactor) { + long diff = Math.abs(expected - value); + assertTrue(diff < fudgeFactor); + } + + private static String rfc1123Date(long millis) { + DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH); + return df.format(new Date(millis)); + } + + // -------------------------- + + @Test public void parseCharset() { + // Like the ones we usually see + headers.put("Content-Type", "text/plain; charset=utf-8"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // Charset specified, ignore default charset + headers.put("Content-Type", "text/plain; charset=utf-8"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "ISO-8859-1")); + + // Extra whitespace + headers.put("Content-Type", "text/plain; charset=utf-8 "); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // Extra parameters + headers.put("Content-Type", "text/plain; charset=utf-8; frozzle=bar"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers)); + + // No Content-Type header + headers.clear(); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // No Content-Type header, use default charset + headers.clear(); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); + + // Empty value + headers.put("Content-Type", "text/plain; charset="); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // None specified + headers.put("Content-Type", "text/plain"); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + + // None charset specified, use default charset + headers.put("Content-Type", "application/json"); + assertEquals("utf-8", HttpHeaderParser.parseCharset(headers, "utf-8")); + + // None specified, extra semicolon + headers.put("Content-Type", "text/plain;"); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + } + + @Test public void parseCaseInsensitive() { + + long now = System.currentTimeMillis(); + + Header[] headersArray = new Header[5]; + headersArray[0] = new BasicHeader("eTAG", "Yow!"); + headersArray[1] = new BasicHeader("DATE", rfc1123Date(now)); + headersArray[2] = new BasicHeader("expires", rfc1123Date(now + ONE_HOUR_MILLIS)); + headersArray[3] = new BasicHeader("cache-control", "public, max-age=86400"); + headersArray[4] = new BasicHeader("content-type", "text/plain"); + + Map headers = BasicNetwork.convertHeaders(headersArray); + NetworkResponse response = new NetworkResponse(0, null, headers, false); + Cache.Entry entry = HttpHeaderParser.parseCacheHeaders(response); + + assertNotNull(entry); + assertEquals("Yow!", entry.etag); + assertEqualsWithin(now + ONE_DAY_MILLIS, entry.ttl, ONE_MINUTE_MILLIS); + assertEquals(entry.softTtl, entry.ttl); + assertEquals("ISO-8859-1", HttpHeaderParser.parseCharset(headers)); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/HurlStackTest.java b/volley/src/test/java/com/android/volley/toolbox/HurlStackTest.java new file mode 100644 index 0000000..42aeea8 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/HurlStackTest.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Request.Method; +import com.android.volley.mock.MockHttpURLConnection; +import com.android.volley.mock.TestRequest; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class HurlStackTest { + + private MockHttpURLConnection mMockConnection; + + @Before public void setUp() throws Exception { + mMockConnection = new MockHttpURLConnection(); + } + + @Test public void connectionForDeprecatedGetRequest() throws Exception { + TestRequest.DeprecatedGet request = new TestRequest.DeprecatedGet(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("GET", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForDeprecatedPostRequest() throws Exception { + TestRequest.DeprecatedPost request = new TestRequest.DeprecatedPost(); + assertEquals(request.getMethod(), Method.DEPRECATED_GET_OR_POST); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("POST", mMockConnection.getRequestMethod()); + assertTrue(mMockConnection.getDoOutput()); + } + + @Test public void connectionForGetRequest() throws Exception { + TestRequest.Get request = new TestRequest.Get(); + assertEquals(request.getMethod(), Method.GET); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("GET", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForPostRequest() throws Exception { + TestRequest.Post request = new TestRequest.Post(); + assertEquals(request.getMethod(), Method.POST); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("POST", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForPostWithBodyRequest() throws Exception { + TestRequest.PostWithBody request = new TestRequest.PostWithBody(); + assertEquals(request.getMethod(), Method.POST); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("POST", mMockConnection.getRequestMethod()); + assertTrue(mMockConnection.getDoOutput()); + } + + @Test public void connectionForPutRequest() throws Exception { + TestRequest.Put request = new TestRequest.Put(); + assertEquals(request.getMethod(), Method.PUT); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("PUT", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForPutWithBodyRequest() throws Exception { + TestRequest.PutWithBody request = new TestRequest.PutWithBody(); + assertEquals(request.getMethod(), Method.PUT); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("PUT", mMockConnection.getRequestMethod()); + assertTrue(mMockConnection.getDoOutput()); + } + + @Test public void connectionForDeleteRequest() throws Exception { + TestRequest.Delete request = new TestRequest.Delete(); + assertEquals(request.getMethod(), Method.DELETE); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("DELETE", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForHeadRequest() throws Exception { + TestRequest.Head request = new TestRequest.Head(); + assertEquals(request.getMethod(), Method.HEAD); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("HEAD", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForOptionsRequest() throws Exception { + TestRequest.Options request = new TestRequest.Options(); + assertEquals(request.getMethod(), Method.OPTIONS); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("OPTIONS", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForTraceRequest() throws Exception { + TestRequest.Trace request = new TestRequest.Trace(); + assertEquals(request.getMethod(), Method.TRACE); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("TRACE", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForPatchRequest() throws Exception { + TestRequest.Patch request = new TestRequest.Patch(); + assertEquals(request.getMethod(), Method.PATCH); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("PATCH", mMockConnection.getRequestMethod()); + assertFalse(mMockConnection.getDoOutput()); + } + + @Test public void connectionForPatchWithBodyRequest() throws Exception { + TestRequest.PatchWithBody request = new TestRequest.PatchWithBody(); + assertEquals(request.getMethod(), Method.PATCH); + + HurlStack.setConnectionParametersForRequest(mMockConnection, request); + assertEquals("PATCH", mMockConnection.getRequestMethod()); + assertTrue(mMockConnection.getDoOutput()); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java b/volley/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java new file mode 100644 index 0000000..8a19817 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/ImageLoaderTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.graphics.Bitmap; +import android.widget.ImageView; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +@RunWith(RobolectricTestRunner.class) +public class ImageLoaderTest { + private RequestQueue mRequestQueue; + private ImageLoader.ImageCache mImageCache; + private ImageLoader mImageLoader; + + @Before + public void setUp() { + mRequestQueue = mock(RequestQueue.class); + mImageCache = mock(ImageLoader.ImageCache.class); + mImageLoader = new ImageLoader(mRequestQueue, mImageCache); + } + + @Test + public void isCachedChecksCache() throws Exception { + when(mImageCache.getBitmap(anyString())).thenReturn(null); + Assert.assertFalse(mImageLoader.isCached("http://foo", 0, 0)); + } + + @Test + public void getWithCacheHit() throws Exception { + Bitmap bitmap = Bitmap.createBitmap(1, 1, null); + ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class); + when(mImageCache.getBitmap(anyString())).thenReturn(bitmap); + ImageLoader.ImageContainer ic = mImageLoader.get("http://foo", listener); + Assert.assertSame(bitmap, ic.getBitmap()); + verify(listener).onResponse(ic, true); + } + + @Test + public void getWithCacheMiss() throws Exception { + when(mImageCache.getBitmap(anyString())).thenReturn(null); + ImageLoader.ImageListener listener = mock(ImageLoader.ImageListener.class); + // Ask for the image to be loaded. + mImageLoader.get("http://foo", listener); + // Second pass to test deduping logic. + mImageLoader.get("http://foo", listener); + // Response callback should be called both times. + verify(listener, times(2)).onResponse(any(ImageLoader.ImageContainer.class), eq(true)); + // But request should be enqueued only once. + verify(mRequestQueue, times(1)).add(any(Request.class)); + } + + @Test + public void publicMethods() throws Exception { + // Catch API breaking changes. + ImageLoader.getImageListener(null, -1, -1); + mImageLoader.setBatchedResponseDelay(1000); + + assertNotNull(ImageLoader.class.getConstructor(RequestQueue.class, + ImageLoader.ImageCache.class)); + + assertNotNull(ImageLoader.class.getMethod("getImageListener", ImageView.class, + int.class, int.class)); + assertNotNull(ImageLoader.class.getMethod("isCached", String.class, int.class, int.class)); + assertNotNull(ImageLoader.class.getMethod("isCached", String.class, int.class, int.class, + ImageView.ScaleType.class)); + assertNotNull(ImageLoader.class.getMethod("get", String.class, + ImageLoader.ImageListener.class)); + assertNotNull(ImageLoader.class.getMethod("get", String.class, + ImageLoader.ImageListener.class, int.class, int.class)); + assertNotNull(ImageLoader.class.getMethod("get", String.class, + ImageLoader.ImageListener.class, int.class, int.class, ImageView.ScaleType.class)); + assertNotNull(ImageLoader.class.getMethod("setBatchedResponseDelay", int.class)); + + assertNotNull(ImageLoader.ImageListener.class.getMethod("onResponse", + ImageLoader.ImageContainer.class, boolean.class)); + } +} + diff --git a/volley/src/test/java/com/android/volley/toolbox/ImageRequestTest.java b/volley/src/test/java/com/android/volley/toolbox/ImageRequestTest.java new file mode 100644 index 0000000..a99363e --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/ImageRequestTest.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import com.android.volley.NetworkResponse; +import com.android.volley.Response; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.shadows.ShadowBitmapFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class ImageRequestTest { + + @Test public void parseNetworkResponse_resizing() throws Exception { + // This is a horrible hack but Robolectric doesn't have a way to provide + // width and height hints for decodeByteArray. It works because the byte array + // "file:fake" is ASCII encodable and thus the name in Robolectric's fake + // bitmap creator survives as-is, and provideWidthAndHeightHints puts + // "file:" + name in its lookaside map. I write all this because it will + // probably break mysteriously at some point and I feel terrible about your + // having to debug it. + byte[] jpegBytes = "file:fake".getBytes(); + ShadowBitmapFactory.provideWidthAndHeightHints("fake", 1024, 500); + NetworkResponse jpeg = new NetworkResponse(jpegBytes); + + // Scale the image uniformly (maintain the image's aspect ratio) so that + // both dimensions (width and height) of the image will be equal to or + // less than the corresponding dimension of the view. + ScaleType scalteType = ScaleType.CENTER_INSIDE; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); // exactly half + verifyResize(jpeg, 511, 249, scalteType, 509, 249); // just under half + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); // larger + verifyResize(jpeg, 500, 500, scalteType, 500, 244); // keep same ratio + + // Specify only width, preserve aspect ratio + verifyResize(jpeg, 512, 0, scalteType, 512, 250); + verifyResize(jpeg, 800, 0, scalteType, 800, 390); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height, preserve aspect ratio + verifyResize(jpeg, 0, 250, scalteType, 512, 250); + verifyResize(jpeg, 0, 391, scalteType, 800, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + + + // Scale the image uniformly (maintain the image's aspect ratio) so that + // both dimensions (width and height) of the image will be equal to or + // larger than the corresponding dimension of the view. + scalteType = ScaleType.CENTER_CROP; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); + verifyResize(jpeg, 511, 249, scalteType, 511, 249); + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); + verifyResize(jpeg, 500, 500, scalteType, 1024, 500); + + // Specify only width + verifyResize(jpeg, 512, 0, scalteType, 512, 250); + verifyResize(jpeg, 800, 0, scalteType, 800, 390); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height + verifyResize(jpeg, 0, 250, scalteType, 512, 250); + verifyResize(jpeg, 0, 391, scalteType, 800, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + + + // Scale in X and Y independently, so that src matches dst exactly. This + // may change the aspect ratio of the src. + scalteType = ScaleType.FIT_XY; + + // Exact sizes + verifyResize(jpeg, 512, 250, scalteType, 512, 250); + verifyResize(jpeg, 511, 249, scalteType, 511, 249); + verifyResize(jpeg, 1080, 500, scalteType, 1024, 500); + verifyResize(jpeg, 500, 500, scalteType, 500, 500); + + // Specify only width + verifyResize(jpeg, 512, 0, scalteType, 512, 500); + verifyResize(jpeg, 800, 0, scalteType, 800, 500); + verifyResize(jpeg, 1024, 0, scalteType, 1024, 500); + + // Specify only height + verifyResize(jpeg, 0, 250, scalteType, 1024, 250); + verifyResize(jpeg, 0, 391, scalteType, 1024, 391); + verifyResize(jpeg, 0, 500, scalteType, 1024, 500); + + // No resize + verifyResize(jpeg, 0, 0, scalteType, 1024, 500); + } + + private void verifyResize(NetworkResponse networkResponse, int maxWidth, int maxHeight, + ScaleType scaleType, int expectedWidth, int expectedHeight) { + ImageRequest request = new ImageRequest("", null, maxWidth, maxHeight, scaleType, + Config.RGB_565, null); + Response response = request.parseNetworkResponse(networkResponse); + assertNotNull(response); + assertTrue(response.isSuccess()); + Bitmap bitmap = response.result; + assertNotNull(bitmap); + assertEquals(expectedWidth, bitmap.getWidth()); + assertEquals(expectedHeight, bitmap.getHeight()); + } + + @Test public void findBestSampleSize() { + // desired == actual == 1 + assertEquals(1, ImageRequest.findBestSampleSize(100, 150, 100, 150)); + + // exactly half == 2 + assertEquals(2, ImageRequest.findBestSampleSize(280, 160, 140, 80)); + + // just over half == 1 + assertEquals(1, ImageRequest.findBestSampleSize(1000, 800, 501, 401)); + + // just under 1/4 == 4 + assertEquals(4, ImageRequest.findBestSampleSize(100, 200, 24, 50)); + } + + private static byte[] readInputStream(InputStream in) throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int count; + while ((count = in.read(buffer)) != -1) { + bytes.write(buffer, 0, count); + } + in.close(); + return bytes.toByteArray(); + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(ImageRequest.class.getConstructor(String.class, Response.Listener.class, + int.class, int.class, Bitmap.Config.class, Response.ErrorListener.class)); + assertNotNull(ImageRequest.class.getConstructor(String.class, Response.Listener.class, + int.class, int.class, ImageView.ScaleType.class, Bitmap.Config.class, + Response.ErrorListener.class)); + assertEquals(ImageRequest.DEFAULT_IMAGE_TIMEOUT_MS, 1000); + assertEquals(ImageRequest.DEFAULT_IMAGE_MAX_RETRIES, 2); + assertEquals(ImageRequest.DEFAULT_IMAGE_BACKOFF_MULT, 2f, 0); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java b/volley/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java new file mode 100644 index 0000000..db6f648 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/JsonRequestCharsetTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import com.android.volley.toolbox.JsonArrayRequest; +import com.android.volley.toolbox.JsonObjectRequest; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.lang.Exception; +import java.lang.String; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class JsonRequestCharsetTest { + + /** + * String in Czech - "Retezec v cestine." + */ + private static final String TEXT_VALUE = "\u0158et\u011bzec v \u010de\u0161tin\u011b."; + private static final String TEXT_NAME = "text"; + private static final int TEXT_INDEX = 0; + + /** + * Copyright symbol has different encoding in utf-8 and ISO-8859-1, + * and it doesn't exists in ISO-8859-2 + */ + private static final String COPY_VALUE = "\u00a9"; + private static final String COPY_NAME = "copyright"; + private static final int COPY_INDEX = 1; + + @Test public void defaultCharsetJsonObject() throws Exception { + // UTF-8 is default charset for JSON + byte[] data = jsonObjectString().getBytes(Charset.forName("UTF-8")); + NetworkResponse network = new NetworkResponse(data); + JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null); + Response objectResponse = objectRequest.parseNetworkResponse(network); + + assertNotNull(objectResponse); + assertTrue(objectResponse.isSuccess()); + assertEquals(TEXT_VALUE, objectResponse.result.getString(TEXT_NAME)); + assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME)); + } + + @Test public void defaultCharsetJsonArray() throws Exception { + // UTF-8 is default charset for JSON + byte[] data = jsonArrayString().getBytes(Charset.forName("UTF-8")); + NetworkResponse network = new NetworkResponse(data); + JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null); + Response arrayResponse = arrayRequest.parseNetworkResponse(network); + + assertNotNull(arrayResponse); + assertTrue(arrayResponse.isSuccess()); + assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX)); + assertEquals(COPY_VALUE, arrayResponse.result.getString(COPY_INDEX)); + } + + @Test public void specifiedCharsetJsonObject() throws Exception { + byte[] data = jsonObjectString().getBytes(Charset.forName("ISO-8859-1")); + Map headers = new HashMap(); + headers.put("Content-Type", "application/json; charset=iso-8859-1"); + NetworkResponse network = new NetworkResponse(data, headers); + JsonObjectRequest objectRequest = new JsonObjectRequest("", null, null, null); + Response objectResponse = objectRequest.parseNetworkResponse(network); + + assertNotNull(objectResponse); + assertTrue(objectResponse.isSuccess()); + //don't check the text in Czech, ISO-8859-1 doesn't support some Czech characters + assertEquals(COPY_VALUE, objectResponse.result.getString(COPY_NAME)); + } + + @Test public void specifiedCharsetJsonArray() throws Exception { + byte[] data = jsonArrayString().getBytes(Charset.forName("ISO-8859-2")); + Map headers = new HashMap(); + headers.put("Content-Type", "application/json; charset=iso-8859-2"); + NetworkResponse network = new NetworkResponse(data, headers); + JsonArrayRequest arrayRequest = new JsonArrayRequest("", null, null); + Response arrayResponse = arrayRequest.parseNetworkResponse(network); + + assertNotNull(arrayResponse); + assertTrue(arrayResponse.isSuccess()); + assertEquals(TEXT_VALUE, arrayResponse.result.getString(TEXT_INDEX)); + // don't check the copyright symbol, ISO-8859-2 doesn't have it, but it has Czech characters + } + + private static String jsonObjectString() throws Exception { + JSONObject json = new JSONObject().put(TEXT_NAME, TEXT_VALUE).put(COPY_NAME, COPY_VALUE); + return json.toString(); + } + + private static String jsonArrayString() throws Exception { + JSONArray json = new JSONArray().put(TEXT_INDEX, TEXT_VALUE).put(COPY_INDEX, COPY_VALUE); + return json.toString(); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/JsonRequestTest.java b/volley/src/test/java/com/android/volley/toolbox/JsonRequestTest.java new file mode 100644 index 0000000..e39c8c8 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/JsonRequestTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Response; +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +public class JsonRequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(JsonRequest.class.getConstructor(String.class, String.class, + Response.Listener.class, Response.ErrorListener.class)); + assertNotNull(JsonRequest.class.getConstructor(int.class, String.class, String.class, + Response.Listener.class, Response.ErrorListener.class)); + + assertNotNull(JsonArrayRequest.class.getConstructor(String.class, + Response.Listener.class, Response.ErrorListener.class)); + assertNotNull(JsonArrayRequest.class.getConstructor(int.class, String.class, JSONArray.class, + Response.Listener.class, Response.ErrorListener.class)); + + assertNotNull(JsonObjectRequest.class.getConstructor(String.class, JSONObject.class, + Response.Listener.class, Response.ErrorListener.class)); + assertNotNull(JsonObjectRequest.class.getConstructor(int.class, String.class, + JSONObject.class, Response.Listener.class, Response.ErrorListener.class)); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java b/volley/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java new file mode 100644 index 0000000..917ddb4 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/NetworkImageViewTest.java @@ -0,0 +1,69 @@ +package com.android.volley.toolbox; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ViewGroup.LayoutParams; +import android.widget.ImageView.ScaleType; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.*; + +@RunWith(RobolectricTestRunner.class) +public class NetworkImageViewTest { + private NetworkImageView mNIV; + private MockImageLoader mMockImageLoader; + + @Before public void setUp() throws Exception { + mMockImageLoader = new MockImageLoader(); + mNIV = new NetworkImageView(RuntimeEnvironment.application); + } + + @Test public void setImageUrl_requestsImage() { + mNIV.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); + mNIV.setImageUrl("http://foo", mMockImageLoader); + assertEquals("http://foo", mMockImageLoader.lastRequestUrl); + assertEquals(0, mMockImageLoader.lastMaxWidth); + assertEquals(0, mMockImageLoader.lastMaxHeight); + } + + // public void testSetImageUrl_setsMaxSize() { + // // TODO: Not sure how to make getWidth() return something from an + // // instrumentation test. Write this test once it's figured out. + // } + + private class MockImageLoader extends ImageLoader { + public MockImageLoader() { + super(null, null); + } + + public String lastRequestUrl; + public int lastMaxWidth; + public int lastMaxHeight; + + public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, + int maxHeight, ScaleType scaleType) { + lastRequestUrl = requestUrl; + lastMaxWidth = maxWidth; + lastMaxHeight = maxHeight; + return null; + } + } + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(NetworkImageView.class.getConstructor(Context.class)); + assertNotNull(NetworkImageView.class.getConstructor(Context.class, AttributeSet.class)); + assertNotNull(NetworkImageView.class.getConstructor(Context.class, AttributeSet.class, + int.class)); + + assertNotNull(NetworkImageView.class.getMethod("setImageUrl", String.class, ImageLoader.class)); + assertNotNull(NetworkImageView.class.getMethod("setDefaultImageResId", int.class)); + assertNotNull(NetworkImageView.class.getMethod("setErrorImageResId", int.class)); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java b/volley/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java new file mode 100644 index 0000000..c3bfac7 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/PoolingByteArrayOutputStreamTest.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import java.io.IOException; +import java.util.Arrays; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class PoolingByteArrayOutputStreamTest { + @Test public void pooledOneBuffer() throws IOException { + ByteArrayPool pool = new ByteArrayPool(32768); + writeOneBuffer(pool); + writeOneBuffer(pool); + writeOneBuffer(pool); + } + + @Test public void pooledIndividualWrites() throws IOException { + ByteArrayPool pool = new ByteArrayPool(32768); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + } + + @Test public void unpooled() throws IOException { + ByteArrayPool pool = new ByteArrayPool(0); + writeOneBuffer(pool); + writeOneBuffer(pool); + writeOneBuffer(pool); + } + + @Test public void unpooledIndividualWrites() throws IOException { + ByteArrayPool pool = new ByteArrayPool(0); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + writeBytesIndividually(pool); + } + + private void writeOneBuffer(ByteArrayPool pool) throws IOException { + byte[] data = new byte[16384]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i & 0xff); + } + PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool); + os.write(data); + + assertTrue(Arrays.equals(data, os.toByteArray())); + } + + private void writeBytesIndividually(ByteArrayPool pool) { + byte[] data = new byte[16384]; + for (int i = 0; i < data.length; i++) { + data[i] = (byte) (i & 0xff); + } + PoolingByteArrayOutputStream os = new PoolingByteArrayOutputStream(pool); + for (int i = 0; i < data.length; i++) { + os.write(data[i]); + } + + assertTrue(Arrays.equals(data, os.toByteArray())); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/RequestFutureTest.java b/volley/src/test/java/com/android/volley/toolbox/RequestFutureTest.java new file mode 100644 index 0000000..c8e23e7 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/RequestFutureTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Request; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +public class RequestFutureTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(RequestFuture.class.getMethod("newFuture")); + assertNotNull(RequestFuture.class.getMethod("setRequest", Request.class)); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/RequestQueueTest.java b/volley/src/test/java/com/android/volley/toolbox/RequestQueueTest.java new file mode 100644 index 0000000..1e4b82e --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/RequestQueueTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +public class RequestQueueTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class, int.class, + ResponseDelivery.class)); + assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class, int.class)); + assertNotNull(RequestQueue.class.getConstructor(Cache.class, Network.class)); + + assertNotNull(RequestQueue.class.getMethod("start")); + assertNotNull(RequestQueue.class.getMethod("stop")); + assertNotNull(RequestQueue.class.getMethod("getSequenceNumber")); + assertNotNull(RequestQueue.class.getMethod("getCache")); + assertNotNull(RequestQueue.class.getMethod("cancelAll", RequestQueue.RequestFilter.class)); + assertNotNull(RequestQueue.class.getMethod("cancelAll", Object.class)); + assertNotNull(RequestQueue.class.getMethod("add", Request.class)); + assertNotNull(RequestQueue.class.getDeclaredMethod("finish", Request.class)); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/RequestTest.java b/volley/src/test/java/com/android/volley/toolbox/RequestTest.java new file mode 100644 index 0000000..22d2ef2 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/RequestTest.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.*; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +public class RequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(Request.class.getConstructor(int.class, String.class, + Response.ErrorListener.class)); + + assertNotNull(Request.class.getMethod("getMethod")); + assertNotNull(Request.class.getMethod("setTag", Object.class)); + assertNotNull(Request.class.getMethod("getTag")); + assertNotNull(Request.class.getMethod("getErrorListener")); + assertNotNull(Request.class.getMethod("getTrafficStatsTag")); + assertNotNull(Request.class.getMethod("setRetryPolicy", RetryPolicy.class)); + assertNotNull(Request.class.getMethod("addMarker", String.class)); + assertNotNull(Request.class.getDeclaredMethod("finish", String.class)); + assertNotNull(Request.class.getMethod("setRequestQueue", RequestQueue.class)); + assertNotNull(Request.class.getMethod("setSequence", int.class)); + assertNotNull(Request.class.getMethod("getSequence")); + assertNotNull(Request.class.getMethod("getUrl")); + assertNotNull(Request.class.getMethod("getCacheKey")); + assertNotNull(Request.class.getMethod("setCacheEntry", Cache.Entry.class)); + assertNotNull(Request.class.getMethod("getCacheEntry")); + assertNotNull(Request.class.getMethod("cancel")); + assertNotNull(Request.class.getMethod("isCanceled")); + assertNotNull(Request.class.getMethod("getHeaders")); + assertNotNull(Request.class.getDeclaredMethod("getParams")); + assertNotNull(Request.class.getDeclaredMethod("getParamsEncoding")); + assertNotNull(Request.class.getMethod("getBodyContentType")); + assertNotNull(Request.class.getMethod("getBody")); + assertNotNull(Request.class.getMethod("setShouldCache", boolean.class)); + assertNotNull(Request.class.getMethod("shouldCache")); + assertNotNull(Request.class.getMethod("getPriority")); + assertNotNull(Request.class.getMethod("getTimeoutMs")); + assertNotNull(Request.class.getMethod("getRetryPolicy")); + assertNotNull(Request.class.getMethod("markDelivered")); + assertNotNull(Request.class.getMethod("hasHadResponseDelivered")); + assertNotNull(Request.class.getDeclaredMethod("parseNetworkResponse", NetworkResponse.class)); + assertNotNull(Request.class.getDeclaredMethod("parseNetworkError", VolleyError.class)); + assertNotNull(Request.class.getDeclaredMethod("deliverResponse", Object.class)); + assertNotNull(Request.class.getMethod("deliverError", VolleyError.class)); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/ResponseTest.java b/volley/src/test/java/com/android/volley/toolbox/ResponseTest.java new file mode 100644 index 0000000..e830eb5 --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/ResponseTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Cache; +import com.android.volley.NetworkResponse; +import com.android.volley.Response; +import com.android.volley.VolleyError; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Map; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +public class ResponseTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(Response.class.getMethod("success", Object.class, Cache.Entry.class)); + assertNotNull(Response.class.getMethod("error", VolleyError.class)); + assertNotNull(Response.class.getMethod("isSuccess")); + + assertNotNull(Response.Listener.class.getDeclaredMethod("onResponse", Object.class)); + + assertNotNull(Response.ErrorListener.class.getDeclaredMethod("onErrorResponse", + VolleyError.class)); + + assertNotNull(NetworkResponse.class.getConstructor(int.class, byte[].class, Map.class, + boolean.class, long.class)); + assertNotNull(NetworkResponse.class.getConstructor(int.class, byte[].class, Map.class, + boolean.class)); + assertNotNull(NetworkResponse.class.getConstructor(byte[].class)); + assertNotNull(NetworkResponse.class.getConstructor(byte[].class, Map.class)); + } +} diff --git a/volley/src/test/java/com/android/volley/toolbox/StringRequestTest.java b/volley/src/test/java/com/android/volley/toolbox/StringRequestTest.java new file mode 100644 index 0000000..eadd73f --- /dev/null +++ b/volley/src/test/java/com/android/volley/toolbox/StringRequestTest.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.volley.toolbox; + +import com.android.volley.Response; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import static org.junit.Assert.assertNotNull; + +@RunWith(RobolectricTestRunner.class) +public class StringRequestTest { + + @Test + public void publicMethods() throws Exception { + // Catch-all test to find API-breaking changes. + assertNotNull(StringRequest.class.getConstructor(String.class, Response.Listener.class, + Response.ErrorListener.class)); + assertNotNull(StringRequest.class.getConstructor(int.class, String.class, + Response.Listener.class, Response.ErrorListener.class)); + } +} diff --git a/volley/src/test/java/com/android/volley/utils/CacheTestUtils.java b/volley/src/test/java/com/android/volley/utils/CacheTestUtils.java new file mode 100644 index 0000000..898d055 --- /dev/null +++ b/volley/src/test/java/com/android/volley/utils/CacheTestUtils.java @@ -0,0 +1,40 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package com.android.volley.utils; + +import com.android.volley.Cache; + +import java.util.Random; + +public class CacheTestUtils { + + /** + * Makes a random cache entry. + * @param data Data to use, or null to use random data + * @param isExpired Whether the TTLs should be set such that this entry is expired + * @param needsRefresh Whether the TTLs should be set such that this entry needs refresh + */ + public static Cache.Entry makeRandomCacheEntry( + byte[] data, boolean isExpired, boolean needsRefresh) { + Random random = new Random(); + Cache.Entry entry = new Cache.Entry(); + if (data != null) { + entry.data = data; + } else { + entry.data = new byte[random.nextInt(1024)]; + } + entry.etag = String.valueOf(random.nextLong()); + entry.lastModified = random.nextLong(); + entry.ttl = isExpired ? 0 : Long.MAX_VALUE; + entry.softTtl = needsRefresh ? 0 : Long.MAX_VALUE; + return entry; + } + + /** + * Like {@link #makeRandomCacheEntry(byte[], boolean, boolean)} but + * defaults to an unexpired entry. + */ + public static Cache.Entry makeRandomCacheEntry(byte[] data) { + return makeRandomCacheEntry(data, false, false); + } +} diff --git a/volley/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java b/volley/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java new file mode 100644 index 0000000..666e0d0 --- /dev/null +++ b/volley/src/test/java/com/android/volley/utils/ImmediateResponseDelivery.java @@ -0,0 +1,23 @@ +// Copyright 2011 Google Inc. All rights reserved. + +package com.android.volley.utils; + +import com.android.volley.ExecutorDelivery; + +import java.util.concurrent.Executor; + +/** + * A ResponseDelivery for testing that immediately delivers responses + * instead of posting back to the main thread. + */ +public class ImmediateResponseDelivery extends ExecutorDelivery { + + public ImmediateResponseDelivery() { + super(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }); + } +} diff --git a/volley/src/test/resources/org.robolectric.Config.properties b/volley/src/test/resources/org.robolectric.Config.properties new file mode 100644 index 0000000..9daf692 --- /dev/null +++ b/volley/src/test/resources/org.robolectric.Config.properties @@ -0,0 +1 @@ +manifest=src/main/AndroidManifest.xml From 6e8ebe1b5d39fe25facad235c16ed1a94c5d3aa3 Mon Sep 17 00:00:00 2001 From: Fabio Pires Prado Date: Sat, 15 Dec 2018 14:30:47 -0200 Subject: [PATCH 2/2] =?UTF-8?q?Corre=C3=A7=C3=A3o=20tamanho=20da=20imagem?= =?UTF-8?q?=20da=20Splash=20Screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/compiler.xml | 22 +++++++++ .idea/copyright/profiles_settings.xml | 3 ++ .idea/gradle.xml | 19 ++++++++ .idea/misc.xml | 36 ++++++++++++++ .idea/modules.xml | 10 ++++ .idea/runConfigurations.xml | 12 +++++ .idea/vcs.xml | 7 +++ app/src/main/AndroidManifest.xml | 4 +- .../moviesdatabase/Activity/SplashScreen.java | 48 +++++++++++++++++++ app/src/main/res/layout/activity_main.xml | 1 - 10 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 .idea/compiler.xml create mode 100644 .idea/copyright/profiles_settings.xml create mode 100644 .idea/gradle.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/runConfigurations.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..96cc43e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml new file mode 100644 index 0000000..e7bedf3 --- /dev/null +++ b/.idea/copyright/profiles_settings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..bc3eb86 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b0a270f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5525e4c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..555bd48 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2aadf95..6df06e8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -11,12 +11,12 @@ android:label="@string/app_name" android:roundIcon="@mipmap/imdb" android:supportsRtl="true" + android:largeHeap="true" android:theme="@style/AppTheme"> + android:screenOrientation="portrait"> diff --git a/app/src/main/java/android_challenge/com/moviesdatabase/Activity/SplashScreen.java b/app/src/main/java/android_challenge/com/moviesdatabase/Activity/SplashScreen.java index 58e435c..3ebaa86 100644 --- a/app/src/main/java/android_challenge/com/moviesdatabase/Activity/SplashScreen.java +++ b/app/src/main/java/android_challenge/com/moviesdatabase/Activity/SplashScreen.java @@ -1,6 +1,9 @@ package android_challenge.com.moviesdatabase.Activity; import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.os.Handler; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; @@ -8,18 +11,63 @@ import android.view.animation.AnimationUtils; import android.widget.ImageView; import android.widget.LinearLayout; + import android_challenge.com.moviesdatabase.R; public class SplashScreen extends AppCompatActivity { + ImageView imageView; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + + imageView = (ImageView) findViewById(R.id.imageview_logo); + + imageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.drawable.logo_imdb, 600, 600)); + StartAnimations(); } + public static int calculateInSampleSize( + BitmapFactory.Options options, int reqWidth, int reqHeight) { + + final int height = options.outHeight; + final int width = options.outWidth; + int inSampleSize = 1; + + if (height > reqHeight || width > reqWidth) { + + final int halfHeight = height / 2; + final int halfWidth = width / 2; + + + while ((halfHeight / inSampleSize) > reqHeight + && (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2; + } + } + + return inSampleSize; + } + + public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, + int reqWidth, int reqHeight) { + + + final BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeResource(res, resId, options); + + + options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); + + options.inJustDecodeBounds = false; + return BitmapFactory.decodeResource(res, resId, options); + } + private void StartAnimations(){ Animation anim = AnimationUtils.loadAnimation(this, R.anim.alpha); diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4396300..19ccd10 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -18,7 +18,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" - android:src="@drawable/logo_imdb" android:layout_gravity="center">