commit c327de1c23b89049d66fe73ff55be3504d0c32dc Author: hmalik144 Date: Sat Aug 29 22:33:48 2020 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..42a2d30 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Flavoured News App \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/dictionaries b/.idea/dictionaries new file mode 100644 index 0000000..ed3680a --- /dev/null +++ b/.idea/dictionaries @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..b9f8a5e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,20 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..8b7f4af --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..703e5d4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..0987232 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,91 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 30 + buildToolsVersion "29.0.3" + + defaultConfig { + applicationId "com.example.h_mal.flavourednewsapp" + minSdkVersion 16 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + buildConfigField "String", "ParamOne", "${paramOne}" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + // Specifies one flavor dimension. + flavorDimensions "version" + productFlavors { + purple { + // Assigns this product flavor to the "version" flavor dimension. + // If you are using only one dimension, this property is optional, + // and the plugin automatically assigns all the module's flavors to + // that dimension. + applicationId "com.example.h_mal.flavourednewsapp.purple" +// versionNameSuffix "-purple" + } + stock { + applicationId "com.example.h_mal.flavourednewsapp.stock" +// versionNameSuffix "-stock" + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.cardview:cardview:1.0.0' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + + // android unit testing and espresso + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + implementation 'androidx.test.espresso:espresso-core:3.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + //mock websever for testing retrofit responses + testImplementation "com.squareup.okhttp3:mockwebserver:4.6.0" + testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" + + //mockito and livedata testing + testImplementation 'org.mockito:mockito-inline:2.13.0' + implementation 'android.arch.core:core-testing' + androidTestImplementation 'androidx.test:rules:1.3.0-rc01' + + //Retrofit and GSON + implementation 'com.squareup.retrofit2:retrofit:2.8.1' + implementation 'com.squareup.retrofit2:converter-gson:2.8.1' + implementation 'com.squareup.okhttp3:logging-interceptor:4.0.0' + + // Shared prefs + implementation "androidx.preference:preference-ktx:1.1.1" + + // Picasso image display + implementation 'com.squareup.picasso:picasso:2.71828' + + //Android Room + def room_version = "2.2.5" + implementation "androidx.room:room-runtime:$room_version" + annotationProcessor "androidx.room:room-compiler:$room_version" + + // Dagger 2 dependency injection + implementation 'com.google.dagger:dagger:2.27' + annotationProcessor 'com.google.dagger:dagger-compiler:2.27' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/h_mal/flavourednewsapp/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/h_mal/flavourednewsapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..73ae5d6 --- /dev/null +++ b/app/src/androidTest/java/com/example/h_mal/flavourednewsapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.h_mal.flavourednewsapp; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.assertEquals; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.h_mal.flavourednewsapp", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b4b4400 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/app/ApplicationComponent.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/ApplicationComponent.java new file mode 100644 index 0000000..9231c1c --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/ApplicationComponent.java @@ -0,0 +1,20 @@ +package com.example.h_mal.flavourednewsapp.app; + + +import com.example.h_mal.flavourednewsapp.ui.main.MainActivity; + +import javax.inject.Singleton; + +import dagger.Component; + +/** + * create dagger2 interface for dependency injection + * define Context module to be used later in dependency injection + */ +@Singleton +@Component(modules = {ContextModule.class, RetrofitModule.class, RoomModule.class}) +public interface ApplicationComponent { + + void inject(MainActivity mainActivity); +} + diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/app/ContextModule.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/ContextModule.java new file mode 100644 index 0000000..6f57b1c --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/ContextModule.java @@ -0,0 +1,24 @@ +package com.example.h_mal.flavourednewsapp.app; + +import android.content.Context; + +import dagger.Module; +import dagger.Provides; + +/* + * Module used for injecting context in ResourcesFile class + */ +@Module +class ContextModule { + + private Context context; + + public ContextModule(Context context) { + this.context = context; + } + + @Provides + Context provideContext() { + return context; + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/app/FlavouredNewsAppClass.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/FlavouredNewsAppClass.java new file mode 100644 index 0000000..0a78916 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/FlavouredNewsAppClass.java @@ -0,0 +1,21 @@ +package com.example.h_mal.flavourednewsapp.app; + +import android.app.Application; + +public class FlavouredNewsAppClass extends Application { + + public ApplicationComponent appComponent; + + @Override + public void onCreate() { + super.onCreate(); + // Create dagger2 component to be used in the application + ContextModule contextModule = new ContextModule(getApplicationContext()); + + appComponent = DaggerApplicationComponent.builder() + .contextModule(contextModule) + .retrofitModule(new RetrofitModule()) + .roomModule(new RoomModule(contextModule.provideContext())) + .build(); + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/app/RetrofitModule.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/RetrofitModule.java new file mode 100644 index 0000000..04e1a6f --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/RetrofitModule.java @@ -0,0 +1,65 @@ +package com.example.h_mal.flavourednewsapp.app; + +import android.content.Context; + +import com.example.h_mal.flavourednewsapp.data.network.api.NewsApi; +import com.example.h_mal.flavourednewsapp.data.network.api.interceptors.NetworkConnectionInterceptor; +import com.example.h_mal.flavourednewsapp.data.network.api.interceptors.QueryInterceptor; + +import java.util.concurrent.TimeUnit; + +import dagger.Module; +import dagger.Provides; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +@Module +public class RetrofitModule { + + @Provides + NewsApi getApiInterface(Retrofit retroFit) { + return retroFit.create(NewsApi.class); + } + + @Provides + Retrofit getRetrofit(OkHttpClient okHttpClient) { + return new Retrofit.Builder() + .baseUrl("https://newsapi.org/") + .addConverterFactory(GsonConverterFactory.create()) + .client(okHttpClient) + .build(); + } + + @Provides + OkHttpClient getOkHttpClient( + HttpLoggingInterceptor httpLoggingInterceptor, + NetworkConnectionInterceptor networkConnectionInterceptor, + QueryInterceptor queryInterceptor + ) { + return new OkHttpClient.Builder() + .addNetworkInterceptor(networkConnectionInterceptor) + .addInterceptor(queryInterceptor) + .addInterceptor(httpLoggingInterceptor) + .readTimeout(5 * 60, TimeUnit.SECONDS) + .build(); + } + + @Provides + NetworkConnectionInterceptor getNetworkInterceptor(Context context) { + return new NetworkConnectionInterceptor(context); + } + + @Provides + QueryInterceptor getQueryInterceptor() { + return new QueryInterceptor(); + } + + @Provides + HttpLoggingInterceptor getHttpLoggingInterceptor() { + HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(); + httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + return httpLoggingInterceptor; + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/app/RoomModule.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/RoomModule.java new file mode 100644 index 0000000..8546be4 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/app/RoomModule.java @@ -0,0 +1,39 @@ +package com.example.h_mal.flavourednewsapp.app; + +import android.content.Context; + +import com.example.h_mal.flavourednewsapp.data.room.AppDatabase; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class RoomModule { + + AppDatabase appDatabase; + + public RoomModule(Context context) { + appDatabase = AppDatabase.getInstance(context); + } + + @Singleton + @Provides + AppDatabase providesAppDatabase() { + return appDatabase; + } + +// @Singleton +// @Provides +// ProductDao providesProductDao(DemoDatabase demoDatabase) { +// return demoDatabase.getProductDao(); +// } +// +// @Singleton +// @Provides +// ProductRepository productRepository(ProductDao productDao) { +// return new ProductDataSource(productDao); +// } + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/ResponseHandler.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/ResponseHandler.java new file mode 100644 index 0000000..441b203 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/ResponseHandler.java @@ -0,0 +1,34 @@ +package com.example.h_mal.flavourednewsapp.data.network; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; + +import retrofit2.Response; + +public abstract class ResponseHandler { + + public ResponseHandler() { + } + + public T unwrapResponse(Response response) throws IOException { + if (response.isSuccessful()) { + return response.body(); + } else { + String error = response.errorBody().string(); + int code = response.code(); + String errorMessage; + try { + errorMessage = new JSONObject(error).getString("error_message"); + } catch (JSONException e) { + e.printStackTrace(); + errorMessage = "Error Code " + code; + } + + throw new IOException(errorMessage); + } + } + + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/NewsApi.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/NewsApi.java new file mode 100644 index 0000000..f196cb6 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/NewsApi.java @@ -0,0 +1,14 @@ +package com.example.h_mal.flavourednewsapp.data.network.api; + +import com.example.h_mal.flavourednewsapp.data.network.model.NewsResponse; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface NewsApi { + + @GET("v2/everything") + Call getNewsFromApi(@Query("q") String query); + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/interceptors/NetworkConnectionInterceptor.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/interceptors/NetworkConnectionInterceptor.java new file mode 100644 index 0000000..02a5edb --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/interceptors/NetworkConnectionInterceptor.java @@ -0,0 +1,42 @@ +package com.example.h_mal.flavourednewsapp.data.network.api.interceptors; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +import java.io.IOException; + +import javax.inject.Inject; + +import okhttp3.Interceptor; +import okhttp3.Response; + +public class NetworkConnectionInterceptor implements Interceptor { + Context context; + + @Inject + public NetworkConnectionInterceptor(Context context) { + this.context = context; + } + + + @Override + public Response intercept(Chain chain) throws IOException { + if (!isInternetAvailable()){ + throw new IOException("Make sure you have an active data connection"); + } + return chain.proceed(chain.request()); + } + + private Boolean isInternetAvailable(){ + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager == null){ + return false; + } + NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo(); + if (networkInfo == null){ + return false; + } + return networkInfo.isConnected(); + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/interceptors/QueryInterceptor.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/interceptors/QueryInterceptor.java new file mode 100644 index 0000000..da3c526 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/api/interceptors/QueryInterceptor.java @@ -0,0 +1,32 @@ +package com.example.h_mal.flavourednewsapp.data.network.api.interceptors; + +import com.example.h_mal.flavourednewsapp.BuildConfig; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +public class QueryInterceptor implements Interceptor { + + @NotNull + @Override + public Response intercept(Chain chain) throws IOException { + Request original = chain.request(); + HttpUrl originalHttpUrl = original.url(); + + HttpUrl url = originalHttpUrl.newBuilder() + .addQueryParameter("apiKey", BuildConfig.ParamOne) + .build(); + +// // Request customization: add request headers + Request.Builder requestBuilder = original.newBuilder().url(url); + Request request= requestBuilder.build(); + return chain.proceed(request); + } + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/Article.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/Article.java new file mode 100644 index 0000000..bb1c6ea --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/Article.java @@ -0,0 +1,34 @@ + +package com.example.h_mal.flavourednewsapp.data.network.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Article { + + @SerializedName("source") + @Expose + public Source source; + @SerializedName("author") + @Expose + public String author; + @SerializedName("title") + @Expose + public String title; + @SerializedName("description") + @Expose + public String description; + @SerializedName("url") + @Expose + public String url; + @SerializedName("urlToImage") + @Expose + public String urlToImage; + @SerializedName("publishedAt") + @Expose + public String publishedAt; + @SerializedName("content") + @Expose + public String content; + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/NewsResponse.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/NewsResponse.java new file mode 100644 index 0000000..b839205 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/NewsResponse.java @@ -0,0 +1,21 @@ + +package com.example.h_mal.flavourednewsapp.data.network.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class NewsResponse { + + @SerializedName("status") + @Expose + public String status; + @SerializedName("totalResults") + @Expose + public Integer totalResults; + @SerializedName("articles") + @Expose + public List
articles = null; + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/Source.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/Source.java new file mode 100644 index 0000000..993d7af --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/network/model/Source.java @@ -0,0 +1,16 @@ + +package com.example.h_mal.flavourednewsapp.data.network.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class Source { + + @SerializedName("id") + @Expose + public String id; + @SerializedName("name") + @Expose + public String name; + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/preferences/PreferenceProvider.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/preferences/PreferenceProvider.java new file mode 100644 index 0000000..35f7e27 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/preferences/PreferenceProvider.java @@ -0,0 +1,44 @@ +package com.example.h_mal.flavourednewsapp.data.preferences; + +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.Nullable; +import androidx.preference.PreferenceManager; + +import javax.inject.Inject; + + +public class PreferenceProvider { + private final static String LAST_SAVED = "late_saved"; + private final static String NEWS_SAVED = "news_saved"; + SharedPreferences preference; + + @Inject + public PreferenceProvider(Context context) { + preference = PreferenceManager.getDefaultSharedPreferences(context); + } + + public void saveLastSavedAt(String user, Long savedAt) { + preference.edit().putString( + NEWS_SAVED, + user + ).putLong( + LAST_SAVED, + savedAt + ).apply(); + } + + @Nullable + public Long getLastSavedAt(String news){ + + String savedUser = preference.getString(NEWS_SAVED, null); + if (savedUser == null){ + return null; + } + if (savedUser.equals(news)){ + return preference.getLong(LAST_SAVED, 1595076034403L); + } + return null; + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/repositories/Repository.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/repositories/Repository.java new file mode 100644 index 0000000..2b7ebf4 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/repositories/Repository.java @@ -0,0 +1,19 @@ +package com.example.h_mal.flavourednewsapp.data.repositories; + +import androidx.lifecycle.LiveData; + +import com.example.h_mal.flavourednewsapp.data.network.model.Article; +import com.example.h_mal.flavourednewsapp.data.network.model.NewsResponse; +import com.example.h_mal.flavourednewsapp.data.room.entities.NewsEntity; + +import java.util.List; + +public interface Repository { + + LiveData> getNewsFromDatabase(); + LiveData getSingleNewsFromDatabase(String url); + void saveNewsToDatabase(List
news); + void getNewsFromApi(String searchTerm, RepositoryImpl.AsyncTaskResultListener asyncTaskResultListener); +// NewsResponse getNewsFromApi(String searchTerm) throws IOException; + void saveCurrentSearchToPrefs(String searchTerm); +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/repositories/RepositoryImpl.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/repositories/RepositoryImpl.java new file mode 100644 index 0000000..cdaf75b --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/repositories/RepositoryImpl.java @@ -0,0 +1,106 @@ +package com.example.h_mal.flavourednewsapp.data.repositories; + +import android.util.Log; + +import androidx.lifecycle.LiveData; + +import com.example.h_mal.flavourednewsapp.data.network.ResponseHandler; +import com.example.h_mal.flavourednewsapp.data.network.api.NewsApi; +import com.example.h_mal.flavourednewsapp.data.network.model.Article; +import com.example.h_mal.flavourednewsapp.data.network.model.NewsResponse; +import com.example.h_mal.flavourednewsapp.data.preferences.PreferenceProvider; +import com.example.h_mal.flavourednewsapp.data.room.AppDatabase; +import com.example.h_mal.flavourednewsapp.data.room.entities.NewsEntity; +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class RepositoryImpl extends ResponseHandler implements Repository{ + private static final int MILLISECONDS_ONE_MIN = 60000; + + private NewsApi api; + private AppDatabase database; + private PreferenceProvider preference; + + @Inject + public RepositoryImpl(NewsApi api, AppDatabase database, PreferenceProvider preference) { + this.api = api; + this.database = database; + this.preference = preference; + } + + // Current list of news in the database + @Override + public LiveData> getNewsFromDatabase() { + return database.getNewsDao().getAllUsers(); + } + + // retrieving a single news article from an unique url + @Override + public LiveData getSingleNewsFromDatabase(String url) { + return database.getNewsDao().getUser(url); + } + + // save a list of news to the room database + @Override + public void saveNewsToDatabase(List
news) { + List newsEntities = new ArrayList<>(); + for (Article article : news){ + newsEntities.add(new NewsEntity(article)); + } + database.getNewsDao().upsertNewUsers(newsEntities); + } + + // fetch news from an api call + @Override + public void getNewsFromApi(String searchTerm, AsyncTaskResultListener asyncTaskResultListener){ + if (isSearchValid(searchTerm)) { + api.getNewsFromApi(searchTerm).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + Gson gson = new Gson(); + + Log.i("ApiResponse", gson.toJson(response.body())); + asyncTaskResultListener.onSuccess(response.body()); + } + + @Override + public void onFailure(Call call, Throwable t) { + asyncTaskResultListener.onFailed(t.getMessage()); + } + }); + + } + } + + @Override + public void saveCurrentSearchToPrefs(String searchTerm) { + Long time = System.currentTimeMillis(); + preference.saveLastSavedAt(searchTerm, time); + } + + // boolean response of validity of search + // if the same search is taking place again with a minute return false + private Boolean isSearchValid(String searchTerm){ + Long time = preference.getLastSavedAt(searchTerm); + if (time == null){ + return true; + } + Long currentTime = System.currentTimeMillis(); + long difference = currentTime - time; + + return difference > MILLISECONDS_ONE_MIN; + } + + public interface AsyncTaskResultListener { + void onSuccess(T value); + void onFailed(String error); + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/AppDatabase.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/AppDatabase.java new file mode 100644 index 0000000..bb79d97 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/AppDatabase.java @@ -0,0 +1,33 @@ +package com.example.h_mal.flavourednewsapp.data.room; + +import android.content.Context; + +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; + +import com.example.h_mal.flavourednewsapp.data.room.entities.NewsEntity; + +@Database(entities = NewsEntity.class, version = 1) +public abstract class AppDatabase extends RoomDatabase { + + private static AppDatabase instance; + + public static synchronized AppDatabase getInstance(Context context) { + if (instance == null) { + instance = createDatabase(context); + } + return instance; + } + + private static AppDatabase createDatabase(Context context) { + return Room.databaseBuilder( + context.getApplicationContext(), + AppDatabase.class, + "MyDatabase.db" + ).build(); + } + + public abstract NewsDao getNewsDao(); + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/NewsDao.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/NewsDao.java new file mode 100644 index 0000000..436a93c --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/NewsDao.java @@ -0,0 +1,35 @@ +package com.example.h_mal.flavourednewsapp.data.room; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; +import androidx.room.Transaction; + +import com.example.h_mal.flavourednewsapp.data.room.entities.NewsEntity; + +import java.util.List; + +@Dao +public abstract class NewsDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + public abstract void saveAllUsers(List news); + + @Query("SELECT * FROM News") + public abstract LiveData> getAllUsers(); + + // clear database and add new entries + @Transaction + public void upsertNewUsers(List news){ + deleteEntries(); + saveAllUsers(news); + } + + @Query("DELETE FROM News") + public abstract void deleteEntries(); + + @Query("SELECT * FROM News WHERE url = :url") + public abstract LiveData getUser(String url); +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/entities/NewsEntity.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/entities/NewsEntity.java new file mode 100644 index 0000000..910bef1 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/data/room/entities/NewsEntity.java @@ -0,0 +1,98 @@ +package com.example.h_mal.flavourednewsapp.data.room.entities; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +import com.example.h_mal.flavourednewsapp.data.network.model.Article; + + +@Entity(tableName = "News") +public class NewsEntity { + + public String author; + public String title; + public String description; + @NonNull + @PrimaryKey(autoGenerate = false) + public String url; + public String urlToImage; + public String publishedAt; + public String content; + + public NewsEntity(String author, String title, String description, @NonNull String url, String urlToImage, String publishedAt, String content) { + this.author = author; + this.title = title; + this.description = description; + this.url = url; + this.urlToImage = urlToImage; + this.publishedAt = publishedAt; + this.content = content; + } + + public NewsEntity(Article article) { + this.author = article.author; + this.title = article.title; + this.description = article.description; + this.url = article.url; + this.urlToImage = article.urlToImage; + this.publishedAt = article.publishedAt; + this.content = article.content; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUrlToImage() { + return urlToImage; + } + + public void setUrlToImage(String urlToImage) { + this.urlToImage = urlToImage; + } + + public String getPublishedAt() { + return publishedAt; + } + + public void setPublishedAt(String publishedAt) { + this.publishedAt = publishedAt; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainActivity.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainActivity.java new file mode 100644 index 0000000..2b22075 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainActivity.java @@ -0,0 +1,65 @@ +package com.example.h_mal.flavourednewsapp.ui.main; + +import android.os.Bundle; +import android.widget.ProgressBar; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProvider; + +import com.example.h_mal.flavourednewsapp.R; +import com.example.h_mal.flavourednewsapp.app.FlavouredNewsAppClass; +import com.example.h_mal.flavourednewsapp.ui.main.home.MainFragment; + +import javax.inject.Inject; + +import static com.example.h_mal.flavourednewsapp.utils.ViewUtils.displayToast; +import static com.example.h_mal.flavourednewsapp.utils.ViewUtils.hide; +import static com.example.h_mal.flavourednewsapp.utils.ViewUtils.show; + +public class MainActivity extends AppCompatActivity { + @Inject + MainViewModelFactory mainViewModelFactory; + + public MainViewModel mViewModel; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + ProgressBar progressBar = findViewById(R.id.progress_circular); + + // Retrieve Dagger2 component from Application class + ((FlavouredNewsAppClass) getApplication()).appComponent.inject(this); + // Create viewmodel + mViewModel = new ViewModelProvider(this, mainViewModelFactory).get(MainViewModel.class); + + // Observe operation state to display progress bar + mViewModel.operationState.observe(this, aBoolean -> { + if (aBoolean){ + show(progressBar); + }else { + hide(progressBar); + } + }); + + // Display a toast error if no internet + mViewModel.operationError.observe(this, stringEvent -> { + String mes = stringEvent.getContentIfNotHandled(); + if (mes != null){ + displayToast(this, mes); + } + }); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.container, MainFragment.newInstance()) + .commit(); + } + } + + @Override + public void onBackPressed() { + + super.onBackPressed(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainViewModel.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainViewModel.java new file mode 100644 index 0000000..285a0e0 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainViewModel.java @@ -0,0 +1,71 @@ +package com.example.h_mal.flavourednewsapp.ui.main; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.example.h_mal.flavourednewsapp.data.network.model.Article; +import com.example.h_mal.flavourednewsapp.data.network.model.NewsResponse; +import com.example.h_mal.flavourednewsapp.data.repositories.RepositoryImpl; +import com.example.h_mal.flavourednewsapp.data.room.entities.NewsEntity; +import com.example.h_mal.flavourednewsapp.utils.Event; + +import java.util.List; +import java.util.concurrent.Executors; + + +public class MainViewModel extends ViewModel { + @NonNull + private RepositoryImpl repository; + + public MainViewModel(@NonNull RepositoryImpl repository) { + this.repository = repository; + + } + + // livedata for user items in room database + public LiveData> getNewsLiveData(){ + return repository.getNewsFromDatabase(); + } + + MutableLiveData operationState = new MutableLiveData<>(); + MutableLiveData> operationError = new MutableLiveData<>(); + + public void getNews(String searchTerm){ + // validate that search term is not empty + if (searchTerm.isEmpty()){ + operationError.postValue(new Event("Enter a valid username")); + return; + } + + repository.getNewsFromApi(searchTerm, new RepositoryImpl.AsyncTaskResultListener() { + @Override + public void onSuccess(NewsResponse value) { + List
articles = value.articles; + if (!articles.isEmpty()) { + // save news articles to database + saveResultsToDatabase(articles); + // save last search term + repository.saveCurrentSearchToPrefs(searchTerm); + } + } + + @Override + public void onFailed(String error) { + operationError.postValue(new Event(error)); + } + }); + + } + + private void saveResultsToDatabase(List
articles){ + Executors.newSingleThreadScheduledExecutor().submit(() -> repository.saveNewsToDatabase(articles)); + } + + + public LiveData getSingleNews(String url){ + return repository.getSingleNewsFromDatabase(url); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainViewModelFactory.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainViewModelFactory.java new file mode 100644 index 0000000..bffad49 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/MainViewModelFactory.java @@ -0,0 +1,25 @@ +package com.example.h_mal.flavourednewsapp.ui.main; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import com.example.h_mal.flavourednewsapp.data.repositories.RepositoryImpl; + +import javax.inject.Inject; + +class MainViewModelFactory implements ViewModelProvider.Factory { + RepositoryImpl repository; + + @Inject + public MainViewModelFactory(RepositoryImpl repository) { + this.repository = repository; + } + + @NonNull + @Override + @SuppressWarnings("UNCHECKED_CAST") + public T create(@NonNull Class modelClass) { + return (T) new MainViewModel(repository); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/home/MainFragment.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/home/MainFragment.java new file mode 100644 index 0000000..7aad99e --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/home/MainFragment.java @@ -0,0 +1,77 @@ +package com.example.h_mal.flavourednewsapp.ui.main.home; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.h_mal.flavourednewsapp.R; +import com.example.h_mal.flavourednewsapp.ui.main.MainActivity; +import com.example.h_mal.flavourednewsapp.ui.main.MainViewModel; + +public class MainFragment extends Fragment implements SearchView.OnQueryTextListener { + + private MainViewModel mViewModel; + + private RecyclerView recyclerView; + + public static MainFragment newInstance() { + return new MainFragment(); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.main_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + // setup menu for searching + setHasOptionsMenu(true); + recyclerView = view.findViewById(R.id.recycler_view); + mViewModel = ((MainActivity) requireActivity()).mViewModel; + + // observe livedata to populate recycler view of articles + mViewModel.getNewsLiveData().observe(getViewLifecycleOwner(), newsEntities -> + recyclerView.setAdapter( + new NewsRecyclerAdapter((MainActivity) requireActivity(), newsEntities) + ) + ); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + super.onCreateOptionsMenu(menu, inflater); + inflater.inflate(R.menu.menu, menu); + + // Setup search bar in fragment + MenuItem filter = menu.findItem(R.id.app_bar_search); + SearchView searchView = (SearchView) filter.getActionView(); + searchView.setOnQueryTextListener(this); + } + + @Override + public boolean onQueryTextSubmit(String query) { + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + if (newText.length() >= 3){ + mViewModel.getNews(newText); + } + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/home/NewsRecyclerAdapter.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/home/NewsRecyclerAdapter.java new file mode 100644 index 0000000..98bed8a --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/home/NewsRecyclerAdapter.java @@ -0,0 +1,120 @@ +package com.example.h_mal.flavourednewsapp.ui.main.home; + +import android.content.Intent; +import android.net.Uri; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.h_mal.flavourednewsapp.R; +import com.example.h_mal.flavourednewsapp.data.room.entities.NewsEntity; +import com.example.h_mal.flavourednewsapp.ui.main.MainActivity; +import com.example.h_mal.flavourednewsapp.ui.main.overview.OverviewFragment; +import com.squareup.picasso.Picasso; + +import java.util.List; + +import static com.example.h_mal.flavourednewsapp.utils.DateUtils.dateStringOnly; + +public class NewsRecyclerAdapter extends RecyclerView.Adapter { + MainActivity mainActivity; + List newsList; + + public NewsRecyclerAdapter(MainActivity mainActivity, List newsList) { + this.mainActivity = mainActivity; + this.newsList = newsList; + } + + public void updateList(List newsList) { + this.newsList.clear(); + this.newsList.addAll(newsList); + this.notifyDataSetChanged(); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (newsList.isEmpty()) { + View EmptyView = LayoutInflater.from(mainActivity).inflate(R.layout.empty_list_cell, parent, false); + return new RecyclerView.ViewHolder(EmptyView) { + @Override + public String toString() { + return super.toString(); + } + }; + } else { + View itemOne = LayoutInflater.from(mainActivity).inflate(R.layout.news_item_cell, parent, false); + return new ItemOne(itemOne); + } + + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof ItemOne) { + NewsEntity currentNews = newsList.get(position); + ((ItemOne) holder).bindView(currentNews); + ((ItemOne) holder).buttonOpen.setOnClickListener(v -> { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(currentNews.url)); + mainActivity.startActivity(intent); + }); + ((ItemOne) holder).buttonShare.setOnClickListener(v -> { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, currentNews.title); + mainActivity.startActivity(Intent.createChooser(shareIntent, "choose one")); + }); + holder.itemView.setOnClickListener(view -> + mainActivity.getSupportFragmentManager().beginTransaction() + .replace(R.id.container, OverviewFragment.newInstance(currentNews.url)) + .addToBackStack("details") + .commit() + ); + } + } + + @Override + public int getItemCount() { + if (newsList.isEmpty()) { + return 1; + } else { + return newsList.size(); + } + } + + static class ItemOne extends RecyclerView.ViewHolder { + TextView dateTv; + TextView titleTv; + TextView authorTv; + ImageView cellImg; + CardView buttonShare; + CardView buttonOpen; + + public ItemOne(@NonNull View itemView) { + super(itemView); + dateTv = itemView.findViewById(R.id.card_date); + titleTv = itemView.findViewById(R.id.card_title); + authorTv = itemView.findViewById(R.id.card_author); + buttonShare = itemView.findViewById(R.id.button_share); + buttonOpen = itemView.findViewById(R.id.button_open); + cellImg = itemView.findViewById(R.id.cell_image_view); + } + + public void bindView(NewsEntity news) { + String date = dateStringOnly(news.publishedAt); + dateTv.setText(date); + titleTv.setText(news.title); + authorTv.setText(news.author); + if (news.urlToImage != null && !news.urlToImage.isEmpty()) { + Picasso.get().load(news.urlToImage).into(cellImg); + } + } + } + +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/overview/OverviewFragment.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/overview/OverviewFragment.java new file mode 100644 index 0000000..f94fb9f --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/overview/OverviewFragment.java @@ -0,0 +1,68 @@ +package com.example.h_mal.flavourednewsapp.ui.main.overview; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.h_mal.flavourednewsapp.R; +import com.example.h_mal.flavourednewsapp.ui.main.MainActivity; +import com.example.h_mal.flavourednewsapp.ui.main.MainViewModel; + + +public class OverviewFragment extends Fragment { + private static final String ARG_PARAM1 = "param1"; + + private MainViewModel mViewModel; + + private String mUrl; + + public OverviewFragment() { + // Required empty public constructor + } + + /** + * Use this factory method to create a new instance of + * this fragment using the provided parameters. + * + * @param url Parameter 1. + * @return A new instance of fragment OverviewFragment. + */ + public static OverviewFragment newInstance(String url) { + OverviewFragment fragment = new OverviewFragment(); + Bundle args = new Bundle(); + args.putString(ARG_PARAM1, url); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + mUrl = getArguments().getString(ARG_PARAM1); + } + mViewModel = ((MainActivity) requireActivity()).mViewModel; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_overview, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + RecyclerView overviewRecycler = view.findViewById(R.id.overview_recycler_view); + + mViewModel.getSingleNews(mUrl).observe(getViewLifecycleOwner(), newsEntity -> + overviewRecycler.setAdapter( new OverviewRecyclerAdapter(requireContext(), newsEntity)) + ); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/overview/OverviewRecyclerAdapter.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/overview/OverviewRecyclerAdapter.java new file mode 100644 index 0000000..9213ee8 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/ui/main/overview/OverviewRecyclerAdapter.java @@ -0,0 +1,110 @@ +package com.example.h_mal.flavourednewsapp.ui.main.overview; + +import android.content.Context; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.h_mal.flavourednewsapp.R; +import com.example.h_mal.flavourednewsapp.data.room.entities.NewsEntity; +import com.squareup.picasso.Picasso; + +class OverviewRecyclerAdapter extends RecyclerView.Adapter { + Context context; + NewsEntity news; + + + public OverviewRecyclerAdapter(Context context, NewsEntity news) { + this.context = context; + this.news = news; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == 1) { + View itemOne = LayoutInflater.from(context).inflate(R.layout.item_one_layout, parent, false); + return new ItemOne(itemOne); + }else { + View itemTwo = LayoutInflater.from(context).inflate(R.layout.item_two_layout, parent, false); + return new ItemTwo(itemTwo); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + if (holder instanceof ItemOne){ + ((ItemOne) holder).bindView(news); + }else { + ((ItemTwo) holder).bindView(getItemDetails(position)); + } + } + + private Pair getItemDetails(int position){ + switch (position){ + case 1: + return new Pair("Title", news.getTitle()); + case 2: + return new Pair("Content", news.getContent()); + case 3: + return new Pair("Description", news.getDescription()); + case 4: + return new Pair("Author", news.getAuthor()); + case 5: + return new Pair("Url", news.getUrl()); + default: + return new Pair("", ""); + } + } + + @Override + public int getItemCount() { + return 6; + } + + @Override + public int getItemViewType(int position) { + if (position == 0) { + return 1; + }else { + return 2; + } + } + + static class ItemOne extends RecyclerView.ViewHolder { + ImageView cellImageView; + + public ItemOne(@NonNull View itemView) { + super(itemView); + cellImageView = itemView.findViewById(R.id.image_header); + } + + public void bindView(NewsEntity news){ + if (news.urlToImage != null && !news.urlToImage.isEmpty()){ + Picasso.get().load(news.urlToImage).into(cellImageView); + } + } + } + + static class ItemTwo extends RecyclerView.ViewHolder { + TextView top; + TextView bottom; + + public ItemTwo(@NonNull View itemView) { + super(itemView); + top = itemView.findViewById(android.R.id.text1); + bottom = itemView.findViewById(android.R.id.text2); + } + + public void bindView(Pair entry){ + top.setText(entry.first); + bottom.setText(entry.second); + } + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/DateUtils.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/DateUtils.java new file mode 100644 index 0000000..6557f27 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/DateUtils.java @@ -0,0 +1,12 @@ +package com.example.h_mal.flavourednewsapp.utils; + +public class DateUtils { + + public static String dateStringOnly(String date){ + try{ + return date.split("T")[0]; + }catch (Exception e){ + return date; + } + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/Event.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/Event.java new file mode 100644 index 0000000..f54eba1 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/Event.java @@ -0,0 +1,20 @@ +package com.example.h_mal.flavourednewsapp.utils; + + +public class Event { + private T content; + private Boolean hasBeenHandled = false; + + public Event(T content) { + this.content = content; + } + + public T getContentIfNotHandled(){ + if (hasBeenHandled) { + return null; + } else { + hasBeenHandled = true; + return content; + } + } +} diff --git a/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/ViewUtils.java b/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/ViewUtils.java new file mode 100644 index 0000000..187c796 --- /dev/null +++ b/app/src/main/java/com/example/h_mal/flavourednewsapp/utils/ViewUtils.java @@ -0,0 +1,20 @@ +package com.example.h_mal.flavourednewsapp.utils; + +import android.content.Context; +import android.view.View; +import android.widget.Toast; + +public class ViewUtils { + + public static void hide(View view){ + view.setVisibility(View.GONE); + } + + public static void show(View view){ + view.setVisibility(View.VISIBLE); + } + + public static void displayToast(Context context, String message){ + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dims.jpg b/app/src/main/res/drawable/dims.jpg new file mode 100644 index 0000000..69a2e36 Binary files /dev/null and b/app/src/main/res/drawable/dims.jpg differ diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_overview.xml b/app/src/main/res/layout/fragment_overview.xml new file mode 100644 index 0000000..3970e69 --- /dev/null +++ b/app/src/main/res/layout/fragment_overview.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_one_layout.xml b/app/src/main/res/layout/item_one_layout.xml new file mode 100644 index 0000000..d0f900f --- /dev/null +++ b/app/src/main/res/layout/item_one_layout.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_two_layout.xml b/app/src/main/res/layout/item_two_layout.xml new file mode 100644 index 0000000..ad15b13 --- /dev/null +++ b/app/src/main/res/layout/item_two_layout.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml new file mode 100644 index 0000000..81de51b --- /dev/null +++ b/app/src/main/res/layout/main_activity.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/layout/main_fragment.xml b/app/src/main/res/layout/main_fragment.xml new file mode 100644 index 0000000..37ab553 --- /dev/null +++ b/app/src/main/res/layout/main_fragment.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu.xml b/app/src/main/res/menu/menu.xml new file mode 100644 index 0000000..cc820fe --- /dev/null +++ b/app/src/main/res/menu/menu.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..65155f8 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Flavoured News App + + Hello blank fragment + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..9745ba2 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/purple/res/layout/empty_list_cell.xml b/app/src/purple/res/layout/empty_list_cell.xml new file mode 100644 index 0000000..2289d18 --- /dev/null +++ b/app/src/purple/res/layout/empty_list_cell.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/purple/res/layout/news_item_cell.xml b/app/src/purple/res/layout/news_item_cell.xml new file mode 100644 index 0000000..6d27575 --- /dev/null +++ b/app/src/purple/res/layout/news_item_cell.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/purple/res/values/colors.xml b/app/src/purple/res/values/colors.xml new file mode 100644 index 0000000..7cf0cff --- /dev/null +++ b/app/src/purple/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #6200EE + #3700B3 + #03DAC5 + #8C5DFF + diff --git a/app/src/purple/res/values/strings.xml b/app/src/purple/res/values/strings.xml new file mode 100644 index 0000000..ef6eadc --- /dev/null +++ b/app/src/purple/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Purple News + \ No newline at end of file diff --git a/app/src/stock/res/layout/empty_list_cell.xml b/app/src/stock/res/layout/empty_list_cell.xml new file mode 100644 index 0000000..12fbac2 --- /dev/null +++ b/app/src/stock/res/layout/empty_list_cell.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + diff --git a/app/src/stock/res/layout/news_item_cell.xml b/app/src/stock/res/layout/news_item_cell.xml new file mode 100644 index 0000000..94b6d70 --- /dev/null +++ b/app/src/stock/res/layout/news_item_cell.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/stock/res/values/colors.xml b/app/src/stock/res/values/colors.xml new file mode 100644 index 0000000..3331b89 --- /dev/null +++ b/app/src/stock/res/values/colors.xml @@ -0,0 +1,7 @@ + + + #7e8287 + #54494b + #9da39a + + diff --git a/app/src/stock/res/values/strings.xml b/app/src/stock/res/values/strings.xml new file mode 100644 index 0000000..14e3ea8 --- /dev/null +++ b/app/src/stock/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Stock News + diff --git a/app/src/test/java/com/example/h_mal/flavourednewsapp/ExampleUnitTest.java b/app/src/test/java/com/example/h_mal/flavourednewsapp/ExampleUnitTest.java new file mode 100644 index 0000000..e4ce9d6 --- /dev/null +++ b/app/src/test/java/com/example/h_mal/flavourednewsapp/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.h_mal.flavourednewsapp; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/app/src/test/java/com/example/h_mal/flavourednewsapp/repositories/MockCallable.java b/app/src/test/java/com/example/h_mal/flavourednewsapp/repositories/MockCallable.java new file mode 100644 index 0000000..15bc09a --- /dev/null +++ b/app/src/test/java/com/example/h_mal/flavourednewsapp/repositories/MockCallable.java @@ -0,0 +1,62 @@ +package com.example.h_mal.flavourednewsapp.repositories; + +import com.example.h_mal.flavourednewsapp.data.network.model.NewsResponse; + +import java.io.IOException; + +import okhttp3.Request; +import okio.Timeout; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static org.mockito.Mockito.mock; + +class MockCallable implements Call { + final Class typeParameterClass; + + public MockCallable(Class typeParameterClass) { + this.typeParameterClass = typeParameterClass; + } + + @Override + public Response execute() throws IOException { + T mockResponse = mock(typeParameterClass); + return Response.success(mockResponse); + } + + @Override + public void enqueue(Callback callback) { + + } + + @Override + public boolean isExecuted() { + return false; + } + + @Override + public void cancel() { + + } + + @Override + public boolean isCanceled() { + return false; + } + + @Override + public Call clone() { + return null; + } + + @Override + public Request request() { + return null; + } + + @Override + public Timeout timeout() { + return null; + } +} diff --git a/app/src/test/java/com/example/h_mal/flavourednewsapp/repositories/RepositoryImplTest.java b/app/src/test/java/com/example/h_mal/flavourednewsapp/repositories/RepositoryImplTest.java new file mode 100644 index 0000000..e09645f --- /dev/null +++ b/app/src/test/java/com/example/h_mal/flavourednewsapp/repositories/RepositoryImplTest.java @@ -0,0 +1,127 @@ +package com.example.h_mal.flavourednewsapp.repositories; + +import com.example.h_mal.flavourednewsapp.data.network.api.NewsApi; +import com.example.h_mal.flavourednewsapp.data.network.model.NewsResponse; +import com.example.h_mal.flavourednewsapp.data.preferences.PreferenceProvider; +import com.example.h_mal.flavourednewsapp.data.repositories.RepositoryImpl; +import com.example.h_mal.flavourednewsapp.data.room.AppDatabase; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +import okhttp3.Request; +import okhttp3.ResponseBody; +import okio.Timeout; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RepositoryImplTest { + + @Mock + NewsApi api; + @Mock + AppDatabase db; + @Mock + PreferenceProvider prefs; + + RepositoryImpl repository; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + repository = new RepositoryImpl(api, db, prefs); + } + + @Test + public void fetchUserFromApi_positiveResponse() { + // GIVEN + String input = "12345"; + NewsResponse mockApiResponse = mock(NewsResponse.class); + Call mockResponse = new MockCallable<>(NewsResponse.class); + + // WHEN + Mockito.when(api.getNewsFromApi(input)).thenReturn(mockResponse); + Mockito.when(prefs.getLastSavedAt(input)).thenReturn(null); + + // THEN + repository.getNewsFromApi(input, new RepositoryImpl.AsyncTaskResultListener() { + @Override + public void onSuccess(NewsResponse value) { + assertEquals(mockApiResponse, value); + } + + @Override + public void onFailed(String error) { } + }); + } + + @Test + public void fetchUserFromApi_negativeResponse() throws IOException { + // GIVEN + String input = "12345"; + String errorString = "error"; + Call mockResponse = new Call(){ + @Override + public Response execute() throws IOException { + throw new IOException(errorString); + } + @Override + public void enqueue(Callback callback) { } + + @Override + public boolean isExecuted() { + return false; + } + + @Override + public void cancel() { } + + @Override + public boolean isCanceled() { + return false; + } + + @Override + public Call clone() { + return null; + } + + @Override + public Request request() { + return null; + } + + @Override + public Timeout timeout() { + return null; + } + }; + + // WHEN + Mockito.when(api.getNewsFromApi(input)).thenReturn(mockResponse); + Mockito.when(prefs.getLastSavedAt(input)).thenReturn(null); + + // THEN + repository.getNewsFromApi(input, new RepositoryImpl.AsyncTaskResultListener() { + @Override + public void onSuccess(NewsResponse value) { + } + + @Override + public void onFailed(String error) { + assertEquals(error, errorString); + } + }); + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..da807a2 --- /dev/null +++ b/build.gradle @@ -0,0 +1,24 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.0.0" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..ce34574 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# key +paramOne="68b3929c358f4cb59bf32793106ce752" \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..233f01d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Aug 28 13:46:04 BST 2020 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..016c45f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name = "Flavoured News App" \ No newline at end of file