diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index 8598a75..0000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Yapplication \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 97626ba..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 2ab1e27..4c73708 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -3,22 +3,10 @@ diff --git a/app/.gitignore b/app/.gitignore index 796b96d..09fb41b 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,2 @@ /build +.idea/* \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index e4ec7db..9f51b5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,10 +2,22 @@ apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'me.tatarka.retrolambda' +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + android { compileSdkVersion 23 - buildToolsVersion "23.0.2" - +// signingConfigs { +// Yappl { +// keyAlias keystoreProperties['keyAlias'] +// keyPassword keystoreProperties['keyPassword'] +// storeFile file(keystoreProperties['storeFile']) +// storePassword keystoreProperties['storePassword'] +// } +// } + compileSdkVersion 23 + buildToolsVersion "23.0.3" defaultConfig { applicationId "ru.aleien.yapplication" minSdkVersion 15 @@ -15,7 +27,7 @@ android { } buildTypes { release { - minifyEnabled false + minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } @@ -24,12 +36,6 @@ android { } } - sourceSets { - androidTest { - setRoot('src/test') - } - } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -38,34 +44,49 @@ android { dependencies { ext.supportVersion = '23.3.0' - compile fileTree(dir: 'libs', include: ['*.jar']) - compile "com.android.support:support-v4:$supportVersion" - compile "com.android.support:appcompat-v7:$supportVersion" - compile "com.android.support:design:$supportVersion" + compile fileTree(include: ['*.jar'], dir: 'libs') + compile 'com.android.support:support-v4:24.1.1' + compile 'com.android.support:appcompat-v7:24.1.1' + compile 'com.android.support:design:24.1.1' // Annotation heaven - compile 'com.jakewharton:butterknife:7.0.1' + compile 'com.jakewharton:butterknife:8.2.1' + apt 'com.jakewharton:butterknife-compiler:8.2.1' compile 'javax.annotation:jsr250-api:1.0' // UI - compile "com.android.support:cardview-v7:$supportVersion" - compile "com.android.support:recyclerview-v7:$supportVersion" + compile 'com.android.support:cardview-v7:24.1.1' + compile 'com.android.support:recyclerview-v7:24.1.1' // Testing testCompile 'junit:junit:4.12' - androidTestCompile "com.android.support:support-annotations:$supportVersion" - androidTestCompile 'com.android.support.test:runner:0.4.1' - testCompile "org.robolectric:robolectric:3.0" - testCompile "org.mockito:mockito-core:1.10.19" + androidTestCompile "com.android.support:support-annotations:24.1.1" + androidTestCompile 'com.android.support.test:runner:0.5' + testCompile 'org.mockito:mockito-core:2.0.99-beta' + testCompile "org.robolectric:robolectric:3.1.2" + testCompile 'org.robolectric:shadows-support-v4:3.1.2' // Network compile 'com.squareup.okhttp:okhttp:2.7.5' - compile 'com.squareup.okhttp3:logging-interceptor:3.0.1' - compile 'com.squareup.retrofit2:converter-gson:2.0.0' - compile 'com.squareup.retrofit2:retrofit:2.0.0' - + compile 'com.squareup.okhttp3:logging-interceptor:3.4.1' + compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0' + compile 'com.squareup.retrofit2:converter-gson:2.1.0' + compile 'com.squareup.retrofit2:retrofit:2.1.0' debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2' // Image processor compile 'com.github.bumptech.glide:glide:3.7.0' + + // DI + compile 'com.google.dagger:dagger:2.6' + apt 'com.google.dagger:dagger-compiler:2.6' + provided 'javax.annotation:jsr250-api:1.0' + + // Rx + compile 'io.reactivex:rxandroid:1.2.1' + compile 'io.reactivex:rxjava:1.1.8' + + debugCompile 'com.facebook.stetho:stetho:1.2.0' + compile 'com.jakewharton.timber:timber:4.1.2' + } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 0a0c3b5..49e0649 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -15,3 +15,26 @@ #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} + +# Butterknife +-keep public class * implements butterknife.internal.ViewBinder { public (); } + +# Prevent obfuscation of types which use ButterKnife annotations since the simple name +# is used to reflectively look up the generated ViewBinder. +-keep class butterknife.* +-keepclasseswithmembernames class * { @butterknife.* ; } +-keepclasseswithmembernames class * { @butterknife.* ; } +-keepnames class * { @butterknife.Bind *;} + +# Dagger +-keepclassmembers,allowobfuscation class * { + @javax.inject.* *; + @dagger.* *; + (); +} + +-keep class javax.inject.** { *; } +-keep class **$$ModuleAdapter +-keep class **$$InjectAdapter +-keep class **$$StaticInjection +-keep class dagger.** { *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cd8d13b..bdf3034 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,24 +1,33 @@ + package="ru.aleien.yapplication"> - - + + + - + - + - + + + diff --git a/app/src/main/java/ru/aleien/yapplication/App.java b/app/src/main/java/ru/aleien/yapplication/App.java new file mode 100644 index 0000000..c91dcac --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/App.java @@ -0,0 +1,30 @@ +package ru.aleien.yapplication; + +import android.app.Application; + +import com.facebook.stetho.Stetho; + +import ru.aleien.yapplication.di.AppComponent; +import ru.aleien.yapplication.di.AppModule; +import ru.aleien.yapplication.di.DaggerAppComponent; +import timber.log.Timber; + +public class App extends Application { + private AppComponent component; + + + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree()); + Stetho.initializeWithDefaults(this.getApplicationContext()); + component = DaggerAppComponent.builder() + .appModule(new AppModule(this)) + .build(); + + } + + public AppComponent dagger() { + return component; + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/ArtistsPresenter.java b/app/src/main/java/ru/aleien/yapplication/ArtistsPresenter.java index 0496e90..0c4ef32 100644 --- a/app/src/main/java/ru/aleien/yapplication/ArtistsPresenter.java +++ b/app/src/main/java/ru/aleien/yapplication/ArtistsPresenter.java @@ -1,14 +1,17 @@ package ru.aleien.yapplication; -import android.content.Context; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; +import android.util.Log; import java.io.Serializable; import java.lang.ref.WeakReference; import java.util.List; +import javax.inject.Inject; + import ru.aleien.yapplication.base.BasePresenter; +import ru.aleien.yapplication.database.DBBackend; import ru.aleien.yapplication.dataprovider.ArtistsProvider; import ru.aleien.yapplication.dataprovider.WebArtistsProvider; import ru.aleien.yapplication.model.Artist; @@ -17,6 +20,10 @@ import ru.aleien.yapplication.screens.list.ArtistsListView; import ru.aleien.yapplication.screens.list.ArtistsRecyclerFragment; import ru.aleien.yapplication.utils.adapters.ArtistsRecyclerAdapter; +import rx.Completable; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; +import timber.log.Timber; /** * Created by aleien on 09.04.16. @@ -25,17 +32,33 @@ */ public class ArtistsPresenter extends BasePresenter implements ArtistsRequester, ArtistClickHandler, Serializable { ArtistsProvider artistsProvider; + private final DBBackend dbSource; private WeakReference> artistsListView; private WeakReference currentFragment; - public ArtistsPresenter(Context context) { - artistsProvider = new WebArtistsProvider(this, context); + @Inject + public ArtistsPresenter(DBBackend dbSource, + // Как здесь получать интерфейс? + WebArtistsProvider artistsProvider) { + this.dbSource = dbSource; + this.artistsProvider = artistsProvider; } @Override public void takeListView(ArtistsListView list) { artistsListView = new WeakReference<>(list); - artistsProvider.requestData(); + + subscribe(dbSource.getAllArtists() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::provideData, + throwable -> Timber.e("DBError", "Error while reading cached artists"))); + + artistsProvider.requestData() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(this::provideData, + e -> Timber.e(e, "takeListView -> requestData")); } @Override @@ -47,6 +70,15 @@ public void takeDetailedView(ArtistInfoView info, Artist artist) { @Override public void provideData(List response) { artistsListView.get().setAdapter(new ArtistsRecyclerAdapter(response, this)); + dbSource.clearArtists(); + Completable.fromAction(() -> { + Timber.e("Working on: " + Thread.currentThread().getName()); + for (Artist artist : response) { + dbSource.insertArtist(artist); + } + }).subscribeOn(Schedulers.io()).subscribe(); + + } @Override diff --git a/app/src/main/java/ru/aleien/yapplication/ListArtistsActivity.java b/app/src/main/java/ru/aleien/yapplication/ListArtistsActivity.java deleted file mode 100644 index 4c72636..0000000 --- a/app/src/main/java/ru/aleien/yapplication/ListArtistsActivity.java +++ /dev/null @@ -1,83 +0,0 @@ -package ru.aleien.yapplication; - -import android.os.Bundle; -import android.os.PersistableBundle; -import android.support.v4.app.Fragment; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; -import android.view.MenuItem; - -public class ListArtistsActivity extends AppCompatActivity implements MainView { - private ArtistsPresenter artistsPresenter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - setupToolbar(); - instantiatePresenter(savedInstanceState); - - } - - private void instantiatePresenter(Bundle savedInstanceState) { - if (savedInstanceState != null && savedInstanceState.containsKey("presenterState")) { - artistsPresenter = (ArtistsPresenter) savedInstanceState.get("presenterState"); - } else { - artistsPresenter = new ArtistsPresenter(this); - } - } - - @Override - protected void onStart() { - super.onStart(); - artistsPresenter.attachView(this); - artistsPresenter.onStart(); - } - - @Override - protected void onStop() { - super.onStop(); - artistsPresenter.detachView(); - } - - @Override - public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { - super.onSaveInstanceState(outState, outPersistentState); - outState.putSerializable("presenterState", artistsPresenter); - } - - @SuppressWarnings("ConstantConditions") - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - if (menuItem.getItemId() == android.R.id.home) { - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - getSupportFragmentManager().popBackStack(); - } - return super.onOptionsItemSelected(menuItem); - } - - @SuppressWarnings("ConstantConditions") - private void setupToolbar() { - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - getSupportActionBar().setDisplayShowHomeEnabled(true); - } - - @SuppressWarnings("ConstantConditions") - @Override - public void changeFragmentTo(Fragment fragment, boolean hideBackButton) { - getSupportActionBar().setDisplayHomeAsUpEnabled(hideBackButton); - getSupportFragmentManager().beginTransaction() - .replace(R.id.fragment_container, fragment) - .addToBackStack(null) - .commit(); - } - - @Override - public void onBackPressed() { - super.onBackPressed(); - //noinspection ConstantConditions - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } -} diff --git a/app/src/main/java/ru/aleien/yapplication/MainActivity.java b/app/src/main/java/ru/aleien/yapplication/MainActivity.java new file mode 100644 index 0000000..3cb4beb --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/MainActivity.java @@ -0,0 +1,188 @@ +package ru.aleien.yapplication; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.Cursor; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.support.v4.app.DialogFragment; +import android.support.v4.app.Fragment; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; + +import javax.inject.Inject; + +import butterknife.BindString; +import butterknife.ButterKnife; +import ru.aleien.yapplication.database.DBContract; +import ru.aleien.yapplication.model.Artist; +import ru.aleien.yapplication.screens.list.AboutDialogFragment; +import ru.aleien.yapplication.utils.PendingIntentBuilder; +import timber.log.Timber; + +public class MainActivity extends AppCompatActivity implements MainView { + private final static int MUSIC_ID = 1010; + private final static int RADIO_ID = 1020; + + private BroadcastReceiver broadcastReceiver; + @Inject ArtistsPresenter artistsPresenter; + + @BindString(R.string.about_title) String aboutTitle; + @BindString(R.string.about_message) String aboutMessage; + @BindString(R.string.title_dismiss) String dismissTitle; + @BindString(R.string.email_author) String emailTo; + @BindString(R.string.email_title) String emailTitle; + private final static Intent EMAIL_INTENT = new Intent(Intent.ACTION_SENDTO) + .setType("text/plain") + .setData(Uri.parse("mailto:")) + .putExtra(Intent.EXTRA_EMAIL, new String[]{"technogenom@gmail.com"}) + .putExtra(Intent.EXTRA_SUBJECT, "Re: Yapplication"); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + ((App) getApplication()).dagger().inject(this); + broadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + showHeadphonesNotification(audioManager.isWiredHeadsetOn() + || audioManager.isBluetoothA2dpOn() + || audioManager.isBluetoothScoOn()); + + } + }; + + setContentView(R.layout.activity_main); + ButterKnife.bind(this); + setupToolbar(); + } + + @Override + protected void onStart() { + super.onStart(); + artistsPresenter.attachView(this); + artistsPresenter.onStart(); + + registerReceiver(broadcastReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG)); + } + + @Override + protected void onStop() { + super.onStop(); + artistsPresenter.detachView(); + unregisterReceiver(broadcastReceiver); + } + + @Override + public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { + super.onSaveInstanceState(outState, outPersistentState); + outState.putSerializable("presenterState", artistsPresenter); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main_menu, menu); + return true; + } + + @SuppressWarnings("ConstantConditions") + @Override + public boolean onOptionsItemSelected(MenuItem menuItem) { + switch (menuItem.getItemId()) { + case android.R.id.home: + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + getSupportFragmentManager().popBackStack(); + break; + case R.id.menu_about: + showAbout(); + break; + case R.id.menu_contact: + composeEmail(); + break; + } + + return super.onOptionsItemSelected(menuItem); + } + + @SuppressWarnings("ConstantConditions") + private void setupToolbar() { + Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + + getSupportActionBar().setDisplayShowHomeEnabled(true); + } + + @SuppressWarnings("ConstantConditions") + @Override + public void changeFragmentTo(Fragment fragment, boolean hideBackButton) { + getSupportActionBar().setDisplayHomeAsUpEnabled(hideBackButton); + getSupportFragmentManager().beginTransaction() + .replace(R.id.fragment_container, fragment) + .addToBackStack(null) + .commit(); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + //noinspection ConstantConditions + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + } + + // TODO: Вынести в отдельный класс + // TODO: При открытой странице инфо об артисте, открывать страницу артиста + private void showHeadphonesNotification(boolean wiredHeadsetOn) { + + PendingIntent musicPendingIntent = PendingIntentBuilder.buildOpenMarketPendingIntent(MUSIC_ID, "ru.yandex.music", this); + PendingIntent radioPendingIntent = PendingIntentBuilder.buildOpenMarketPendingIntent(RADIO_ID, "ru.yandex.radio", this); + + int musicNotificationId = 001; + + if (wiredHeadsetOn) { + NotificationCompat.Builder musicNotificationBuilder = + new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_stat_hardware_headset) + .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) + .setContentTitle("Headphones plugged in") + .addAction(R.drawable.ic_stat_yamusic, "Ya.Music", musicPendingIntent) + .addAction(R.drawable.ic_stat_hardware_headset, "Ya.Radio", radioPendingIntent) + .setContentText("Open in:"); + + NotificationManager mNotifyMgr = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + mNotifyMgr.notify(musicNotificationId, musicNotificationBuilder.build()); + } else { + NotificationManager mNotifyMgr = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + mNotifyMgr.cancel(musicNotificationId); + } + } + + private void composeEmail() { + if (EMAIL_INTENT.resolveActivity(getPackageManager()) != null) { + startActivity(EMAIL_INTENT); + } + } + + private void showAbout() { + DialogFragment aboutFragment = AboutDialogFragment + .newInstance(aboutTitle, aboutMessage); + aboutFragment.show(getSupportFragmentManager(), "dialog"); + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/base/BasePresenter.java b/app/src/main/java/ru/aleien/yapplication/base/BasePresenter.java index 39c9ab4..e7c685c 100644 --- a/app/src/main/java/ru/aleien/yapplication/base/BasePresenter.java +++ b/app/src/main/java/ru/aleien/yapplication/base/BasePresenter.java @@ -2,6 +2,9 @@ import java.lang.ref.WeakReference; +import rx.Subscription; +import rx.subscriptions.CompositeSubscription; + /** * Created by aleien on 21.04.16. * Базовый класс для презентера, отвечает за сохранение ссылки на представление (вьюху). @@ -9,14 +12,20 @@ */ public abstract class BasePresenter { private WeakReference view; + private CompositeSubscription subs = new CompositeSubscription(); public abstract void onStart(); + public void subscribe(Subscription sub) { + subs.add(sub); + } + public void attachView(V view) { this.view = new WeakReference<>(view); } public void detachView() { + subs.clear(); view.clear(); view = null; } diff --git a/app/src/main/java/ru/aleien/yapplication/contentprovider/ArtistsContentProvider.java b/app/src/main/java/ru/aleien/yapplication/contentprovider/ArtistsContentProvider.java new file mode 100644 index 0000000..05163b3 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/contentprovider/ArtistsContentProvider.java @@ -0,0 +1,175 @@ +package ru.aleien.yapplication.contentprovider; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import ru.aleien.yapplication.database.DBContract; +import ru.aleien.yapplication.database.DBHelper; +import timber.log.Timber; + +import static ru.aleien.yapplication.contentprovider.ProviderContract.ARTISTS_PATH; +import static ru.aleien.yapplication.contentprovider.ProviderContract.AUTHORITY; +import static ru.aleien.yapplication.contentprovider.ProviderContract.URI_ARTISTS; +import static ru.aleien.yapplication.contentprovider.ProviderContract.URI_ARTISTS_ID; + +public class ArtistsContentProvider extends ContentProvider { + + private static final UriMatcher uriMatcher; + + static { + uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + uriMatcher.addURI(AUTHORITY, ARTISTS_PATH, URI_ARTISTS); + uriMatcher.addURI(AUTHORITY, ARTISTS_PATH + "/#", URI_ARTISTS_ID); + } + + DBHelper dbHelper; + SQLiteDatabase db; + + @Override + public boolean onCreate() { + Timber.d("Content provider created"); + dbHelper = new DBHelper(getContext()); + return true; + } + + @Nullable + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + Timber.v("Making query to content provider"); + Timber.d("Uri: " + uri.toString()); + switch (uriMatcher.match(uri)) { + case URI_ARTISTS: + Timber.v("Resolving query to ARTISTS"); + break; + case URI_ARTISTS_ID: + Timber.v("Resolving query to specific ARTIST"); + String id = uri.getLastPathSegment(); + + if (TextUtils.isEmpty(selection)) { + selection = DBContract.Artists.ID + " = " + id; + } else { + selection = selection + " AND " + DBContract.Artists.ID + " = " + id; + } + break; + default: + throw new IllegalArgumentException("Wrong URI: " + uri); + } + + db = dbHelper.getReadableDatabase(); + return db.query(DBContract.Artists.TABLE, + projection, selection, selectionArgs, null, null, sortOrder); + } + + @Nullable + @Override + public String getType(Uri uri) { + return null; + } + + @Nullable + @Override + public Uri insert(Uri uri, ContentValues values) { + if (uriMatcher.match(uri) != ProviderContract.URI_ARTISTS) { + throw new IllegalArgumentException( + "Unsupported URI for insertion: " + uri); + } + SQLiteDatabase db = dbHelper.getWritableDatabase(); + if (uriMatcher.match(uri) == URI_ARTISTS) { + long id = db.insert( + DBContract.Artists.TABLE, + null, + values); + return getUriForId(id, uri); + } + + return null; + } + + private Uri getUriForId(long id, Uri uri) { + if (id > 0) { + Uri itemUri = ContentUris.withAppendedId(uri, id); + ContentResolver resolver = getContext() == null ? null : getContext().getContentResolver(); + if (resolver != null) { + getContext().getContentResolver() + .notifyChange(itemUri, null); + } + + return itemUri; + } + throw new SQLException( + "Problem while inserting into uri: " + uri); + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + int delCount = 0; + switch (uriMatcher.match(uri)) { + case URI_ARTISTS: + delCount = db.delete( + DBContract.Artists.TABLE, + selection, + selectionArgs); + break; + case URI_ARTISTS_ID: + String idStr = uri.getLastPathSegment(); + String where = DBContract.Artists.ID + " = " + idStr; + if (!TextUtils.isEmpty(selection)) { + where += " AND " + selection; + } + delCount = db.delete( + DBContract.Artists.TABLE, + where, + selectionArgs); + break; + default: + throw new IllegalArgumentException("Unsupported URI: " + uri); + } + if (delCount > 0) { + getContext().getContentResolver().notifyChange(uri, null); + } + return delCount; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + SQLiteDatabase db = dbHelper.getWritableDatabase(); + int updateCount = 0; + switch (uriMatcher.match(uri)) { + case URI_ARTISTS: + updateCount = db.update( + DBContract.Artists.TABLE, + values, + selection, + selectionArgs); + break; + case URI_ARTISTS_ID: + String idStr = uri.getLastPathSegment(); + String where = DBContract.Artists.ID + " = " + idStr; + if (!TextUtils.isEmpty(selection)) { + where += " AND " + selection; + } + updateCount = db.update( + DBContract.Artists.TABLE, + values, + where, + selectionArgs); + break; + default: + throw new IllegalArgumentException("Unsupported URI: " + uri); + } + if (updateCount > 0) { + getContext().getContentResolver().notifyChange(uri, null); + } + return updateCount; + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/contentprovider/ProviderContract.java b/app/src/main/java/ru/aleien/yapplication/contentprovider/ProviderContract.java new file mode 100644 index 0000000..490a65d --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/contentprovider/ProviderContract.java @@ -0,0 +1,16 @@ +package ru.aleien.yapplication.contentprovider; + +/** + * Created by aleien on 12.08.16. + * Контракт поставщика контента + */ + +public final class ProviderContract { + + public static final String AUTHORITY = "ru.aleien.yapplication.provider"; + + public static final String ARTISTS_PATH = "Artists"; + public static final int URI_ARTISTS = 1; + public static final int URI_ARTISTS_ID = 2; + +} diff --git a/app/src/main/java/ru/aleien/yapplication/database/DBBackend.java b/app/src/main/java/ru/aleien/yapplication/database/DBBackend.java new file mode 100644 index 0000000..95064e4 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/database/DBBackend.java @@ -0,0 +1,134 @@ +package ru.aleien.yapplication.database; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; + +import ru.aleien.yapplication.model.Artist; +import rx.Observable; + +import static ru.aleien.yapplication.database.DBContract.GenreToArtist.ARTIST_ID; +import static ru.aleien.yapplication.database.DBContract.GenreToArtist.GENRE_ID; +import static ru.aleien.yapplication.database.DBContract.GenreToArtist.TABLE; +import static ru.aleien.yapplication.database.DBContract.allColumns; + +public class DBBackend { + + private DBHelper dbOpenHelper; + + @Inject + public DBBackend(DBHelper helper) { + dbOpenHelper = helper; + } + + public void insertArtist(Artist artist) { + insertArtist(artist.id, + artist.name, + artist.tracks, + artist.albums, + artist.link, + artist.description, + artist.cover.small, + artist.cover.big, + artist.genres); + } + + void insertArtist(int id, + String name, + int tracks, + int albums, + String link, + String description, + String small_cover, + String big_cover, + List genres) { + SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); + db.beginTransaction(); + + try { + ContentValues contentValues = new ContentValues(); + contentValues.put(DBContract.Artists.ID, id); + contentValues.put(DBContract.Artists.NAME, name); + contentValues.put(DBContract.Artists.TRACKS, tracks); + contentValues.put(DBContract.Artists.ALBUMS, albums); + contentValues.put(DBContract.Artists.LINK, link); + contentValues.put(DBContract.Artists.DESCRIPTION, description); + contentValues.put(DBContract.Artists.SMALL_COVER, small_cover); + contentValues.put(DBContract.Artists.BIG_COVER, big_cover); + + long artistId = db.insert(DBContract.Artists.TABLE, null, contentValues); + List genresIds = insertGenres(genres); + + insertRelation(artistId, genresIds); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + + void insertRelation(long artistId, List genresIds) { + SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); + for (Long genreId : genresIds) { + ContentValues contentValues = new ContentValues(); + contentValues.put(ARTIST_ID, artistId); + contentValues.put(GENRE_ID, genreId); + db.insert(TABLE, null, contentValues); + } + } + + List insertGenres(List genres) { + SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); + List rowIds = new ArrayList<>(); + ContentValues contentValues = new ContentValues(); + for (String genre : genres) { + contentValues.put(DBContract.Genres.NAME, genre); + long rowId = db.insertWithOnConflict(DBContract.Genres.TABLE, null, contentValues, SQLiteDatabase.CONFLICT_IGNORE); + rowIds.add(rowId); + } + + return rowIds; + } + + public Observable> getAllArtists() { + return Observable.fromCallable(this::loadArtists); + } + + List loadArtists() { + List artists = new ArrayList<>(); + SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(DBContract.Artists.TABLE, + allColumns, null, null, null, null, null); + cursor.moveToFirst(); + + while (!cursor.isAfterLast()) { + artists.add(cursorToArtist(cursor)); + cursor.moveToNext(); + } + + cursor.close(); + + return artists; + } + + public void clearArtists() { + dbOpenHelper.getWritableDatabase().execSQL("DELETE FROM " + DBContract.Artists.TABLE); + } + + private Artist cursorToArtist(Cursor cursor) { + return new Artist(cursor.getInt(cursor.getColumnIndex(DBContract.Artists.ID)), + cursor.getString(cursor.getColumnIndex(DBContract.Artists.NAME)), + new ArrayList<>(), + cursor.getInt(cursor.getColumnIndex(DBContract.Artists.TRACKS)), + cursor.getInt(cursor.getColumnIndex(DBContract.Artists.ALBUMS)), + cursor.getString(cursor.getColumnIndex(DBContract.Artists.LINK)), + cursor.getString(cursor.getColumnIndex(DBContract.Artists.DESCRIPTION)), + new Artist.Cover(cursor.getString(cursor.getColumnIndex(DBContract.Artists.SMALL_COVER)), + cursor.getString(cursor.getColumnIndex(DBContract.Artists.BIG_COVER)))); + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/database/DBContract.java b/app/src/main/java/ru/aleien/yapplication/database/DBContract.java new file mode 100644 index 0000000..01fdc3c --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/database/DBContract.java @@ -0,0 +1,48 @@ +package ru.aleien.yapplication.database; + +/** + * Created by aleien on 08.08.16. + * Класс для хранения и структурирования полей в базе данных + */ + +public interface DBContract { + String DBNAME = "ArtistsDB"; + int DB_VERSION = 1; + + interface Artists { + String TABLE = "artists"; + String ID = "id"; + String NAME = "name"; + String TRACKS = "tracks"; + String ALBUMS = "albums"; + String LINK = "link"; + String DESCRIPTION = "description"; + String SMALL_COVER = "small_cover"; + String BIG_COVER = "big_cover"; + } + + interface Genres { + String TABLE = "genres"; + String NAME = "genre"; + } + + // А может быть тут можно не genre_id, а genre_name? + interface GenreToArtist { + String TABLE = "genre_to_artist"; + String ARTIST_ID = "artist_id"; + String GENRE_ID = "genre_id"; + } + + String DROP_TABLE_IF_EXISTS = "DROP TABLE IF EXISTS "; + + String[] allColumns = new String[]{ + Artists.ID, + Artists.NAME, + Artists.TRACKS, + Artists.ALBUMS, + Artists.LINK, + Artists.DESCRIPTION, + Artists.SMALL_COVER, + Artists.BIG_COVER + }; +} diff --git a/app/src/main/java/ru/aleien/yapplication/database/DBHelper.java b/app/src/main/java/ru/aleien/yapplication/database/DBHelper.java new file mode 100644 index 0000000..cef9051 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/database/DBHelper.java @@ -0,0 +1,61 @@ +package ru.aleien.yapplication.database; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import ru.aleien.yapplication.model.Artist; + +import static ru.aleien.yapplication.database.DBContract.DB_VERSION; +import static ru.aleien.yapplication.database.DBContract.DROP_TABLE_IF_EXISTS; + +@Singleton +public class DBHelper extends SQLiteOpenHelper { + + @Inject + public DBHelper(Context context) { + super(context, DBContract.DBNAME, null, DB_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL( + "CREATE TABLE " + DBContract.Artists.TABLE + + "(" + + DBContract.Artists.ID + " INTEGER PRIMARY KEY, " + + DBContract.Artists.NAME + " TEXT NOT NULL," + + DBContract.Artists.TRACKS + " INTEGER," + + DBContract.Artists.ALBUMS + " INTEGER," + + DBContract.Artists.LINK + " TEXT," + + DBContract.Artists.DESCRIPTION + " TEXT," + + DBContract.Artists.SMALL_COVER + " TEXT," + + DBContract.Artists.BIG_COVER + " TEXT" + + ")" + ); + + db.execSQL( + "CREATE TABLE " + + DBContract.Genres.TABLE + + " (" + + DBContract.Genres.NAME + " TEXT UNIQUE NOT NULL" + + ")"); + + db.execSQL( + "CREATE TABLE " + DBContract.GenreToArtist.TABLE + + " (" + + DBContract.GenreToArtist.ARTIST_ID + " INTEGER NOT NULL," + + DBContract.GenreToArtist.GENRE_ID + " INTEGER NOT NULL)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int i, int i1) { + db.execSQL(DROP_TABLE_IF_EXISTS + DBContract.Artists.TABLE); + db.execSQL(DROP_TABLE_IF_EXISTS + DBContract.Genres.TABLE); + db.execSQL(DROP_TABLE_IF_EXISTS + DBContract.GenreToArtist.TABLE); + onCreate(db); + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/database/DBProvider.java b/app/src/main/java/ru/aleien/yapplication/database/DBProvider.java new file mode 100644 index 0000000..b03b6e8 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/database/DBProvider.java @@ -0,0 +1,9 @@ +package ru.aleien.yapplication.database; + +/** + * Created by aleien on 10.08.16. + */ + +public class DBProvider { + +} diff --git a/app/src/main/java/ru/aleien/yapplication/dataprovider/ArtistsProvider.java b/app/src/main/java/ru/aleien/yapplication/dataprovider/ArtistsProvider.java index 901e925..a9854fc 100644 --- a/app/src/main/java/ru/aleien/yapplication/dataprovider/ArtistsProvider.java +++ b/app/src/main/java/ru/aleien/yapplication/dataprovider/ArtistsProvider.java @@ -1,9 +1,15 @@ package ru.aleien.yapplication.dataprovider; +import java.util.List; + +import ru.aleien.yapplication.model.Artist; +import rx.Observable; + /** * Created by aleien on 09.04.16. * Интерфейс между контроллером и поставщиком данных */ public interface ArtistsProvider { - void requestData(); + // Лучше дженерик? + Observable> requestData(); } diff --git a/app/src/main/java/ru/aleien/yapplication/dataprovider/WebArtistsProvider.java b/app/src/main/java/ru/aleien/yapplication/dataprovider/WebArtistsProvider.java index 962b84f..720db16 100644 --- a/app/src/main/java/ru/aleien/yapplication/dataprovider/WebArtistsProvider.java +++ b/app/src/main/java/ru/aleien/yapplication/dataprovider/WebArtistsProvider.java @@ -1,23 +1,12 @@ package ru.aleien.yapplication.dataprovider; -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import java.io.IOException; import java.util.List; -import okhttp3.Cache; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Interceptor; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import ru.aleien.yapplication.ArtistsRequester; +import javax.inject.Inject; + +import ru.aleien.yapplication.di.AppModule; import ru.aleien.yapplication.model.Artist; -import ru.aleien.yapplication.utils.Utils; +import rx.Observable; /** * Created by aleien on 09.04.16. @@ -25,40 +14,17 @@ */ public class WebArtistsProvider implements ArtistsProvider { - private static final String JSON_URL = "http://cache-default03g.cdn.yandex.net/download.cdn.yandex.net/mobilization-2016/artists.json"; - private final ArtistsRequester artistsRequester; - private OkHttpClient client; - public WebArtistsProvider(ArtistsRequester artistsRequester, Context context) { - this.artistsRequester = artistsRequester; - Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = Utils.createInterceptor(context); - Cache cache = Utils.getCache(context); - client = new OkHttpClient.Builder() - .cache(cache) - .addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR) - .build(); + private AppModule.Api api; + + @Inject + public WebArtistsProvider(AppModule.Api api) { + this.api = api; } @Override - public void requestData() { - Handler mainHandler = new Handler(Looper.getMainLooper()); - Request request = new Request.Builder() - .url(JSON_URL) - .build(); - - client.newCall(request).enqueue(new Callback() { - @Override - public void onFailure(Call call, IOException e) { - Log.d("Main", "FAIL"); - } - - @Override - public void onResponse(Call call, Response response) throws IOException { - List responseList = Utils.decodeResponse(response); - mainHandler.post(() -> artistsRequester.provideData(responseList)); // Очень странно, что onResponse выполняется не в main-треде - // Вместо хэндлера можно, например, использовать rx-яву, которая очень хорошо дружит с ретрофитом - } - }); + public Observable> requestData() { + return api.getArtists(); } } diff --git a/app/src/main/java/ru/aleien/yapplication/di/AppComponent.java b/app/src/main/java/ru/aleien/yapplication/di/AppComponent.java new file mode 100644 index 0000000..6a7ab21 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/di/AppComponent.java @@ -0,0 +1,14 @@ +package ru.aleien.yapplication.di; + +import javax.inject.Singleton; + +import dagger.Component; +import ru.aleien.yapplication.MainActivity; + +@Singleton +@Component(modules = AppModule.class) +public interface AppComponent { + + void inject(MainActivity mainActivity); + +} diff --git a/app/src/main/java/ru/aleien/yapplication/di/AppModule.java b/app/src/main/java/ru/aleien/yapplication/di/AppModule.java new file mode 100644 index 0000000..d0a7119 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/di/AppModule.java @@ -0,0 +1,76 @@ +package ru.aleien.yapplication.di; + +import android.app.Application; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; + +import java.util.List; + +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory; +import retrofit2.converter.gson.GsonConverterFactory; +import retrofit2.http.GET; +import ru.aleien.yapplication.database.DBContract; +import ru.aleien.yapplication.model.Artist; +import rx.Observable; + +import static android.content.Context.MODE_PRIVATE; + +@Module +public class AppModule { + private static final String BASE_URL = "http://cache-default03g.cdn.yandex.net/download.cdn.yandex.net/mobilization-2016/"; + + private Context context; + + public AppModule(Application application) { + this.context = application; + } + + @Provides + @Singleton + Context provideContext() { + return this.context; + } + + @Provides + @Singleton + SQLiteDatabase provideDatabase(Context context) { + return context.openOrCreateDatabase(DBContract.DBNAME, MODE_PRIVATE, null); + } + + @Provides + @Singleton + Retrofit provideRestClient(OkHttpClient client) { + return new Retrofit.Builder() + .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) + .addConverterFactory(GsonConverterFactory.create()) + .baseUrl(BASE_URL) + .client(client) + .build(); + } + + @Provides + @Singleton + OkHttpClient provideOkhttpClient() { + return new OkHttpClient.Builder() + .addInterceptor(new HttpLoggingInterceptor()) + .build(); + } + + @Provides + Api provideApi(Retrofit rest) { + return rest.create(Api.class); + } + + public interface Api { + @GET("artists.json") + Observable> getArtists(); + } +} diff --git a/app/src/main/java/ru/aleien/yapplication/model/Artist.java b/app/src/main/java/ru/aleien/yapplication/model/Artist.java index b9d39de..66a5a24 100644 --- a/app/src/main/java/ru/aleien/yapplication/model/Artist.java +++ b/app/src/main/java/ru/aleien/yapplication/model/Artist.java @@ -23,6 +23,36 @@ public Artist(int id, String name, List genres, int tracks, int albums, this.cover = cover; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Artist artist = (Artist) o; + + if (id != artist.id) return false; + if (tracks != artist.tracks) return false; + if (albums != artist.albums) return false; + if (!name.equals(artist.name)) return false; + if (!genres.equals(artist.genres)) return false; + if (!link.equals(artist.link)) return false; + if (!description.equals(artist.description)) return false; + return cover.equals(artist.cover); + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + name.hashCode(); + result = 31 * result + genres.hashCode(); + result = 31 * result + tracks; + result = 31 * result + albums; + result = 31 * result + link.hashCode(); + result = 31 * result + description.hashCode(); + result = 31 * result + cover.hashCode(); + return result; + } + public static class Cover { public final String small; public final String big; @@ -31,6 +61,25 @@ public Cover(String small, String big) { this.small = small; this.big = big; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Cover cover = (Cover) o; + + if (!small.equals(cover.small)) return false; + return big.equals(cover.big); + + } + + @Override + public int hashCode() { + int result = small.hashCode(); + result = 31 * result + big.hashCode(); + return result; + } } } diff --git a/app/src/main/java/ru/aleien/yapplication/screens/detailedinfo/ArtistInfoFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/detailedinfo/ArtistInfoFragment.java index 90019a1..ecf63b1 100644 --- a/app/src/main/java/ru/aleien/yapplication/screens/detailedinfo/ArtistInfoFragment.java +++ b/app/src/main/java/ru/aleien/yapplication/screens/detailedinfo/ArtistInfoFragment.java @@ -12,7 +12,7 @@ import java.util.Locale; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; import ru.aleien.yapplication.R; import ru.aleien.yapplication.model.Artist; @@ -25,13 +25,13 @@ * Фрагмент для отображения информации о музыканте. */ public class ArtistInfoFragment extends Fragment implements ArtistInfoView { - @Bind(R.id.info_cover) + @BindView(R.id.info_cover) ImageView cover; - @Bind(R.id.info_genres) + @BindView(R.id.info_genres) TextView genres; - @Bind(R.id.info_music) + @BindView(R.id.info_music) TextView infoMusic; - @Bind(R.id.info_bio) + @BindView(R.id.info_bio) TextView bio; private Artist artist; diff --git a/app/src/main/java/ru/aleien/yapplication/screens/list/AboutDialogFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/list/AboutDialogFragment.java new file mode 100644 index 0000000..270ad35 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/screens/list/AboutDialogFragment.java @@ -0,0 +1,34 @@ +package ru.aleien.yapplication.screens.list; + +import android.app.Dialog; +import android.os.Bundle; +import android.support.v4.app.DialogFragment; +import android.support.v7.app.AlertDialog; + +import ru.aleien.yapplication.R; + +public class AboutDialogFragment extends DialogFragment { + + public static AboutDialogFragment newInstance(String title, String message) { + AboutDialogFragment frag = new AboutDialogFragment(); + Bundle args = new Bundle(); + args.putString("title", title); + args.putString("message", message); + frag.setArguments(args); + return frag; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + String title = getArguments().getString("title"); + String message = getArguments().getString("message"); + + return new AlertDialog.Builder(getActivity()) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.alert_dialog_ok, + (dialog, whichButton) -> dismiss() + ) + .create(); + } +} \ No newline at end of file diff --git a/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsRecyclerFragment.java b/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsRecyclerFragment.java index d7a3a77..cfc2a05 100644 --- a/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsRecyclerFragment.java +++ b/app/src/main/java/ru/aleien/yapplication/screens/list/ArtistsRecyclerFragment.java @@ -9,8 +9,9 @@ import android.view.View; import android.view.ViewGroup; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; +import butterknife.Unbinder; import ru.aleien.yapplication.R; import ru.aleien.yapplication.utils.adapters.ArtistsRecyclerAdapter; @@ -19,23 +20,30 @@ * Фрагмент для отображения списка музыкантов. */ public class ArtistsRecyclerFragment extends Fragment implements ArtistsListView { - @Bind(R.id.artists_list) + @BindView(R.id.artists_list) RecyclerView artistsRecycler; private ArtistsRecyclerAdapter adapter; + private Unbinder unbinder; @Nullable @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View fragmentView = inflater.inflate(R.layout.fragment_artists_recycler, container, false); - ButterKnife.bind(this, fragmentView); + unbinder = ButterKnife.bind(this, fragmentView); return fragmentView; } + @Override + public void onDestroyView() { + super.onDestroyView(); + unbinder.unbind(); + } + @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - artistsRecycler.setLayoutManager(new LinearLayoutManager(view.getContext(), LinearLayoutManager.VERTICAL, false)); + artistsRecycler.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); artistsRecycler.setAdapter(adapter); artistsRecycler.setHasFixedSize(true); } diff --git a/app/src/main/java/ru/aleien/yapplication/utils/PendingIntentBuilder.java b/app/src/main/java/ru/aleien/yapplication/utils/PendingIntentBuilder.java new file mode 100644 index 0000000..04075e1 --- /dev/null +++ b/app/src/main/java/ru/aleien/yapplication/utils/PendingIntentBuilder.java @@ -0,0 +1,42 @@ +package ru.aleien.yapplication.utils; + +import android.app.PendingIntent; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; + +/** + * Created by user on 19.07.16. + */ +public class PendingIntentBuilder { + private static final String playStore = "market://details?id="; + + public static PendingIntent buildOpenMarketPendingIntent(int id, String pack, Context context) { + Intent intent = buildOpenAppOrMarketPageIntent(pack, context); + return PendingIntent.getActivity(context, id, intent, 0); + } + + public static Intent buildOpenAppOrMarketPageIntent(String pack, Context context) { + + try { + if (Utils.checkPackageExists(context, pack)) { + PackageManager pm = context.getPackageManager(); + return pm.getLaunchIntentForPackage(pack); + } else { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(playStore + pack)); + return intent; + } + } catch (ActivityNotFoundException e) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse(playStore + pack)); + return intent; + } + + } + + + +} diff --git a/app/src/main/java/ru/aleien/yapplication/utils/Utils.java b/app/src/main/java/ru/aleien/yapplication/utils/Utils.java index 6b5c718..b46b6ce 100644 --- a/app/src/main/java/ru/aleien/yapplication/utils/Utils.java +++ b/app/src/main/java/ru/aleien/yapplication/utils/Utils.java @@ -1,22 +1,11 @@ package ru.aleien.yapplication.utils; import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.support.annotation.NonNull; +import android.content.pm.PackageManager; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Type; import java.util.List; -import okhttp3.Cache; -import okhttp3.Interceptor; -import okhttp3.Response; -import ru.aleien.yapplication.model.Artist; +import timber.log.Timber; public class Utils { @@ -33,49 +22,25 @@ public static String convertToString(List list, Character separator) { return resultString.toString(); } - public static boolean isNetworkAvailable(Context context) { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE); - - NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); - return (activeNetworkInfo != null) && (activeNetworkInfo.isConnected()); - } - - @NonNull - public static Interceptor createInterceptor(Context context) { - return chain -> { - Response originalResponse = chain.proceed(chain.request()); - if (Utils.isNetworkAvailable(context)) { - int maxAge = 60; - return originalResponse.newBuilder() - .header("Cache-Control", "public, max-age=" + maxAge) - .build(); - } else { - int maxStale = 60 * 60 * 24 * 28; - return originalResponse.newBuilder() - .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale) - .build(); - } - }; - } + public static String getAppVersion(Context context) { + String versionCode = "1.0"; + try { + versionCode = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + } catch (PackageManager.NameNotFoundException e) { + Timber.e(e, "getAppVersion: Could not get package info"); + e.printStackTrace(); + } - public static Cache getCache(Context context) { - File httpCacheDirectory = new File(context.getCacheDir(), "responses"); - int cacheSize = 10 * 1024 * 1024; // 10 MiB - return new Cache(httpCacheDirectory, cacheSize); + return versionCode; } - public static List decodeResponse(Response response) { - List resultList = null; - Type listType = new TypeToken>() { - }.getType(); + public static boolean checkPackageExists(Context context, String targetPackage) { try { - resultList = new Gson().fromJson(response.body().string(), listType); - } catch (IOException e) { - e.printStackTrace(); + context.getPackageManager().getPackageInfo(targetPackage, PackageManager.GET_META_DATA); + } catch (PackageManager.NameNotFoundException e) { + return false; } - - return resultList; + return true; } diff --git a/app/src/main/java/ru/aleien/yapplication/utils/adapters/ArtistsRecyclerAdapter.java b/app/src/main/java/ru/aleien/yapplication/utils/adapters/ArtistsRecyclerAdapter.java index e291a57..b9172ec 100644 --- a/app/src/main/java/ru/aleien/yapplication/utils/adapters/ArtistsRecyclerAdapter.java +++ b/app/src/main/java/ru/aleien/yapplication/utils/adapters/ArtistsRecyclerAdapter.java @@ -12,7 +12,7 @@ import java.util.List; import java.util.Locale; -import butterknife.Bind; +import butterknife.BindView; import butterknife.ButterKnife; import ru.aleien.yapplication.ArtistClickHandler; import ru.aleien.yapplication.R; @@ -59,15 +59,15 @@ public int getItemCount() { } static class ArtistHolder extends RecyclerView.ViewHolder { - @Bind(R.id.item_container) + @BindView(R.id.item_container) RelativeLayout container; - @Bind(R.id.cover) + @BindView(R.id.cover) ImageView cover; - @Bind(R.id.name) + @BindView(R.id.name) TextView name; - @Bind(R.id.genres) + @BindView(R.id.genres) TextView genres; - @Bind(R.id.music_info) + @BindView(R.id.music_info) TextView musicInfo; public ArtistHolder(View itemView) { diff --git a/app/src/main/res/drawable-hdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-hdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..c9507cf Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-hdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-hdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..95c6e2f Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-hdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..6b70e37 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-hdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..19c7e81 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_yamusic.png b/app/src/main/res/drawable-hdpi/ic_yamusic.png new file mode 100755 index 0000000..ea3eeda Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-mdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-mdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..c4a65a8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-mdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-mdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..28d132b Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-mdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..deb07a4 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-mdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..7870732 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_yamusic.png b/app/src/main/res/drawable-mdpi/ic_yamusic.png new file mode 100755 index 0000000..b06a63a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_yaradio.png b/app/src/main/res/drawable-mdpi/ic_yaradio.png new file mode 100755 index 0000000..cec8db2 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_yaradio.png differ diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xhdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..dae527f Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xhdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-xhdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..701c0d1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xhdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..176946e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-xhdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..6ae61a8 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_yamusic.png b/app/src/main/res/drawable-xhdpi/ic_yamusic.png new file mode 100755 index 0000000..4a906d4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_yaradio.png b/app/src/main/res/drawable-xhdpi/ic_yaradio.png new file mode 100755 index 0000000..83956e2 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_yaradio.png differ diff --git a/app/src/main/res/drawable-xxhdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xxhdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..b919e7f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xxhdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-xxhdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..f6a8d6c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xxhdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..296a767 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-xxhdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..108b35e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_yamusic.png b/app/src/main/res/drawable-xxhdpi/ic_yamusic.png new file mode 100755 index 0000000..e67d93b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_yaradio.png b/app/src/main/res/drawable-xxhdpi/ic_yaradio.png new file mode 100755 index 0000000..928db32 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_yaradio.png differ diff --git a/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_hardware_headset.png new file mode 100755 index 0000000..84bf4ef Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_yamusic.png b/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_yamusic.png new file mode 100755 index 0000000..6cdb8b8 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi-v11/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_hardware_headset.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_hardware_headset.png new file mode 100755 index 0000000..44e371e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_hardware_headset.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_yamusic.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_yamusic.png new file mode 100755 index 0000000..a44264b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_yamusic.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_yamusic.png b/app/src/main/res/drawable-xxxhdpi/ic_yamusic.png new file mode 100755 index 0000000..87be3f0 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_yamusic.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_yaradio.png b/app/src/main/res/drawable-xxxhdpi/ic_yaradio.png new file mode 100755 index 0000000..958eb25 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_yaradio.png differ diff --git a/app/src/main/res/drawable/shadow.xml b/app/src/main/res/drawable/shadow.xml new file mode 100644 index 0000000..4567863 --- /dev/null +++ b/app/src/main/res/drawable/shadow.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 48e82b4..f8e0c7f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,24 +1,35 @@ - + - + android:layout_height="wrap_content" + android:orientation="vertical"> + + + + + + - + android:layout_height="match_parent" + android:layout_marginTop="@dimen/toolbar"/> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml new file mode 100644 index 0000000..0f2126c --- /dev/null +++ b/app/src/main/res/menu/main_menu.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index adca2aa..7ab8dc7 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -4,4 +4,9 @@ Биография %d альбомов · %d песен %d альбомов, %d песен + О программе + Контакты + Простое приложение с простыми потребностями. Никаких проблем.\nНу разве что несмного. + О программе + ЗАКРЫТЬ \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index fb3aa08..8a8146c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,8 +1,8 @@ - #6945b6 - #44218b - #1ff4e6 + #e74c3c + #c0392b + #3498db #000000 #ffffff diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7e7ae64..612dd69 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,4 +4,5 @@ 8dp 3dp 100dp + 55dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 587c55f..e6724ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,5 +4,16 @@ Biography %d albums · %d songs %d albums, %d songs + About + Contact + + About + This is a simple application with simple needs. No worries, no trouble.\n Well, maybe just some. + DISMISS + technogenom@gmail.com + Re: Yapplication + OK + CANCEL + diff --git a/app/src/test/java/ru/aleien/yapplication/ArtistsPresenterTest.java b/app/src/test/java/ru/aleien/yapplication/ArtistsPresenterTest.java index f73e8c1..0e707f4 100644 --- a/app/src/test/java/ru/aleien/yapplication/ArtistsPresenterTest.java +++ b/app/src/test/java/ru/aleien/yapplication/ArtistsPresenterTest.java @@ -7,45 +7,53 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; import java.util.ArrayList; +import ru.aleien.yapplication.database.DBBackend; +import ru.aleien.yapplication.database.DBHelper; import ru.aleien.yapplication.dataprovider.ArtistsProvider; +import ru.aleien.yapplication.dataprovider.WebArtistsProvider; import ru.aleien.yapplication.model.Artist; import ru.aleien.yapplication.screens.detailedinfo.ArtistInfoFragment; import ru.aleien.yapplication.screens.detailedinfo.ArtistInfoView; import ru.aleien.yapplication.screens.list.ArtistsListView; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyBoolean; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; + /** * Created by aleien on 09.04.16. * Тесты!. */ -@RunWith(org.robolectric.RobolectricGradleTestRunner.class) +@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) public class ArtistsPresenterTest { ArtistsPresenter presenter; @Mock Context context; @Mock ArtistsProvider provider; + @Mock + WebArtistsProvider webArtistsProvider; @Mock Artist artistMock; @Mock MainView mainMock; @Mock ArtistsListView listMock; @Mock ArtistInfoView infoMock; - + @Mock + DBBackend dbBackend; + @Mock + DBHelper dbHelper; @Before public void setup() { MockitoAnnotations.initMocks(this); - presenter = new ArtistsPresenter(context); + presenter = new ArtistsPresenter(dbBackend, webArtistsProvider); presenter.artistsProvider = provider; presenter.attachView(mainMock); @@ -58,7 +66,6 @@ public void setup() { })).when(provider).requestData(); } - @Test public void requestData() { presenter.takeListView(listMock); @@ -77,7 +84,4 @@ public void artistClicked() { verify(mainMock, times(1)).changeFragmentTo(any(ArtistInfoFragment.class), anyBoolean()); } - - - } diff --git a/app/src/test/java/ru/aleien/yapplication/UtilsTest.java b/app/src/test/java/ru/aleien/yapplication/UtilsTest.java index f49c172..0998579 100644 --- a/app/src/test/java/ru/aleien/yapplication/UtilsTest.java +++ b/app/src/test/java/ru/aleien/yapplication/UtilsTest.java @@ -13,7 +13,7 @@ import static junit.framework.Assert.assertEquals; -@RunWith(org.robolectric.RobolectricGradleTestRunner.class) +@RunWith(org.robolectric.RobolectricTestRunner.class) @Config(constants = BuildConfig.class, sdk = 21) public class UtilsTest { @Test diff --git a/app/src/test/java/ru/aleien/yapplication/database/DBBackendTest.java b/app/src/test/java/ru/aleien/yapplication/database/DBBackendTest.java new file mode 100644 index 0000000..a3042ec --- /dev/null +++ b/app/src/test/java/ru/aleien/yapplication/database/DBBackendTest.java @@ -0,0 +1,76 @@ +package ru.aleien.yapplication.database; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.ArrayList; +import java.util.List; + +import ru.aleien.yapplication.BuildConfig; +import ru.aleien.yapplication.model.Artist; + +/** + * Created by aleien on 09.08.16. + */ +@RunWith(RobolectricTestRunner.class) +@Config(constants = BuildConfig.class, sdk = 23) +public class DBBackendTest { + DBHelper dbHelper = new DBHelper(RuntimeEnvironment.application, "testDB"); + DBBackend dbBackend = new DBBackend(dbHelper); + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + dbBackend.clearArtists(); + + } + + @Test + public void insertArtist() throws Exception { + List genres = new ArrayList<>(); + genres.add("pop"); + genres.add("jazz"); + Artist artist = new Artist(500, "Tove Lo", genres, 20, 2, "http://lalala.com", "Description", new Artist.Cover("http://firstimage", "http://seconimage")); + dbBackend.insertArtist(artist); + + List artists = dbBackend.loadArtists(); + Assert.assertEquals(artists.size(), 1); +// Assert.assertTrue(artist.equals(artists.get(0))); + } + + @Test + public void insertArtist1() throws Exception { + + } + + @Test + public void insertRelation() throws Exception { + + } + + @Test + public void insertGenres() throws Exception { + + } + + @Test + public void getAllArtists() throws Exception { + + } + + @Test + public void clearArtists() throws Exception { + + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index b206c45..0d7486e 100644 --- a/build.gradle +++ b/build.gradle @@ -5,13 +5,15 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.0.0' + classpath 'com.android.tools.build:gradle:2.2.0-alpha7' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' - classpath 'me.tatarka:gradle-retrolambda:3.2.5' + classpath 'me.tatarka:gradle-retrolambda:3.3.0-beta4' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } + +// configurations.classpath.exclude group: 'com.android.tools.external.lombok' } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f37b0f3..00ece73 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Apr 10 21:36:34 MSK 2016 +#Wed Aug 10 00:21:08 MSK 2016 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/keystore.properties b/keystore.properties new file mode 100644 index 0000000..e69de29