├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-butterknife-7.pro ├── proguard-google-play-services.pro ├── proguard-guava.pro ├── proguard-rules.pro ├── proguard-square-okhttp3.pro ├── proguard-square-okio.pro ├── proguard-support-v7-appcompat.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── udacity │ │ └── stockhawk │ │ ├── StockHawkApp.java │ │ ├── data │ │ ├── Contract.java │ │ ├── DbHelper.java │ │ ├── PrefUtils.java │ │ └── StockProvider.java │ │ ├── sync │ │ ├── QuoteIntentService.java │ │ ├── QuoteJobService.java │ │ └── QuoteSyncJob.java │ │ └── ui │ │ ├── AddStockDialog.java │ │ ├── MainActivity.java │ │ └── StockAdapter.java │ └── res │ ├── drawable-hdpi │ ├── ic_dollar.png │ └── ic_percentage.png │ ├── drawable-mdpi │ ├── ic_dollar.png │ └── ic_percentage.png │ ├── drawable-xhdpi │ ├── ic_dollar.png │ └── ic_percentage.png │ ├── drawable-xxhdpi │ ├── ic_dollar.png │ └── ic_percentage.png │ ├── drawable-xxxhdpi │ ├── ic_dollar.png │ └── ic_percentage.png │ ├── drawable │ ├── fab_plus.xml │ ├── percent_change_pill_green.xml │ └── percent_change_pill_red.xml │ ├── layout │ ├── activity_main.xml │ ├── add_stock_dialog.xml │ └── list_item_quote.xml │ ├── menu │ └── main_activity_settings.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── arrays.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stock Hawk 2 | 3 | This was formerly the starter code for project 3 in Udacity's [Android Developer Nanodegree](https://www.udacity.com/course/android-developer-nanodegree-by-google--nd801). 4 | 5 | It corresponds to the skills taught in Udacity's [Advanced Android App Development](https://www.udacity.com/course/advanced-android-app-development--ud855) course. 6 | 7 | # Contributing 8 | 9 | Pull requests gratefully accepted. 10 | 11 | # Archival Note 12 | This repository is deprecated; therefore, we are going to archive it. However, learners will be able to fork it to their personal Github account but cannot submit PRs to this repository. If you have any issues or suggestions to make, feel free to: 13 | - Utilize the https://knowledge.udacity.com/ forum to seek help on content-specific issues. 14 | - Submit a support ticket along with the link to your forked repository if (learners are) blocked for other reasons. Here are the links for the [retail consumers](https://udacity.zendesk.com/hc/en-us/requests/new) and [enterprise learners](https://udacityenterprise.zendesk.com/hc/en-us/requests/new?ticket_form_id=360000279131). -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'com.noveogroup.android.check' 3 | 4 | repositories { 5 | maven { url "https://jitpack.io" } 6 | } 7 | 8 | android { 9 | compileSdkVersion 25 10 | buildToolsVersion "25.0.2" 11 | 12 | defaultConfig { 13 | applicationId "com.udacity.stockhawk" 14 | minSdkVersion 21 15 | targetSdkVersion 25 16 | versionCode 1 17 | versionName "1.0" 18 | multiDexEnabled true 19 | } 20 | 21 | buildTypes { 22 | 23 | release { 24 | minifyEnabled false 25 | shrinkResources false 26 | proguardFile "proguard-butterknife-7.pro" 27 | proguardFile "proguard-google-play-services.pro" 28 | proguardFile "proguard-guava.pro" 29 | proguardFile "proguard-square-okhttp3.pro" 30 | proguardFile "proguard-square-okio.pro" 31 | proguardFile "proguard-support-v7-appcompat.pro" 32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 33 | } 34 | } 35 | 36 | //noinspection GroovyMissingReturnStatement 37 | lintOptions { 38 | warning 'InvalidPackage' 39 | } 40 | 41 | } 42 | 43 | check { 44 | checkstyle { config hard() } 45 | findbugs { config hard() } 46 | pmd { 47 | config hard() 48 | skip true 49 | } 50 | } 51 | 52 | dependencies { 53 | compile fileTree(dir: 'libs', include: ['*.jar']) 54 | testCompile 'junit:junit:4.12' 55 | compile 'com.android.support:appcompat-v7:25.3.0' 56 | compile 'com.android.support:recyclerview-v7:25.3.0' 57 | compile 'com.android.support:design:25.3.0' 58 | compile 'com.jakewharton:butterknife:8.8.0' 59 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.0' 60 | 61 | compile 'com.google.guava:guava:20.0' 62 | 63 | compile 'com.jakewharton.threetenabp:threetenabp:1.0.5' 64 | 65 | compile 'com.squareup.okhttp3:okhttp:3.8.1' 66 | compile 'com.jakewharton.timber:timber:4.4.0' 67 | compile 'net.sf.opencsv:opencsv:2.3' 68 | compile 'com.github.PhilJay:MPAndroidChart:v3.0.1' 69 | } 70 | -------------------------------------------------------------------------------- /app/proguard-butterknife-7.pro: -------------------------------------------------------------------------------- 1 | # ButterKnife 7 2 | 3 | -keep class butterknife.** { *; } 4 | -dontwarn butterknife.internal.** 5 | -keep class **$$ViewBinder { *; } 6 | 7 | -keepclasseswithmembernames class * { 8 | @butterknife.* ; 9 | } 10 | 11 | -keepclasseswithmembernames class * { 12 | @butterknife.* ; 13 | } -------------------------------------------------------------------------------- /app/proguard-google-play-services.pro: -------------------------------------------------------------------------------- 1 | ## Google Play Services 4.3.23 specific rules ## 2 | ## https://developer.android.com/google/play-services/setup.html#Proguard ## 3 | 4 | -keep class * extends java.util.ListResourceBundle { 5 | protected Object[][] getContents(); 6 | } 7 | 8 | -keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable { 9 | public static final *** NULL; 10 | } 11 | 12 | -keepnames @com.google.android.gms.common.annotation.KeepName class * 13 | -keepclassmembernames class * { 14 | @com.google.android.gms.common.annotation.KeepName *; 15 | } 16 | 17 | -keepnames class * implements android.os.Parcelable { 18 | public static final ** CREATOR; 19 | } 20 | -------------------------------------------------------------------------------- /app/proguard-guava.pro: -------------------------------------------------------------------------------- 1 | # Configuration for Guava 18.0 2 | # 3 | # disagrees with instructions provided by Guava project: https://code.google.com/p/guava-libraries/wiki/UsingProGuardWithGuava 4 | 5 | -keep class com.google.common.io.Resources { 6 | public static ; 7 | } 8 | -keep class com.google.common.collect.Lists { 9 | public static ** reverse(**); 10 | } 11 | -keep class com.google.common.base.Charsets { 12 | public static ; 13 | } 14 | 15 | -keep class com.google.common.base.Joiner { 16 | public static com.google.common.base.Joiner on(java.lang.String); 17 | public ** join(...); 18 | } 19 | 20 | -keep class com.google.common.collect.MapMakerInternalMap$ReferenceEntry 21 | -keep class com.google.common.cache.LocalCache$ReferenceEntry 22 | 23 | # http://stackoverflow.com/questions/9120338/proguard-configuration-for-guava-with-obfuscation-and-optimization 24 | -dontwarn javax.annotation.** 25 | -dontwarn javax.inject.** 26 | -dontwarn sun.misc.Unsafe 27 | 28 | # Guava 19.0 29 | -dontwarn java.lang.ClassValue 30 | -dontwarn com.google.j2objc.annotations.Weak 31 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 32 | -------------------------------------------------------------------------------- /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/silver/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | 20 | -assumenosideeffects class java.util.logging.Logger { 21 | public *** log(...); 22 | } 23 | 24 | -keep class com.github.mikephil.charting.** { *; } 25 | 26 | -dontwarn io.realm.** 27 | -------------------------------------------------------------------------------- /app/proguard-square-okhttp3.pro: -------------------------------------------------------------------------------- 1 | # OkHttp 2 | -keepattributes Signature 3 | -keepattributes *Annotation* 4 | -keep class okhttp3.** { *; } 5 | -keep interface okhttp3.** { *; } 6 | -dontwarn okhttp3.** -------------------------------------------------------------------------------- /app/proguard-square-okio.pro: -------------------------------------------------------------------------------- 1 | # Okio 2 | -keep class sun.misc.Unsafe { *; } 3 | -dontwarn java.nio.file.* 4 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 5 | -dontwarn okio.** -------------------------------------------------------------------------------- /app/proguard-support-v7-appcompat.pro: -------------------------------------------------------------------------------- 1 | -keep public class android.support.v7.widget.** { *; } 2 | -keep public class android.support.v7.internal.widget.** { *; } 3 | -keep public class android.support.v7.internal.view.menu.** { *; } 4 | 5 | -keep public class * extends android.support.v4.view.ActionProvider { 6 | public (android.content.Context); 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 34 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/StockHawkApp.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk; 2 | 3 | import android.app.Application; 4 | 5 | import com.jakewharton.threetenabp.AndroidThreeTen; 6 | 7 | import timber.log.Timber; 8 | 9 | public class StockHawkApp extends Application { 10 | 11 | @Override 12 | public void onCreate() { 13 | super.onCreate(); 14 | AndroidThreeTen.init(this); 15 | 16 | if (BuildConfig.DEBUG) { 17 | Timber.uprootAll(); 18 | Timber.plant(new Timber.DebugTree()); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/data/Contract.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.data; 2 | 3 | 4 | import android.net.Uri; 5 | import android.provider.BaseColumns; 6 | 7 | import com.google.common.collect.ImmutableList; 8 | 9 | public final class Contract { 10 | 11 | static final String AUTHORITY = "com.udacity.stockhawk"; 12 | static final String PATH_QUOTE = "quote"; 13 | static final String PATH_QUOTE_WITH_SYMBOL = "quote/*"; 14 | private static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY); 15 | 16 | private Contract() { 17 | } 18 | 19 | @SuppressWarnings("unused") 20 | public static final class Quote implements BaseColumns { 21 | 22 | public static final Uri URI = BASE_URI.buildUpon().appendPath(PATH_QUOTE).build(); 23 | public static final String COLUMN_SYMBOL = "symbol"; 24 | public static final String COLUMN_PRICE = "price"; 25 | public static final String COLUMN_ABSOLUTE_CHANGE = "absolute_change"; 26 | public static final String COLUMN_PERCENTAGE_CHANGE = "percentage_change"; 27 | public static final String COLUMN_HISTORY = "history"; 28 | public static final int POSITION_ID = 0; 29 | public static final int POSITION_SYMBOL = 1; 30 | public static final int POSITION_PRICE = 2; 31 | public static final int POSITION_ABSOLUTE_CHANGE = 3; 32 | public static final int POSITION_PERCENTAGE_CHANGE = 4; 33 | public static final int POSITION_HISTORY = 5; 34 | public static final String[] QUOTE_COLUMNS = { 35 | _ID, 36 | COLUMN_SYMBOL, 37 | COLUMN_PRICE, 38 | COLUMN_ABSOLUTE_CHANGE, 39 | COLUMN_PERCENTAGE_CHANGE, 40 | COLUMN_HISTORY 41 | }; 42 | static final String TABLE_NAME = "quotes"; 43 | 44 | public static Uri makeUriForStock(String symbol) { 45 | return URI.buildUpon().appendPath(symbol).build(); 46 | } 47 | 48 | static String getStockFromUri(Uri queryUri) { 49 | return queryUri.getLastPathSegment(); 50 | } 51 | 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/data/DbHelper.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.data; 2 | 3 | import android.content.Context; 4 | import android.database.sqlite.SQLiteDatabase; 5 | import android.database.sqlite.SQLiteOpenHelper; 6 | 7 | import com.udacity.stockhawk.data.Contract.Quote; 8 | 9 | 10 | class DbHelper extends SQLiteOpenHelper { 11 | 12 | 13 | private static final String NAME = "StockHawk.db"; 14 | private static final int VERSION = 1; 15 | 16 | 17 | DbHelper(Context context) { 18 | super(context, NAME, null, VERSION); 19 | } 20 | 21 | @Override 22 | public void onCreate(SQLiteDatabase db) { 23 | String builder = "CREATE TABLE " + Quote.TABLE_NAME + " (" 24 | + Quote._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " 25 | + Quote.COLUMN_SYMBOL + " TEXT NOT NULL, " 26 | + Quote.COLUMN_PRICE + " REAL NOT NULL, " 27 | + Quote.COLUMN_ABSOLUTE_CHANGE + " REAL NOT NULL, " 28 | + Quote.COLUMN_PERCENTAGE_CHANGE + " REAL NOT NULL, " 29 | + Quote.COLUMN_HISTORY + " TEXT NOT NULL, " 30 | + "UNIQUE (" + Quote.COLUMN_SYMBOL + ") ON CONFLICT REPLACE);"; 31 | 32 | db.execSQL(builder); 33 | 34 | } 35 | 36 | @Override 37 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 38 | 39 | db.execSQL(" DROP TABLE IF EXISTS " + Quote.TABLE_NAME); 40 | 41 | onCreate(db); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/data/PrefUtils.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.data; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | 7 | import com.udacity.stockhawk.R; 8 | 9 | import java.util.Arrays; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | public final class PrefUtils { 14 | 15 | private PrefUtils() { 16 | } 17 | 18 | public static Set getStocks(Context context) { 19 | String stocksKey = context.getString(R.string.pref_stocks_key); 20 | String initializedKey = context.getString(R.string.pref_stocks_initialized_key); 21 | String[] defaultStocksList = context.getResources().getStringArray(R.array.default_stocks); 22 | 23 | HashSet defaultStocks = new HashSet<>(Arrays.asList(defaultStocksList)); 24 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 25 | 26 | 27 | boolean initialized = prefs.getBoolean(initializedKey, false); 28 | 29 | if (!initialized) { 30 | SharedPreferences.Editor editor = prefs.edit(); 31 | editor.putBoolean(initializedKey, true); 32 | editor.putStringSet(stocksKey, defaultStocks); 33 | editor.apply(); 34 | return defaultStocks; 35 | } 36 | return prefs.getStringSet(stocksKey, new HashSet()); 37 | 38 | } 39 | 40 | private static void editStockPref(Context context, String symbol, Boolean add) { 41 | String key = context.getString(R.string.pref_stocks_key); 42 | Set stocks = getStocks(context); 43 | 44 | if (add) { 45 | stocks.add(symbol); 46 | } else { 47 | stocks.remove(symbol); 48 | } 49 | 50 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 51 | SharedPreferences.Editor editor = prefs.edit(); 52 | editor.putStringSet(key, stocks); 53 | editor.apply(); 54 | } 55 | 56 | public static void addStock(Context context, String symbol) { 57 | editStockPref(context, symbol, true); 58 | } 59 | 60 | public static void removeStock(Context context, String symbol) { 61 | editStockPref(context, symbol, false); 62 | } 63 | 64 | public static String getDisplayMode(Context context) { 65 | String key = context.getString(R.string.pref_display_mode_key); 66 | String defaultValue = context.getString(R.string.pref_display_mode_default); 67 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 68 | return prefs.getString(key, defaultValue); 69 | } 70 | 71 | public static void toggleDisplayMode(Context context) { 72 | String key = context.getString(R.string.pref_display_mode_key); 73 | String absoluteKey = context.getString(R.string.pref_display_mode_absolute_key); 74 | String percentageKey = context.getString(R.string.pref_display_mode_percentage_key); 75 | 76 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 77 | 78 | String displayMode = getDisplayMode(context); 79 | 80 | SharedPreferences.Editor editor = prefs.edit(); 81 | 82 | if (displayMode.equals(absoluteKey)) { 83 | editor.putString(key, percentageKey); 84 | } else { 85 | editor.putString(key, absoluteKey); 86 | } 87 | 88 | editor.apply(); 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/data/StockProvider.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.data; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.content.Context; 6 | import android.content.UriMatcher; 7 | import android.database.Cursor; 8 | import android.database.sqlite.SQLiteDatabase; 9 | import android.net.Uri; 10 | import android.support.annotation.NonNull; 11 | import android.support.annotation.Nullable; 12 | 13 | 14 | public class StockProvider extends ContentProvider { 15 | 16 | private static final int QUOTE = 100; 17 | private static final int QUOTE_FOR_SYMBOL = 101; 18 | 19 | private static final UriMatcher uriMatcher = buildUriMatcher(); 20 | 21 | private DbHelper dbHelper; 22 | 23 | private static UriMatcher buildUriMatcher() { 24 | UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 25 | matcher.addURI(Contract.AUTHORITY, Contract.PATH_QUOTE, QUOTE); 26 | matcher.addURI(Contract.AUTHORITY, Contract.PATH_QUOTE_WITH_SYMBOL, QUOTE_FOR_SYMBOL); 27 | return matcher; 28 | } 29 | 30 | 31 | @Override 32 | public boolean onCreate() { 33 | dbHelper = new DbHelper(getContext()); 34 | return true; 35 | } 36 | 37 | @Nullable 38 | @Override 39 | public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { 40 | Cursor returnCursor; 41 | SQLiteDatabase db = dbHelper.getReadableDatabase(); 42 | 43 | switch (uriMatcher.match(uri)) { 44 | case QUOTE: 45 | returnCursor = db.query( 46 | Contract.Quote.TABLE_NAME, 47 | projection, 48 | selection, 49 | selectionArgs, 50 | null, 51 | null, 52 | sortOrder 53 | ); 54 | break; 55 | 56 | case QUOTE_FOR_SYMBOL: 57 | returnCursor = db.query( 58 | Contract.Quote.TABLE_NAME, 59 | projection, 60 | Contract.Quote.COLUMN_SYMBOL + " = ?", 61 | new String[]{Contract.Quote.getStockFromUri(uri)}, 62 | null, 63 | null, 64 | sortOrder 65 | ); 66 | 67 | break; 68 | default: 69 | throw new UnsupportedOperationException("Unknown URI:" + uri); 70 | } 71 | 72 | Context context = getContext(); 73 | if (context != null){ 74 | returnCursor.setNotificationUri(context.getContentResolver(), uri); 75 | } 76 | 77 | return returnCursor; 78 | } 79 | 80 | @Nullable 81 | @Override 82 | public String getType(@NonNull Uri uri) { 83 | return null; 84 | } 85 | 86 | @Nullable 87 | @Override 88 | public Uri insert(@NonNull Uri uri, ContentValues values) { 89 | SQLiteDatabase db = dbHelper.getWritableDatabase(); 90 | Uri returnUri; 91 | 92 | switch (uriMatcher.match(uri)) { 93 | case QUOTE: 94 | db.insert( 95 | Contract.Quote.TABLE_NAME, 96 | null, 97 | values 98 | ); 99 | returnUri = Contract.Quote.URI; 100 | break; 101 | default: 102 | throw new UnsupportedOperationException("Unknown URI:" + uri); 103 | } 104 | 105 | Context context = getContext(); 106 | if (context != null){ 107 | context.getContentResolver().notifyChange(uri, null); 108 | } 109 | 110 | return returnUri; 111 | } 112 | 113 | @Override 114 | public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { 115 | final SQLiteDatabase db = dbHelper.getWritableDatabase(); 116 | int rowsDeleted; 117 | 118 | if (null == selection) { 119 | selection = "1"; 120 | } 121 | switch (uriMatcher.match(uri)) { 122 | case QUOTE: 123 | rowsDeleted = db.delete( 124 | Contract.Quote.TABLE_NAME, 125 | selection, 126 | selectionArgs 127 | ); 128 | 129 | break; 130 | 131 | case QUOTE_FOR_SYMBOL: 132 | String symbol = Contract.Quote.getStockFromUri(uri); 133 | rowsDeleted = db.delete( 134 | Contract.Quote.TABLE_NAME, 135 | '"' + symbol + '"' + " =" + Contract.Quote.COLUMN_SYMBOL, 136 | selectionArgs 137 | ); 138 | break; 139 | default: 140 | throw new UnsupportedOperationException("Unknown URI:" + uri); 141 | } 142 | 143 | if (rowsDeleted != 0) { 144 | Context context = getContext(); 145 | if (context != null){ 146 | context.getContentResolver().notifyChange(uri, null); 147 | } 148 | } 149 | 150 | return rowsDeleted; 151 | } 152 | 153 | @Override 154 | public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { 155 | return 0; 156 | } 157 | 158 | @Override 159 | public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { 160 | 161 | final SQLiteDatabase db = dbHelper.getWritableDatabase(); 162 | 163 | switch (uriMatcher.match(uri)) { 164 | case QUOTE: 165 | db.beginTransaction(); 166 | int returnCount = 0; 167 | try { 168 | for (ContentValues value : values) { 169 | db.insert( 170 | Contract.Quote.TABLE_NAME, 171 | null, 172 | value 173 | ); 174 | } 175 | db.setTransactionSuccessful(); 176 | } finally { 177 | db.endTransaction(); 178 | } 179 | 180 | Context context = getContext(); 181 | if (context != null) { 182 | context.getContentResolver().notifyChange(uri, null); 183 | } 184 | 185 | return returnCount; 186 | default: 187 | return super.bulkInsert(uri, values); 188 | } 189 | 190 | 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/sync/QuoteIntentService.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.sync; 2 | 3 | import android.app.IntentService; 4 | import android.content.Intent; 5 | 6 | import timber.log.Timber; 7 | 8 | 9 | public class QuoteIntentService extends IntentService { 10 | 11 | public QuoteIntentService() { 12 | super(QuoteIntentService.class.getSimpleName()); 13 | } 14 | 15 | @Override 16 | protected void onHandleIntent(Intent intent) { 17 | Timber.d("Intent handled"); 18 | QuoteSyncJob.getQuotes(getApplicationContext()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/sync/QuoteJobService.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.sync; 2 | 3 | import android.app.job.JobParameters; 4 | import android.app.job.JobService; 5 | import android.content.Intent; 6 | 7 | import timber.log.Timber; 8 | 9 | public class QuoteJobService extends JobService { 10 | 11 | 12 | @Override 13 | public boolean onStartJob(JobParameters jobParameters) { 14 | Timber.d("Intent handled"); 15 | Intent nowIntent = new Intent(getApplicationContext(), QuoteIntentService.class); 16 | getApplicationContext().startService(nowIntent); 17 | return true; 18 | } 19 | 20 | @Override 21 | public boolean onStopJob(JobParameters jobParameters) { 22 | return false; 23 | } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/sync/QuoteSyncJob.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.sync; 2 | 3 | import android.app.job.JobInfo; 4 | import android.app.job.JobScheduler; 5 | import android.content.ComponentName; 6 | import android.content.ContentValues; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.icu.math.BigDecimal; 10 | import android.icu.text.SimpleDateFormat; 11 | import android.net.ConnectivityManager; 12 | import android.net.NetworkInfo; 13 | 14 | import com.udacity.stockhawk.data.Contract; 15 | import com.udacity.stockhawk.data.PrefUtils; 16 | 17 | import org.json.JSONArray; 18 | import org.json.JSONException; 19 | import org.json.JSONObject; 20 | import org.threeten.bp.LocalDate; 21 | import org.threeten.bp.format.DateTimeFormatter; 22 | import org.threeten.bp.temporal.ChronoUnit; 23 | 24 | import java.io.IOException; 25 | import java.util.ArrayList; 26 | import java.util.Calendar; 27 | import java.util.HashSet; 28 | import java.util.Iterator; 29 | import java.util.List; 30 | import java.util.Map; 31 | import java.util.Set; 32 | import java.util.concurrent.TimeUnit; 33 | 34 | import okhttp3.Call; 35 | import okhttp3.Callback; 36 | import okhttp3.HttpUrl; 37 | import okhttp3.OkHttpClient; 38 | import okhttp3.Request; 39 | import okhttp3.Response; 40 | import timber.log.Timber; 41 | 42 | public final class QuoteSyncJob { 43 | 44 | private static final int ONE_OFF_ID = 2; 45 | private static final String ACTION_DATA_UPDATED = "com.udacity.stockhawk.ACTION_DATA_UPDATED"; 46 | private static final int PERIOD = 300000; 47 | private static final int INITIAL_BACKOFF = 10000; 48 | private static final int PERIODIC_ID = 1; 49 | private static final int YEARS_OF_HISTORY = 2; 50 | 51 | private static final String QUANDL_ROOT = "https://www.quandl.com/api/v3/datasets/WIKI/"; 52 | 53 | private static final OkHttpClient client = new OkHttpClient(); 54 | private static final LocalDate startDate = LocalDate.now().minus(YEARS_OF_HISTORY, ChronoUnit.YEARS); 55 | private static final LocalDate endDate = LocalDate.now(); 56 | private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 57 | private static StringBuilder historyBuilder = new StringBuilder(); 58 | 59 | 60 | 61 | private QuoteSyncJob() { } 62 | 63 | static HttpUrl createQuery(String symbol) { 64 | 65 | HttpUrl.Builder httpUrl = HttpUrl.parse(QUANDL_ROOT+symbol+".json").newBuilder(); 66 | httpUrl.addQueryParameter("column_index", "4") //closing price 67 | .addQueryParameter("start_date", formatter.format(startDate)) 68 | .addQueryParameter("end_date", formatter.format(endDate)); 69 | return httpUrl.build(); 70 | } 71 | 72 | static ContentValues processStock(JSONObject jsonObject) throws JSONException{ 73 | 74 | String stockSymbol = jsonObject.getString("dataset_code"); 75 | 76 | JSONArray historicData = jsonObject.getJSONArray("data"); 77 | 78 | double price = historicData.getJSONArray(0).getDouble(1); 79 | double change = price - historicData.getJSONArray(1).getDouble(1); 80 | double percentChange = 100 * (( price - historicData.getJSONArray(1).getDouble(1) ) / historicData.getJSONArray(1).getDouble(1)); 81 | 82 | historyBuilder = new StringBuilder(); 83 | 84 | for (int i = 0; i stockPref = PrefUtils.getStocks(context); 113 | 114 | for (String stock : stockPref) { 115 | Request request = new Request.Builder() 116 | .url(createQuery(stock)).build(); 117 | 118 | client.newCall(request).enqueue(new Callback() { 119 | @Override 120 | public void onFailure(Call call, IOException e) { 121 | Timber.e("OKHTTP", e.getMessage()); 122 | } 123 | 124 | @Override 125 | public void onResponse(Call call, Response response) throws IOException { 126 | try { 127 | String body = response.body().string(); 128 | JSONObject jsonObject = new JSONObject(body); 129 | ContentValues quotes = processStock(jsonObject.getJSONObject("dataset")); 130 | 131 | context.getContentResolver().insert(Contract.Quote.URI,quotes); 132 | } catch(JSONException ex){} 133 | } 134 | }); 135 | 136 | 137 | } 138 | 139 | Intent dataUpdatedIntent = new Intent(ACTION_DATA_UPDATED); 140 | context.sendBroadcast(dataUpdatedIntent); 141 | 142 | } catch (Exception exception) { 143 | Timber.e(exception, "Error fetching stock quotes"); 144 | } 145 | } 146 | 147 | private static void schedulePeriodic(Context context) { 148 | Timber.d("Scheduling a periodic task"); 149 | 150 | 151 | JobInfo.Builder builder = new JobInfo.Builder(PERIODIC_ID, new ComponentName(context, QuoteJobService.class)); 152 | 153 | 154 | builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) 155 | .setPeriodic(PERIOD) 156 | .setBackoffCriteria(INITIAL_BACKOFF, JobInfo.BACKOFF_POLICY_EXPONENTIAL); 157 | 158 | 159 | JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); 160 | 161 | scheduler.schedule(builder.build()); 162 | } 163 | 164 | 165 | public static synchronized void initialize(final Context context) { 166 | 167 | schedulePeriodic(context); 168 | syncImmediately(context); 169 | 170 | } 171 | 172 | public static synchronized void syncImmediately(Context context) { 173 | 174 | ConnectivityManager cm = 175 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); 176 | NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 177 | if (networkInfo != null && networkInfo.isConnectedOrConnecting()) { 178 | Intent nowIntent = new Intent(context, QuoteIntentService.class); 179 | context.startService(nowIntent); 180 | } else { 181 | 182 | JobInfo.Builder builder = new JobInfo.Builder(ONE_OFF_ID, new ComponentName(context, QuoteJobService.class)); 183 | 184 | 185 | builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY) 186 | .setBackoffCriteria(INITIAL_BACKOFF, JobInfo.BACKOFF_POLICY_EXPONENTIAL); 187 | 188 | 189 | JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); 190 | 191 | scheduler.schedule(builder.build()); 192 | 193 | 194 | } 195 | } 196 | 197 | 198 | } 199 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/ui/AddStockDialog.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.ui; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.app.AlertDialog; 6 | import android.app.Dialog; 7 | import android.app.DialogFragment; 8 | import android.content.DialogInterface; 9 | import android.os.Bundle; 10 | import android.view.KeyEvent; 11 | import android.view.LayoutInflater; 12 | import android.view.View; 13 | import android.view.Window; 14 | import android.view.WindowManager; 15 | import android.widget.EditText; 16 | import android.widget.TextView; 17 | 18 | import com.udacity.stockhawk.R; 19 | 20 | import butterknife.BindView; 21 | import butterknife.ButterKnife; 22 | 23 | 24 | public class AddStockDialog extends DialogFragment { 25 | 26 | @SuppressWarnings("WeakerAccess") 27 | @BindView(R.id.dialog_stock) 28 | EditText stock; 29 | 30 | @Override 31 | public Dialog onCreateDialog(Bundle savedInstanceState) { 32 | 33 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 34 | 35 | LayoutInflater inflater = LayoutInflater.from(getActivity()); 36 | @SuppressLint("InflateParams") View custom = inflater.inflate(R.layout.add_stock_dialog, null); 37 | 38 | ButterKnife.bind(this, custom); 39 | 40 | stock.setOnEditorActionListener(new TextView.OnEditorActionListener() { 41 | @Override 42 | public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 43 | addStock(); 44 | return true; 45 | } 46 | }); 47 | builder.setView(custom); 48 | 49 | builder.setMessage(getString(R.string.dialog_title)); 50 | builder.setPositiveButton(getString(R.string.dialog_add), 51 | new DialogInterface.OnClickListener() { 52 | public void onClick(DialogInterface dialog, int id) { 53 | addStock(); 54 | } 55 | }); 56 | builder.setNegativeButton(getString(R.string.dialog_cancel), null); 57 | 58 | Dialog dialog = builder.create(); 59 | 60 | Window window = dialog.getWindow(); 61 | if (window != null) { 62 | window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 63 | } 64 | 65 | return dialog; 66 | } 67 | 68 | private void addStock() { 69 | Activity parent = getActivity(); 70 | if (parent instanceof MainActivity) { 71 | ((MainActivity) parent).addStock(stock.getText().toString()); 72 | } 73 | dismissAllowingStateLoss(); 74 | } 75 | 76 | 77 | } 78 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/ui/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.ui; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.net.ConnectivityManager; 6 | import android.net.NetworkInfo; 7 | import android.os.Bundle; 8 | import android.support.v4.app.LoaderManager; 9 | import android.support.v4.content.CursorLoader; 10 | import android.support.v4.content.Loader; 11 | import android.support.v4.widget.SwipeRefreshLayout; 12 | import android.support.v7.app.AppCompatActivity; 13 | import android.support.v7.widget.LinearLayoutManager; 14 | import android.support.v7.widget.RecyclerView; 15 | import android.support.v7.widget.helper.ItemTouchHelper; 16 | import android.view.Menu; 17 | import android.view.MenuItem; 18 | import android.view.View; 19 | import android.widget.TextView; 20 | import android.widget.Toast; 21 | 22 | import com.udacity.stockhawk.R; 23 | import com.udacity.stockhawk.data.Contract; 24 | import com.udacity.stockhawk.data.PrefUtils; 25 | import com.udacity.stockhawk.sync.QuoteSyncJob; 26 | 27 | import butterknife.BindView; 28 | import butterknife.ButterKnife; 29 | import timber.log.Timber; 30 | 31 | public class MainActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks, 32 | SwipeRefreshLayout.OnRefreshListener, 33 | StockAdapter.StockAdapterOnClickHandler { 34 | 35 | private static final int STOCK_LOADER = 0; 36 | @SuppressWarnings("WeakerAccess") 37 | @BindView(R.id.recycler_view) 38 | RecyclerView stockRecyclerView; 39 | @SuppressWarnings("WeakerAccess") 40 | @BindView(R.id.swipe_refresh) 41 | SwipeRefreshLayout swipeRefreshLayout; 42 | @SuppressWarnings("WeakerAccess") 43 | @BindView(R.id.error) 44 | TextView error; 45 | private StockAdapter adapter; 46 | 47 | @Override 48 | public void onClick(String symbol) { 49 | Timber.d("Symbol clicked: %s", symbol); 50 | } 51 | 52 | @Override 53 | protected void onCreate(Bundle savedInstanceState) { 54 | super.onCreate(savedInstanceState); 55 | 56 | setContentView(R.layout.activity_main); 57 | ButterKnife.bind(this); 58 | 59 | adapter = new StockAdapter(this, this); 60 | stockRecyclerView.setAdapter(adapter); 61 | stockRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 62 | 63 | swipeRefreshLayout.setOnRefreshListener(this); 64 | swipeRefreshLayout.setRefreshing(true); 65 | onRefresh(); 66 | 67 | QuoteSyncJob.initialize(this); 68 | getSupportLoaderManager().initLoader(STOCK_LOADER, null, this); 69 | 70 | new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) { 71 | @Override 72 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { 73 | return false; 74 | } 75 | 76 | @Override 77 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { 78 | String symbol = adapter.getSymbolAtPosition(viewHolder.getAdapterPosition()); 79 | PrefUtils.removeStock(MainActivity.this, symbol); 80 | getContentResolver().delete(Contract.Quote.makeUriForStock(symbol), null, null); 81 | } 82 | }).attachToRecyclerView(stockRecyclerView); 83 | 84 | 85 | } 86 | 87 | private boolean networkUp() { 88 | ConnectivityManager cm = 89 | (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); 90 | NetworkInfo networkInfo = cm.getActiveNetworkInfo(); 91 | return networkInfo != null && networkInfo.isConnectedOrConnecting(); 92 | } 93 | 94 | @Override 95 | public void onRefresh() { 96 | 97 | QuoteSyncJob.syncImmediately(this); 98 | 99 | if (!networkUp() && adapter.getItemCount() == 0) { 100 | swipeRefreshLayout.setRefreshing(false); 101 | error.setText(getString(R.string.error_no_network)); 102 | error.setVisibility(View.VISIBLE); 103 | } else if (!networkUp()) { 104 | swipeRefreshLayout.setRefreshing(false); 105 | Toast.makeText(this, R.string.toast_no_connectivity, Toast.LENGTH_LONG).show(); 106 | } else if (PrefUtils.getStocks(this).size() == 0) { 107 | swipeRefreshLayout.setRefreshing(false); 108 | error.setText(getString(R.string.error_no_stocks)); 109 | error.setVisibility(View.VISIBLE); 110 | } else { 111 | error.setVisibility(View.GONE); 112 | } 113 | } 114 | 115 | public void button(@SuppressWarnings("UnusedParameters") View view) { 116 | new AddStockDialog().show(getFragmentManager(), "StockDialogFragment"); 117 | } 118 | 119 | void addStock(String symbol) { 120 | if (symbol != null && !symbol.isEmpty()) { 121 | 122 | if (networkUp()) { 123 | swipeRefreshLayout.setRefreshing(true); 124 | } else { 125 | String message = getString(R.string.toast_stock_added_no_connectivity, symbol); 126 | Toast.makeText(this, message, Toast.LENGTH_LONG).show(); 127 | } 128 | 129 | PrefUtils.addStock(this, symbol); 130 | QuoteSyncJob.syncImmediately(this); 131 | } 132 | } 133 | 134 | @Override 135 | public Loader onCreateLoader(int id, Bundle args) { 136 | return new CursorLoader(this, 137 | Contract.Quote.URI, 138 | Contract.Quote.QUOTE_COLUMNS, 139 | null, null, Contract.Quote.COLUMN_SYMBOL); 140 | } 141 | 142 | @Override 143 | public void onLoadFinished(Loader loader, Cursor data) { 144 | swipeRefreshLayout.setRefreshing(false); 145 | 146 | if (data.getCount() != 0) { 147 | error.setVisibility(View.GONE); 148 | } 149 | adapter.setCursor(data); 150 | } 151 | 152 | 153 | @Override 154 | public void onLoaderReset(Loader loader) { 155 | swipeRefreshLayout.setRefreshing(false); 156 | adapter.setCursor(null); 157 | } 158 | 159 | 160 | private void setDisplayModeMenuItemIcon(MenuItem item) { 161 | if (PrefUtils.getDisplayMode(this) 162 | .equals(getString(R.string.pref_display_mode_absolute_key))) { 163 | item.setIcon(R.drawable.ic_percentage); 164 | } else { 165 | item.setIcon(R.drawable.ic_dollar); 166 | } 167 | } 168 | 169 | @Override 170 | public boolean onCreateOptionsMenu(Menu menu) { 171 | getMenuInflater().inflate(R.menu.main_activity_settings, menu); 172 | MenuItem item = menu.findItem(R.id.action_change_units); 173 | setDisplayModeMenuItemIcon(item); 174 | return true; 175 | } 176 | 177 | @Override 178 | public boolean onOptionsItemSelected(MenuItem item) { 179 | int id = item.getItemId(); 180 | 181 | if (id == R.id.action_change_units) { 182 | PrefUtils.toggleDisplayMode(this); 183 | setDisplayModeMenuItemIcon(item); 184 | adapter.notifyDataSetChanged(); 185 | return true; 186 | } 187 | return super.onOptionsItemSelected(item); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /app/src/main/java/com/udacity/stockhawk/ui/StockAdapter.java: -------------------------------------------------------------------------------- 1 | package com.udacity.stockhawk.ui; 2 | 3 | 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.TextView; 11 | 12 | import com.udacity.stockhawk.R; 13 | import com.udacity.stockhawk.data.Contract; 14 | import com.udacity.stockhawk.data.PrefUtils; 15 | 16 | import java.text.DecimalFormat; 17 | import java.text.NumberFormat; 18 | import java.util.Locale; 19 | 20 | import butterknife.BindView; 21 | import butterknife.ButterKnife; 22 | 23 | class StockAdapter extends RecyclerView.Adapter { 24 | 25 | private final Context context; 26 | private final DecimalFormat dollarFormatWithPlus; 27 | private final DecimalFormat dollarFormat; 28 | private final DecimalFormat percentageFormat; 29 | private Cursor cursor; 30 | private final StockAdapterOnClickHandler clickHandler; 31 | 32 | StockAdapter(Context context, StockAdapterOnClickHandler clickHandler) { 33 | this.context = context; 34 | this.clickHandler = clickHandler; 35 | 36 | dollarFormat = (DecimalFormat) NumberFormat.getCurrencyInstance(Locale.US); 37 | dollarFormatWithPlus = (DecimalFormat) NumberFormat.getCurrencyInstance(Locale.US); 38 | dollarFormatWithPlus.setPositivePrefix("+$"); 39 | percentageFormat = (DecimalFormat) NumberFormat.getPercentInstance(Locale.getDefault()); 40 | percentageFormat.setMaximumFractionDigits(2); 41 | percentageFormat.setMinimumFractionDigits(2); 42 | percentageFormat.setPositivePrefix("+"); 43 | } 44 | 45 | void setCursor(Cursor cursor) { 46 | this.cursor = cursor; 47 | notifyDataSetChanged(); 48 | } 49 | 50 | String getSymbolAtPosition(int position) { 51 | 52 | cursor.moveToPosition(position); 53 | return cursor.getString(Contract.Quote.POSITION_SYMBOL); 54 | } 55 | 56 | @Override 57 | public StockViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 58 | 59 | View item = LayoutInflater.from(context).inflate(R.layout.list_item_quote, parent, false); 60 | 61 | return new StockViewHolder(item); 62 | } 63 | 64 | @Override 65 | public void onBindViewHolder(StockViewHolder holder, int position) { 66 | 67 | cursor.moveToPosition(position); 68 | 69 | 70 | holder.symbol.setText(cursor.getString(Contract.Quote.POSITION_SYMBOL)); 71 | holder.price.setText(dollarFormat.format(cursor.getFloat(Contract.Quote.POSITION_PRICE))); 72 | 73 | 74 | float rawAbsoluteChange = cursor.getFloat(Contract.Quote.POSITION_ABSOLUTE_CHANGE); 75 | float percentageChange = cursor.getFloat(Contract.Quote.POSITION_PERCENTAGE_CHANGE); 76 | 77 | if (rawAbsoluteChange > 0) { 78 | holder.change.setBackgroundResource(R.drawable.percent_change_pill_green); 79 | } else { 80 | holder.change.setBackgroundResource(R.drawable.percent_change_pill_red); 81 | } 82 | 83 | String change = dollarFormatWithPlus.format(rawAbsoluteChange); 84 | String percentage = percentageFormat.format(percentageChange / 100); 85 | 86 | if (PrefUtils.getDisplayMode(context) 87 | .equals(context.getString(R.string.pref_display_mode_absolute_key))) { 88 | holder.change.setText(change); 89 | } else { 90 | holder.change.setText(percentage); 91 | } 92 | 93 | 94 | } 95 | 96 | @Override 97 | public int getItemCount() { 98 | int count = 0; 99 | if (cursor != null) { 100 | count = cursor.getCount(); 101 | } 102 | return count; 103 | } 104 | 105 | 106 | interface StockAdapterOnClickHandler { 107 | void onClick(String symbol); 108 | } 109 | 110 | class StockViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener { 111 | 112 | @BindView(R.id.symbol) 113 | TextView symbol; 114 | 115 | @BindView(R.id.price) 116 | TextView price; 117 | 118 | @BindView(R.id.change) 119 | TextView change; 120 | 121 | StockViewHolder(View itemView) { 122 | super(itemView); 123 | ButterKnife.bind(this, itemView); 124 | itemView.setOnClickListener(this); 125 | } 126 | 127 | @Override 128 | public void onClick(View v) { 129 | int adapterPosition = getAdapterPosition(); 130 | cursor.moveToPosition(adapterPosition); 131 | int symbolColumn = cursor.getColumnIndex(Contract.Quote.COLUMN_SYMBOL); 132 | clickHandler.onClick(cursor.getString(symbolColumn)); 133 | 134 | } 135 | 136 | 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_dollar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-hdpi/ic_dollar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_percentage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-hdpi/ic_percentage.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_dollar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-mdpi/ic_dollar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_percentage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-mdpi/ic_percentage.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_dollar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-xhdpi/ic_dollar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_percentage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-xhdpi/ic_percentage.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_dollar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-xxhdpi/ic_dollar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_percentage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-xxhdpi/ic_percentage.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_dollar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-xxxhdpi/ic_dollar.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_percentage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/drawable-xxxhdpi/ic_percentage.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/fab_plus.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/percent_change_pill_green.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/percent_change_pill_red.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 21 | 22 | 23 | 24 | 37 | 38 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/add_stock_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_quote.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 18 | 19 | 24 | 25 | 32 | 33 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/src/main/res/menu/main_activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @string/default_stocks_apple 5 | @string/default_stocks_yahoo 6 | @string/default_stocks_microsoft 7 | @string/default_stocks_facebook 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | #2196F3 8 | #D50000 9 | #00C853 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Stock Hawk 3 | 4 | initialized 5 | 6 | stocks 7 | 8 | 9 | displayMode 10 | @string/pref_display_mode_percentage_key 11 | 12 | absolute 13 | percentage 14 | 15 | YHOO 16 | AAPL 17 | MSFT 18 | FB 19 | 20 | 21 | @string/pref_display_mode_key 22 | 23 | 24 | Add Stock 25 | Symbol (e.g. GOOG) 26 | Cancel 27 | Add 28 | 29 | No network connectivity! Will load stock cursor when the network is available. 30 | No stocks added! Hit the floating action button to add one. 31 | 32 | Symbol %s added. Will refresh when network available. 33 | Will refresh when network available. 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 14 | 15 | 18 | 19 | 23 | 24 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.android.tools.build:gradle:2.3.1' 7 | classpath 'com.noveogroup.android:check:1.2.3' 8 | } 9 | } 10 | 11 | allprojects { 12 | repositories { 13 | jcenter() 14 | } 15 | } 16 | 17 | task clean(type: Delete) { 18 | delete rootProject.buildDir 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/StockHawk/6cfcef3ce463ce9f44959fb8faaf8121c875d324/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jan 06 08:11:27 PST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | --------------------------------------------------------------------------------