├── .gitignore ├── .travis.yml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ ├── assets │ │ └── search_results.json │ └── java │ │ └── com │ │ └── btellez │ │ └── solidandroid │ │ └── test │ │ ├── MockApplicationInjectable.java │ │ ├── SolidInstrumentationTestRunner.java │ │ ├── model │ │ └── DeserializeTest.java │ │ └── module │ │ └── SingletonTestModule.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── btellez │ │ └── solidandroid │ │ ├── SimpleAndroidApplication.java │ │ ├── activity │ │ ├── SearchResultsActivity.java │ │ ├── SelectionActivity.java │ │ └── SettingsActivity.java │ │ ├── configuration │ │ ├── Configuration.java │ │ └── KeyValueStore.java │ │ ├── model │ │ ├── ApiKeys.java │ │ ├── Icon.java │ │ ├── IconParser.java │ │ └── Uploader.java │ │ ├── module │ │ ├── DependencyInjector.java │ │ └── SingletonModule.java │ │ ├── network │ │ ├── NetworkBitmapClient.java │ │ ├── NounProjectApi.java │ │ ├── NounProjectOAuth.java │ │ └── OkNounProjectApi.java │ │ ├── utility │ │ ├── AnalyticsEvents.java │ │ ├── Strings.java │ │ └── Tracker.java │ │ └── view │ │ ├── EmptyView.java │ │ ├── FadeAnimator.java │ │ ├── SearchResultsView.java │ │ ├── SelectionScreenView.java │ │ └── SingleIconResultItem.java │ └── res │ ├── drawable-hdpi │ ├── ic_launcher.png │ └── ic_outbound.png │ ├── drawable-mdpi │ ├── ic_launcher.png │ └── ic_outbound.png │ ├── drawable-xhdpi │ ├── ic_launcher.png │ └── ic_outbound.png │ ├── drawable-xxhdpi │ ├── ic_launcher.png │ └── ic_outbound.png │ ├── drawable │ ├── solid_search.png │ └── solid_upload.png │ ├── layout │ ├── activity_main_user.xml │ ├── activity_search_results.xml │ ├── activity_selection.xml │ ├── error_view.xml │ └── single_icon_result_item.xml │ ├── menu │ └── main_user.xml │ ├── values-w820dp │ └── dimens.xml │ ├── values │ ├── colors.xml │ ├── config.xml │ ├── dimens.xml │ ├── strings.xml │ ├── strings_activity_settings.xml │ └── styles.xml │ └── xml │ └── pref_general.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Android Studio Files + OS Files 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .idea/ 7 | .DS_Store 8 | *.iml 9 | # Built application files 10 | *.apk 11 | *.ap_ 12 | 13 | # Files for the Dalvik VM 14 | *.dex 15 | 16 | # Java class files 17 | *.class 18 | 19 | # Generated files 20 | bin/ 21 | gen/ 22 | 23 | # Gradle files 24 | .gradle/ 25 | build/ 26 | 27 | # Local configuration file (sdk path, etc) 28 | local.properties 29 | 30 | # Proguard folder generated by Eclipse 31 | proguard/ 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | 3 | script: ./gradlew build assembleDebug -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SOLID: Noun Project Browser (Circa 2015) 2 | ============================ 3 | 4 | [![Build Status](https://travis-ci.org/blad/solid-android.svg?branch=master)](https://travis-ci.org/blad/solid-android) 5 | 6 | This project's goal is to be a demonstration of how [SOLID principles](http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29) can 7 | be applied to Android development. 8 | 9 |

10 |
11 | 12 |

13 | 14 | ## Contributing 15 | 16 | Contributions are welcome to the project. The goal is to adhere to the SOLID principles. 17 | 18 | ### Bug Fixes 19 | 20 | Bug fixes can be contributed via pull requests for this project. 21 | 22 | ### Features 23 | 24 | Features can be contributed via pull-requests for this project. 25 | 26 | ## Tools & Libraries 27 | 28 | - [Android Studio - Android Development Environment](https://developer.android.com/sdk/installing/studio.html) 29 | - [ButterKnife - View Injection Library](http://jakewharton.github.io/butterknife/) 30 | - [Dagger - Dependency Injection Library](http://square.github.io/dagger/) 31 | - [Gogole Gson - A Java library to convert JSON to Java objects](https://code.google.com/p/google-gson/) 32 | - [okHttp - An HTTP & SPDY client for Android and Java applications](http://square.github.io/okhttp/) 33 | - [Picasso - A powerful image downloading and caching library for Android](http://square.github.io/picasso/) 34 | - [Otto - Event Bus Library](http://square.github.io/otto/) 35 | - [Travis CI - Continuos integration platform](https://travis-ci.org/) 36 | 37 | ## Graphics & Icons 38 | 39 | ### The Noun Project 40 | 41 | [The Noun Project](http://www.thenounproject.com) is the source for some the graphics used in this application. The following users' work was used: 42 | 43 | - [Dice](http://thenounproject.com/term/dice/20125/) created by [Derek Palladino](http://thenounproject.com/derekjp/) 44 | - [Cloud-Upload](http://thenounproject.com/term/cloud-upload/9947/) created by [Scott Lewis](http://thenounproject.com/iconify/) 45 | - [Magnifying-Glass](http://thenounproject.com/term/magnifying-glass/89626/) public domain icon 46 | 47 | The noun project is a great resource for finding clip art for use in applications. 48 | 49 | ### Android Asset Studio 50 | 51 | Icon generator that allows you to quickly and easily generate icons from existing source images, clipart, or text. You can generate Launcher icons, Action bar and tab icons, Notification icons and Generic icons. The asset studio allows you to adjust sizing, padding, and tint icons. 52 | 53 | [Android Asset Studio](http://romannurik.github.io/AndroidAssetStudio/) 54 | 55 | ### Material Palette 56 | 57 | Material Pallet is a simple web app that allows you to generate a color 58 | pallet and export the corresponding xml. This allows non-designers to pick a pallet 59 | that makes sense visually, and aligns with the guidelines for Material Design. 60 | 61 | Additionally this helps enforce the correct use of color names in Android themes. 62 | 63 | [www.MaterialPalette.com](http://www.materialpalette.com/) 64 | 65 | ## Running this Project 66 | 67 | From Android Studio simply choose to import and select the `build.gradle` in the 68 | root directory of the repository. Android Studio will set everything else up 69 | automatically. 70 | 71 | ### The Noun Project API Keys 72 | 73 | To obtain api keys for The Noun Project visit the [Getting Started](http://api.thenounproject.com/getting_started.html) page for additional information 74 | 75 | Once you have the API key and secret, replace the placeholder values in: `app/src/main/res/values/nounproject_api_config.xml`. 76 | 77 | 78 | 79 | ## License 80 | 81 | > Copyright 2014 Bladymir Tellez 82 | > 83 | > Licensed under the Apache License, Version 2.0 (the "License"); 84 | > you may not use this file except in compliance with the License. 85 | > You may obtain a copy of the License at 86 | > 87 | > http://www.apache.org/licenses/LICENSE-2.0 88 | > 89 | > Unless required by applicable law or agreed to in writing, software 90 | > distributed under the License is distributed on an "AS IS" BASIS, 91 | > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 92 | > See the License for the specific language governing permissions and 93 | > limitations under the License. 94 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'android-sdk-manager' 2 | apply plugin: 'com.android.application' 3 | apply plugin: 'hugo' // call after applying android plugin 4 | 5 | android { 6 | compileSdkVersion 23 7 | buildToolsVersion "21.1.2" 8 | 9 | defaultConfig { 10 | testApplicationId "com.btellez.solidandroid.test" 11 | testInstrumentationRunner "com.btellez.solidandroid.test.SolidInstrumentationTestRunner" 12 | testHandleProfiling true 13 | testFunctionalTest true 14 | 15 | applicationId "com.btellez.solidandroid" 16 | minSdkVersion 15 17 | targetSdkVersion 23 18 | 19 | versionCode 2 20 | versionName "1.1" 21 | } 22 | 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_1_7 25 | targetCompatibility JavaVersion.VERSION_1_7 26 | } 27 | 28 | buildTypes { 29 | debug { 30 | testCoverageEnabled true 31 | } 32 | release { 33 | minifyEnabled false 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | } 37 | 38 | lintOptions { 39 | disable 'InvalidPackage' 40 | abortOnError false 41 | } 42 | 43 | packagingOptions { 44 | exclude 'META-INF/services/javax.annotation.processing.Processor' 45 | } 46 | } 47 | 48 | dependencies { 49 | // Include Any jar dependencies 50 | compile fileTree(dir: 'libs', include: ['*.jar']) 51 | 52 | // Testing Dependencies 53 | androidTestCompile 'com.google.dexmaker:dexmaker:1.1' 54 | androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.1' 55 | androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.1' 56 | androidTestCompile 'org.mockito:mockito-core:1.9.5' 57 | compile 'com.android.support:support-v4:23.0.1' 58 | compile 'com.android.support:support-v13:23.0.1' 59 | compile 'com.android.support:appcompat-v7:23.0.1' 60 | compile 'com.google.code.gson:gson:2.3.1' 61 | compile 'com.jakewharton:butterknife:5.1.1' 62 | compile 'com.squareup.dagger:dagger-compiler:1.2.2' 63 | compile 'com.squareup.dagger:dagger:1.2.2' 64 | compile 'com.squareup.okhttp:okhttp:2.1.0' 65 | compile 'com.squareup.okhttp:okhttp-urlconnection:2.1.0' 66 | compile 'com.squareup:otto:1.3.5' 67 | compile 'com.squareup.picasso:picasso:2.4.0' 68 | compile 'io.replay:replay-android:0.9.6' 69 | } 70 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/tellez/Libraries/Java/android-sdk-macosx/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | -keepclassmembers class ** { 20 | @com.squareup.otto.Subscribe public *; 21 | @com.squareup.otto.Produce public *; 22 | } 23 | 24 | -dontwarn butterknife.internal.** 25 | -keep class **$$ViewInjector { *; } 26 | -keepnames class * { @butterknife.InjectView *;} -------------------------------------------------------------------------------- /app/src/androidTest/java/com/btellez/solidandroid/test/MockApplicationInjectable.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.test; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import dagger.Module; 7 | import dagger.ObjectGraph; 8 | import dagger.Provides; 9 | 10 | import com.btellez.solidandroid.module.DependencyInjector; 11 | 12 | /** 13 | * This class defined standard behaviour that DaggerInjector needs, and provides an 14 | * application context for tests 15 | * 16 | * Define the getModules() method in an anonymous inner-class implementation defined 17 | * within each Test, since not all modules will be required for all tests. 18 | */ 19 | public abstract class MockApplicationInjectable extends Application implements DependencyInjector { 20 | 21 | //region Application Context Module 22 | @Module(library = true) 23 | public final class MockApplicationContextModule { 24 | Context context; 25 | public MockApplicationContextModule(Context app) { 26 | context = app; 27 | } 28 | 29 | @Provides 30 | public Context provideContext(){ 31 | return context; 32 | } 33 | } 34 | //endregion 35 | 36 | private ObjectGraph mObjectGraph; 37 | 38 | public MockApplicationInjectable(Context context) { 39 | super.attachBaseContext(context); 40 | mObjectGraph = ObjectGraph.create(getModules()); 41 | } 42 | 43 | @Override 44 | public void inject(Object o) { 45 | mObjectGraph.inject(o); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/btellez/solidandroid/test/SolidInstrumentationTestRunner.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.test; 2 | 3 | import android.test.InstrumentationTestRunner; 4 | import android.test.InstrumentationTestSuite; 5 | 6 | import com.btellez.solidandroid.test.model.DeserializeTest; 7 | 8 | import junit.framework.TestSuite; 9 | 10 | 11 | /** 12 | * Testing Fundamentals 13 | */ 14 | public class SolidInstrumentationTestRunner extends InstrumentationTestRunner { 15 | @Override 16 | public TestSuite getAllTests() { 17 | InstrumentationTestSuite tests = new InstrumentationTestSuite(this); 18 | 19 | // Add Test's To Be Ran Here: 20 | tests.addTestSuite(DeserializeTest.class); 21 | 22 | return tests; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/btellez/solidandroid/test/model/DeserializeTest.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.test.model; 2 | 3 | import android.test.InstrumentationTestCase; 4 | 5 | import com.btellez.solidandroid.model.Icon; 6 | import com.btellez.solidandroid.model.IconParser; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.util.List; 11 | 12 | public class DeserializeTest extends InstrumentationTestCase { 13 | 14 | public void testSearchResultDeserialization() throws Exception { 15 | String jsonString = readFileFromAssets("search_results.json"); 16 | IconParser.GsonIconParser parser = new IconParser.GsonIconParser(); 17 | List icons = parser.fromJson(jsonString, null); 18 | assertEquals(72, icons.size()); 19 | } 20 | 21 | /** 22 | * Helper method for instrumentation test to load asset serialized objects 23 | * from the assets directory. 24 | * * 25 | * @param fileName 26 | * @return 27 | */ 28 | private String readFileFromAssets(String fileName) { 29 | try { 30 | InputStream is = getInstrumentation().getContext().getAssets().open(fileName); 31 | int size = is.available(); 32 | byte[] buffer = new byte[size]; 33 | is.read(buffer); 34 | is.close(); 35 | return new String(buffer); 36 | } catch (IOException e) { 37 | throw new RuntimeException(e); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/btellez/solidandroid/test/module/SingletonTestModule.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.test.module; 2 | 3 | import android.content.Context; 4 | 5 | import com.squareup.otto.Bus; 6 | 7 | import org.mockito.Mockito; 8 | 9 | import javax.inject.Singleton; 10 | 11 | import dagger.Module; 12 | import dagger.Provides; 13 | 14 | import com.btellez.solidandroid.test.MockApplicationInjectable; 15 | 16 | /** 17 | * This class provides mock instances of the singletons so that 18 | * behaviours can be verified using Mockito during tests. 19 | */ 20 | @Module(library = true, includes = MockApplicationInjectable.MockApplicationContextModule.class) 21 | public class SingletonTestModule { 22 | 23 | @Provides @Singleton Bus provideEventBus() { 24 | return Mockito.mock(Bus.class); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/SimpleAndroidApplication.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | 6 | import com.btellez.solidandroid.activity.SearchResultsActivity; 7 | import com.btellez.solidandroid.activity.SelectionActivity; 8 | import com.btellez.solidandroid.activity.SettingsActivity; 9 | import com.btellez.solidandroid.module.DependencyInjector; 10 | 11 | import dagger.Module; 12 | import dagger.ObjectGraph; 13 | import dagger.Provides; 14 | 15 | public class SimpleAndroidApplication extends Application implements DependencyInjector { 16 | 17 | //region Application Context Module 18 | @Module(library = true) 19 | public final class ApplicationContextModule { 20 | Context context; 21 | public ApplicationContextModule(Context app) { 22 | context = app; 23 | } 24 | 25 | @Provides public Context provideContext() { 26 | return context; 27 | } 28 | } 29 | //endregion 30 | 31 | private ObjectGraph mObjectGraph; 32 | 33 | @Override 34 | public void onCreate() { 35 | super.onCreate(); 36 | mObjectGraph = ObjectGraph.create(getModules()); 37 | } 38 | 39 | 40 | @Override 41 | public Object[] getModules() { 42 | return new Object[]{ 43 | new ApplicationContextModule(this), 44 | new SearchResultsActivity.SearchResultDepedencyModule(), 45 | new SelectionActivity.SelectionActivityDepedencyModule(), 46 | new SettingsActivity.GeneralPreferenceFragment.GeneralPreferenceDepedencyModule() 47 | }; 48 | } 49 | 50 | 51 | @Override 52 | public void inject(Object o) { 53 | mObjectGraph.inject(o); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/activity/SearchResultsActivity.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.activity; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.os.Bundle; 7 | import android.support.v4.app.FragmentActivity; 8 | import android.view.Menu; 9 | import android.view.MenuInflater; 10 | import android.view.MenuItem; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | import android.widget.BaseAdapter; 14 | import android.widget.ImageView; 15 | 16 | import com.btellez.solidandroid.R; 17 | import com.btellez.solidandroid.configuration.Configuration; 18 | import com.btellez.solidandroid.model.Icon; 19 | import com.btellez.solidandroid.module.DependencyInjector; 20 | import com.btellez.solidandroid.module.SingletonModule; 21 | import com.btellez.solidandroid.network.NetworkBitmapClient; 22 | import com.btellez.solidandroid.network.NounProjectApi; 23 | import com.btellez.solidandroid.utility.Strings; 24 | import com.btellez.solidandroid.utility.Tracker; 25 | import com.btellez.solidandroid.view.EmptyView; 26 | import com.btellez.solidandroid.view.SearchResultsView; 27 | import com.btellez.solidandroid.view.SingleIconResultItem; 28 | import com.google.gson.JsonSyntaxException; 29 | 30 | import java.util.List; 31 | 32 | import javax.inject.Inject; 33 | 34 | import dagger.Module; 35 | 36 | public class SearchResultsActivity extends FragmentActivity { 37 | 38 | private static final String EXTRA_QUERY_STRING = "extra_query_string"; 39 | private static final String ACTION_DISPLAY_SEARCH_RESULTS = "action_display_search_results"; 40 | private static final String ACTION_DISPLAY_RECENT_UPLOADS = "action_display_recent_uploads"; 41 | 42 | @Inject NetworkBitmapClient networkBitmap; 43 | @Inject NounProjectApi nounProjectApi; 44 | @Inject Configuration configuration; 45 | @Inject Tracker tracker; 46 | 47 | protected List data; 48 | protected BaseAdapter adapter; 49 | protected SingleIconResultItem.Listener itemListener; 50 | protected SearchResultsView contentView; 51 | 52 | 53 | @Override 54 | protected void onCreate(Bundle savedInstanceState) { 55 | super.onCreate(savedInstanceState); 56 | ((DependencyInjector) getApplication()).inject(this); 57 | contentView = new SearchResultsView(this); 58 | setContentView(contentView); 59 | itemListener = new ViewListener(); 60 | adapter = new IconListAdapter(); 61 | contentView.setAdapter(adapter); 62 | contentView.setState(SearchResultsView.State.Loading); 63 | contentView.setEmptyActionListener(getEmptyActionListener()); 64 | contentView.setErrorActionListener(getErrorActionListener()); 65 | executeIntentAction(); 66 | } 67 | 68 | @Override 69 | public boolean onCreateOptionsMenu(Menu menu) { 70 | MenuInflater inflater = getMenuInflater(); 71 | inflater.inflate(R.menu.main_user, menu); 72 | return true; 73 | } 74 | 75 | @Override 76 | public boolean onOptionsItemSelected(MenuItem item) { 77 | switch (item.getItemId()) { 78 | case R.id.menu_settings: 79 | startActivity(new Intent(this, SettingsActivity.class)); 80 | return true; 81 | } 82 | return super.onOptionsItemSelected(item); 83 | } 84 | 85 | private EmptyView.Listener getEmptyActionListener() { 86 | return new EmptyView.Listener() { 87 | @Override 88 | public void onPrimaryActionClicked() { 89 | tracker.track("Empty Action Clicked", "button", "primary"); 90 | finish(); 91 | } 92 | 93 | @Override 94 | public void onSecondaryActionClicked() { 95 | tracker.track("Empty Action Clicked", "button", "secondary"); 96 | finish(); 97 | } 98 | }; 99 | } 100 | 101 | private EmptyView.Listener getErrorActionListener() { 102 | return new EmptyView.Listener() { 103 | @Override 104 | public void onPrimaryActionClicked() { 105 | tracker.track("Error Action Clicked", "button", "primary"); 106 | executeIntentAction(); 107 | contentView.setState(SearchResultsView.State.Loading); 108 | } 109 | 110 | @Override 111 | public void onSecondaryActionClicked() { 112 | tracker.track("Error Action Clicked", "button", "secondary"); 113 | finish(); 114 | } 115 | }; 116 | } 117 | 118 | private void executeIntentAction() { 119 | if (ACTION_DISPLAY_RECENT_UPLOADS.equals(getIntent().getAction())) { 120 | nounProjectApi.recent(new ApiCallback()); 121 | setTitle(R.string.recent_uploads); 122 | } else { 123 | String query = getIntent().getStringExtra(EXTRA_QUERY_STRING); 124 | nounProjectApi.search(query, new ApiCallback()); 125 | setTitle(getString(R.string.query_pattern, query)); 126 | } 127 | } 128 | 129 | public static class Builder { 130 | String query; 131 | Context context; 132 | 133 | public Builder(Context context) { 134 | this.context = context; 135 | } 136 | 137 | public Builder withSearchTerm(String query) { 138 | this.query = query; 139 | return this; 140 | } 141 | 142 | public Intent build() { 143 | Intent intent = new Intent(context, SearchResultsActivity.class); 144 | if (!Strings.isEmpty(query)) { 145 | intent.setAction(ACTION_DISPLAY_SEARCH_RESULTS); 146 | intent.putExtra(EXTRA_QUERY_STRING, query); 147 | } else { 148 | intent.setAction(ACTION_DISPLAY_RECENT_UPLOADS); 149 | } 150 | return intent; 151 | } 152 | } 153 | 154 | private class ViewListener implements SingleIconResultItem.Listener { 155 | @Override 156 | public void onLinkClicked(String path) { 157 | tracker.track("Icon Selected", "icon", path); 158 | String finalUrl = configuration.getNounProjectBaseUrl() + path; 159 | Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalUrl)); 160 | startActivity(intent); 161 | } 162 | 163 | @Override 164 | public void requestDownloadInto(String url, ImageView imageView) { 165 | networkBitmap.downloadInto(url, imageView); 166 | } 167 | } 168 | 169 | private class IconListAdapter extends BaseAdapter { 170 | @Override 171 | public int getCount() { 172 | if (data == null) 173 | return 0; 174 | return data.size(); 175 | } 176 | @Override 177 | public Object getItem(int position) { 178 | return data.get(position); 179 | } 180 | @Override 181 | public long getItemId(int position) { 182 | return position; 183 | } 184 | @Override 185 | public boolean areAllItemsEnabled() { 186 | return false; 187 | } 188 | @Override 189 | public boolean isEnabled(int position) { 190 | return false; 191 | } 192 | @Override 193 | public View getView(int position, View convertView, ViewGroup parent) { 194 | SingleIconResultItem listItem = (SingleIconResultItem) convertView; 195 | if (listItem == null) { 196 | listItem = new SingleIconResultItem(SearchResultsActivity.this); 197 | listItem.setListener(itemListener); 198 | } 199 | listItem.setIconData((Icon) getItem(position)); 200 | return listItem; 201 | } 202 | } 203 | 204 | private class ApiCallback implements NounProjectApi.Callback { 205 | @Override 206 | public void onSuccess(List icons) { 207 | data = icons; 208 | adapter.notifyDataSetChanged(); 209 | contentView.setState(SearchResultsView.State.Loaded); 210 | tracker.track("Success Load", "count", String.valueOf(icons.size())); 211 | } 212 | 213 | @Override 214 | public void onFailure(Throwable error) { 215 | tracker.track("Failed Load", "error", error.toString()); 216 | boolean isSearch = !ACTION_DISPLAY_RECENT_UPLOADS.equals(getIntent().getAction()); 217 | boolean isNoResultJson = error instanceof JsonSyntaxException; 218 | if (isSearch && isNoResultJson) { 219 | contentView.setState(SearchResultsView.State.Empty); 220 | } else { 221 | contentView.setState(SearchResultsView.State.Error); 222 | } 223 | adapter.notifyDataSetChanged(); 224 | } 225 | } 226 | 227 | @Module(injects = {SearchResultsActivity.class}, includes = SingletonModule.class) 228 | public static class SearchResultDepedencyModule { } 229 | } 230 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/activity/SelectionActivity.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.activity; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.app.FragmentActivity; 6 | import android.view.Menu; 7 | import android.view.MenuInflater; 8 | import android.view.MenuItem; 9 | 10 | import com.btellez.solidandroid.R; 11 | import com.btellez.solidandroid.module.DependencyInjector; 12 | import com.btellez.solidandroid.module.SingletonModule; 13 | import com.btellez.solidandroid.utility.Tracker; 14 | import com.btellez.solidandroid.view.SelectionScreenView; 15 | 16 | import javax.inject.Inject; 17 | 18 | import dagger.Module; 19 | 20 | public class SelectionActivity extends FragmentActivity { 21 | 22 | private SelectionScreenView selectionScreenView; 23 | @Inject Tracker tracker; 24 | 25 | @Override 26 | protected void onCreate(Bundle savedInstanceState) { 27 | super.onCreate(savedInstanceState); 28 | ((DependencyInjector) getApplication()).inject(this); 29 | selectionScreenView = new SelectionScreenView(this); 30 | setContentView(selectionScreenView); 31 | selectionScreenView.setListener(new SelectionScreenView.SimpleListener() { 32 | @Override 33 | public void onRecentGroupSelected() { 34 | Intent intent = new SearchResultsActivity 35 | .Builder(SelectionActivity.this) 36 | .build(); 37 | startActivity(intent); 38 | tracker.track("Recent Selected"); 39 | } 40 | 41 | @Override 42 | public void onSubmitSearchQuery(String query) { 43 | Intent intent = new SearchResultsActivity 44 | .Builder(SelectionActivity.this) 45 | .withSearchTerm(query).build(); 46 | startActivity(intent); 47 | tracker.track("Search Submitted", "query", query); 48 | } 49 | }); 50 | } 51 | 52 | @Override 53 | public boolean onCreateOptionsMenu(Menu menu) { 54 | MenuInflater inflater = getMenuInflater(); 55 | inflater.inflate(R.menu.main_user, menu); 56 | return true; 57 | } 58 | 59 | @Override 60 | public boolean onOptionsItemSelected(MenuItem item) { 61 | switch (item.getItemId()) { 62 | case R.id.menu_settings: 63 | startActivity(new Intent(this, SettingsActivity.class)); 64 | return true; 65 | } 66 | return super.onOptionsItemSelected(item); 67 | } 68 | 69 | @Module(injects = {SelectionActivity.class}, includes = SingletonModule.class) 70 | public static class SelectionActivityDepedencyModule { } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/activity/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.activity; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.preference.Preference; 6 | import android.preference.PreferenceFragment; 7 | 8 | import com.btellez.solidandroid.R; 9 | import com.btellez.solidandroid.module.DependencyInjector; 10 | import com.btellez.solidandroid.module.SingletonModule; 11 | import com.btellez.solidandroid.utility.Tracker; 12 | 13 | import javax.inject.Inject; 14 | 15 | import dagger.Module; 16 | 17 | public class SettingsActivity extends Activity { 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | getFragmentManager() 22 | .beginTransaction() 23 | .replace(android.R.id.content, new GeneralPreferenceFragment()) 24 | .commit(); 25 | } 26 | 27 | public static class GeneralPreferenceFragment extends PreferenceFragment { 28 | 29 | @Inject Tracker tracker; 30 | 31 | @Override 32 | public void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | addPreferencesFromResource(R.xml.pref_general); 35 | 36 | findPreference(getString(R.string.pref_key_enable_history)) 37 | .setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { 38 | @Override 39 | public boolean onPreferenceClick(Preference preference) { 40 | tracker.track("Toggled History", "from", preference.getSummary().toString()); 41 | // TODO: Clear History. 42 | return true; 43 | } 44 | }); 45 | } 46 | 47 | @Override 48 | public void onAttach(Activity activity) { 49 | super.onAttach(activity); 50 | ((DependencyInjector) activity.getApplication()).inject(this); 51 | } 52 | 53 | @Module(injects = {GeneralPreferenceFragment.class}, includes = SingletonModule.class) 54 | public static class GeneralPreferenceDepedencyModule { } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/configuration/Configuration.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.configuration; 2 | 3 | import android.content.Context; 4 | 5 | import com.btellez.solidandroid.R; 6 | import com.btellez.solidandroid.model.ApiKeys; 7 | 8 | public interface Configuration { 9 | ApiKeys getNounProjectApiKeys(); 10 | String getNounProjectBaseUrl(); 11 | String getNounProjectBaseApiUrl(); 12 | String getReplayAPIKey(); 13 | 14 | /** 15 | * Shared configuration settings for Release and Development. 16 | */ 17 | abstract class BaseConfiguration implements Configuration { 18 | protected Context context; 19 | 20 | protected BaseConfiguration(Context context) { 21 | this.context = context; 22 | } 23 | 24 | @Override 25 | public String getNounProjectBaseApiUrl() { 26 | return context.getString(R.string.noun_project_base_api_url); 27 | } 28 | 29 | @Override 30 | public String getNounProjectBaseUrl() { 31 | return context.getString(R.string.noun_project_base_url); 32 | } 33 | 34 | @Override 35 | public String getReplayAPIKey() { 36 | return context.getString(R.string.replay_io_api_key); 37 | } 38 | } 39 | 40 | /** 41 | * Configuration for the Release Version of our Application. 42 | */ 43 | class ReleaseConfiguration extends BaseConfiguration { 44 | 45 | protected ReleaseConfiguration(Context context) { 46 | super(context); 47 | } 48 | 49 | @Override 50 | public ApiKeys getNounProjectApiKeys() { 51 | return new ApiKeys() { 52 | @Override public String getKey() { 53 | return context.getResources().getStringArray(R.array.noun_project_api_key)[1]; 54 | } 55 | 56 | @Override public String getSecret() { 57 | return context.getResources().getStringArray(R.array.noun_project_api_secret)[1]; 58 | } 59 | }; 60 | } 61 | } 62 | 63 | 64 | /** 65 | * Configuration for the Development version of our application 66 | */ 67 | class DevelopmentConfiguration extends BaseConfiguration { 68 | 69 | public DevelopmentConfiguration(Context context) { 70 | super(context); 71 | } 72 | 73 | @Override 74 | public ApiKeys getNounProjectApiKeys() { 75 | return new ApiKeys() { 76 | @Override public String getKey() { 77 | return context.getResources().getStringArray(R.array.noun_project_api_key)[0]; 78 | } 79 | 80 | @Override public String getSecret() { 81 | return context.getResources().getStringArray(R.array.noun_project_api_secret)[0]; 82 | } 83 | }; 84 | } 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/configuration/KeyValueStore.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.configuration; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import java.util.HashMap; 7 | 8 | /** 9 | * KeyValueStore interface defined common methods that 10 | * may be used to store a set of key's and values to 11 | * a more persistant storage. 12 | */ 13 | public interface KeyValueStore { 14 | 15 | void putFloat(String key, float value); 16 | float getFloat(String key, float defaultValue); 17 | void putInt(String key, int value); 18 | int getInt(String key, int defaultValue); 19 | String getString(String key, String defaultValue); 20 | String getString(String key); 21 | void putString(String key, String value); 22 | boolean getBool(String key); 23 | void putBool(String key, boolean value); 24 | 25 | 26 | /** 27 | * No-Op implementation of KeyValue Store. 28 | * 29 | * This allows us to selectively override some and not all methods. 30 | * This allows us to use this instead as to satisfy dependencies in 31 | * tests where we are not relying on the behaviour of KeyValueStore. 32 | */ 33 | class SimpleKeyValueStore implements KeyValueStore { 34 | @Override public void putFloat(String key, float value) {} 35 | @Override public float getFloat(String key, float defaultValue) {return 0;} 36 | @Override public void putInt(String key, int value) {} 37 | @Override public int getInt(String key, int defaultValue) {return 0;} 38 | @Override public String getString(String key, String defaultValue) {return null;} 39 | @Override public String getString(String key) {return null;} 40 | @Override public void putString(String key, String value) {} 41 | @Override public boolean getBool(String key) {return false;} 42 | @Override public void putBool(String key, boolean value) {} 43 | } 44 | 45 | /** 46 | * Shared Preferences implementation of the key value store. 47 | * The values stored in with this key-value store implementation 48 | * will persist as long as the application is not unisntalled. 49 | */ 50 | class SharedPrefs implements KeyValueStore { 51 | private static final String SHARE_PREFS_KEY_VALUE_STORE = "share_prefs_key_value_store"; 52 | private SharedPreferences prefs; 53 | 54 | public SharedPrefs(Context context) { 55 | this(context, SHARE_PREFS_KEY_VALUE_STORE); 56 | } 57 | 58 | public SharedPrefs(Context context, String storeIdentifier) { 59 | prefs = context.getSharedPreferences(storeIdentifier, Context.MODE_PRIVATE); 60 | } 61 | 62 | @Override 63 | public void putFloat(String key, float value) { 64 | prefs.edit().putFloat(key, value).apply(); 65 | } 66 | 67 | @Override 68 | public float getFloat(String key, float defaultValue) { 69 | return prefs.getFloat(key, defaultValue); 70 | } 71 | 72 | @Override 73 | public void putInt(String key, int value) { 74 | prefs.edit().putInt(key, value).apply(); 75 | } 76 | 77 | @Override 78 | public int getInt(String key, int defaultValue) { 79 | return prefs.getInt(key, defaultValue); 80 | } 81 | 82 | @Override 83 | public void putString(String key, String value) { 84 | prefs.edit().putString(key, value).apply(); 85 | } 86 | 87 | @Override 88 | public boolean getBool(String key) { 89 | return prefs.getBoolean(key, false); 90 | } 91 | 92 | @Override 93 | public void putBool(String key, boolean value) { 94 | prefs.edit().putBoolean(key, value).apply(); 95 | } 96 | 97 | @Override 98 | public String getString(String key) { 99 | return getString(key, null); 100 | } 101 | 102 | @Override 103 | public String getString(String key, String defaultValue) { 104 | return prefs.getString(key, defaultValue); 105 | } 106 | } 107 | 108 | /** 109 | * Session is an in-memory key-value store whose data will 110 | * cease to exist when the application is killed. 111 | */ 112 | class Session implements KeyValueStore { 113 | private HashMap map = new HashMap(); 114 | 115 | @Override 116 | public void putFloat(String key, float value) { 117 | map.put(key, value); 118 | } 119 | 120 | @Override 121 | public float getFloat(String key, float defaultValue) { 122 | return map.containsKey(key) ? (Float) map.get(key) : defaultValue; 123 | } 124 | 125 | @Override 126 | public void putInt(String key, int value) { 127 | map.put(key, value); 128 | } 129 | 130 | @Override 131 | public int getInt(String key, int defaultValue) { 132 | return map.containsKey(key) ? (Integer) map.get(key) : defaultValue; 133 | } 134 | 135 | @Override 136 | public String getString(String key) { 137 | return getString(key, null); 138 | } 139 | 140 | @Override 141 | public String getString(String key, String defaultValue) { 142 | return map.containsKey(key) ? (String) map.get(key) : defaultValue; 143 | } 144 | 145 | @Override 146 | public void putString(String key, String value) { 147 | map.put(key, value); 148 | } 149 | 150 | @Override 151 | public boolean getBool(String key) { 152 | return map.containsKey(key) && (boolean) map.get(key); 153 | } 154 | 155 | @Override 156 | public void putBool(String key, boolean value) { 157 | map.put(key, value); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/model/ApiKeys.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.model; 2 | 3 | public interface ApiKeys { 4 | String getKey(); 5 | String getSecret(); 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/model/Icon.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.model; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Object representing icon information returned 7 | * by the NounProject API. 8 | */ 9 | public class Icon implements Serializable { 10 | private String attribution; 11 | private String attribution_icon_url; 12 | private String attribution_preview_url; 13 | private int count_download; 14 | private int count_purchase; 15 | private int count_view; 16 | private String date_uploaded; 17 | private String id; 18 | private String is_active; 19 | private String license_description; 20 | private String permalink; 21 | private String preview_url; 22 | private String preview_url_42; 23 | private String preview_url_84; 24 | private String sponsor_campaign_link; 25 | private String sponsor_id; 26 | private String term; 27 | private String term_id; 28 | private String term_slug; 29 | private Uploader uploader; 30 | private String uploader_id; 31 | private int year; 32 | 33 | public String getAttribution() { 34 | return attribution; 35 | } 36 | 37 | public int getDownloadCount() { 38 | return count_download; 39 | } 40 | 41 | public int getPurchaseCount() { 42 | return count_purchase; 43 | } 44 | 45 | public int getViewCount() { 46 | return count_view; 47 | } 48 | 49 | public String getPermalink() { 50 | return permalink; 51 | } 52 | 53 | public String getTerm() { 54 | return term; 55 | } 56 | 57 | public String getPreviewUrl() { 58 | return preview_url; 59 | } 60 | 61 | public Uploader getUploader() { 62 | return uploader; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/model/IconParser.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.model; 2 | 3 | import com.btellez.solidandroid.utility.Strings; 4 | import com.google.gson.Gson; 5 | import com.google.gson.GsonBuilder; 6 | import com.google.gson.JsonArray; 7 | import com.google.gson.JsonDeserializationContext; 8 | import com.google.gson.JsonDeserializer; 9 | import com.google.gson.JsonElement; 10 | import com.google.gson.JsonObject; 11 | import com.google.gson.JsonParseException; 12 | import com.google.gson.JsonParser; 13 | 14 | import java.lang.reflect.Type; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | public interface IconParser { 19 | List fromJson(String jsonString, String dataKey); 20 | 21 | /** 22 | * Google Gson Implementation of our Icon Parser 23 | */ 24 | class GsonIconParser implements IconParser { 25 | Gson gson = new GsonBuilder().registerTypeAdapter(Icon.class, new IconDeserializer()).create(); 26 | JsonParser parser = new JsonParser(); 27 | 28 | @Override 29 | public List fromJson(String jsonString, String dataKey) { 30 | JsonElement json = parser.parse(jsonString); 31 | JsonArray iconList; 32 | if (Strings.isEmpty(dataKey)) { 33 | iconList = json.getAsJsonArray(); 34 | } else { 35 | iconList = json.getAsJsonObject().getAsJsonArray(dataKey); 36 | } 37 | 38 | List icons = new ArrayList<>(iconList.size()); 39 | for (JsonElement item : iconList) { 40 | icons.add(gson.fromJson(item, Icon.class)); 41 | } 42 | return icons; 43 | } 44 | 45 | private class IconDeserializer implements JsonDeserializer { 46 | Gson defaultGson = new Gson(); 47 | @Override 48 | public Icon deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 49 | JsonElement uploader = json.getAsJsonObject().get("uploader"); 50 | if (!uploader.isJsonObject()) { 51 | json.getAsJsonObject().remove("uploader"); 52 | json.getAsJsonObject().add("uploader", new JsonObject()); 53 | } 54 | return defaultGson.fromJson(json, Icon.class); 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/model/Uploader.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.model; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * Object Representing user information returned 7 | * by the NounProject API. 8 | */ 9 | public class Uploader implements Serializable{ 10 | private String location; 11 | private String name; 12 | private String permalink; 13 | private String username; 14 | 15 | public String getLocation() { 16 | return location; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public String getPermalink() { 24 | return permalink; 25 | } 26 | 27 | public String getUsername() { 28 | return username; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/module/DependencyInjector.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.module; 2 | 3 | import android.content.Context; 4 | 5 | import dagger.ObjectGraph; 6 | 7 | /** 8 | * This interface defines behaviour that must be implemented by any 9 | * class that is to act as an injector. Ideally it should be a class 10 | * that will be a singleton instance and global in scope. Extensions 11 | * of `Application` are ideal for this. 12 | */ 13 | public interface DependencyInjector { 14 | public Object[] getModules(); 15 | public void inject(Object o); 16 | 17 | 18 | public static class SolidInjector implements DependencyInjector { 19 | private ObjectGraph mObjectGraph; 20 | private Context context; 21 | 22 | public SolidInjector(Context context) { 23 | this.context = context; 24 | } 25 | 26 | @Override 27 | public Object[] getModules() { 28 | return new Object[]{ 29 | // Each Activity and Class Defined their own Injector, 30 | // Shared Modules should go under the ../Module package 31 | }; 32 | } 33 | 34 | @Override 35 | public void inject(Object o) { 36 | mObjectGraph.inject(o); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/module/SingletonModule.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.module; 2 | 3 | import android.content.Context; 4 | 5 | import com.btellez.solidandroid.BuildConfig; 6 | import com.btellez.solidandroid.SimpleAndroidApplication; 7 | import com.btellez.solidandroid.configuration.Configuration; 8 | import com.btellez.solidandroid.model.IconParser; 9 | import com.btellez.solidandroid.network.NetworkBitmapClient; 10 | import com.btellez.solidandroid.network.NounProjectApi; 11 | import com.btellez.solidandroid.network.OkNounProjectApi; 12 | import com.btellez.solidandroid.utility.Tracker; 13 | import com.squareup.otto.Bus; 14 | 15 | import javax.inject.Singleton; 16 | 17 | import dagger.Module; 18 | import dagger.Provides; 19 | 20 | /** 21 | * This module is a provider for singletons across the application. 22 | * 23 | * If you find yourself creating a singleton class, you may be able to simply 24 | * use the @Singleton annotation and inject the singleton into your activities 25 | * or fragments. 26 | * 27 | * This module is intended to be extended. Use `includes = SingletonModule.class` 28 | * to include the injections defined here. 29 | * 30 | * including the ApplicationContextModule allows us to resolve the Context dependencies 31 | * automatically. Ensure that a ApplicationContextModule is initialize and added to the 32 | * list of modules being initialized. 33 | */ 34 | @Module(library = true, includes = SimpleAndroidApplication.ApplicationContextModule.class) 35 | public class SingletonModule { 36 | 37 | @Provides @Singleton Configuration provideConfiguration(Context context) { 38 | return new Configuration.DevelopmentConfiguration(context); 39 | } 40 | 41 | @Provides @Singleton IconParser provudeIconParser() { 42 | return new IconParser.GsonIconParser(); 43 | } 44 | 45 | @Provides @Singleton NounProjectApi provideNounProjectApi(Configuration configuration, IconParser parser) { 46 | return new OkNounProjectApi(configuration, parser); 47 | } 48 | 49 | @Provides @Singleton NetworkBitmapClient provideNetworkBitmapClient() { 50 | return new NetworkBitmapClient.PicassoBitmapClient(); 51 | } 52 | 53 | @Provides @Singleton Bus provideEventBus() { 54 | return new Bus(); 55 | } 56 | 57 | @Provides @Singleton Tracker provideTracker(Context context, Configuration appConfig) { 58 | if (BuildConfig.DEBUG) { 59 | return new Tracker.SimpleTracker(); 60 | } else { 61 | return new Tracker.ReplayTracker(context, appConfig); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/network/NetworkBitmapClient.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.network; 2 | 3 | import android.widget.ImageView; 4 | 5 | import com.squareup.picasso.Picasso; 6 | 7 | public interface NetworkBitmapClient { 8 | void downloadInto(String url, ImageView imageView); 9 | 10 | /** 11 | * Picasso Implementation of NetworkBitmapClient 12 | */ 13 | class PicassoBitmapClient implements NetworkBitmapClient { 14 | @Override public void downloadInto(String url, ImageView imageView) { 15 | Picasso.with(imageView.getContext()).load(url).into(imageView); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/network/NounProjectApi.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.network; 2 | 3 | import com.btellez.solidandroid.configuration.Configuration; 4 | import com.btellez.solidandroid.model.Icon; 5 | 6 | import java.util.List; 7 | 8 | public interface NounProjectApi { 9 | void search(String term, Callback callback); 10 | void recent(Callback callback); 11 | 12 | /** 13 | * Endpoint Helper that builds correctly formatter URLs 14 | * for the NounProjectAPI. 15 | */ 16 | class EndpointBuilder { 17 | public String buildSearchUrl(Configuration config, String term) { 18 | return config.getNounProjectBaseUrl() + "/search/json/icon/?q=" + term + "/?page=1&limit=100&offset=0&raw_html=false"; 19 | } 20 | 21 | public String buildRecentUrl(Configuration config) { 22 | return config.getNounProjectBaseApiUrl() + "icons/recent_uploads"; 23 | } 24 | } 25 | 26 | /** 27 | * Callback that alerts any listeners about success or failure of the api call. 28 | */ 29 | interface Callback { 30 | void onSuccess(List icons); 31 | void onFailure(Throwable error); 32 | } 33 | 34 | 35 | /** 36 | * No-Op Implementation of the Callback that allows overriding select methods, 37 | * instead of forcing implementor to override all methods. 38 | */ 39 | class SimpleCallback implements Callback { 40 | @Override public void onSuccess(List icons) {/* no-op */} 41 | @Override public void onFailure(Throwable error) {/* no-op */} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/network/NounProjectOAuth.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.network; 2 | 3 | import android.net.Uri; 4 | import android.support.v4.util.Pair; 5 | import android.util.Base64; 6 | 7 | import com.btellez.solidandroid.model.ApiKeys; 8 | import com.btellez.solidandroid.utility.Strings; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.Comparator; 13 | import java.util.List; 14 | 15 | import javax.crypto.Mac; 16 | import javax.crypto.spec.SecretKeySpec; 17 | 18 | /** 19 | * Based on OAuth Implementation on TheNounProject Api Explorer: 20 | * http://api.thenounproject.com/lib/oauth-1.0a.js 21 | */ 22 | public class NounProjectOAuth { 23 | 24 | public enum RequestType {GET, POST} 25 | 26 | private static final String AMP = "&"; 27 | private static final String EQ = "="; 28 | 29 | private ApiKeys keys; 30 | private String endpoint; 31 | private RequestType requestType; 32 | 33 | public NounProjectOAuth(ApiKeys keys) { 34 | this.keys = keys; 35 | } 36 | 37 | public NounProjectOAuth withEnpoint(String endpoint) { 38 | this.endpoint = endpoint; 39 | return this; 40 | } 41 | 42 | public NounProjectOAuth withRequestType(RequestType requestType) { 43 | this.requestType = requestType; 44 | return this; 45 | } 46 | 47 | public String getOAuthHeader() { 48 | assertReady(endpoint, requestType); 49 | List> oAuthHeaderList = new ArrayList<>(); 50 | oAuthHeaderList.add(new Pair<>("oauth_signature_method", "HMAC-SHA1")); 51 | oAuthHeaderList.add(new Pair<>("oauth_version", "1.0")); 52 | oAuthHeaderList.add(new Pair<>("oauth_consumer_key", keys.getKey())); 53 | oAuthHeaderList.add(new Pair<>("oauth_nonce", getNounce(32))); 54 | oAuthHeaderList.add(new Pair<>("oauth_timestamp", String.valueOf(System.currentTimeMillis()/1000))); 55 | 56 | String signature = computeSignature(sortOAuthHeaderParams(oAuthHeaderList)); 57 | oAuthHeaderList.add(new Pair<>("oauth_signature", encode(signature))); 58 | return "OAuth "+ getOAuthheaderString(sortOAuthHeaderParams(oAuthHeaderList)); 59 | } 60 | 61 | private void assertReady(String endpoint, RequestType requestType) { 62 | if (Strings.isEmpty(endpoint) || requestType == null) { 63 | throw new IllegalArgumentException("Endpoint and Request type are both required fields."); 64 | } 65 | } 66 | 67 | private String getOAuthheaderString(List> headerList) { 68 | StringBuffer sb = new StringBuffer(); 69 | for (Pair headers : headerList) { 70 | sb.append(String.format("%s=\"%s\", ", headers.first, headers.second)); 71 | } 72 | return sb.subSequence(0, sb.length()-2).toString(); // trim extra space and comma.; 73 | } 74 | 75 | protected String computeSignature(List> headerList) { 76 | String baseString = requestType.name().toUpperCase() + AMP; 77 | baseString += encode(endpoint) + AMP; 78 | baseString += encode(getParamString(headerList)); 79 | return calculateHMACSHA1(getSignatureKey(), baseString); 80 | } 81 | 82 | private String getParamString(List> headerList) { 83 | StringBuffer paramsString = new StringBuffer(); 84 | for (Pair set : headerList) { 85 | paramsString.append(encode(set.first)).append(EQ) 86 | .append(encode(set.second)).append(AMP); 87 | } 88 | return paramsString.subSequence(0, paramsString.length() - 1).toString(); // trim extra ampersand 89 | } 90 | 91 | // Utility Methods 92 | 93 | private String getSignatureKey() { 94 | return encode(keys.getSecret()) + AMP; 95 | } 96 | 97 | private String getNounce(int length) { 98 | String alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; 99 | StringBuilder result = new StringBuilder(32); 100 | 101 | for(int i = 0; i < length; i++) { 102 | result.append(alphabet.charAt((int) Math.floor(Math.random() * alphabet.length()))); 103 | } 104 | return result.toString(); 105 | } 106 | 107 | private String encode(String s) { 108 | return Uri.encode(s); 109 | } 110 | 111 | private List> sortOAuthHeaderParams(List> headerList) { 112 | Collections.sort(headerList, new Comparator>() { 113 | @Override 114 | public int compare(Pair lhs, Pair rhs) { 115 | return lhs.first.compareTo(rhs.first); 116 | } 117 | }); 118 | return headerList; 119 | } 120 | 121 | public String calculateHMACSHA1(String key, String data) 122 | { 123 | try { 124 | Mac mac = Mac.getInstance("HmacSHA1"); 125 | SecretKeySpec secret = new SecretKeySpec(key.getBytes("UTF-8"), mac.getAlgorithm()); 126 | mac.init(secret); 127 | byte[] digest = mac.doFinal(data.getBytes()); 128 | return Base64.encodeToString(digest, Base64.NO_WRAP); 129 | } catch (Exception e) { 130 | return ""; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/network/OkNounProjectApi.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.network; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | import com.btellez.solidandroid.configuration.Configuration; 7 | import com.btellez.solidandroid.model.Icon; 8 | import com.btellez.solidandroid.model.IconParser; 9 | import com.google.gson.JsonParseException; 10 | import com.squareup.okhttp.OkHttpClient; 11 | import com.squareup.okhttp.Request; 12 | import com.squareup.okhttp.Response; 13 | 14 | import java.io.IOException; 15 | import java.util.List; 16 | 17 | import javax.inject.Inject; 18 | 19 | /** 20 | * Implementation of the NounProjectApi interface that uses 21 | * the OkHttp Library. 22 | */ 23 | public class OkNounProjectApi implements NounProjectApi { 24 | @Inject Configuration configuration; 25 | @Inject IconParser iconParser; 26 | 27 | // Implementation Related Dependencies 28 | private NounProjectOAuth oAuth; 29 | private OkHttpClient client; 30 | 31 | public OkNounProjectApi(Configuration configuration, IconParser iconParser) { 32 | this.configuration = configuration; 33 | this.iconParser = iconParser; 34 | 35 | // Fulfil Implementation Related Dependencies 36 | this.client = new OkHttpClient(); 37 | this.oAuth = new NounProjectOAuth(configuration.getNounProjectApiKeys()); 38 | } 39 | 40 | @Override 41 | public void search(String term, final Callback callback) { 42 | String endpoint = new EndpointBuilder().buildSearchUrl(configuration, term); 43 | Request request = buildRequestPublic(endpoint); 44 | client.newCall(request).enqueue(new OkHttpCallback(callback, "icons")); 45 | } 46 | 47 | @Override 48 | public void recent(final Callback callback) { 49 | String endpoint = new EndpointBuilder().buildRecentUrl(configuration); 50 | Request request = buildRequestOAuth(endpoint, NounProjectOAuth.RequestType.GET); 51 | client.newCall(request).enqueue(new OkHttpCallback(callback, "recent_uploads")); 52 | } 53 | 54 | 55 | /** 56 | * Builds Requests for public endpoints that do not require authentication. 57 | * 58 | * @param endpoint 59 | * @return Request 60 | */ 61 | private Request buildRequestPublic(String endpoint) { 62 | return new Request.Builder().url(endpoint) 63 | .addHeader("X-Requested-With", "XMLHttpRequest") 64 | .build(); 65 | } 66 | 67 | 68 | /** 69 | * Builds Request for endpoints that do require authentication. 70 | * @param endpoint 71 | * @param method 72 | * @return Request 73 | */ 74 | private Request buildRequestOAuth(String endpoint, NounProjectOAuth.RequestType method) { 75 | String oAuthString = oAuth.withEnpoint(endpoint) 76 | .withRequestType(method) 77 | .getOAuthHeader(); 78 | 79 | return new Request.Builder().url(endpoint) 80 | .addHeader("Authorization", oAuthString) 81 | .build(); 82 | } 83 | 84 | /** 85 | * OkHttpCallback wrapper the app's defined callback interface. 86 | * This allows the the callback implementation to be decoupled form okHttp. 87 | */ 88 | private class OkHttpCallback implements com.squareup.okhttp.Callback { 89 | public Callback callback; 90 | public String dataKey; 91 | private Handler uiHandler = new Handler(Looper.getMainLooper()); 92 | 93 | private OkHttpCallback(Callback callback) { 94 | this(callback, null); 95 | } 96 | 97 | private OkHttpCallback(Callback callback, String dataKey) { 98 | this.callback = callback; 99 | this.dataKey = dataKey; 100 | } 101 | 102 | @Override public void onFailure(final Request request,final IOException e) { 103 | // We need to post update to the UI thread. 104 | uiHandler.post(new Runnable() { 105 | @Override 106 | public void run() { 107 | callback.onFailure(e); 108 | } 109 | }); 110 | } 111 | 112 | @Override public void onResponse(final Response response) throws IOException { 113 | try { 114 | String jsonString = response.body().string(); 115 | final List result = iconParser.fromJson(jsonString, dataKey); 116 | // We need to post update to the UI thread. 117 | uiHandler.post(new Runnable() { 118 | @Override 119 | public void run() { 120 | callback.onSuccess(result); 121 | } 122 | }); 123 | } catch (final JsonParseException nonJsonResponse) { 124 | uiHandler.post(new Runnable() { 125 | @Override 126 | public void run() { 127 | callback.onFailure(nonJsonResponse); 128 | } 129 | }); 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/utility/AnalyticsEvents.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.utility; 2 | 3 | public interface AnalyticsEvents { 4 | interface Lifecycle { 5 | String Start = "Application Start"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/utility/Strings.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.utility; 2 | 3 | import android.text.TextUtils; 4 | 5 | public class Strings { 6 | public static boolean isEmpty(String s) { 7 | return TextUtils.isEmpty(s) || TextUtils.getTrimmedLength(s) == 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/utility/Tracker.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.utility; 2 | 3 | import android.content.Context; 4 | 5 | import com.btellez.solidandroid.BuildConfig; 6 | import com.btellez.solidandroid.configuration.Configuration; 7 | 8 | import java.util.Map; 9 | 10 | import hugo.weaving.DebugLog; 11 | import io.replay.framework.Config; 12 | import io.replay.framework.ReplayIO; 13 | 14 | public interface Tracker { 15 | void track(String eventName); 16 | void track(String eventName, String...properties); 17 | void track(String eventName, Map properties); 18 | 19 | /** 20 | * Simple No-op implementation of tracking. 21 | */ 22 | class SimpleTracker implements Tracker { 23 | @DebugLog @Override public void track(String eventName) {} 24 | @DebugLog @Override public void track(String eventName, String... properties) {} 25 | @DebugLog @Override public void track(String eventName, Map properties) {} 26 | } 27 | 28 | /** 29 | * Replay.io implementation of tracking 30 | */ 31 | class ReplayTracker implements Tracker { 32 | 33 | public ReplayTracker(Context context, Configuration appConfig) { 34 | // Set-Up Replay Settings 35 | String replayApiKey = appConfig.getReplayAPIKey(); 36 | Config config = new Config(replayApiKey); 37 | config.setDebug(BuildConfig.DEBUG); 38 | config.setDispatchInterval(60000); 39 | config.setEnabled(true); 40 | config.setFlushAt(20); 41 | ReplayIO.init(context, config); 42 | track(AnalyticsEvents.Lifecycle.Start); 43 | } 44 | 45 | @Override 46 | public void track(String eventName) { 47 | ReplayIO.track(eventName); 48 | } 49 | 50 | @Override 51 | public void track(String eventName, String... properties) { 52 | ReplayIO.track(eventName, (Object[]) properties); 53 | } 54 | 55 | @Override 56 | public void track(String eventName, Map properties) { 57 | ReplayIO.track(eventName, properties); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/view/EmptyView.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.view; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.widget.Button; 8 | import android.widget.FrameLayout; 9 | import android.widget.TextView; 10 | 11 | import com.btellez.solidandroid.R; 12 | 13 | import butterknife.ButterKnife; 14 | import butterknife.InjectView; 15 | import butterknife.OnClick; 16 | 17 | public class EmptyView extends FrameLayout { 18 | 19 | public interface Listener { 20 | void onPrimaryActionClicked(); 21 | void onSecondaryActionClicked(); 22 | } 23 | 24 | public static class SimpleListener implements Listener { 25 | @Override public void onPrimaryActionClicked() {} 26 | @Override public void onSecondaryActionClicked() {} 27 | } 28 | 29 | private Listener listener; 30 | @InjectView(R.id.headline) TextView headline; 31 | @InjectView(R.id.subheadline) TextView subheadline; 32 | @InjectView(R.id.primary_action) Button buttonPrimary; 33 | @InjectView(R.id.secondary_action) Button buttonSecondary; 34 | 35 | public EmptyView(Context context) { 36 | super(context); 37 | init(context); 38 | } 39 | 40 | public EmptyView(Context context, AttributeSet attrs) { 41 | super(context, attrs); 42 | init(context); 43 | } 44 | 45 | public EmptyView(Context context, AttributeSet attrs, int defStyleAttr) { 46 | super(context, attrs, defStyleAttr); 47 | init(context); 48 | } 49 | 50 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 51 | public EmptyView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 52 | super(context, attrs, defStyleAttr, defStyleRes); 53 | init(context); 54 | } 55 | 56 | public void setHeadline(int resString) { 57 | setHeadline(getResources().getString(resString)); 58 | } 59 | 60 | public void setHeadline(String value) { 61 | headline.setText(value); 62 | } 63 | 64 | public void setSubheadline(int resString) { 65 | setSubheadline(getResources().getString(resString)); 66 | } 67 | 68 | public void setSubheadline(String value) { 69 | subheadline.setText(value); 70 | } 71 | 72 | public void setListener(Listener listener) { 73 | if (listener == null) 74 | listener = new SimpleListener(); 75 | this.listener = listener; 76 | } 77 | 78 | public void setPrimaryActionName(int resString) { 79 | setPrimaryActionName(getResources().getString(resString)); 80 | } 81 | 82 | public void setPrimaryActionName(String value) { 83 | buttonPrimary.setText(value); 84 | } 85 | 86 | public void setSecondaryActionName(int resString) { 87 | setSecondaryActionName(getResources().getString(resString)); 88 | } 89 | 90 | public void setSecondaryActionName(String value) { 91 | buttonSecondary.setText(value); 92 | } 93 | 94 | private void init(Context context) { 95 | inflate(context, R.layout.error_view, this); 96 | ButterKnife.inject(this); 97 | listener = new SimpleListener(); 98 | } 99 | 100 | @OnClick(R.id.primary_action) 101 | protected void onPrimaryActionClicked() { 102 | listener.onPrimaryActionClicked(); 103 | } 104 | 105 | @OnClick(R.id.secondary_action) 106 | protected void onSecondaryActionClicked() { 107 | listener.onSecondaryActionClicked(); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/view/FadeAnimator.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.view; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ObjectAnimator; 5 | import android.view.View; 6 | import android.view.animation.AccelerateDecelerateInterpolator; 7 | import android.view.animation.Interpolator; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | public class FadeAnimator { 13 | private Interpolator interpolator = new AccelerateDecelerateInterpolator(); 14 | private Animator.AnimatorListener hideListener = new SimpleAnimatorListener(); 15 | private Animator.AnimatorListener showListener = new SimpleAnimatorListener(); 16 | private List showAnimations = new ArrayList<>(); 17 | private List hideAnimations = new ArrayList<>(); 18 | 19 | public FadeAnimator hide(View view, long duration, long delay) { 20 | if (view != null) { 21 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 1.0f, 0f); 22 | objectAnimator.setDuration(duration); 23 | objectAnimator.setStartDelay(delay); 24 | objectAnimator.setInterpolator(interpolator); 25 | hideAnimations.add(objectAnimator); 26 | } 27 | return this; 28 | } 29 | 30 | public FadeAnimator show(View view, long duration, long delay) { 31 | if (view != null) { 32 | view.setAlpha(0); 33 | view.setVisibility(View.VISIBLE); 34 | ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, "alpha", 0, 1.0f); 35 | objectAnimator.setDuration(duration); 36 | objectAnimator.setStartDelay(delay); 37 | objectAnimator.setInterpolator(interpolator); 38 | showAnimations.add(objectAnimator); 39 | } 40 | return this; 41 | } 42 | 43 | public FadeAnimator withShowListener(Animator.AnimatorListener listener) { 44 | this.hideListener = validate(listener); 45 | return this; 46 | } 47 | 48 | public FadeAnimator withHideListener(Animator.AnimatorListener listener) { 49 | this.hideListener = validate(listener); 50 | return this; 51 | } 52 | 53 | private Animator.AnimatorListener validate(Animator.AnimatorListener listener) { 54 | if (listener == null) 55 | listener = new SimpleAnimatorListener(); 56 | return listener; 57 | } 58 | 59 | public void setInterpolator(Interpolator interpolator) { 60 | if (interpolator == null) { 61 | interpolator = new AccelerateDecelerateInterpolator(); 62 | } 63 | this.interpolator = interpolator; 64 | } 65 | 66 | public void start() { 67 | startAnimations(hideAnimations, hideListener); 68 | startAnimations(showAnimations, showListener); 69 | } 70 | 71 | private void startAnimations(List animations, Animator.AnimatorListener listener) { 72 | for (ObjectAnimator animator : animations) { 73 | animator.addListener(listener); 74 | animator.start(); 75 | } 76 | } 77 | 78 | public static class SimpleAnimatorListener implements Animator.AnimatorListener { 79 | @Override public void onAnimationStart(Animator animation) {} 80 | @Override public void onAnimationEnd(Animator animation) {} 81 | @Override public void onAnimationCancel(Animator animation) {} 82 | @Override public void onAnimationRepeat(Animator animation) {} 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/view/SearchResultsView.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.view; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.BaseAdapter; 9 | import android.widget.FrameLayout; 10 | import android.widget.ListView; 11 | import android.widget.ProgressBar; 12 | 13 | import com.btellez.solidandroid.R; 14 | 15 | import butterknife.ButterKnife; 16 | import butterknife.InjectView; 17 | 18 | public class SearchResultsView extends FrameLayout { 19 | 20 | @InjectView(R.id.result_list) ListView listView; 21 | @InjectView(R.id.progress_indicator) ProgressBar progress; 22 | 23 | protected EmptyView emptyView; 24 | private EmptyView.Listener emptyActionListener; 25 | private EmptyView.Listener errorActionListener; 26 | 27 | public enum State {Loading, Error, Empty, Loaded} 28 | 29 | public SearchResultsView(Context context) { 30 | super(context); 31 | init(context); 32 | } 33 | 34 | public SearchResultsView(Context context, AttributeSet attrs) { 35 | super(context, attrs); 36 | init(context); 37 | } 38 | 39 | public SearchResultsView(Context context, AttributeSet attrs, int defStyleAttr) { 40 | super(context, attrs, defStyleAttr); 41 | init(context); 42 | } 43 | 44 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 45 | public SearchResultsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 46 | super(context, attrs, defStyleAttr, defStyleRes); 47 | init(context); 48 | } 49 | 50 | private void init(Context context) { 51 | inflate(context, R.layout.activity_search_results, this); 52 | ButterKnife.inject(this); 53 | emptyView = new EmptyView(context); 54 | addView(emptyView); 55 | listView.setEmptyView(emptyView); 56 | setState(State.Loading); 57 | } 58 | 59 | public void setAdapter(BaseAdapter adapter) { 60 | listView.setAdapter(adapter); 61 | } 62 | 63 | public void setEmptyActionListener(EmptyView.Listener listener) { 64 | if (listener == null) 65 | listener = new EmptyView.SimpleListener(); 66 | this.emptyActionListener = listener; 67 | } 68 | 69 | public void setErrorActionListener(EmptyView.Listener listener) { 70 | if (listener == null) 71 | listener = new EmptyView.SimpleListener(); 72 | this.errorActionListener = listener; 73 | } 74 | 75 | public void setState(State state) { 76 | switch (state) { 77 | case Loading: 78 | emptyView.setVisibility(View.INVISIBLE); 79 | listView.setVisibility(View.INVISIBLE); 80 | progress.setVisibility(View.VISIBLE); 81 | break; 82 | case Error: 83 | emptyView.setVisibility(View.VISIBLE); 84 | listView.setVisibility(View.INVISIBLE); 85 | progress.setVisibility(View.INVISIBLE); 86 | onErrorState(); 87 | break; 88 | case Empty: 89 | emptyView.setVisibility(View.VISIBLE); 90 | listView.setVisibility(View.INVISIBLE); 91 | progress.setVisibility(View.INVISIBLE); 92 | onEmptyState(); 93 | break; 94 | case Loaded: 95 | emptyView.setVisibility(View.INVISIBLE); 96 | listView.setVisibility(View.VISIBLE); 97 | progress.setVisibility(View.INVISIBLE); 98 | break; 99 | } 100 | } 101 | 102 | private void onEmptyState() { 103 | emptyView.setHeadline(R.string.no_results); 104 | emptyView.setSubheadline(R.string.no_results_detail); 105 | emptyView.setPrimaryActionName(R.string.change_search); 106 | emptyView.setSecondaryActionName(R.string.go_back); 107 | emptyView.setListener(emptyActionListener); 108 | } 109 | 110 | private void onErrorState() { 111 | emptyView.setHeadline(R.string.unable_to_load); 112 | emptyView.setSubheadline(R.string.unable_to_load_detail); 113 | emptyView.setPrimaryActionName(R.string.try_again); 114 | emptyView.setSecondaryActionName(R.string.go_back); 115 | emptyView.setListener(errorActionListener); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/view/SelectionScreenView.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.view; 2 | 3 | import android.animation.Animator; 4 | import android.annotation.TargetApi; 5 | import android.content.Context; 6 | import android.os.Build; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | import android.view.inputmethod.InputMethodManager; 10 | import android.widget.EditText; 11 | import android.widget.FrameLayout; 12 | import android.widget.RelativeLayout; 13 | import android.widget.TextView; 14 | 15 | import com.btellez.solidandroid.R; 16 | import com.btellez.solidandroid.utility.Strings; 17 | 18 | import butterknife.ButterKnife; 19 | import butterknife.InjectView; 20 | import butterknife.OnClick; 21 | 22 | public class SelectionScreenView extends FrameLayout { 23 | 24 | // Static Screen items: 25 | @InjectView(R.id.search) View searchGroup; 26 | @InjectView(R.id.recent) View recentGroup; 27 | 28 | // Views to be animated in: 29 | @InjectView(R.id.overlay) RelativeLayout overlay; 30 | @InjectView(R.id.dark_background) View overlayBackground; 31 | @InjectView(R.id.search_input_group) View searchInputGroup; 32 | @InjectView(R.id.search_input) EditText searchInput; 33 | @InjectView(R.id.error) TextView error; 34 | @InjectView(R.id.submit) View submit; 35 | 36 | private Listener listener = new SimpleListener(); 37 | 38 | private enum State {ShowSelection, ShowOverlay} 39 | 40 | /** 41 | * Interface to alert any listeners about events 42 | * in our view. This helps to keep non-view logic out of 43 | * the the view. 44 | */ 45 | public interface Listener { 46 | void onSeachGroupSelected(); 47 | void onRecentGroupSelected(); 48 | void onSubmitSearchQuery(String query); 49 | } 50 | 51 | public static class SimpleListener implements Listener { 52 | @Override public void onSeachGroupSelected() {} 53 | @Override public void onRecentGroupSelected() {} 54 | @Override public void onSubmitSearchQuery(String query) {} 55 | } 56 | 57 | public SelectionScreenView(Context context) { 58 | super(context); 59 | init(context); 60 | } 61 | 62 | public SelectionScreenView(Context context, AttributeSet attrs) { 63 | super(context, attrs); 64 | init(context); 65 | } 66 | 67 | public SelectionScreenView(Context context, AttributeSet attrs, int defStyleAttr) { 68 | super(context, attrs, defStyleAttr); 69 | init(context); 70 | } 71 | 72 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 73 | public SelectionScreenView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 74 | super(context, attrs, defStyleAttr, defStyleRes); 75 | init(context); 76 | } 77 | 78 | public void setListener(Listener listener) { 79 | if (listener == null) 80 | listener = new SimpleListener(); 81 | this.listener = listener; 82 | } 83 | 84 | private void init(Context context) { 85 | inflate(context, R.layout.activity_selection, this); 86 | ButterKnife.inject(this); 87 | setState(State.ShowSelection); 88 | } 89 | 90 | @OnClick(R.id.dark_background) 91 | protected void onOverlayBackgroundClicked() { 92 | setState(State.ShowSelection); 93 | } 94 | 95 | @OnClick(R.id.search) 96 | protected void onSearchSelected() { 97 | listener.onSeachGroupSelected(); 98 | setState(State.ShowOverlay); 99 | } 100 | 101 | @OnClick(R.id.recent) 102 | protected void onRecentSelected() { 103 | listener.onRecentGroupSelected(); 104 | } 105 | 106 | @OnClick(R.id.submit) 107 | protected void onSubmitClicked() { 108 | String query = searchInput.getText().toString(); 109 | if (Strings.isEmpty(query)) { 110 | setError(R.string.input_error); 111 | } else { 112 | listener.onSubmitSearchQuery(searchInput.getText().toString()); 113 | } 114 | } 115 | 116 | private void setState(State state) { 117 | switch (state) { 118 | case ShowSelection: 119 | setKeyboardVisibility(View.GONE); 120 | hideOverlay(); 121 | break; 122 | case ShowOverlay: 123 | showOverlay(); 124 | resetError(); 125 | setKeyboardVisibility(View.VISIBLE); 126 | break; 127 | } 128 | } 129 | 130 | private void showOverlay() { 131 | if (!isVisible(overlay)) { 132 | new FadeAnimator() 133 | .show(overlay, 0, 0) 134 | .show(overlayBackground, 250, 0) 135 | .show(searchInputGroup, 500, 200) 136 | .start(); 137 | } 138 | } 139 | 140 | private void hideOverlay() { 141 | if (isVisible(overlay)) { 142 | new FadeAnimator() 143 | .hide(overlayBackground, 500, 0) 144 | .hide(searchInputGroup, 250, 200) 145 | .withHideListener(new FadeAnimator.SimpleAnimatorListener() { 146 | @Override 147 | public void onAnimationEnd(Animator animation) { 148 | overlay.setVisibility(INVISIBLE); 149 | } 150 | }) 151 | .start(); 152 | } 153 | } 154 | 155 | private void setKeyboardVisibility(int visiblity) { 156 | InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 157 | if (visiblity == View.VISIBLE) { 158 | searchInput.requestFocus(); 159 | imm.toggleSoftInputFromWindow(searchInput.getApplicationWindowToken(), InputMethodManager.SHOW_FORCED, 0); 160 | } else { 161 | searchInput.clearFocus(); 162 | imm.hideSoftInputFromWindow(searchInput.getWindowToken(), 0); 163 | } 164 | } 165 | 166 | private boolean isVisible(View view) { 167 | return view.getVisibility() == VISIBLE; 168 | } 169 | 170 | private void resetError() { 171 | new FadeAnimator() 172 | .hide(error, 250, 0) 173 | .start(); 174 | } 175 | 176 | private void setError(int resString) { 177 | error.setText(resString); 178 | new FadeAnimator() 179 | .show(error, 250, 0) 180 | .start(); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /app/src/main/java/com/btellez/solidandroid/view/SingleIconResultItem.java: -------------------------------------------------------------------------------- 1 | package com.btellez.solidandroid.view; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build; 6 | import android.util.AttributeSet; 7 | import android.widget.FrameLayout; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import com.btellez.solidandroid.R; 12 | import com.btellez.solidandroid.model.Icon; 13 | import com.btellez.solidandroid.utility.Strings; 14 | 15 | import butterknife.ButterKnife; 16 | import butterknife.InjectView; 17 | import butterknife.OnClick; 18 | 19 | public class SingleIconResultItem extends FrameLayout { 20 | 21 | @InjectView(R.id.link) protected ImageView link; 22 | @InjectView(R.id.icon) protected ImageView icon; 23 | @InjectView(R.id.term) protected TextView term; 24 | @InjectView(R.id.attribution) protected TextView attribution; 25 | protected Listener listener = new Listener.SimpleListener(); 26 | 27 | /** 28 | * Listener is used to keep non-ui logic out of the view. 29 | */ 30 | public interface Listener { 31 | void onLinkClicked(String url); 32 | void requestDownloadInto(String url, ImageView imageView); 33 | 34 | class SimpleListener implements Listener { 35 | @Override public void onLinkClicked(String url) {/* no-op */} 36 | @Override public void requestDownloadInto(String url, ImageView imageView) {/* no-op */} 37 | } 38 | } 39 | 40 | public SingleIconResultItem(Context context) { 41 | super(context); 42 | init(context); 43 | } 44 | 45 | public SingleIconResultItem(Context context, AttributeSet attrs) { 46 | super(context, attrs); 47 | init(context); 48 | } 49 | 50 | public SingleIconResultItem(Context context, AttributeSet attrs, int defStyleAttr) { 51 | super(context, attrs, defStyleAttr); 52 | init(context); 53 | } 54 | 55 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 56 | public SingleIconResultItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 57 | super(context, attrs, defStyleAttr, defStyleRes); 58 | init(context); 59 | } 60 | 61 | private void init(Context context) { 62 | inflate(context, R.layout.single_icon_result_item, this); 63 | ButterKnife.inject(this); 64 | listener = new Listener.SimpleListener(); 65 | } 66 | 67 | public void setListener(Listener listener) { 68 | if (listener == null) 69 | listener = new Listener.SimpleListener(); 70 | this.listener = listener; 71 | } 72 | 73 | public void setIconData(Icon data) { 74 | term.setText(data.getTerm().toUpperCase()); 75 | attribution.setText(getAttributionString(data)); 76 | link.setTag(data.getPermalink()); 77 | listener.requestDownloadInto(data.getPreviewUrl(), icon); 78 | } 79 | 80 | protected String getAttributionString(Icon data) { 81 | if (Strings.isEmpty(data.getUploader().getName())) { 82 | return "The Noun Project"; 83 | } 84 | return data.getUploader().getName() +" from The Noun Project"; 85 | } 86 | 87 | @OnClick(R.id.link) 88 | protected void onLinkClicked() { 89 | listener.onLinkClicked((String) link.getTag()); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_outbound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable-hdpi/ic_outbound.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_outbound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable-mdpi/ic_outbound.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_outbound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable-xhdpi/ic_outbound.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_outbound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable-xxhdpi/ic_outbound.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/solid_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable/solid_search.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/solid_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blad/solid-android/5895bbf5ef549f8eb150d6ee7046eeaa137604b4/app/src/main/res/drawable/solid_upload.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main_user.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 15 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search_results.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_selection.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 16 | 21 | 22 | 29 | 30 | 39 | 40 | 41 | 46 | 47 | 54 | 55 | 65 | 66 | 67 | 68 | 69 | 74 | 75 | 81 | 82 | 87 | 88 | 99 | 100 | 113 | 114 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /app/src/main/res/layout/error_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 31 | 32 |