diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 00000000..f842d2f0 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,35 @@ +name: Android CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: Test with Gradle + run: ./gradlew test + + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: Verify Gradle + uses: gradle/actions/wrapper-validation@v4 + - name: Build with Gradle + run: ./gradlew build + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: app-debug + path: app/build/outputs/apk/standard/debug/app-standard-debug.apk diff --git a/.gitignore b/.gitignore index 56e87749..06fd74a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ build/ .gradle/ local.properties +.idea/ +*.iml +app/fdroid/ diff --git a/README.md b/README.md index 547193eb..03d495fc 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,16 @@ # Episodes +![GitHub Workflow Status (branch)](https://img.shields.io/github/workflow/status/red-coracle/episodes/Android%20CI/master?style=flat-square) +[![Translation status](https://img.shields.io/weblate/progress/episodes?style=flat-square)](https://hosted.weblate.org/engage/episodes/) +[![F-Droid Version](https://img.shields.io/f-droid/v/com.redcoracle.episodes?style=flat-square&color=%235183C0)](https://f-droid.org/en/packages/com.redcoracle.episodes) -Keep track of which episodes you've watched of your favourite TV shows. +Keep track of which episodes you've watched of your favourite shows. -TV Show information is supplied by TheTVDB.com. +This product uses the [TMDB](https://www.themoviedb.org) API but is not endorsed or certified by TMDB. ## Contributing -Contributions are very welcome. Please file bugs, fork the repository, and send pull requests. - -If you'd like to buy me a beer you can send bitcoins to 149XkMSs84ZyzNMqiQeJLt5DbPru16amwA +Contributions are very welcome. Please file bugs, fork the repository, [translate](https://hosted.weblate.org/projects/episodes/) and send pull requests. ## License -Licensed under the [GPLv3](http://www.gnu.org/licenses/gpl-3.0.txt) or later. - -## Google Play - - - Get it on Google Play - +Copylefted libre software licensed [GPLv3](http://www.gnu.org/licenses/gpl-3.0.txt)+. diff --git a/app/build.gradle b/app/build.gradle index 6fc21ceb..0c5fe629 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,45 +1,82 @@ apply plugin: 'com.android.application' -android { - compileSdkVersion 21 - buildToolsVersion "22.0.1" +def getCommitId = { -> + def gitOutput = new ByteArrayOutputStream() + exec { + commandLine 'git', 'rev-parse', '--short', 'HEAD' + standardOutput = gitOutput + } + return gitOutput.toString().trim() +} +android { + namespace 'com.redcoracle.episodes' + compileSdkVersion 33 defaultConfig { - applicationId "org.jamienicol.episodes" - minSdkVersion 11 - targetSdkVersion 21 - versionCode 11 - versionName "0.11" + minSdkVersion 21 + targetSdkVersion 33 + versionCode 25 + versionName "0.16.1" + buildConfigField "String", "TMDB_KEY", "\"1553d2e4fa2912fc0953305d4d3e7c44\"" + buildConfigField "String", "GIT_COMMIT_ID", "\"${getCommitId()}\"" } + flavorDimensions "flavor" productFlavors { - fdroid { - applicationId = "org.jamienicol.episodes" - } - playstore { - applicationId = "com.vindustries.episodes" + standard { + dimension "flavor" + applicationId = "com.redcoracle.episodes" } } buildTypes { debug { applicationIdSuffix '.debug' versionNameSuffix '-DEBUG' + multiDexEnabled true } release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - lintOptions { + lint { abortOnError false } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + if (project.hasProperty('signingStoreLocation') && + project.hasProperty('signingStorePassword') && + project.hasProperty('signingKeyAlias') && + project.hasProperty('signingKeyPassword')) { + println "Found sign properties in gradle.properties! Signing build…" + + signingConfigs { + release { + storeFile file(signingStoreLocation) + storePassword signingStorePassword + keyAlias signingKeyAlias + keyPassword signingKeyPassword + } + } + + buildTypes.release.signingConfig = signingConfigs.release + } else { + buildTypes.release.signingConfig = null + } } dependencies { - compile fileTree(dir: 'libs', include: ['*.jar']) - compile 'com.android.support:support-v4:22.2.0' - compile 'com.android.support:appcompat-v7:22.2.0' - compile 'com.android.support:design:22.2.0' - compile 'com.android.support:recyclerview-v7:22.2.0' - compile 'com.squareup.okhttp:okhttp:2.1.0' - compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3' + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.preference:preference:1.2.0' + implementation 'androidx.recyclerview:recyclerview:1.2.1' + implementation 'com.google.android.material:material:1.6.1' + implementation 'com.github.bumptech.glide:glide:4.14.1' + implementation 'com.uwetrottmann.tmdb2:tmdb-java:2.8.1' + implementation 'org.apache.commons:commons-collections4:4.4' + debugImplementation 'com.android.support:multidex:2.0.1' + annotationProcessor 'com.github.bumptech.glide:compiler:4.14.1' } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 45dc58a5..72740ea1 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -9,9 +9,23 @@ # Add any project specific keep options here: -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} +# Glide +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} + +# TMDB +-keep class com.uwetrottmann.tmdb2.entities.** { *; } +-keep class com.uwetrottmann.tmdb2.enumerations.** { *; } + +# OkHttp platform used only on JVM and when Conscrypt and other security providers are available. +-dontwarn okhttp3.internal.platform.** +-dontwarn org.conscrypt.** +-dontwarn org.bouncycastle.** +-dontwarn org.openjsse.** + +# R8 said to add these +-dontwarn kotlin.coroutines.Continuation diff --git a/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100755 index 00000000..e2c420d6 --- /dev/null +++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png old mode 100644 new mode 100755 index 579898fe..d211bdeb Binary files a/app/src/debug/res/mipmap-hdpi/ic_launcher.png and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_background.png new file mode 100755 index 00000000..2c4aefef Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..90722c92 Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png old mode 100644 new mode 100755 index 9b530e2f..5c3f69d2 Binary files a/app/src/debug/res/mipmap-mdpi/ic_launcher.png and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_background.png new file mode 100755 index 00000000..85ddadd2 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..fd92fee8 Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png old mode 100644 new mode 100755 index df0a4758..9650cc2a Binary files a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100755 index 00000000..ea36b519 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..5d7626d1 Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png old mode 100644 new mode 100755 index 8a880ceb..9246046b Binary files a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100755 index 00000000..207597d9 Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..d86cb12f Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 00000000..d138d265 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100755 index 00000000..1b38c1ce Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..af231731 Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 18172eea..1688b8d1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,18 +1,12 @@ + xmlns:android="http://schemas.android.com/apk/res/android"> - - - - - + + + + + + android:launchMode="singleTop" + android:exported="true"> @@ -71,14 +66,6 @@ android:label="" > - - - - - - diff --git a/app/src/main/java/org/jamienicol/episodes/AboutActivity.java b/app/src/main/java/com/redcoracle/episodes/AboutActivity.java similarity index 77% rename from app/src/main/java/org/jamienicol/episodes/AboutActivity.java rename to app/src/main/java/com/redcoracle/episodes/AboutActivity.java index f9e2ade7..ed89d4c4 100644 --- a/app/src/main/java/org/jamienicol/episodes/AboutActivity.java +++ b/app/src/main/java/com/redcoracle/episodes/AboutActivity.java @@ -15,14 +15,16 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; import android.view.MenuItem; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; public class AboutActivity - extends ActionBarActivity + extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { @@ -30,6 +32,13 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.about_activity); getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + TextView versionInfoView = findViewById(R.id.version_view); + versionInfoView.setText( + getBaseContext().getString( + R.string.version, + BuildConfig.VERSION_NAME, + BuildConfig.GIT_COMMIT_ID)); } @Override diff --git a/app/src/main/java/org/jamienicol/episodes/AddShowPreviewActivity.java b/app/src/main/java/com/redcoracle/episodes/AddShowPreviewActivity.java similarity index 91% rename from app/src/main/java/org/jamienicol/episodes/AddShowPreviewActivity.java rename to app/src/main/java/com/redcoracle/episodes/AddShowPreviewActivity.java index 88660760..5c375683 100644 --- a/app/src/main/java/org/jamienicol/episodes/AddShowPreviewActivity.java +++ b/app/src/main/java/com/redcoracle/episodes/AddShowPreviewActivity.java @@ -15,18 +15,21 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.content.Intent; import android.os.Bundle; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.ActionBarActivity; import android.view.MenuItem; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentTransaction; + +import com.redcoracle.episodes.tvdb.Show; + import java.util.List; -import org.jamienicol.episodes.tvdb.Show; public class AddShowPreviewActivity - extends ActionBarActivity + extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) diff --git a/app/src/main/java/org/jamienicol/episodes/AddShowPreviewFragment.java b/app/src/main/java/com/redcoracle/episodes/AddShowPreviewFragment.java similarity index 88% rename from app/src/main/java/org/jamienicol/episodes/AddShowPreviewFragment.java rename to app/src/main/java/com/redcoracle/episodes/AddShowPreviewFragment.java index 8dff6add..44a159bb 100644 --- a/app/src/main/java/org/jamienicol/episodes/AddShowPreviewFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/AddShowPreviewFragment.java @@ -15,12 +15,9 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; -import android.content.Intent; -import android.database.Cursor; import android.os.Bundle; -import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -28,11 +25,16 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; + +import androidx.fragment.app.Fragment; + +import com.redcoracle.episodes.services.AddShowTask; +import com.redcoracle.episodes.services.AsyncTask; +import com.redcoracle.episodes.tvdb.Show; + import java.text.DateFormat; import java.util.Date; import java.util.List; -import org.jamienicol.episodes.services.AddShowService; -import org.jamienicol.episodes.tvdb.Show; public class AddShowPreviewFragment extends Fragment @@ -110,10 +112,6 @@ public boolean onOptionsItemSelected(MenuItem item) { } private void addShow() { - Intent intent = new Intent(getActivity(), AddShowService.class); - intent.putExtra("tvdbId", show.getId()); - intent.putExtra("showName", show.getName()); - - getActivity().startService(intent); + new AsyncTask().executeAsync(new AddShowTask(show.getId(), show.getName(), show.getLanguage())); } } diff --git a/app/src/main/java/org/jamienicol/episodes/AddShowSearchActivity.java b/app/src/main/java/com/redcoracle/episodes/AddShowSearchActivity.java similarity index 92% rename from app/src/main/java/org/jamienicol/episodes/AddShowSearchActivity.java rename to app/src/main/java/com/redcoracle/episodes/AddShowSearchActivity.java index a504e3d0..32e917a5 100644 --- a/app/src/main/java/org/jamienicol/episodes/AddShowSearchActivity.java +++ b/app/src/main/java/com/redcoracle/episodes/AddShowSearchActivity.java @@ -15,17 +15,18 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.content.Intent; import android.os.Bundle; -import android.support.v4.app.FragmentTransaction; -import android.support.v7.app.ActionBarActivity; import android.view.MenuItem; import android.view.Window; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentTransaction; + public class AddShowSearchActivity - extends ActionBarActivity + extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) diff --git a/app/src/main/java/org/jamienicol/episodes/AddShowSearchFragment.java b/app/src/main/java/com/redcoracle/episodes/AddShowSearchFragment.java similarity index 59% rename from app/src/main/java/org/jamienicol/episodes/AddShowSearchFragment.java rename to app/src/main/java/com/redcoracle/episodes/AddShowSearchFragment.java index 28d64019..c62aae9d 100644 --- a/app/src/main/java/org/jamienicol/episodes/AddShowSearchFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/AddShowSearchFragment.java @@ -15,28 +15,30 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.app.Activity; -import android.content.ContentValues; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; -import android.support.v4.app.ListFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.Loader; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.fragment.app.ListFragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.AsyncTaskLoader; +import androidx.loader.content.Loader; + +import com.redcoracle.episodes.tvdb.Client; +import com.redcoracle.episodes.tvdb.Show; + import java.util.List; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.db.ShowsTable; -import org.jamienicol.episodes.tvdb.Client; -import org.jamienicol.episodes.tvdb.Show; public class AddShowSearchFragment extends ListFragment @@ -53,39 +55,28 @@ public static AddShowSearchFragment newInstance(String query) { } @Override - public View onCreateView(LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.add_show_search_fragment, - container, - false); + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + return inflater.inflate(R.layout.add_show_search_fragment, container, false); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - // if the loader exists and it's busy loading - // then spin the progress bar. - Loader loader = getLoaderManager().getLoader(0); - if (loader != null) { - getActivity().setProgressBarIndeterminateVisibility(loader.isStarted()); - } - String query = getArguments().getString("query"); Bundle loaderArgs = new Bundle(); loaderArgs.putString("query", query); - getLoaderManager().initLoader(0, loaderArgs, this); + LoaderManager.getInstance(this).initLoader(0, loaderArgs, this); } + @NonNull @Override public Loader> onCreateLoader(int id, Bundle args) { - getActivity().setProgressBarIndeterminateVisibility(true); - SearchLoader loader = new SearchLoader(getActivity(), - args.getString("query")); - return loader; + getActivity().findViewById(R.id.search_progress_bar).setVisibility(View.VISIBLE); + getActivity().findViewById(R.id.search_no_results_found).setVisibility(View.GONE); + return new SearchLoader(getActivity(), args.getString("query")); } @Override @@ -94,12 +85,15 @@ public void onLoadFinished(Loader> loader, List data) { results.setData(data); Activity activity = getActivity(); - activity.setProgressBarIndeterminateVisibility(false); - SearchResultsAdapter adapter = null; + if (data != null) { + if (data.size() == 0) { + activity.findViewById(R.id.search_no_results_found).setVisibility(View.VISIBLE); + } adapter = new SearchResultsAdapter(activity, data); } + activity.findViewById(R.id.search_progress_bar).setVisibility(View.GONE); setListAdapter(adapter); } @@ -107,17 +101,15 @@ public void onLoadFinished(Loader> loader, List data) { public void onLoaderReset(Loader> loader) { AddShowSearchResults results = AddShowSearchResults.getInstance(); results.setData(null); - setListAdapter(null); } - private static class SearchLoader - extends AsyncTaskLoader> - { + private static class SearchLoader extends AsyncTaskLoader> { private final String query; private List cachedResult; + private final SharedPreferences preferences = Preferences.getSharedPreferences(); - public SearchLoader(Context context, String query) { + SearchLoader(Context context, String query) { super(context); this.query = query; @@ -126,9 +118,18 @@ public SearchLoader(Context context, String query) { @Override public List loadInBackground() { - Client tvdbClient = new Client("25B864A8BC56AFAD"); + Client tmdbClient = new Client(); + String language = preferences.getString("pref_language", "en"); - List results = tvdbClient.searchShows(query); + List results = tmdbClient.searchShows(query, language); + + // If there are no results, try searching all languages or substituting & + if (results.size() == 0 && query.contains(" and ")) { + results = tmdbClient.searchShows(query.replace(" and ", " & "), null); + } + if (results.size() == 0) { + results = tmdbClient.searchShows(query, null); + } return results; } @@ -164,34 +165,27 @@ public void onReset() { } public void onListItemClick(ListView l, View v, int position, long id) { - Intent intent = new Intent(getActivity(), - AddShowPreviewActivity.class); + Intent intent = new Intent(getActivity(), AddShowPreviewActivity.class); intent.putExtra("searchResultIndex", position); startActivity(intent); } - private static class SearchResultsAdapter - extends ArrayAdapter - { - private LayoutInflater inflater; + private static class SearchResultsAdapter extends ArrayAdapter { + private final LayoutInflater inflater; - public SearchResultsAdapter(Context context, List objects) { + SearchResultsAdapter(Context context, List objects) { super(context, 0, 0, objects); - inflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); } - @Override - public View getView(int position, View convertView, ViewGroup parent) { + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { if (convertView == null) { - convertView = - inflater.inflate(R.layout.add_show_search_results_list_item, - parent, - false); + convertView = inflater.inflate(R.layout.add_show_search_results_list_item, parent, false); } - TextView textView = - (TextView)convertView.findViewById(R.id.show_name_view); + TextView textView = convertView.findViewById(R.id.show_name_view); textView.setText(getItem(position).getName()); return convertView; diff --git a/app/src/main/java/org/jamienicol/episodes/AddShowSearchResults.java b/app/src/main/java/com/redcoracle/episodes/AddShowSearchResults.java similarity index 93% rename from app/src/main/java/org/jamienicol/episodes/AddShowSearchResults.java rename to app/src/main/java/com/redcoracle/episodes/AddShowSearchResults.java index 7603f1f1..04863bf4 100644 --- a/app/src/main/java/org/jamienicol/episodes/AddShowSearchResults.java +++ b/app/src/main/java/com/redcoracle/episodes/AddShowSearchResults.java @@ -15,10 +15,11 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; + +import com.redcoracle.episodes.tvdb.Show; import java.util.List; -import org.jamienicol.episodes.tvdb.Show; public class AddShowSearchResults { diff --git a/app/src/main/java/org/jamienicol/episodes/AutoRefreshHelper.java b/app/src/main/java/com/redcoracle/episodes/AutoRefreshHelper.java similarity index 59% rename from app/src/main/java/org/jamienicol/episodes/AutoRefreshHelper.java rename to app/src/main/java/com/redcoracle/episodes/AutoRefreshHelper.java index 8bb23c63..b2484e97 100644 --- a/app/src/main/java/org/jamienicol/episodes/AutoRefreshHelper.java +++ b/app/src/main/java/com/redcoracle/episodes/AutoRefreshHelper.java @@ -15,43 +15,36 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.app.AlarmManager; import android.app.IntentService; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.ComponentName; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.database.Cursor; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.os.SystemClock; +import android.os.Build; import android.preference.PreferenceManager; -import android.support.v4.net.ConnectivityManagerCompat; import android.util.Log; -import org.jamienicol.episodes.db.ShowsTable; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.RefreshShowUtil; -public class AutoRefreshHelper - implements SharedPreferences.OnSharedPreferenceChangeListener -{ - private static final String TAG = AutoRefreshHelper.class.getName(); +import androidx.core.net.ConnectivityManagerCompat; + +import com.redcoracle.episodes.services.AsyncTask; +import com.redcoracle.episodes.services.RefreshAllShowsTask; - private static final String KEY_PREF_AUTO_REFRESH_ENABLED = - "pref_auto_refresh_enabled"; - private static final String KEY_PREF_AUTO_REFRESH_PERIOD = - "pref_auto_refresh_period"; - private static final String KEY_PREF_AUTO_REFRESH_WIFI_ONLY = - "pref_auto_refresh_wifi_only"; +public class AutoRefreshHelper implements SharedPreferences.OnSharedPreferenceChangeListener { + private static final String TAG = AutoRefreshHelper.class.getName(); - private static final String KEY_LAST_AUTO_REFRESH_TIME = - "last_auto_refresh_time"; + private static final String KEY_PREF_AUTO_REFRESH_ENABLED = "pref_auto_refresh_enabled"; + private static final String KEY_PREF_AUTO_REFRESH_PERIOD = "pref_auto_refresh_period"; + private static final String KEY_PREF_AUTO_REFRESH_WIFI_ONLY = "pref_auto_refresh_wifi_only"; + private static final String KEY_LAST_AUTO_REFRESH_TIME = "last_auto_refresh_time"; + private static final String KEY_PREF_CONFIRMED_BACKUP = "pref_confirmed_backup"; private static AutoRefreshHelper instance; @@ -67,14 +60,13 @@ public AutoRefreshHelper(Context context) { public static synchronized AutoRefreshHelper getInstance(Context context) { if (instance == null) { - instance = new AutoRefreshHelper(context); + instance = new AutoRefreshHelper(context.getApplicationContext()); } return instance; } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key.equals(KEY_PREF_AUTO_REFRESH_ENABLED)) { onAutoRefreshEnabledChanged(); } else if (key.equals(KEY_PREF_AUTO_REFRESH_PERIOD)) { @@ -101,8 +93,7 @@ private boolean getAutoRefreshEnabled() { } private long getAutoRefreshPeriod() { - final String hours = - preferences.getString(KEY_PREF_AUTO_REFRESH_PERIOD, "0"); + final String hours = preferences.getString(KEY_PREF_AUTO_REFRESH_PERIOD, "0"); // convert hours to milliseconds return Long.parseLong(hours) * 60 * 60 * 1000; @@ -117,62 +108,62 @@ private long getPrevAutoRefreshTime() { } private void setPrevAutoRefreshTime(long time) { - final SharedPreferences.Editor editor = - preferences.edit(); + final SharedPreferences.Editor editor = preferences.edit(); editor.putLong(KEY_LAST_AUTO_REFRESH_TIME, time); editor.apply(); } private boolean checkNetwork() { - final ConnectivityManager connManager = - (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + final ConnectivityManager connManager = (ConnectivityManager)context.getSystemService( + Context.CONNECTIVITY_SERVICE + ); final NetworkInfo net = connManager.getActiveNetworkInfo(); final boolean connected = net != null && net.isConnected(); - final boolean metered = - ConnectivityManagerCompat.isActiveNetworkMetered(connManager); + final boolean metered = ConnectivityManagerCompat.isActiveNetworkMetered(connManager); final boolean unmeteredOnly = getAutoRefreshWifiOnly(); final boolean okay = connected && !(metered && unmeteredOnly); - Log.i(TAG, - String.format("connected=%b, metered=%b, unmeteredOnly=%b, checkNetwork() %s.", - connected, metered, unmeteredOnly, - okay ? "passes" : "fails")); + Log.i(TAG, String.format("connected=%b, metered=%b, unmeteredOnly=%b, checkNetwork() %s.", + connected, metered, unmeteredOnly, okay ? "passes" : "fails")); return okay; } + private boolean checkBackup() { + return preferences.getBoolean(KEY_PREF_CONFIRMED_BACKUP, false); + } + public void rescheduleAlarm() { NetworkStateReceiver.disable(context); - final AlarmManager alarmManager = - (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); - - final Intent intent = - new Intent(context, AutoRefreshHelper.Service.class); - final PendingIntent pendingIntent = - PendingIntent.getService(context, 0, intent, 0); + final AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + final Intent intent = new Intent(context, AutoRefreshHelper.Service.class); + final int intentFlag; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + intentFlag = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; + } else { + intentFlag = PendingIntent.FLAG_UPDATE_CURRENT; + } + final PendingIntent pendingIntent = PendingIntent.getService( + context, + 0, + intent, + intentFlag + ); if (getAutoRefreshEnabled() && getAutoRefreshPeriod() != 0) { - final long alarmTime = - getPrevAutoRefreshTime() + getAutoRefreshPeriod(); - + final long alarmTime = getPrevAutoRefreshTime() + getAutoRefreshPeriod(); Log.i(TAG, String.format("Scheduling auto refresh alarm for %d.", alarmTime)); - - alarmManager.set(AlarmManager.RTC, - alarmTime, - pendingIntent); + alarmManager.set(AlarmManager.RTC, alarmTime, pendingIntent); } else { Log.i(TAG, "Cancelling auto refresh alarm."); - alarmManager.cancel(pendingIntent); } } - public static class Service - extends IntentService - { + public static class Service extends IntentService { private static final String TAG = Service.class.getName(); public Service() { @@ -181,60 +172,29 @@ public Service() { @Override protected void onHandleIntent(Intent intent) { - final AutoRefreshHelper helper = - AutoRefreshHelper.getInstance(getApplicationContext()); + final AutoRefreshHelper helper = AutoRefreshHelper.getInstance(getApplicationContext()); - if (helper.checkNetwork()) { + if (helper.checkNetwork() && helper.checkBackup()) { Log.i(TAG, "Refreshing all shows."); - - final ContentResolver contentResolver = getContentResolver(); - final Cursor cursor = getShowsCursor(contentResolver); - - while (cursor.moveToNext()) { - final int showIdColumnIndex = - cursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); - final int showId = cursor.getInt(showIdColumnIndex); - - RefreshShowUtil.refreshShow(showId, contentResolver); - } - + new AsyncTask().executeAsync(new RefreshAllShowsTask()); helper.setPrevAutoRefreshTime(System.currentTimeMillis()); helper.rescheduleAlarm(); - } else { NetworkStateReceiver.enable(this); } } - private static Cursor getShowsCursor(ContentResolver contentResolver) { - final String[] projection = { - ShowsTable.COLUMN_ID - }; - - final Cursor cursor = - contentResolver.query(ShowsProvider.CONTENT_URI_SHOWS, - projection, - null, - null, - null); - - return cursor; - } } - public static class BootReceiver - extends BroadcastReceiver - { + public static class BootReceiver extends BroadcastReceiver { private static final String TAG = BootReceiver.class.getName(); @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) { Log.i(TAG, "Boot received."); - // ensure that the auto-refresh alarm is scheduled. - AutoRefreshHelper.getInstance(context.getApplicationContext()) - .rescheduleAlarm(); + AutoRefreshHelper.getInstance(context.getApplicationContext()).rescheduleAlarm(); } } } @@ -242,18 +202,13 @@ public void onReceive(Context context, Intent intent) { // This receiver is disabled by default in the manifest. // It should only be enabled when it needed, and should be // disabled again straight afterwards. - public static class NetworkStateReceiver - extends BroadcastReceiver - { + public static class NetworkStateReceiver extends BroadcastReceiver { private static final String TAG = NetworkStateReceiver.class.getName(); @Override public void onReceive(Context context, Intent intent) { Log.i(TAG, "Network state change received."); - - final AutoRefreshHelper helper = - AutoRefreshHelper.getInstance(context.getApplicationContext()); - + final AutoRefreshHelper helper = AutoRefreshHelper.getInstance(context.getApplicationContext()); if (helper.checkNetwork()) { helper.rescheduleAlarm(); } @@ -262,35 +217,35 @@ public void onReceive(Context context, Intent intent) { public static void enable(Context context) { final PackageManager packageManager = context.getPackageManager(); - final ComponentName receiver = - new ComponentName(context, NetworkStateReceiver.class); + final ComponentName receiver = new ComponentName(context, NetworkStateReceiver.class); if (packageManager.getComponentEnabledSetting(receiver) != - PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { Log.i(TAG, "Enabling network state receiver."); } packageManager.setComponentEnabledSetting( - receiver, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP); + receiver, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ); } public static void disable(Context context) { final PackageManager packageManager = context.getPackageManager(); - final ComponentName receiver = - new ComponentName(context, NetworkStateReceiver.class); + final ComponentName receiver = new ComponentName(context, NetworkStateReceiver.class); if (packageManager.getComponentEnabledSetting(receiver) != - PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { Log.i(TAG, "Disabling network state receiver."); } packageManager.setComponentEnabledSetting( - receiver, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP); + receiver, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ); } } } diff --git a/app/src/main/java/org/jamienicol/episodes/EpisodeActivity.java b/app/src/main/java/com/redcoracle/episodes/EpisodeActivity.java similarity index 90% rename from app/src/main/java/org/jamienicol/episodes/EpisodeActivity.java rename to app/src/main/java/com/redcoracle/episodes/EpisodeActivity.java index d85420d6..3080bbe4 100644 --- a/app/src/main/java/org/jamienicol/episodes/EpisodeActivity.java +++ b/app/src/main/java/com/redcoracle/episodes/EpisodeActivity.java @@ -15,25 +15,27 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.content.Intent; import android.database.Cursor; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentStatePagerAdapter; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v4.view.ViewPager; -import android.support.v7.app.ActionBarActivity; import android.view.MenuItem; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsProvider; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import androidx.viewpager.widget.ViewPager; + +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; public class EpisodeActivity - extends ActionBarActivity + extends AppCompatActivity implements LoaderManager.LoaderCallbacks { int initialEpisodeId; diff --git a/app/src/main/java/org/jamienicol/episodes/EpisodeDetailsFragment.java b/app/src/main/java/com/redcoracle/episodes/EpisodeDetailsFragment.java similarity index 95% rename from app/src/main/java/org/jamienicol/episodes/EpisodeDetailsFragment.java rename to app/src/main/java/com/redcoracle/episodes/EpisodeDetailsFragment.java index ad563705..da9e609b 100644 --- a/app/src/main/java/org/jamienicol/episodes/EpisodeDetailsFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/EpisodeDetailsFragment.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.content.AsyncQueryHandler; import android.content.ContentResolver; @@ -23,20 +23,23 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.TextView; + +import androidx.fragment.app.Fragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; + +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; + import java.text.DateFormat; import java.util.Date; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsProvider; public class EpisodeDetailsFragment extends Fragment diff --git a/app/src/main/java/com/redcoracle/episodes/EpisodesApplication.java b/app/src/main/java/com/redcoracle/episodes/EpisodesApplication.java new file mode 100644 index 00000000..0e1c29a6 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/EpisodesApplication.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes; + +import android.app.Application; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.os.Build; +import android.util.Log; + +import com.uwetrottmann.tmdb2.Tmdb; + +public class EpisodesApplication extends Application { + private static final String TAG = EpisodesApplication.class.getName(); + private static EpisodesApplication instance; + private Tmdb tmdbClient; + + @Override + public void onCreate() { + super.onCreate(); + + instance = this; + + try { + this.tmdbClient = new Tmdb(BuildConfig.TMDB_KEY); + } catch (Exception e) { + Log.d(TAG, "Error initialising TmdbClient", e); + } + + createNotificationChannel(); + } + + public static EpisodesApplication getInstance() { + return instance; + } + + public Tmdb getTmdbClient() { + return this.tmdbClient; + } + + private void createNotificationChannel(){ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = getString(R.string.channel_name); + String description = getString(R.string.channel_description); + int importance = NotificationManager.IMPORTANCE_LOW; + NotificationChannel channel = new NotificationChannel("episodes_channel_id", name, importance); + channel.setDescription(description); + NotificationManager notificationManager = getSystemService(NotificationManager.class); + notificationManager.createNotificationChannel(channel); + } + } +} diff --git a/app/src/main/java/org/jamienicol/episodes/EpisodesCounter.java b/app/src/main/java/com/redcoracle/episodes/EpisodesCounter.java similarity index 89% rename from app/src/main/java/org/jamienicol/episodes/EpisodesCounter.java rename to app/src/main/java/com/redcoracle/episodes/EpisodesCounter.java index 8bc5143f..1afe530e 100644 --- a/app/src/main/java/org/jamienicol/episodes/EpisodesCounter.java +++ b/app/src/main/java/com/redcoracle/episodes/EpisodesCounter.java @@ -15,14 +15,18 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; +import android.content.SharedPreferences; import android.database.Cursor; import android.util.SparseIntArray; + +import com.redcoracle.episodes.db.EpisodesTable; + +import java.util.Collections; import java.util.Date; import java.util.Set; import java.util.TreeSet; -import org.jamienicol.episodes.db.EpisodesTable; public class EpisodesCounter { @@ -35,7 +39,15 @@ public class EpisodesCounter public EpisodesCounter(String keyColumn) { this.keyColumn = keyColumn; - keys = new TreeSet(); + SharedPreferences preferences = Preferences.getSharedPreferences(); + + if (preferences.getBoolean("reverse_sort_order", false)) { + keys = new TreeSet(Collections.reverseOrder()); + } else { + keys = new TreeSet(); + } + + // keys = new TreeSet(); numAiredEpisodesMap = new SparseIntArray(); numWatchedEpisodesMap = new SparseIntArray(); numUpcomingEpisodesMap = new SparseIntArray(); diff --git a/app/src/main/java/org/jamienicol/episodes/EpisodesListFragment.java b/app/src/main/java/com/redcoracle/episodes/EpisodesListFragment.java similarity index 95% rename from app/src/main/java/org/jamienicol/episodes/EpisodesListFragment.java rename to app/src/main/java/com/redcoracle/episodes/EpisodesListFragment.java index 48067e7a..838cbd2d 100644 --- a/app/src/main/java/org/jamienicol/episodes/EpisodesListFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/EpisodesListFragment.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.app.Activity; import android.content.AsyncQueryHandler; @@ -25,11 +25,6 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.ListFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v4.widget.CursorAdapter; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -37,10 +32,18 @@ import android.widget.CompoundButton; import android.widget.ListView; import android.widget.TextView; + +import androidx.cursoradapter.widget.CursorAdapter; +import androidx.fragment.app.ListFragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; + +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; + import java.text.DateFormat; import java.util.Date; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsProvider; public class EpisodesListFragment extends ListFragment diff --git a/app/src/main/java/com/redcoracle/episodes/FileUtilities.java b/app/src/main/java/com/redcoracle/episodes/FileUtilities.java new file mode 100644 index 00000000..d771822e --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/FileUtilities.java @@ -0,0 +1,40 @@ +package com.redcoracle.episodes; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.OpenableColumns; + +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class FileUtilities { + public static String get_suggested_filename() { + final Date today = new Date(); + final DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd_HHmm", Locale.getDefault()); + return String.format("episodes_%s.db", formatter.format(today)); + } + + public static String uri_to_filename(Context context, Uri uri) { + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + cursor.moveToFirst(); + String filename = cursor.getString(nameIndex); + cursor.close(); + return filename; + } + + public static void copy_file(FileChannel source, FileChannel destination) { + try { + destination.transferFrom(source, 0, source.size()); + source.close(); + destination.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/MainActivity.java b/app/src/main/java/com/redcoracle/episodes/MainActivity.java new file mode 100644 index 00000000..59d8f303 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/MainActivity.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2012-2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes; + +import android.Manifest; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.core.view.MenuItemCompat; +import androidx.fragment.app.FragmentManager; + +import com.bumptech.glide.Glide; +import com.redcoracle.episodes.db.DatabaseOpenHelper; +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.services.AsyncTask; +import com.redcoracle.episodes.services.BackupTask; +import com.redcoracle.episodes.services.RestoreTask; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; + +public class MainActivity + extends AppCompatActivity + implements ShowsListFragment.OnShowSelectedListener, + SelectBackupDialog.OnBackupSelectedListener, + ActivityCompat.OnRequestPermissionsResultCallback { + + private static Context context; + private static final int WRITE_REQUEST_CODE = 0; + private static final int READ_REQUEST_CODE = 1; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main_activity); + + MainActivity.context = getApplicationContext(); + AutoRefreshHelper.getInstance(getApplicationContext()).rescheduleAlarm(); + } + + public static Context getAppContext() { + return MainActivity.context; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + final MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.main, menu); + + final MenuItem menuItem = menu.findItem(R.id.menu_add_new_show); + final SearchView addShow = + (SearchView)MenuItemCompat.getActionView(menuItem); + addShow.setQueryHint(getString(R.string.menu_add_show_search_hint)); + addShow.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextChange(String query) { + return true; + } + + @Override + public boolean onQueryTextSubmit(String query) { + final Intent intent = + new Intent(MainActivity.this, + AddShowSearchActivity.class); + intent.putExtra("query", query); + startActivity(intent); + MenuItemCompat.collapseActionView(menuItem); + return true; + } + }); + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_back_up: + back_up(); + return true; + + case R.id.menu_restore: + restore(); + return true; + + case R.id.menu_settings: + showSettings(); + return true; + + case R.id.menu_about: + showAbout(); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public void onShowSelected(int showId) { + final Intent intent = new Intent(this, ShowActivity.class); + intent.putExtra("showId", showId); + startActivity(intent); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + + private boolean hasStoragePermission() { + return ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; + } + + private void back_up() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/x-sqlite3"); + intent.putExtra(Intent.EXTRA_TITLE, FileUtilities.get_suggested_filename()); + startActivityForResult(intent, WRITE_REQUEST_CODE); + } else { + // For now, keep the existing functionality on pre-API19 + if (hasStoragePermission()) { + new AsyncTask().executeAsync(new BackupTask(FileUtilities.get_suggested_filename())); + } + } + } + + private void restore() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/x-sqlite3"); + // On API 31 the file was not selectable without this + intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[] {"application/octet-stream"}); + startActivityForResult(intent, READ_REQUEST_CODE); + } else { + // For now, keep the existing functionality on pre-API19 + if (hasStoragePermission()) { + final FragmentManager fm = getSupportFragmentManager(); + final SelectBackupDialog dialog = new SelectBackupDialog(); + dialog.show(fm, "select_backup_dialog"); + } + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + try { + if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + Uri uri = data.getData(); + FileUtilities.copy_file( + new FileInputStream(this.getDatabasePath(DatabaseOpenHelper.getDbName())).getChannel(), + new FileOutputStream(getContentResolver().openFileDescriptor(uri, "w").getFileDescriptor()).getChannel() + ); + Toast.makeText( + this, + String.format(this.getString(R.string.back_up_success_message), FileUtilities.uri_to_filename(this, uri)), + Toast.LENGTH_LONG + ).show(); + } else if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + Uri uri = data.getData(); + FileUtilities.copy_file( + new FileInputStream(getContentResolver().openFileDescriptor(uri, "r").getFileDescriptor()).getChannel(), + new FileOutputStream(this.getDatabasePath(DatabaseOpenHelper.getDbName())).getChannel() + ); + ShowsProvider.reloadDatabase(this); + android.os.AsyncTask.execute(() -> Glide.get(getApplicationContext()).clearDiskCache()); + Toast.makeText(this, this.getString(R.string.restore_success_message), Toast.LENGTH_LONG).show(); + } + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + @Override + public void onBackupSelected(String backupFilename) { + new AsyncTask().executeAsync(new RestoreTask(backupFilename)); + } + + private void showSettings() { + final Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + } + + private void showAbout() { + final Intent intent = new Intent(this, AboutActivity.class); + startActivity(intent); + } +} diff --git a/app/src/main/java/org/jamienicol/episodes/NextEpisodeFragment.java b/app/src/main/java/com/redcoracle/episodes/NextEpisodeFragment.java similarity index 95% rename from app/src/main/java/org/jamienicol/episodes/NextEpisodeFragment.java rename to app/src/main/java/com/redcoracle/episodes/NextEpisodeFragment.java index 8c3c0495..f21d126b 100644 --- a/app/src/main/java/org/jamienicol/episodes/NextEpisodeFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/NextEpisodeFragment.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.content.AsyncQueryHandler; import android.content.ContentResolver; @@ -23,10 +23,6 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -34,10 +30,17 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.TextView; + +import androidx.fragment.app.Fragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; + +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; + import java.text.DateFormat; import java.util.Date; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsProvider; public class NextEpisodeFragment extends Fragment diff --git a/app/src/main/java/com/redcoracle/episodes/Preferences.java b/app/src/main/java/com/redcoracle/episodes/Preferences.java new file mode 100644 index 00000000..5cc79ca6 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/Preferences.java @@ -0,0 +1,12 @@ +package com.redcoracle.episodes; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +public class Preferences { + public static SharedPreferences getSharedPreferences() { + Context context = MainActivity.getAppContext(); + return PreferenceManager.getDefaultSharedPreferences(context); + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/RefreshShowUtil.java b/app/src/main/java/com/redcoracle/episodes/RefreshShowUtil.java new file mode 100644 index 00000000..2ba8a217 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/RefreshShowUtil.java @@ -0,0 +1,224 @@ +/* + * Copyright (C) 2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; +import android.util.SparseArray; + +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.db.ShowsTable; +import com.redcoracle.episodes.tvdb.Client; +import com.redcoracle.episodes.tvdb.Episode; +import com.redcoracle.episodes.tvdb.Show; + +import org.apache.commons.collections4.map.MultiKeyMap; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +public class RefreshShowUtil { + private static final String TAG = RefreshShowUtil.class.getName(); + + public static void refreshShow(int showId, ContentResolver contentResolver) { + Log.i(TAG, String.format("Refreshing show %d", showId)); + + final Client tmdbClient = new Client(); + SharedPreferences preferences = Preferences.getSharedPreferences(); + + final String showLanguage = preferences.getString("pref_language", "en"); + final HashMap showIds = getShowIds(showId, contentResolver); + final Show show = tmdbClient.getShow(showIds, showLanguage); + + if (show != null) { + updateShow(showId, show, contentResolver); + if (show.getEpisodes() != null) { + updateEpisodes(showId, show.getEpisodes(), contentResolver); + } + } + } + + private static HashMap getShowIds(int showId, ContentResolver contentResolver) { + final Uri showUri = Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, String.valueOf(showId)); + final String[] projection = { + ShowsTable.COLUMN_TVDB_ID, + ShowsTable.COLUMN_TMDB_ID, + ShowsTable.COLUMN_IMDB_ID + }; + final Cursor showCursor = contentResolver.query(showUri, projection, null, null, null); + showCursor.moveToFirst(); + final int tvdbIdColumnIndex = showCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_TVDB_ID); + final int tmdbIdColumnIndex = showCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_TMDB_ID); + final int imdbIdColumnIndex = showCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_IMDB_ID); + HashMap showIds = new HashMap() {{ + put("tvdbId", showCursor.getString(tvdbIdColumnIndex)); + put("tmdbId", showCursor.getString(tmdbIdColumnIndex)); + put("imdbId", showCursor.getString(imdbIdColumnIndex)); + }}; + showCursor.close(); + return showIds; + } + + private static void updateShow(int showId, Show show, ContentResolver contentResolver) { + final ContentValues showValues = new ContentValues(); + if (show.getTvdbId() != 0) { + showValues.put(ShowsTable.COLUMN_TVDB_ID, show.getTvdbId()); + } + showValues.put(ShowsTable.COLUMN_TMDB_ID, show.getTmdbId()); + showValues.put(ShowsTable.COLUMN_IMDB_ID, show.getImdbId()); + showValues.put(ShowsTable.COLUMN_NAME, show.getName()); + showValues.put(ShowsTable.COLUMN_LANGUAGE, show.getLanguage()); + showValues.put(ShowsTable.COLUMN_OVERVIEW, show.getOverview()); + if (show.getFirstAired() != null) { + showValues.put(ShowsTable.COLUMN_FIRST_AIRED, show.getFirstAired().getTime() / 1000); + } + showValues.put(ShowsTable.COLUMN_BANNER_PATH, show.getBannerPath()); + showValues.put(ShowsTable.COLUMN_FANART_PATH, show.getFanartPath()); + showValues.put(ShowsTable.COLUMN_POSTER_PATH, show.getPosterPath()); + + final Uri showUri = Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, String.valueOf(showId)); + contentResolver.update(showUri, showValues, null, null); + } + + private static void updateEpisodes(int showId, List episodes, ContentResolver contentResolver) { + // TODO: likely performance gains to be had in here + final MultiKeyMap seasonPairMap = new MultiKeyMap(); + final HashSet seen = new HashSet<>(); + final SparseArray episodeMap = new SparseArray<>(); + final ArrayList updates = new ArrayList<>(); + + for (Episode episode : episodes) { + episodeMap.append(episode.getTmdbId(), episode); + seasonPairMap.put(episode.getSeasonNumber(), episode.getEpisodeNumber(), episode); + } + + final Cursor cursor = getEpisodesCursor(showId, contentResolver); + + while (cursor.moveToNext()) { + final int idColumnIndex = cursor.getColumnIndexOrThrow(EpisodesTable.COLUMN_ID); + final int episodeId = cursor.getInt(idColumnIndex); + final int tmdbColumnIndex = cursor.getColumnIndexOrThrow(EpisodesTable.COLUMN_TMDB_ID); + final int episodeTmdbId = cursor.getInt(tmdbColumnIndex); + + Episode episode = episodeMap.get(episodeTmdbId); + final Uri episodeUri = Uri.withAppendedPath(ShowsProvider.CONTENT_URI_EPISODES, String.valueOf(episodeId)); + + if (episode == null) { + // Unable to find episode by ID; try season/episode pair instead. + // I think this should only happen when a show needs to migrate from TVDB->TMDB + episode = (Episode) seasonPairMap.get( + cursor.getInt(cursor.getColumnIndexOrThrow(EpisodesTable.COLUMN_SEASON_NUMBER)), + cursor.getInt(cursor.getColumnIndexOrThrow(EpisodesTable.COLUMN_EPISODE_NUMBER)) + ); + if (episode != null) { + Log.d(TAG, String.format("Matched by season/episode number: %s", episodeId)); + if (seen.contains(episode.identifier())) { + // Already matched a different episode by season/episode pair, + // so this will fail on insert and should be deleted instead. + Log.d(TAG, String.format("Deleting duplicate episode %s (%s)",episode.identifier(), episodeId)); + contentResolver.delete(episodeUri, null, null); + } else { + seen.add(episode.identifier()); + episodes.remove(episode); + continue; + } + } + } else if (seen.contains(episode.identifier())) { + Log.d(TAG, String.format("Deleting previously seen episode %s (%s)", episode.identifier(), episode.getId())); + contentResolver.delete(episodeUri, null, null); + continue; + } else { + Log.d(TAG, String.format("Found match by TMDB ID: %s", episodeId)); + seen.add(episode.identifier()); + } + + if (episode == null) { + Log.i(TAG, String.format("No matches found. Deleting episode: %d", episodeId)); + contentResolver.delete(episodeUri, null, null); + } else { + final ContentValues epValues = new ContentValues(); + epValues.put(EpisodesTable.COLUMN_ID, episodeId); + epValues.put(EpisodesTable.COLUMN_SHOW_ID, showId); + epValues.put(EpisodesTable.COLUMN_TVDB_ID, episode.getTvdbId()); + epValues.put(EpisodesTable.COLUMN_TMDB_ID, episode.getTmdbId()); + epValues.put(EpisodesTable.COLUMN_IMDB_ID, episode.getImdbId()); + epValues.put(EpisodesTable.COLUMN_NAME, episode.getName()); + epValues.put(EpisodesTable.COLUMN_LANGUAGE, episode.getLanguage()); + epValues.put(EpisodesTable.COLUMN_OVERVIEW, episode.getOverview()); + epValues.put(EpisodesTable.COLUMN_EPISODE_NUMBER, episode.getEpisodeNumber()); + epValues.put(EpisodesTable.COLUMN_SEASON_NUMBER, episode.getSeasonNumber()); + if (episode.getFirstAired() != null) { + epValues.put(EpisodesTable.COLUMN_FIRST_AIRED, episode.getFirstAired().getTime() / 1000); + } + + Log.i(TAG, String.format("Updating episode %d.", episodeId)); + updates.add(epValues); + + /* remove episode from list of episodes + * returned by tvdb. by the end of this function + * this list will only contain new episodes */ + episodes.remove(episode); + } + } + cursor.close(); + contentResolver.bulkInsert(ShowsProvider.CONTENT_URI_EPISODES, updates.toArray(new ContentValues[0])); + + + for (Episode episode : episodes) { + final ContentValues epValues = new ContentValues(); + epValues.put(EpisodesTable.COLUMN_SHOW_ID, showId); + epValues.put(EpisodesTable.COLUMN_TVDB_ID, episode.getTvdbId()); + epValues.put(EpisodesTable.COLUMN_TMDB_ID, episode.getTmdbId()); + epValues.put(EpisodesTable.COLUMN_IMDB_ID, episode.getImdbId()); + epValues.put(EpisodesTable.COLUMN_NAME, episode.getName()); + epValues.put(EpisodesTable.COLUMN_LANGUAGE, episode.getLanguage()); + epValues.put(EpisodesTable.COLUMN_OVERVIEW, episode.getOverview()); + epValues.put(EpisodesTable.COLUMN_EPISODE_NUMBER, episode.getEpisodeNumber()); + epValues.put(EpisodesTable.COLUMN_SEASON_NUMBER, episode.getSeasonNumber()); + if (episode.getFirstAired() != null) { + epValues.put(EpisodesTable.COLUMN_FIRST_AIRED, episode.getFirstAired().getTime() / 1000); + } + + contentResolver.insert(ShowsProvider.CONTENT_URI_EPISODES, epValues); + } + } + + private static Cursor getEpisodesCursor(int showId, ContentResolver contentResolver) { + final String[] projection = { + EpisodesTable.COLUMN_ID, + EpisodesTable.COLUMN_TVDB_ID, + EpisodesTable.COLUMN_TMDB_ID, + EpisodesTable.COLUMN_IMDB_ID, + EpisodesTable.COLUMN_SEASON_NUMBER, + EpisodesTable.COLUMN_EPISODE_NUMBER + }; + final String selection = String.format("%s=?", EpisodesTable.COLUMN_SHOW_ID); + final String[] selectionArgs = { + String.valueOf(showId) + }; + + return contentResolver.query(ShowsProvider.CONTENT_URI_EPISODES, projection, selection, selectionArgs, null); + } +} diff --git a/app/src/main/java/org/jamienicol/episodes/SeasonActivity.java b/app/src/main/java/com/redcoracle/episodes/SeasonActivity.java similarity index 74% rename from app/src/main/java/org/jamienicol/episodes/SeasonActivity.java rename to app/src/main/java/com/redcoracle/episodes/SeasonActivity.java index 8fec6c04..f7b13bc4 100644 --- a/app/src/main/java/org/jamienicol/episodes/SeasonActivity.java +++ b/app/src/main/java/com/redcoracle/episodes/SeasonActivity.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.content.AsyncQueryHandler; import android.content.ContentResolver; @@ -24,21 +24,26 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.FragmentTransaction; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v7.app.ActionBar; -import android.support.v7.app.ActionBarActivity; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.db.ShowsTable; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.FragmentTransaction; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; + +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.db.ShowsTable; + +import java.util.ArrayList; +import java.util.Date; public class SeasonActivity - extends ActionBarActivity + extends AppCompatActivity implements LoaderManager.LoaderCallbacks, EpisodesListFragment.OnEpisodeSelectedListener { @@ -65,7 +70,7 @@ public void onCreate(Bundle savedInstanceState) final Bundle loaderArgs = new Bundle(); loaderArgs.putInt("showId", showId); - getSupportLoaderManager().initLoader(0, loaderArgs, this); + LoaderManager.getInstance(this).initLoader(0, loaderArgs, this); final ActionBar actionBar = getSupportActionBar(); if (seasonNumber == 0) { @@ -130,28 +135,24 @@ public void onLoaderReset(Loader loader) { @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: + int item_id = item.getItemId(); + if (item_id == android.R.id.home) { finish(); return true; - - case R.id.menu_mark_season_watched: + } else if (item_id == R.id.menu_mark_season_watched) { markSeasonWatched(true); return true; - - case R.id.menu_mark_season_not_watched: + } else if (item_id == R.id.menu_mark_season_not_watched) { markSeasonWatched(false); return true; - - default: + } else { return super.onOptionsItemSelected(item); } } @Override public void onEpisodeSelected(int episodeId) { - final Intent intent = new Intent(this, - EpisodeActivity.class); + final Intent intent = new Intent(this, EpisodeActivity.class); intent.putExtra("showId", showId); intent.putExtra("seasonNumber", seasonNumber); intent.putExtra("initialEpisodeId", episodeId); @@ -160,24 +161,38 @@ public void onEpisodeSelected(int episodeId) { private void markSeasonWatched(boolean watched) { final ContentResolver contentResolver = getContentResolver(); - final AsyncQueryHandler handler = - new AsyncQueryHandler(contentResolver) {}; + final AsyncQueryHandler handler = new AsyncQueryHandler(contentResolver) {}; final ContentValues epValues = new ContentValues(); + final Date now = new Date(); epValues.put(EpisodesTable.COLUMN_WATCHED, watched); - final String selection = - String.format("%s=? AND %s=?", - EpisodesTable.COLUMN_SHOW_ID, - EpisodesTable.COLUMN_SEASON_NUMBER); - final String[] selectionArgs = { - String.valueOf(showId), - String.valueOf(seasonNumber) + String selection = String.format( + "%s=? AND %s=?", + EpisodesTable.COLUMN_SHOW_ID, + EpisodesTable.COLUMN_SEASON_NUMBER + ); + ArrayList selectionArgs = new ArrayList(){ + { + add(String.valueOf(showId)); + add(String.valueOf(seasonNumber)); + } }; + if (watched) { + // Only mark episodes that have aired. + selection = String.format( + "%s AND %s <= ? AND %s IS NOT NULL", + selection, + EpisodesTable.COLUMN_FIRST_AIRED, + EpisodesTable.COLUMN_FIRST_AIRED + ); + selectionArgs.add(String.valueOf(now.getTime() / 1000)); + } + handler.startUpdate(0, null, ShowsProvider.CONTENT_URI_EPISODES, epValues, selection, - selectionArgs); + selectionArgs.toArray(new String[0])); } } diff --git a/app/src/main/java/org/jamienicol/episodes/SeasonsListFragment.java b/app/src/main/java/com/redcoracle/episodes/SeasonsListFragment.java similarity index 94% rename from app/src/main/java/org/jamienicol/episodes/SeasonsListFragment.java rename to app/src/main/java/com/redcoracle/episodes/SeasonsListFragment.java index 667002d6..c9b12183 100644 --- a/app/src/main/java/org/jamienicol/episodes/SeasonsListFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/SeasonsListFragment.java @@ -15,25 +15,27 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v7.widget.LinearLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ProgressBar; import android.widget.TextView; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsProvider; + +import androidx.fragment.app.Fragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; public class SeasonsListFragment extends Fragment diff --git a/app/src/main/java/org/jamienicol/episodes/SelectBackupDialog.java b/app/src/main/java/com/redcoracle/episodes/SelectBackupDialog.java similarity index 53% rename from app/src/main/java/org/jamienicol/episodes/SelectBackupDialog.java rename to app/src/main/java/com/redcoracle/episodes/SelectBackupDialog.java index 993d36bf..f237dd13 100644 --- a/app/src/main/java/org/jamienicol/episodes/SelectBackupDialog.java +++ b/app/src/main/java/com/redcoracle/episodes/SelectBackupDialog.java @@ -15,45 +15,42 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; -import android.content.DialogInterface; +import android.content.Context; import android.os.Bundle; -import android.support.v4.app.DialogFragment; + +import androidx.annotation.NonNull; +import androidx.fragment.app.DialogFragment; + import java.io.File; import java.util.Arrays; -import java.util.Comparator; -import org.jamienicol.episodes.db.BackUpRestoreHelper; -public class SelectBackupDialog - extends DialogFragment -{ +public class SelectBackupDialog extends DialogFragment { public interface OnBackupSelectedListener { - public void onBackupSelected(String backupFilename); + void onBackupSelected(String backupFilename); } private OnBackupSelectedListener onBackupSelectedListener; + private Context context; @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - try { - onBackupSelectedListener = (OnBackupSelectedListener)activity; - } catch (ClassCastException e) { - final String message = - String.format("%s must implement OnBackupSelectedListener", - activity.toString()); - throw new ClassCastException(message); + public void onAttach(@NonNull Context context) { + super.onAttach(context); + this.context = context; + Activity activity; + if (context instanceof Activity) { + activity = (Activity) context; + onBackupSelectedListener = (OnBackupSelectedListener) activity; } } + @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - final AlertDialog.Builder builder = - new AlertDialog.Builder(getActivity()); + final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); final File[] backups = getBackupFiles(); @@ -65,47 +62,35 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { } private File[] getBackupFiles() { - final File[] files = BackUpRestoreHelper.getBackupDir().listFiles(); + final File[] files = new File(this.context.getExternalFilesDir(null), "episodes").listFiles(); if (files != null) { - Arrays.sort(files, new Comparator() { - public int compare(File lhs, File rhs) { - return Long.valueOf(rhs.lastModified()). - compareTo(lhs.lastModified()); - } - }); + Arrays.sort(files, (lhs, rhs) -> Long.compare(rhs.lastModified(), lhs.lastModified())); return files; } else { return null; } } - private Dialog createDialogBackups(AlertDialog.Builder builder, - final File[] backups) { + private Dialog createDialogBackups(AlertDialog.Builder builder, final File[] backups) { final String[] names = new String[backups.length]; for (int i = 0; i < backups.length; i++) { names[i] = backups[i].getName(); } builder.setTitle(R.string.restore_dialog_title) - .setItems(names, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - final String path = backups[which].getPath(); - onBackupSelectedListener.onBackupSelected(path); - } + .setItems(names, (dialog, which) -> { + final String path = backups[which].getPath(); + onBackupSelectedListener.onBackupSelected(path); }); return builder.create(); } private Dialog createDialogNoBackups(AlertDialog.Builder builder) { - final String message = - getActivity().getString(R.string.restore_dialog_no_backups_message, - BackUpRestoreHelper.getBackupDir()); - - builder.setTitle(R.string.restore_dialog_title) - .setMessage(message); - + final File directory = new File(this.context.getExternalFilesDir(null), "episodes"); + final String message = getActivity().getString(R.string.restore_dialog_no_backups_message, directory); + builder.setTitle(R.string.restore_dialog_title).setMessage(message); return builder.create(); } } diff --git a/app/src/main/java/org/jamienicol/episodes/SettingsActivity.java b/app/src/main/java/com/redcoracle/episodes/SettingsActivity.java similarity index 77% rename from app/src/main/java/org/jamienicol/episodes/SettingsActivity.java rename to app/src/main/java/com/redcoracle/episodes/SettingsActivity.java index faae3f51..724dd5f9 100644 --- a/app/src/main/java/org/jamienicol/episodes/SettingsActivity.java +++ b/app/src/main/java/com/redcoracle/episodes/SettingsActivity.java @@ -15,19 +15,21 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; -public class SettingsActivity - extends ActionBarActivity -{ +import androidx.appcompat.app.AppCompatActivity; + +public class SettingsActivity extends AppCompatActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.settings_activity); - + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings_fragment, new SettingsFragment()) + .commit(); getSupportActionBar().setDisplayHomeAsUpEnabled(true); } } diff --git a/app/src/main/java/org/jamienicol/episodes/SettingsFragment.java b/app/src/main/java/com/redcoracle/episodes/SettingsFragment.java similarity index 72% rename from app/src/main/java/org/jamienicol/episodes/SettingsFragment.java rename to app/src/main/java/com/redcoracle/episodes/SettingsFragment.java index 9ff49803..a7ede6e4 100644 --- a/app/src/main/java/org/jamienicol/episodes/SettingsFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/SettingsFragment.java @@ -15,17 +15,15 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; -import android.preference.PreferenceFragment; import android.os.Bundle; -public class SettingsFragment - extends PreferenceFragment -{ +import androidx.preference.PreferenceFragmentCompat; + +public class SettingsFragment extends PreferenceFragmentCompat { @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - addPreferencesFromResource(R.xml.preferences); + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.preferences, rootKey); } } diff --git a/app/src/main/java/org/jamienicol/episodes/ShowActivity.java b/app/src/main/java/com/redcoracle/episodes/ShowActivity.java similarity index 67% rename from app/src/main/java/org/jamienicol/episodes/ShowActivity.java rename to app/src/main/java/com/redcoracle/episodes/ShowActivity.java index 3e4f1d46..23dcfaad 100644 --- a/app/src/main/java/org/jamienicol/episodes/ShowActivity.java +++ b/app/src/main/java/com/redcoracle/episodes/ShowActivity.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.content.AsyncQueryHandler; import android.content.ContentResolver; @@ -24,35 +24,40 @@ import android.content.Intent; import android.content.SharedPreferences; import android.database.Cursor; +import android.graphics.PorterDuff; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; -import android.support.design.widget.TabLayout; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.support.v4.view.ViewPager; -import android.support.v7.app.ActionBarActivity; -import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.ImageLoader; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.db.ShowsTable; -import org.jamienicol.episodes.services.RefreshShowService; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; +import androidx.swiperefreshlayout.widget.CircularProgressDrawable; +import androidx.viewpager.widget.ViewPager; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.google.android.material.tabs.TabLayout; +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.db.ShowsTable; +import com.redcoracle.episodes.services.AsyncTask; +import com.redcoracle.episodes.services.DeleteShowTask; +import com.redcoracle.episodes.services.RefreshShowTask; public class ShowActivity - extends ActionBarActivity + extends AppCompatActivity implements LoaderManager.LoaderCallbacks, ViewPager.OnPageChangeListener, SeasonsListFragment.OnSeasonSelectedListener @@ -61,6 +66,7 @@ public class ShowActivity private int showId; private boolean isShowStarred; + private boolean isShowArchived; private ImageView headerImage; private Toolbar toolbar; @@ -85,28 +91,25 @@ public void onCreate(Bundle savedInstanceState) loaderArgs.putInt("showId", showId); getSupportLoaderManager().initLoader(0, loaderArgs, this); - headerImage = (ImageView)findViewById(R.id.header_image); + headerImage = findViewById(R.id.header_image); - toolbar = (Toolbar)findViewById(R.id.toolbar); + toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - titleView = (TextView)findViewById(R.id.title); - - pagerAdapter = - new PagerAdapter(this, getSupportFragmentManager(), showId); + titleView = findViewById(R.id.title); - pager = (ViewPager)findViewById(R.id.pager); + pagerAdapter = new PagerAdapter(this, getSupportFragmentManager(), showId); + pager = findViewById(R.id.pager); pager.setAdapter(pagerAdapter); - pager.setOnPageChangeListener(this); + pager.addOnPageChangeListener(this); - tabStrip = (TabLayout)findViewById(R.id.tab_strip); + tabStrip = findViewById(R.id.tab_strip); tabStrip.setTabTextColors(getResources().getColorStateList(R.color.tab_text)); tabStrip.setupWithViewPager(pager); // Set the default tab from preferences. - final SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(this); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); pager.setCurrentItem(prefs.getInt(KEY_DEFAULT_TAB, 0)); } @@ -131,6 +134,15 @@ public boolean onPrepareOptionsMenu(Menu menu) { toggleStarred.setTitle(R.string.menu_star_show); } + final MenuItem toggleArchived = menu.findItem(R.id.menu_toggle_show_archived); + if (isShowArchived) { + toggleArchived.setIcon(R.drawable.ic_show_archived); + toggleArchived.setTitle(R.string.menu_unarchive_show); + } else { + toggleArchived.setIcon(R.drawable.ic_show_unarchived); + toggleArchived.setTitle(R.string.menu_archive_show); + } + return super.onPrepareOptionsMenu(menu); } @@ -145,6 +157,10 @@ public boolean onOptionsItemSelected(MenuItem item) { toggleShowStarred(); return true; + case R.id.menu_toggle_show_archived: + toggleShowArchived(); + return true; + case R.id.menu_refresh_show: refreshShow(); return true; @@ -176,7 +192,9 @@ public Loader onCreateLoader(int id, Bundle args) { final String[] projection = { ShowsTable.COLUMN_NAME, ShowsTable.COLUMN_STARRED, - ShowsTable.COLUMN_FANART_PATH + ShowsTable.COLUMN_ARCHIVED, + ShowsTable.COLUMN_FANART_PATH, + ShowsTable.COLUMN_POSTER_PATH }; return new CursorLoader(this, uri, @@ -199,28 +217,36 @@ public void onLoadFinished(Loader loader, Cursor data) { final int starredColumnIndex = data.getColumnIndexOrThrow(ShowsTable.COLUMN_STARRED); final boolean starred = - data.getInt(starredColumnIndex) > 0 ? true : false; + data.getInt(starredColumnIndex) > 0; if (isShowStarred != starred) { isShowStarred = starred; // toggle starred menu item needs updated supportInvalidateOptionsMenu(); } - final int fanartPathColumnIndex = - data.getColumnIndexOrThrow(ShowsTable.COLUMN_FANART_PATH); - final String fanartPath = data.getString(fanartPathColumnIndex); - if (fanartPath != null && !fanartPath.equals("")) { - final String fanartUrl = - String.format("http://thetvdb.com/banners/%s", fanartPath); - - final DisplayImageOptions options = - new DisplayImageOptions.Builder() - .cacheInMemory(true) - .cacheOnDisk(true) - .build(); - ImageLoader.getInstance().displayImage(fanartUrl, - headerImage, - options); + // maybe update the state of the toggle archived menu item + final int archivedColumnIndex = data.getColumnIndexOrThrow(ShowsTable.COLUMN_ARCHIVED); + final boolean archived = data.getInt(archivedColumnIndex) > 0; + if (isShowArchived != archived) { + isShowArchived = archived; + // toggle archived menu item needs updated + supportInvalidateOptionsMenu(); + } + + final int posterPathColumnIndex = data.getColumnIndexOrThrow(ShowsTable.COLUMN_POSTER_PATH); + final String posterPath = data.getString(posterPathColumnIndex); + if (posterPath != null) { + CircularProgressDrawable placeholder = new CircularProgressDrawable(this); + placeholder.setColorFilter(ContextCompat.getColor(this, R.color.accent), PorterDuff.Mode.SRC_IN); + placeholder.setStrokeWidth(5f); + placeholder.setCenterRadius(60f); + placeholder.start(); + final String artUrl = String.format("https://image.tmdb.org/t/p/w1280/%s", posterPath); + Glide.with(this) + .load(artUrl) + .placeholder(placeholder) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(headerImage); } } } @@ -278,11 +304,27 @@ private void toggleShowStarred() { selectionArgs); } - private void refreshShow() { - final Intent intent = new Intent(this, RefreshShowService.class); - intent.putExtra("showId", showId); + private void toggleShowArchived() { + final ContentResolver contentResolver = getContentResolver(); + final AsyncQueryHandler handler = new AsyncQueryHandler(contentResolver) {}; + final ContentValues values = new ContentValues(); + values.put(ShowsTable.COLUMN_ARCHIVED, !isShowArchived); + final String selection = String.format("%s=?", ShowsTable.COLUMN_ID); + final String[] selectionArgs = { + String.valueOf(showId) + }; + + handler.startUpdate(0, + null, + ShowsProvider.CONTENT_URI_SHOWS, + values, + selection, + selectionArgs); + } - startService(intent); + + private void refreshShow() { + new AsyncTask().executeAsync(new RefreshShowTask(this.showId)); } private void markShowWatched(boolean watched) { @@ -309,32 +351,7 @@ private void markShowWatched(boolean watched) { } private void deleteShow() { - final ContentResolver contentResolver = getContentResolver(); - final AsyncQueryHandler handler = - new AsyncQueryHandler(contentResolver) {}; - - /* delete all the show's episodes */ - final String epSelection = - String.format("%s=?", EpisodesTable.COLUMN_SHOW_ID); - final String[] epSelectionArgs = { - String.valueOf(showId) - }; - - handler.startDelete(0, - null, - ShowsProvider.CONTENT_URI_EPISODES, - epSelection, - epSelectionArgs); - - /* delete the show itself */ - final Uri showUri = - Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, - String.valueOf(showId)); - handler.startDelete(0, - null, - showUri, - null, - null); + new AsyncTask().executeAsync(new DeleteShowTask(this.showId)); } private static class PagerAdapter @@ -354,7 +371,7 @@ public PagerAdapter(final Context context, @Override public int getCount() { - return 4; + return 3; } @Override @@ -366,8 +383,6 @@ public CharSequence getPageTitle(final int position) { return context.getString(R.string.show_tab_episodes); case 2: return context.getString(R.string.show_tab_next); - case 3: - return context.getString(R.string.show_tab_notes); default: return null; } @@ -382,8 +397,6 @@ public Fragment getItem(final int position) { return SeasonsListFragment.newInstance(showId); case 2: return NextEpisodeFragment.newInstance(showId); - case 3: - return ShowNotesFragment.newInstance(showId); default: return null; } diff --git a/app/src/main/java/org/jamienicol/episodes/ShowDetailsFragment.java b/app/src/main/java/com/redcoracle/episodes/ShowDetailsFragment.java similarity index 92% rename from app/src/main/java/org/jamienicol/episodes/ShowDetailsFragment.java rename to app/src/main/java/com/redcoracle/episodes/ShowDetailsFragment.java index 1bac285d..c52998ff 100644 --- a/app/src/main/java/org/jamienicol/episodes/ShowDetailsFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/ShowDetailsFragment.java @@ -15,23 +15,26 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; + +import androidx.fragment.app.Fragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; + +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.db.ShowsTable; + import java.text.DateFormat; import java.util.Date; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.db.ShowsTable; public class ShowDetailsFragment extends Fragment diff --git a/app/src/main/java/org/jamienicol/episodes/ShowNotesFragment.java b/app/src/main/java/com/redcoracle/episodes/ShowNotesFragment.java similarity index 82% rename from app/src/main/java/org/jamienicol/episodes/ShowNotesFragment.java rename to app/src/main/java/com/redcoracle/episodes/ShowNotesFragment.java index 9a986840..e5cdcef3 100644 --- a/app/src/main/java/org/jamienicol/episodes/ShowNotesFragment.java +++ b/app/src/main/java/com/redcoracle/episodes/ShowNotesFragment.java @@ -15,32 +15,23 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes; +package com.redcoracle.episodes; -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; -import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -import android.text.Spanned; 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 android.view.inputmethod.InputMethodManager; -import android.widget.EditText; import android.widget.TextView; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.db.ShowsTable; +import androidx.fragment.app.Fragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; + +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.db.ShowsTable; public class ShowNotesFragment extends Fragment diff --git a/app/src/main/java/com/redcoracle/episodes/ShowsListFragment.java b/app/src/main/java/com/redcoracle/episodes/ShowsListFragment.java new file mode 100644 index 00000000..853157b2 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/ShowsListFragment.java @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2012-2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes; + +import android.Manifest; +import android.app.Activity; +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +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 android.widget.BaseAdapter; +import android.widget.CompoundButton; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.ToggleButton; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.ListFragment; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.CursorLoader; +import androidx.loader.content.Loader; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.db.ShowsTable; +import com.redcoracle.episodes.services.AsyncTask; +import com.redcoracle.episodes.services.RefreshAllShowsTask; + +import java.util.ArrayList; +import java.util.List; + +public class ShowsListFragment + extends ListFragment + implements LoaderManager.LoaderCallbacks +{ + private static final int LOADER_ID_SHOWS = 0; + private static final int LOADER_ID_EPISODES = 1; + + private static final String KEY_PREF_SHOWS_FILTER = "pref_shows_filter"; + private static final String KEY_PREF_CONFIRMED_BACKUP = "pref_confirmed_backup"; + + private static final int SHOWS_FILTER_ALL = 0; + private static final int SHOWS_FILTER_STARRED = 1; + private static final int SHOWS_FILTER_UNCOMPLETED = 2; + private static final int SHOWS_FILTER_ARCHIVED = 3; + private static final int SHOWS_FILTER_UPCOMING = 4; + + private ShowsListAdapter listAdapter; + private Cursor showsData; + private Cursor episodesData; + + private ActivityResultLauncher reqNotificationPermission; + + + public interface OnShowSelectedListener { + public void onShowSelected(int showId); + } + private OnShowSelectedListener onShowSelectedListener; + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + + try { + onShowSelectedListener = (OnShowSelectedListener)activity; + } catch (ClassCastException e) { + final String message = + String.format("%s must implement OnShowSelectedListener", + activity.toString()); + throw new ClassCastException(message); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setHasOptionsMenu(true); + reqNotificationPermission = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + granted -> {} + ); + } + + public View onCreateView(LayoutInflater inflater, + ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.shows_list_fragment, container, false); + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + listAdapter = new ShowsListAdapter(getActivity(), + null, + null); + setListAdapter(listAdapter); + + getLoaderManager().initLoader(LOADER_ID_SHOWS, null, this); + getLoaderManager().initLoader(LOADER_ID_EPISODES, null, this); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.shows_list_fragment, menu); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + + // hide refresh all option if no shows exist + final boolean showsExist = (showsData != null && showsData.moveToFirst()); + menu.findItem(R.id.menu_refresh_all_shows).setVisible(showsExist); + + /* set the currently selected filter's menu item as checked */ + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity()); + final int filter = prefs.getInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_ALL); + + switch (filter) { + case SHOWS_FILTER_ALL: + menu.findItem(R.id.menu_filter_all).setChecked(true); + break; + case SHOWS_FILTER_STARRED: + menu.findItem(R.id.menu_filter_starred).setChecked(true); + break; + case SHOWS_FILTER_UNCOMPLETED: + menu.findItem(R.id.menu_filter_uncompleted).setChecked(true); + break; + case SHOWS_FILTER_ARCHIVED: + menu.findItem(R.id.menu_filter_archived).setChecked(true); + break; + case SHOWS_FILTER_UPCOMING: + menu.findItem(R.id.menu_filter_upcoming).setChecked(true); + break; + } + + super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_refresh_all_shows: + refreshAllShows(); + return true; + + case R.id.menu_filter_all: + case R.id.menu_filter_starred: + case R.id.menu_filter_uncompleted: + case R.id.menu_filter_archived: + case R.id.menu_filter_upcoming: + if (!item.isChecked()) { + item.setChecked(true); + } + + final SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(getActivity()); + final SharedPreferences.Editor editor = prefs.edit(); + if (item.getItemId() == R.id.menu_filter_all) { + editor.putInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_ALL); + } else if (item.getItemId() == R.id.menu_filter_starred) { + editor.putInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_STARRED); + } else if (item.getItemId() == R.id.menu_filter_uncompleted) { + editor.putInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_UNCOMPLETED); + } else if (item.getItemId() == R.id.menu_filter_archived) { + editor.putInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_ARCHIVED); + } else if (item.getItemId() == R.id.menu_filter_upcoming) { + editor.putInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_UPCOMING); + } + editor.apply(); + + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + public Loader onCreateLoader(int id, Bundle args) { + if (id == LOADER_ID_SHOWS) { + final String[] projection = { + ShowsTable.COLUMN_ID, + ShowsTable.COLUMN_NAME, + ShowsTable.COLUMN_STARRED, + ShowsTable.COLUMN_ARCHIVED, + ShowsTable.COLUMN_BANNER_PATH + }; + return new CursorLoader(getActivity(), + ShowsProvider.CONTENT_URI_SHOWS, + projection, + null, + null, + ShowsTable.COLUMN_STARRED + " DESC, " + + ShowsTable.COLUMN_NAME + " COLLATE LOCALIZED ASC"); + + } else if (id == LOADER_ID_EPISODES) { + final String[] projection = { + EpisodesTable.COLUMN_SHOW_ID, + EpisodesTable.COLUMN_SEASON_NUMBER, + EpisodesTable.COLUMN_FIRST_AIRED, + EpisodesTable.COLUMN_WATCHED + }; + final String selection = + String.format("%s!=?", EpisodesTable.COLUMN_SEASON_NUMBER); + final String[] selectionArgs = { + "0" + }; + return new CursorLoader(getActivity(), + ShowsProvider.CONTENT_URI_EPISODES, + projection, + selection, + selectionArgs, + null); + + } else { + throw new IllegalArgumentException("invalid loader id"); + } + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + switch (loader.getId()) { + case LOADER_ID_SHOWS: + showsData = data; + listAdapter.swapShowsCursor(data); + break; + + case LOADER_ID_EPISODES: + episodesData = data; + listAdapter.swapEpisodesCursor(data); + break; + } + + getActivity().invalidateOptionsMenu(); + } + + @Override + public void onLoaderReset(Loader loader) { + onLoadFinished(loader, null); + } + + @Override + public void onListItemClick(ListView l, View v, int position, long id) { + onShowSelectedListener.onShowSelected((int)id); + } + + private void checkNotificationPermission() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return; + } + + boolean granted = ContextCompat.checkSelfPermission( + this.requireContext(), + Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED; + + if (!granted) { + reqNotificationPermission.launch(Manifest.permission.POST_NOTIFICATIONS); + } + } + + private void refreshAllShows() { + checkNotificationPermission(); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.requireContext()); + boolean confirmed_backup = prefs.getBoolean(KEY_PREF_CONFIRMED_BACKUP, false); + + if (confirmed_backup) { + new AsyncTask().executeAsync(new RefreshAllShowsTask()); + } else { + new AlertDialog.Builder(this.requireContext()) + .setTitle(R.string.provider_change_warning_title) + .setMessage(R.string.provider_change_warning_detail) + .setPositiveButton(R.string.provider_change_warning_confirm, (dialogInterface, i) -> { + prefs.edit().putBoolean(KEY_PREF_CONFIRMED_BACKUP, true).apply(); + new AsyncTask().executeAsync(new RefreshAllShowsTask()); + }) + .setNegativeButton(R.string.provider_change_warning_cancel, (dialogInterface, i) -> {}) + .show(); + } + } + + private static class ShowsListAdapter + extends BaseAdapter + implements SharedPreferences.OnSharedPreferenceChangeListener + { + private Context context; + private Cursor showsCursor; + private int filter; + private EpisodesCounter episodesCounter; + + // list of shows to be displayed with current filter. maps from + // the show's position in the list to its position in the cursor. + private List filteredShows; + + public ShowsListAdapter(Context context, + Cursor showsCursor, + Cursor episodesCursor) { + this.context = context; + + episodesCounter = new EpisodesCounter(EpisodesTable.COLUMN_SHOW_ID); + episodesCounter.swapCursor(episodesCursor); + + final SharedPreferences prefs = + PreferenceManager.getDefaultSharedPreferences(context); + prefs.registerOnSharedPreferenceChangeListener(this); + filter = prefs.getInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_ALL); + + filteredShows = new ArrayList(); + + swapShowsCursor(showsCursor); + } + + public void swapShowsCursor(Cursor showsCursor) { + this.showsCursor = showsCursor; + + updateFilter(); + notifyDataSetChanged(); + } + + public void swapEpisodesCursor(Cursor episodesCursor) { + episodesCounter.swapCursor(episodesCursor); + + if (showsCursor != null) { + updateFilter(); + notifyDataSetChanged(); + } + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, + String key) { + if (key.equals(KEY_PREF_SHOWS_FILTER)) { + filter = sharedPreferences.getInt(KEY_PREF_SHOWS_FILTER, + SHOWS_FILTER_ALL); + + if (showsCursor != null) { + updateFilter(); + notifyDataSetChanged(); + } + } + } + + private void updateFilter() { + filteredShows.clear(); + + if (showsCursor == null || !showsCursor.moveToFirst()) { + return; + } + + do { + switch (filter) { + case SHOWS_FILTER_STARRED: + final int starredColumnIndex = + showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_STARRED); + if (showsCursor.getInt(starredColumnIndex) > 0) { + filteredShows.add(showsCursor.getPosition()); + } + break; + + case SHOWS_FILTER_ARCHIVED: + final int archivedColumnIndex = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ARCHIVED); + if (showsCursor.getInt(archivedColumnIndex) > 0) { + filteredShows.add(showsCursor.getPosition()); + } + break; + + case SHOWS_FILTER_UNCOMPLETED: + final int idColumnIndexUncompleted = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); + final int idUncompleted = showsCursor.getInt(idColumnIndexUncompleted); + + if ((episodesCounter.getNumWatchedEpisodes(idUncompleted) < episodesCounter.getNumAiredEpisodes(idUncompleted)) && + showsCursor.getInt(showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ARCHIVED)) == 0) + { + filteredShows.add(showsCursor.getPosition()); + } + break; + + case SHOWS_FILTER_UPCOMING: + final int idColumnIndexUpcoming = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); //change to meaningful variable + final int idUpcoming = showsCursor.getInt(idColumnIndexUpcoming); + + if((episodesCounter.getNumUpcomingEpisodes(idUpcoming) > 0) && (episodesCounter.getNumWatchedEpisodes(idUpcoming) == episodesCounter.getNumAiredEpisodes(idUpcoming)) && + showsCursor.getInt(showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ARCHIVED)) == 0) + { + filteredShows.add(showsCursor.getPosition()); + } + break; + + default: + final int columnIndex = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ARCHIVED); + if (showsCursor.getInt(columnIndex) == 0) { + filteredShows.add(showsCursor.getPosition()); + } + break; + } + } while (showsCursor.moveToNext()); + } + + @Override + public int getCount() { + if (showsCursor == null) { + return 0; + } else { + return filteredShows.size(); + } + } + + @Override + public Object getItem(int position) { + return null; + } + + @Override + public long getItemId(int position) { + showsCursor.moveToPosition(filteredShows.get(position)); + + final int idColumnIndex = + showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); + return showsCursor.getInt(idColumnIndex); + } + + @Override + public View getView(int position, + View convertView, + ViewGroup parent) { + + final LayoutInflater inflater = LayoutInflater.from(context); + if(convertView == null) { + convertView = inflater.inflate(R.layout.shows_list_item, + parent, + false); + } + + showsCursor.moveToPosition(filteredShows.get(position)); + + final int idColumnIndex = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); + final int id = showsCursor.getInt(idColumnIndex); + + final ContentResolver contentResolver = context.getContentResolver(); + + final TextView nameView = convertView.findViewById(R.id.show_name_view); + final int nameColumnIndex = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_NAME); + final String name = showsCursor.getString(nameColumnIndex); + nameView.setText(name); + + final ImageView bannerView = convertView.findViewById(R.id.banner_view); + final int bannerPathColumnIndex = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_BANNER_PATH); + final String bannerPath = showsCursor.getString(bannerPathColumnIndex); + + bannerView.setImageResource(R.drawable.blank_show_banner); + if (bannerPath != null && !bannerPath.equals("")) { + final String bannerUrl = String.format("https://image.tmdb.org/t/p/w1280/%s", bannerPath); + + Glide.with(convertView) + .load(bannerUrl) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .centerCrop() + .placeholder(R.drawable.blank_show_banner) + .into(bannerView); + } + + final ToggleButton starredToggle = (ToggleButton)convertView.findViewById(R.id.show_starred_toggle); + final int starredColumnIndex = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_STARRED); + final boolean starred = showsCursor.getInt(starredColumnIndex) > 0; + + starredToggle.setOnCheckedChangeListener(null); + starredToggle.setChecked(starred); + + starredToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, + boolean isChecked) { + final AsyncQueryHandler handler = new AsyncQueryHandler(contentResolver) {}; + final ContentValues showValues = new ContentValues(); + showValues.put(ShowsTable.COLUMN_STARRED, isChecked); + + final Uri showUri = Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, String.valueOf(id)); + handler.startUpdate(0, + null, + showUri, + showValues, + null, + null); + } + }); + + final ToggleButton archivedToggle = (ToggleButton)convertView.findViewById(R.id.show_archived_toggle); + final int archivedColumnIndex = showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ARCHIVED); + final boolean archived = showsCursor.getInt(archivedColumnIndex) > 0; + + archivedToggle.setOnCheckedChangeListener(null); + archivedToggle.setChecked(archived); + + archivedToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + final AsyncQueryHandler handler = new AsyncQueryHandler(contentResolver) {}; + final ContentValues showValues = new ContentValues(); + showValues.put(ShowsTable.COLUMN_ARCHIVED, isChecked); + final Uri showUri = Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, String.valueOf(id)); + handler.startUpdate(0, null, showUri, showValues, null, null); + } + }); + + final int numAired = episodesCounter.getNumAiredEpisodes(id); + final int numWatched = episodesCounter.getNumWatchedEpisodes(id); + final int numUpcoming = episodesCounter.getNumUpcomingEpisodes(id); + + final ProgressBar progressBar = + (ProgressBar)convertView.findViewById(R.id.show_progress_bar); + progressBar.setMax(numAired); + progressBar.setProgress(numWatched); + + final TextView watchedCountView = + (TextView)convertView.findViewById(R.id.watched_count_view); + String watchedCountText = context.getString(R.string.watched_count, + numWatched, + numAired); + if (numUpcoming != 0) { + watchedCountText += " " + + context.getString(R.string.upcoming_count, + numUpcoming); + } + watchedCountView.setText(watchedCountText); + + return convertView; + } + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/db/DatabaseOpenHelper.java b/app/src/main/java/com/redcoracle/episodes/db/DatabaseOpenHelper.java new file mode 100644 index 00000000..d5984ca0 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/db/DatabaseOpenHelper.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2012-2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes.db; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +public class DatabaseOpenHelper extends SQLiteOpenHelper { + private static final String TAG = "DatabaseOpenHelper"; + private static final String name = "episodes.db"; + private static final int version = 9; + + DatabaseOpenHelper(Context context) { + super(context, name, null, version); + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.d(TAG, "creating database"); + ShowsTable.onCreate(db); + EpisodesTable.onCreate(db); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.d(TAG, String.format("upgrading database from version %d to %d", oldVersion, newVersion)); + ShowsTable.onUpgrade(db, oldVersion, newVersion); + EpisodesTable.onUpgrade(db, oldVersion, newVersion); + } + + @Override + public void onOpen(SQLiteDatabase db) { + Log.d(TAG, "opening database."); + } + + public static String getDbName() { + return name; + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/db/EpisodesTable.java b/app/src/main/java/com/redcoracle/episodes/db/EpisodesTable.java new file mode 100644 index 00000000..8d6d8865 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/db/EpisodesTable.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2012-2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes.db; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.provider.BaseColumns; +import android.util.Log; + +import java.util.Arrays; + +public class EpisodesTable { + private static final String TAG = "EpisodesTable"; + + static final String TABLE_NAME = "episodes"; + + public static final String COLUMN_ID = BaseColumns._ID; + public static final String COLUMN_TVDB_ID = "tvdb_id"; + public static final String COLUMN_TMDB_ID = "tmdb_id"; + public static final String COLUMN_IMDB_ID = "imdb_id"; + public static final String COLUMN_SHOW_ID = "show_id"; + public static final String COLUMN_NAME = "name"; + public static final String COLUMN_LANGUAGE = "language"; + public static final String COLUMN_OVERVIEW = "overview"; + public static final String COLUMN_EPISODE_NUMBER = "episode_number"; + public static final String COLUMN_SEASON_NUMBER = "season_number"; + public static final String COLUMN_FIRST_AIRED = "first_aired"; + public static final String COLUMN_WATCHED = "watched"; + + public static String createTableSQL(String table_name) { + return String.format( + "CREATE TABLE %s (" + + "%s INTEGER PRIMARY KEY," + + "%s INTEGER UNIQUE," + + "%s INTEGER UNIQUE," + + "%s TEXT UNIQUE," + + "%s INTEGER NOT NULL," + + "%s VARCHAR(200) NOT NULL," + + "%s TEXT," + + "%s TEXT," + + "%s INTEGER," + + "%s INTEGER," + + "%s DATE," + + "%s BOOLEAN" + + ");", + table_name, + COLUMN_ID, + COLUMN_TVDB_ID, + COLUMN_TMDB_ID, + COLUMN_IMDB_ID, + COLUMN_SHOW_ID, + COLUMN_NAME, + COLUMN_LANGUAGE, + COLUMN_OVERVIEW, + COLUMN_EPISODE_NUMBER, + COLUMN_SEASON_NUMBER, + COLUMN_FIRST_AIRED, + COLUMN_WATCHED + ); + } + + public static void onCreate(SQLiteDatabase db) { + String create = createTableSQL(TABLE_NAME); + + Log.d(TAG, String.format("creating episodes table: %s", create)); + + db.execSQL(create); + } + + static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion <= 7) { + // Add language column + Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, null); + String[] columns = cursor.getColumnNames(); + if (!Arrays.asList(columns).contains(COLUMN_LANGUAGE)) { + Log.d(TAG, "upgrading episodes table: adding language column"); + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s TEXT", TABLE_NAME, COLUMN_LANGUAGE)); + } + cursor.close(); + } + + if (oldVersion <= 8) { + // Add TMDB/IMDB columns + db.beginTransaction(); + try { + final String temp_table_name = String.format("new_%s", TABLE_NAME); + String create_table = createTableSQL(temp_table_name); + String insert_columns = String.format( + "%s, %s, %s, %s, %s, %s, %s, %s, %s, %s", + COLUMN_ID, COLUMN_TVDB_ID, COLUMN_SHOW_ID, COLUMN_NAME, COLUMN_LANGUAGE, + COLUMN_OVERVIEW, COLUMN_EPISODE_NUMBER, COLUMN_SEASON_NUMBER, + COLUMN_FIRST_AIRED, COLUMN_WATCHED + ); + db.execSQL(create_table); + db.execSQL(String.format( + "INSERT INTO %s (%s) SELECT %s FROM %s", + temp_table_name, insert_columns, insert_columns, TABLE_NAME + )); + db.execSQL(String.format("DROP TABLE %s", TABLE_NAME)); + db.execSQL(String.format( + "ALTER TABLE %s RENAME TO %s", + temp_table_name, TABLE_NAME + )); + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + } + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/db/ShowsProvider.java b/app/src/main/java/com/redcoracle/episodes/db/ShowsProvider.java new file mode 100644 index 00000000..84968b12 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/db/ShowsProvider.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2012 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes.db; + +import android.content.ContentProvider; +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteConstraintException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; +import android.net.Uri; +import android.util.Log; + +import androidx.annotation.NonNull; + +import com.redcoracle.episodes.BuildConfig; + +import java.util.ArrayList; +import java.util.Iterator; + +public class ShowsProvider extends ContentProvider { + private static final String TAG = "ShowsProvider"; + public static final String URI_AUTHORITY = BuildConfig.APPLICATION_ID + ".db.ShowsProvider"; + + private static final Uri CONTENT_URI_BASE = + Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + ShowsProvider.URI_AUTHORITY); + + public static final Uri CONTENT_URI_SHOWS = + Uri.parse(ContentResolver.SCHEME_CONTENT + + "://" + ShowsProvider.URI_AUTHORITY + + "/" + ShowsTable.TABLE_NAME + ); + + public static final Uri CONTENT_URI_EPISODES = + Uri.parse(ContentResolver.SCHEME_CONTENT + + "://" + ShowsProvider.URI_AUTHORITY + + "/" + EpisodesTable.TABLE_NAME + ); + + public static final String CONTENT_TYPE_SHOW_DIR = ContentResolver.CURSOR_DIR_BASE_TYPE + "/show"; + public static final String CONTENT_TYPE_SHOW_ITEM = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/show"; + public static final String CONTENT_TYPE_EPISODE_DIR = ContentResolver.CURSOR_DIR_BASE_TYPE + "/episode"; + public static final String CONTENT_TYPE_EPISODE_ITEM = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/episode"; + + private static final int URI_TYPE_SHOWS = 1; + private static final int URI_TYPE_SHOWS_ID = 2; + private static final int URI_TYPE_EPISODES = 3; + private static final int URI_TYPE_EPISODES_ID = 4; + + private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); + static { + uriMatcher.addURI(URI_AUTHORITY, ShowsTable.TABLE_NAME, URI_TYPE_SHOWS); + uriMatcher.addURI(URI_AUTHORITY, ShowsTable.TABLE_NAME + "/#", URI_TYPE_SHOWS_ID); + uriMatcher.addURI(URI_AUTHORITY, EpisodesTable.TABLE_NAME, URI_TYPE_EPISODES); + uriMatcher.addURI(URI_AUTHORITY, EpisodesTable.TABLE_NAME + "/#", URI_TYPE_EPISODES_ID); + } + + private DatabaseOpenHelper databaseOpenHelper; + + @Override + public Cursor query(@NonNull Uri uri, + String[] projection, + String selection, + String[] selectionArgs, + String sortOrder) { + String table; + String sel; + + switch (uriMatcher.match(uri)) { + case URI_TYPE_SHOWS: + table = ShowsTable.TABLE_NAME; + sel = selection; + break; + + case URI_TYPE_SHOWS_ID: + table = ShowsTable.TABLE_NAME; + sel = String.format("%s=%s", ShowsTable.COLUMN_ID, uri.getLastPathSegment()); + if (selection != null) { + sel += " AND " + selection; + } + break; + + case URI_TYPE_EPISODES: + table = EpisodesTable.TABLE_NAME; + sel = selection; + break; + + case URI_TYPE_EPISODES_ID: + table = EpisodesTable.TABLE_NAME; + sel = String.format("%s=%s", EpisodesTable.COLUMN_ID, uri.getLastPathSegment()); + if (selection != null) { + sel += " AND " + selection; + } + break; + + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + SQLiteDatabase db = databaseOpenHelper.getReadableDatabase(); + Cursor cursor = db.query(table, projection, sel, selectionArgs, null, null, sortOrder); + cursor.setNotificationUri(getContext().getContentResolver(), uri); + + return cursor; + } + + @Override + public Uri insert(@NonNull Uri uri, ContentValues values) { + String table; + Uri contentUri; + if (uriMatcher.match(uri) == URI_TYPE_SHOWS) { + table = ShowsTable.TABLE_NAME; + contentUri = CONTENT_URI_SHOWS; + } else if (uriMatcher.match(uri) == URI_TYPE_EPISODES) { + table = EpisodesTable.TABLE_NAME; + contentUri = CONTENT_URI_EPISODES; + } else { + throw new IllegalArgumentException("Unknown URI " + uri); + } + + SQLiteDatabase db = databaseOpenHelper.getWritableDatabase(); + try { + long rowId = db.insertOrThrow(table, null, values); + Log.i(TAG, String.format("succesfully inserted row. id: %d", rowId)); + Uri rowUri = ContentUris.withAppendedId(contentUri, rowId); + getContext().getContentResolver().notifyChange(rowUri, null); + return rowUri; + } catch (SQLiteConstraintException e) { + Log.i(TAG, String.format("constraint error inserting row: %s", e.toString())); + return null; + } + } + + @Override + public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { + String table; + String sel; + + switch (uriMatcher.match(uri)) { + case URI_TYPE_SHOWS: + table = ShowsTable.TABLE_NAME; + sel = selection; + break; + + case URI_TYPE_SHOWS_ID: + table = ShowsTable.TABLE_NAME; + sel = String.format("%s=%s", ShowsTable.COLUMN_ID, uri.getLastPathSegment()); + if (selection != null) { + sel += " AND " + selection; + } + break; + + case URI_TYPE_EPISODES: + table = EpisodesTable.TABLE_NAME; + sel = selection; + break; + + case URI_TYPE_EPISODES_ID: + table = EpisodesTable.TABLE_NAME; + sel = String.format("%s=%s", EpisodesTable.COLUMN_ID, uri.getLastPathSegment()); + if (selection != null) { + sel += " AND " + selection; + } + break; + + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + SQLiteDatabase db = databaseOpenHelper.getWritableDatabase(); + int count = db.delete(table, sel, selectionArgs); + + getContext().getContentResolver().notifyChange(uri, null); + + return count; + } + + @Override + public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) { + String table; + String sel; + + switch (uriMatcher.match(uri)) { + case URI_TYPE_SHOWS: + table = ShowsTable.TABLE_NAME; + sel = selection; + break; + + case URI_TYPE_SHOWS_ID: + table = ShowsTable.TABLE_NAME; + sel = String.format("%s=%s", ShowsTable.COLUMN_ID, uri.getLastPathSegment()); + if (selection != null) { + sel += " AND " + selection; + } + break; + + case URI_TYPE_EPISODES: + table = EpisodesTable.TABLE_NAME; + sel = selection; + break; + + case URI_TYPE_EPISODES_ID: + table = EpisodesTable.TABLE_NAME; + sel = String.format("%s=%s", EpisodesTable.COLUMN_ID, uri.getLastPathSegment()); + if (selection != null) { + sel += " AND " + selection; + } + break; + + default: + throw new IllegalArgumentException("Unknown URI " + uri); + } + + SQLiteDatabase db = databaseOpenHelper.getWritableDatabase(); + int count = db.update(table, values, sel, selectionArgs); + + getContext().getContentResolver().notifyChange(uri, null); + + return count; + } + + @Override + public int bulkInsert(Uri uri, ContentValues[] values) { + // Updating episodes is by far the most time consuming operation so this + // function only optimises that. Moving to an ORM like Room would be ideal, + // but for now this improved syncing a library with ~500 shows/26000 episodes + // from 40+ minutes to less than 10. + final SQLiteDatabase db = databaseOpenHelper.getWritableDatabase(); + int inserted; + if (uriMatcher.match(uri) == URI_TYPE_EPISODES) { + db.beginTransaction(); + try { + for (ContentValues v : values) { + StringBuilder updateBinds = new StringBuilder(); + ArrayList bindKeys = new ArrayList<>(); + for (Iterator iterator = v.keySet().iterator(); iterator.hasNext(); ) { + String key = iterator.next(); + String bindArg = v.getAsString(key); + // Filter out nulls + if (bindArg != null && !key.equals("_id")) { + updateBinds.append(String.format("%s=?", key)); + bindKeys.add(key); + if (iterator.hasNext()) { + updateBinds.append(","); + } + } + } + + SQLiteStatement insert = db.compileStatement(String.format( + "UPDATE %s SET %s WHERE _id=%s", + EpisodesTable.TABLE_NAME, + updateBinds, + v.getAsString("_id") + )); + for (int i = 0; i < bindKeys.size(); i++) { + insert.bindString(i + 1, v.getAsString(bindKeys.get(i))); + } + insert.execute(); + } + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + inserted = values.length; + } + } else { + throw new IllegalArgumentException("Unknown URI " + uri); + } + getContext().getContentResolver().notifyChange(uri, null); + return inserted; + } + + @Override + public String getType(@NonNull Uri uri) { + switch (uriMatcher.match(uri)) { + case URI_TYPE_SHOWS: + return CONTENT_TYPE_SHOW_DIR; + + case URI_TYPE_SHOWS_ID: + return CONTENT_TYPE_SHOW_ITEM; + + case URI_TYPE_EPISODES: + return CONTENT_TYPE_EPISODE_DIR; + + case URI_TYPE_EPISODES_ID: + return CONTENT_TYPE_EPISODE_ITEM; + + default: + return null; + } + } + + @Override + public boolean onCreate() { + databaseOpenHelper = new DatabaseOpenHelper(getContext()); + return true; + } + + public static void reloadDatabase(Context context) { + final ContentResolver resolver = context.getContentResolver(); + final ContentProviderClient client = resolver.acquireContentProviderClient(URI_AUTHORITY); + final ShowsProvider provider = (ShowsProvider)client.getLocalContentProvider(); + + provider.databaseOpenHelper.close(); + provider.databaseOpenHelper = new DatabaseOpenHelper(provider.getContext()); + + resolver.notifyChange(CONTENT_URI_BASE, null); + + client.release(); + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/db/ShowsTable.java b/app/src/main/java/com/redcoracle/episodes/db/ShowsTable.java new file mode 100644 index 00000000..b0dae6da --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/db/ShowsTable.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2012-2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes.db; + +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.provider.BaseColumns; +import android.util.Log; + +public class ShowsTable { + private static final String TAG = "ShowsTable"; + + public static final String TABLE_NAME = "shows"; + + public static final String COLUMN_ID = BaseColumns._ID; + public static final String COLUMN_TVDB_ID = "tvdb_id"; + public static final String COLUMN_TMDB_ID = "tmdb_id"; + public static final String COLUMN_IMDB_ID = "imdb_id"; + public static final String COLUMN_NAME = "name"; + public static final String COLUMN_LANGUAGE = "language"; + public static final String COLUMN_OVERVIEW = "overview"; + public static final String COLUMN_FIRST_AIRED = "first_aired"; + public static final String COLUMN_STARRED = "starred"; + public static final String COLUMN_ARCHIVED = "archived"; + public static final String COLUMN_BANNER_PATH = "banner_path"; + public static final String COLUMN_FANART_PATH = "fanart_path"; + public static final String COLUMN_POSTER_PATH = "poster_path"; + public static final String COLUMN_NOTES = "notes"; + + public static final String COLUMN_TYPE_ID = "INTEGER PRIMARY KEY"; + public static final String COLUMN_TYPE_TVDB_ID = "INTEGER UNIQUE"; + public static final String COLUMN_TYPE_TMDB_ID = "INTEGER UNIQUE"; + public static final String COLUMN_TYPE_IMDB_ID = "STRING UNIQUE"; + public static final String COLUMN_TYPE_NAME = "TEXT NOT NULL"; + public static final String COLUMN_TYPE_LANGUAGE = "TEXT"; + public static final String COLUMN_TYPE_OVERVIEW = "TEXT"; + public static final String COLUMN_TYPE_FIRST_AIRED = "DATE"; + public static final String COLUMN_TYPE_STARRED = "BOOLEAN DEFAULT 0"; + public static final String COLUMN_TYPE_ARCHIVED = "BOOLEAN DEFAULT 0"; + public static final String COLUMN_TYPE_BANNER_PATH = "TEXT"; + public static final String COLUMN_TYPE_FANART_PATH = "TEXT"; + public static final String COLUMN_TYPE_POSTER_PATH = "TEXT"; + public static final String COLUMN_TYPE_NOTES = "TEXT"; + + public static String createTableSQL(String table_name) { + return String.format( + "CREATE TABLE %s (" + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s," + + " %s %s" + + ");", + table_name, + COLUMN_ID, COLUMN_TYPE_ID, + COLUMN_TVDB_ID, COLUMN_TYPE_TVDB_ID, + COLUMN_TMDB_ID, COLUMN_TYPE_TMDB_ID, + COLUMN_IMDB_ID, COLUMN_TYPE_IMDB_ID, + COLUMN_NAME, COLUMN_TYPE_NAME, + COLUMN_LANGUAGE, COLUMN_TYPE_LANGUAGE, + COLUMN_OVERVIEW, COLUMN_TYPE_OVERVIEW, + COLUMN_FIRST_AIRED, COLUMN_TYPE_FIRST_AIRED, + COLUMN_STARRED, COLUMN_TYPE_STARRED, + COLUMN_ARCHIVED, COLUMN_TYPE_ARCHIVED, + COLUMN_BANNER_PATH, COLUMN_TYPE_BANNER_PATH, + COLUMN_FANART_PATH, COLUMN_TYPE_FANART_PATH, + COLUMN_POSTER_PATH, COLUMN_TYPE_POSTER_PATH, + COLUMN_NOTES, COLUMN_TYPE_NOTES + ); + } + + public static void onCreate(SQLiteDatabase db) { + String create = createTableSQL(TABLE_NAME); + Log.d(TAG, String.format("creating shows table: %s", create)); + db.execSQL(create); + } + + public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (oldVersion < 2) { + // Add starred column + Log.d(TAG, "upgrading shows table: adding starred column"); + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", + TABLE_NAME, + COLUMN_STARRED, + COLUMN_TYPE_STARRED)); + } + + if (oldVersion < 3) { + // Add banner path column + Log.d(TAG, "upgrading shows table: adding banner path column"); + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", + TABLE_NAME, + COLUMN_BANNER_PATH, + COLUMN_TYPE_BANNER_PATH)); + } + + if (oldVersion < 4) { + // Add fanart path and poster path columns + Log.d(TAG, "upgrading shows table: adding fanart path column"); + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", + TABLE_NAME, + COLUMN_FANART_PATH, + COLUMN_TYPE_FANART_PATH)); + + Log.d(TAG, "upgrading shows table: adding poster path column"); + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", + TABLE_NAME, + COLUMN_POSTER_PATH, + COLUMN_TYPE_POSTER_PATH)); + } + + if (oldVersion < 5) { + // Add notes column + Log.d(TAG, "upgrading shows table: adding notes column"); + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", + TABLE_NAME, + COLUMN_NOTES, + COLUMN_TYPE_NOTES)); + } + + if (oldVersion < 6) { + // Add language column + Log.d(TAG, "upgrading shows table: adding language column"); + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", + TABLE_NAME, + COLUMN_LANGUAGE, + COLUMN_TYPE_LANGUAGE)); + } + + if (oldVersion < 7) { + // Add archived column + Log.d(TAG, "upgrading shows table: adding archived column"); + db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", + TABLE_NAME, + COLUMN_ARCHIVED, + COLUMN_TYPE_ARCHIVED)); + } + + if (oldVersion < 9) { + // Add TMDB/IMDB columns + db.beginTransaction(); + try { + final String temp_table_name = String.format("new_%s", TABLE_NAME); + + String create_table = createTableSQL(temp_table_name); + String insert_columns = String.format( + "%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s", + COLUMN_ID, COLUMN_TVDB_ID, COLUMN_NAME, COLUMN_LANGUAGE, COLUMN_OVERVIEW, + COLUMN_FIRST_AIRED, COLUMN_STARRED, COLUMN_ARCHIVED, COLUMN_BANNER_PATH, + COLUMN_FANART_PATH, COLUMN_POSTER_PATH, COLUMN_NOTES + ); + + db.execSQL(create_table); + db.execSQL(String.format( + "INSERT INTO %s (%s) SELECT %s FROM %s", + temp_table_name, insert_columns, insert_columns, TABLE_NAME + )); + + db.execSQL(String.format("DROP TABLE %s", TABLE_NAME)); + db.execSQL(String.format( + "ALTER TABLE %s RENAME TO %s", + temp_table_name, TABLE_NAME + )); + + final Cursor fk_query = db.rawQuery("PRAGMA foreign_key_check", null); + final int fk_check = fk_query.getCount(); + fk_query.close(); + if (fk_check == 0) { + db.setTransactionSuccessful(); + } + } finally { + db.endTransaction(); + } + } + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/services/AddShowTask.java b/app/src/main/java/com/redcoracle/episodes/services/AddShowTask.java new file mode 100644 index 00000000..e8e27d89 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/services/AddShowTask.java @@ -0,0 +1,135 @@ +package com.redcoracle.episodes.services; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import android.widget.Toast; + +import com.redcoracle.episodes.EpisodesApplication; +import com.redcoracle.episodes.R; +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.db.ShowsTable; +import com.redcoracle.episodes.tvdb.Client; +import com.redcoracle.episodes.tvdb.Episode; +import com.redcoracle.episodes.tvdb.Show; + +import java.util.LinkedList; +import java.util.concurrent.Callable; + +public class AddShowTask implements Callable { + private static final String TAG = "AddShowTask"; + private final int tmdbId; + private final String showName; + private final String showLanguage; + private final Context context; + + public AddShowTask(int tmdbId, String showName, String showLanguage) { + this.tmdbId = tmdbId; + this.showName = showName; + this.showLanguage = showLanguage; + this.context = EpisodesApplication.getInstance().getApplicationContext(); + } + + @Override + public Void call() { + final Client tmdbClient = new Client(); + Show show = tmdbClient.getShow(this.tmdbId, this.showLanguage, false); + + if (!checkAlreadyAdded(show)) { + this.showMessage(this.context.getString(R.string.adding_show, showName)); + show = tmdbClient.getShow(this.tmdbId, this.showLanguage, true); + final int showId = insertShow(show); + this.insertEpisodes(show.getEpisodes().toArray(new Episode[0]), showId); + showMessage(this.context.getString(R.string.show_added, showName)); + } else { + showMessage(this.context.getString(R.string.show_already_added, showName)); + } + return null; + } + + private boolean checkAlreadyAdded(Show show) { + final String[] projection = {}; + String selection = String.format("%s=?", ShowsTable.COLUMN_TMDB_ID); + LinkedList selectionArgs = new LinkedList<>(); + selectionArgs.add(Integer.valueOf(show.getTmdbId()).toString()); + + if (show.getTvdbId() > 0) { + selection += String.format(" OR %s=?", ShowsTable.COLUMN_TVDB_ID); + selectionArgs.add(Integer.valueOf(show.getTvdbId()).toString()); + } + if (show.getImdbId() != null && !show.getImdbId().equals("")) { + selection += String.format(" OR %s=?", ShowsTable.COLUMN_IMDB_ID); + selectionArgs.add(show.getImdbId()); + } + final ContentResolver resolver = this.context.getContentResolver(); + final Cursor cursor = resolver.query( + ShowsProvider.CONTENT_URI_SHOWS, + projection, + selection, + selectionArgs.toArray(new String[0]), + null + ); + final boolean existing = cursor.moveToFirst(); + cursor.close(); + return existing; + } + + private int insertShow(Show show){ + final ContentValues showValues = new ContentValues(); + if (show.getTvdbId() != 0) { + showValues.put(ShowsTable.COLUMN_TVDB_ID, show.getTvdbId()); + } + showValues.put(ShowsTable.COLUMN_TMDB_ID, show.getTmdbId()); + showValues.put(ShowsTable.COLUMN_IMDB_ID, show.getImdbId()); + showValues.put(ShowsTable.COLUMN_NAME, show.getName()); + showValues.put(ShowsTable.COLUMN_LANGUAGE, show.getLanguage()); + showValues.put(ShowsTable.COLUMN_OVERVIEW, show.getOverview()); + if (show.getFirstAired() != null) { + showValues.put(ShowsTable.COLUMN_FIRST_AIRED, show.getFirstAired().getTime() / 1000); + } + showValues.put(ShowsTable.COLUMN_BANNER_PATH, show.getBannerPath()); + showValues.put(ShowsTable.COLUMN_FANART_PATH, show.getFanartPath()); + showValues.put(ShowsTable.COLUMN_POSTER_PATH, show.getPosterPath()); + + final Uri showUri = this.context.getContentResolver().insert(ShowsProvider.CONTENT_URI_SHOWS, showValues); + final int showId = Integer.parseInt(showUri.getLastPathSegment()); + Log.i(TAG, String.format("show %s successfully added to database as row %d. adding episodes", show.getName(), showId)); + return showId; + } + + private void insertEpisodes(Episode[] episodes, int showId) { + final ContentValues[] values = new ContentValues[episodes.length]; + + for (int i = 0; i < episodes.length; i++) { + ContentValues value = new ContentValues(); + value.put(EpisodesTable.COLUMN_TVDB_ID, episodes[i].getTvdbId()); + value.put(EpisodesTable.COLUMN_TMDB_ID, episodes[i].getTmdbId()); + value.put(EpisodesTable.COLUMN_IMDB_ID, episodes[i].getImdbId()); + value.put(EpisodesTable.COLUMN_SHOW_ID, showId); + value.put(EpisodesTable.COLUMN_NAME, episodes[i].getName()); + value.put(EpisodesTable.COLUMN_LANGUAGE, episodes[i].getLanguage()); + value.put(EpisodesTable.COLUMN_OVERVIEW, episodes[i].getOverview()); + value.put(EpisodesTable.COLUMN_EPISODE_NUMBER, episodes[i].getEpisodeNumber()); + value.put(EpisodesTable.COLUMN_SEASON_NUMBER, episodes[i].getSeasonNumber()); + if (episodes[i].getFirstAired() != null) { + value.put(EpisodesTable.COLUMN_FIRST_AIRED, episodes[i].getFirstAired().getTime() / 1000); + } + values[i] = value; + } + + for (ContentValues value : values) { + this.context.getContentResolver().insert(ShowsProvider.CONTENT_URI_EPISODES, value); + } + } + + private void showMessage(String message) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> Toast.makeText(context, message, Toast.LENGTH_SHORT).show()); + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/services/AsyncTask.java b/app/src/main/java/com/redcoracle/episodes/services/AsyncTask.java new file mode 100644 index 00000000..ca898bbb --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/services/AsyncTask.java @@ -0,0 +1,19 @@ +package com.redcoracle.episodes.services; + +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class AsyncTask { + private final Executor executor = Executors.newSingleThreadExecutor(); + + public void executeAsync(Callable callable) { + executor.execute(() -> { + try { + callable.call(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/services/BackupTask.java b/app/src/main/java/com/redcoracle/episodes/services/BackupTask.java new file mode 100644 index 00000000..d31b379f --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/services/BackupTask.java @@ -0,0 +1,68 @@ +package com.redcoracle.episodes.services; + +import android.content.Context; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; + +import androidx.core.content.ContextCompat; + +import com.redcoracle.episodes.EpisodesApplication; +import com.redcoracle.episodes.R; +import com.redcoracle.episodes.db.DatabaseOpenHelper; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.concurrent.Callable; + +public class BackupTask implements Callable { + private final static String TAG = BackupTask.class.getName(); + private final Context context; + private final String destinationFileName; + + public BackupTask(String destinationFileName) { + this.destinationFileName = destinationFileName; + this.context = EpisodesApplication.getInstance().getApplicationContext(); + } + + public Void call() { + Log.i(TAG, "Backing up library."); + if (!isExternalStorageReadable()) { + Log.i(TAG, "Storage is not readable."); + return null; + } + final File databaseFile = this.context.getDatabasePath(DatabaseOpenHelper.getDbName()); + final File destinationDirectory = new File(this.context.getExternalFilesDir(null), "episodes"); + if (!destinationDirectory.mkdirs()) { + Log.e(TAG, String.format("Error creating backup directory '%s'.", destinationDirectory.getPath())); + } + final File destinationFile = new File(destinationDirectory, this.destinationFileName); + try { + FileChannel src = new FileInputStream(databaseFile).getChannel(); + FileChannel dest = new FileOutputStream(destinationFile).getChannel(); + dest.transferFrom(src, 0, src.size()); + ContextCompat.getMainExecutor(this.context).execute(() -> Toast.makeText( + this.context, + String.format(this.context.getString(R.string.back_up_success_message), this.destinationFileName), + Toast.LENGTH_LONG + ).show()); + Log.i(TAG, String.format("Library backed up successfully: '%s'.", destinationFile.getPath())); + src.close(); + dest.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + private boolean isExternalStorageReadable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state); + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/services/DeleteShowTask.java b/app/src/main/java/com/redcoracle/episodes/services/DeleteShowTask.java new file mode 100644 index 00000000..248e3d2d --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/services/DeleteShowTask.java @@ -0,0 +1,34 @@ +package com.redcoracle.episodes.services; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.util.Log; + +import com.redcoracle.episodes.EpisodesApplication; +import com.redcoracle.episodes.db.EpisodesTable; +import com.redcoracle.episodes.db.ShowsProvider; + +import java.util.concurrent.Callable; + +public class DeleteShowTask implements Callable { + private static final String TAG = DeleteShowTask.class.getName(); + private int showId; + private Context context; + + public DeleteShowTask(int showId) { + this.showId = showId; + this.context = EpisodesApplication.getInstance().getApplicationContext(); + } + + @Override + public Void call() { + final ContentResolver resolver = this.context.getContentResolver(); + final String selection = String.format("%s=?", EpisodesTable.COLUMN_SHOW_ID); + final String[] selectionArgs = {String.valueOf(this.showId)}; + int episodes = resolver.delete(ShowsProvider.CONTENT_URI_EPISODES, selection, selectionArgs); + Log.d(TAG, String.format("Deleted %s episodes", episodes)); + resolver.delete(Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, String.valueOf(showId)), null, null); + return null; + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/services/RefreshAllShowsTask.java b/app/src/main/java/com/redcoracle/episodes/services/RefreshAllShowsTask.java new file mode 100644 index 00000000..4c17a21b --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/services/RefreshAllShowsTask.java @@ -0,0 +1,63 @@ +package com.redcoracle.episodes.services; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationManagerCompat; + +import com.redcoracle.episodes.EpisodesApplication; +import com.redcoracle.episodes.R; +import com.redcoracle.episodes.db.ShowsProvider; +import com.redcoracle.episodes.db.ShowsTable; + +import java.util.concurrent.Callable; + +import static com.redcoracle.episodes.RefreshShowUtil.refreshShow; + +public class RefreshAllShowsTask implements Callable { + @Override + public Void call() { + Context context = EpisodesApplication.getInstance().getApplicationContext(); + ContentResolver resolver = context.getContentResolver(); + final Uri showUri = ShowsProvider.CONTENT_URI_SHOWS; + final String[] projection = { + ShowsTable.COLUMN_ID, + ShowsTable.COLUMN_NAME + }; + final String sort = ShowsTable.COLUMN_NAME + " ASC"; + final Cursor cursor = resolver.query(showUri, projection, null, null, sort); + final int idColumnIndex = cursor.getColumnIndex(ShowsTable.COLUMN_ID); + final int nameColumnIndex = cursor.getColumnIndex(ShowsTable.COLUMN_NAME); + final int total = cursor.getCount(); + + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, "episodes_channel_id"); + notificationBuilder + .setContentTitle("Refreshing Shows") + .setSmallIcon(R.drawable.ic_show_starred) + .setPriority(NotificationCompat.PRIORITY_DEFAULT); + int current = 0; + notificationBuilder.setProgress(total, current, false); + notificationManager.notify(0, notificationBuilder.build()); + + int showId; + String showName; + cursor.moveToFirst(); + do { + showId = cursor.getInt(idColumnIndex); + showName = cursor.getString(nameColumnIndex); + notificationBuilder.setContentText(showName); + notificationBuilder.setProgress(total, current, false); + notificationManager.notify(0, notificationBuilder.build()); + refreshShow(showId, resolver); + current += 1; + } while (cursor.moveToNext()); + cursor.close(); + notificationBuilder.setContentText("Refresh complete!").setProgress(0, 0, false); + notificationManager.notify(0, notificationBuilder.build()); + return null; + } +} diff --git a/app/src/main/java/org/jamienicol/episodes/services/RefreshShowService.java b/app/src/main/java/com/redcoracle/episodes/services/RefreshShowService.java similarity index 92% rename from app/src/main/java/org/jamienicol/episodes/services/RefreshShowService.java rename to app/src/main/java/com/redcoracle/episodes/services/RefreshShowService.java index 6f6a0ac0..da30fe95 100644 --- a/app/src/main/java/org/jamienicol/episodes/services/RefreshShowService.java +++ b/app/src/main/java/com/redcoracle/episodes/services/RefreshShowService.java @@ -15,11 +15,12 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes.services; +package com.redcoracle.episodes.services; import android.app.IntentService; import android.content.Intent; -import org.jamienicol.episodes.RefreshShowUtil; + +import com.redcoracle.episodes.RefreshShowUtil; public class RefreshShowService extends IntentService { diff --git a/app/src/main/java/com/redcoracle/episodes/services/RefreshShowTask.java b/app/src/main/java/com/redcoracle/episodes/services/RefreshShowTask.java new file mode 100644 index 00000000..72a52c2f --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/services/RefreshShowTask.java @@ -0,0 +1,25 @@ +package com.redcoracle.episodes.services; + +import android.content.Context; + +import com.redcoracle.episodes.EpisodesApplication; +import com.redcoracle.episodes.RefreshShowUtil; + +import java.util.concurrent.Callable; + +public class RefreshShowTask implements Callable { + private static final String TAG = DeleteShowTask.class.getName(); + private int showId; + private Context context; + + public RefreshShowTask(int showId) { + this.showId = showId; + this.context = EpisodesApplication.getInstance().getApplicationContext(); + } + + @Override + public Void call() { + RefreshShowUtil.refreshShow(showId, this.context.getContentResolver()); + return null; + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/services/RestoreTask.java b/app/src/main/java/com/redcoracle/episodes/services/RestoreTask.java new file mode 100644 index 00000000..c61c3417 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/services/RestoreTask.java @@ -0,0 +1,63 @@ +package com.redcoracle.episodes.services; + +import android.content.Context; +import android.os.Environment; +import android.util.Log; +import android.widget.Toast; + +import androidx.core.content.ContextCompat; + +import com.bumptech.glide.Glide; +import com.redcoracle.episodes.EpisodesApplication; +import com.redcoracle.episodes.R; +import com.redcoracle.episodes.db.DatabaseOpenHelper; +import com.redcoracle.episodes.db.ShowsProvider; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.concurrent.Callable; + +public class RestoreTask implements Callable { + private final static String TAG = RestoreTask.class.getName(); + private final Context context; + private final String filename; + + + public RestoreTask(String filename) { + this.context = EpisodesApplication.getInstance().getApplicationContext(); + this.filename = filename; + } + + public Void call() { + if (!isExternalStorageWritable()) { + return null; + } + final File backupFile = new File(this.filename); + final File databaseFile = this.context.getDatabasePath(DatabaseOpenHelper.getDbName()); + try { + FileChannel src = new FileInputStream(backupFile).getChannel(); + FileChannel dest = new FileOutputStream(databaseFile).getChannel(); + dest.transferFrom(src, 0, src.size()); + Glide.get(this.context).clearDiskCache(); + ContextCompat.getMainExecutor(this.context).execute(() -> Toast.makeText( + this.context, + this.context.getString(R.string.restore_success_message), + Toast.LENGTH_LONG + ).show()); + Log.i(TAG, "Library restored successfully."); + } catch (IOException e) { + Log.e(TAG, String.format("Error restoring library: %s", e.toString())); + } finally { + ShowsProvider.reloadDatabase(this.context); + } + return null; + } + + private boolean isExternalStorageWritable() { + String state = Environment.getExternalStorageState(); + return Environment.MEDIA_MOUNTED.equals(state); + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/tvdb/Client.java b/app/src/main/java/com/redcoracle/episodes/tvdb/Client.java new file mode 100644 index 00000000..0c146313 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/tvdb/Client.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2012-2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes.tvdb; + +import android.util.Log; + +import com.redcoracle.episodes.EpisodesApplication; +import com.uwetrottmann.tmdb2.Tmdb; +import com.uwetrottmann.tmdb2.entities.AppendToResponse; +import com.uwetrottmann.tmdb2.entities.BaseTvShow; +import com.uwetrottmann.tmdb2.entities.FindResults; +import com.uwetrottmann.tmdb2.entities.TvSeason; +import com.uwetrottmann.tmdb2.entities.TvShow; +import com.uwetrottmann.tmdb2.entities.TvShowResultsPage; +import com.uwetrottmann.tmdb2.enumerations.AppendToResponseItem; +import com.uwetrottmann.tmdb2.enumerations.ExternalSource; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +import retrofit2.Response; + +public class Client { + private static final String TAG = Client.class.getName(); + private final Tmdb tmdb; + + public Client() { + this.tmdb = EpisodesApplication.getInstance().getTmdbClient(); + } + + public List searchShows(String query, String language) { + this.tmdb.searchService().tv(query, null, language, null, false); + + try { + final TvShowResultsPage results = this.tmdb + .searchService() + .tv(query, null, language, null, false) + .execute() + .body(); + if (results != null) { + final SearchShowsParser parser = new SearchShowsParser(); + return parser.parse(results, language); + } else { + return new LinkedList<>(); + } + } catch (IOException e) { + Log.w(TAG, e); + return new LinkedList<>(); + } + } + + public Show getShow(HashMap showIds, String language) { + Show show = null; + try { + TvShow lookupResult = null; + AppendToResponse includes = new AppendToResponse(AppendToResponseItem.EXTERNAL_IDS); + + if (showIds.get("tmdbId") != null) { + int tmdbId = Integer.parseInt(showIds.get("tmdbId")); + Response seriesResponse = this.tmdb.tvService().tv(tmdbId, language, includes).execute(); + if (seriesResponse.isSuccessful() && seriesResponse.body() != null) { + lookupResult = seriesResponse.body(); + } + } + + if (lookupResult == null && showIds.get("tvdbId") != null) { + Response seriesResponse = this.tmdb.findService().find( + showIds.get("tvdbId"), + ExternalSource.TVDB_ID, + language + ).execute(); + if (seriesResponse.isSuccessful() && seriesResponse.body() != null) { + if (seriesResponse.body().tv_results != null && seriesResponse.body().tv_results.size() > 0) { + BaseTvShow sparseShow = seriesResponse.body().tv_results.get(0); + lookupResult = tmdb.tvService().tv(sparseShow.id, language, includes).execute().body(); + } + } + } + + if (lookupResult == null && showIds.get("imbId") != null) { + Response seriesResponse = this.tmdb.findService().find( + showIds.get("imbId"), + ExternalSource.IMDB_ID, + language + ).execute(); + if (seriesResponse.isSuccessful() && seriesResponse.body() != null) { + if (seriesResponse.body().tv_results != null && seriesResponse.body().tv_results.size() > 0) { + BaseTvShow sparseShow = seriesResponse.body().tv_results.get(0); + lookupResult = tmdb.tvService().tv(sparseShow.id, language, includes).execute().body(); + } + } + } + + if (lookupResult != null) { + final GetShowParser parser = new GetShowParser(); + show = parser.parse(lookupResult, language); + show.setEpisodes(getEpisodesForShow(lookupResult, language)); + } + + } catch (IOException e) { + Log.w(TAG, e); + } + return show; + } + + public Show getShow(int id, String language, boolean includeEpisodes) { + try { + AppendToResponse includes = new AppendToResponse(AppendToResponseItem.EXTERNAL_IDS); + Response seriesResponse = this.tmdb.tvService().tv(id, language, includes).execute(); + Log.d(TAG, String.format("Received response %d: %s", seriesResponse.code(), seriesResponse.message())); + if (seriesResponse.isSuccessful() && seriesResponse.body() != null) { + final GetShowParser parser = new GetShowParser(); + final TvShow series = seriesResponse.body(); + Show show = parser.parse(series, language); + + if (show != null && includeEpisodes) { + ArrayList episodes = getEpisodesForShow(series, language); + show.setEpisodes(episodes); + } + return show; + } else { + return null; + } + } catch (IOException e) { + Log.w(TAG, e); + return null; + } + } + + public ArrayList getEpisodesForShow(TvShow series, String language) { + int episode_count = series.number_of_episodes != null ? series.number_of_episodes : 64; + ArrayList episodes = new ArrayList<>(episode_count); + final GetEpisodesParser episodesParser = new GetEpisodesParser(); + if (series.number_of_seasons != null) { + for (TvSeason season : series.seasons) { + try { + AppendToResponse includes = new AppendToResponse(AppendToResponseItem.EXTERNAL_IDS); + season = this.tmdb.tvSeasonsService().season(series.id, season.season_number, language, includes).execute().body(); + if (season != null) { + episodes.addAll(episodesParser.parse(season.episodes)); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return episodes; + } +} diff --git a/app/src/main/java/org/jamienicol/episodes/tvdb/Episode.java b/app/src/main/java/com/redcoracle/episodes/tvdb/Episode.java similarity index 63% rename from app/src/main/java/org/jamienicol/episodes/tvdb/Episode.java rename to app/src/main/java/com/redcoracle/episodes/tvdb/Episode.java index 9ed856c6..60d76f95 100644 --- a/app/src/main/java/org/jamienicol/episodes/tvdb/Episode.java +++ b/app/src/main/java/com/redcoracle/episodes/tvdb/Episode.java @@ -15,26 +15,27 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes.tvdb; +package com.redcoracle.episodes.tvdb; import java.util.Date; -public class Episode -{ +public class Episode { private int id; + private Integer tvdbId; + private Integer tmdbId; + private String imdbId; private String name; + private String language; private String overview; private int episodeNumber; private int seasonNumber; private Date firstAired; - public Episode() { - id = 0; - name = ""; - overview = ""; - episodeNumber = 0; - seasonNumber = 0; - firstAired = null; + Episode() { + } + + public String identifier() { + return String.format("%s-%s", this.seasonNumber, this.episodeNumber); } public int getId() { @@ -45,6 +46,30 @@ public void setId(int id) { this.id = id; } + public Integer getTvdbId() { + return this.tvdbId; + } + + public void setTvdbId(Integer id) { + this.tvdbId = id; + } + + public int getTmdbId() { + return this.tmdbId; + } + + public void setTmdbId(int id) { + this.tmdbId = id; + } + + public String getImdbId() { + return this.imdbId; + } + + public void setImdbId(String id) { + this.imdbId = id; + } + public String getName() { return name; } @@ -53,6 +78,14 @@ public void setName(String name) { this.name = name; } + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + public String getOverview() { return overview; } @@ -65,7 +98,7 @@ public int getEpisodeNumber() { return episodeNumber; } - public void setEpisodeNumber(int episodeNumber) { + void setEpisodeNumber(int episodeNumber) { this.episodeNumber = episodeNumber; } @@ -73,7 +106,7 @@ public int getSeasonNumber() { return seasonNumber; } - public void setSeasonNumber(int seasonNumber) { + void setSeasonNumber(int seasonNumber) { this.seasonNumber = seasonNumber; } @@ -81,7 +114,7 @@ public Date getFirstAired() { return firstAired; } - public void setFirstAired(Date firstAired) { + void setFirstAired(Date firstAired) { this.firstAired = firstAired; } } diff --git a/app/src/main/java/com/redcoracle/episodes/tvdb/GetEpisodesParser.java b/app/src/main/java/com/redcoracle/episodes/tvdb/GetEpisodesParser.java new file mode 100644 index 00000000..86797f5c --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/tvdb/GetEpisodesParser.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012-2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes.tvdb; + +import android.util.Log; + +import com.uwetrottmann.tmdb2.entities.TvEpisode; + +import java.util.ArrayList; +import java.util.List; + +class GetEpisodesParser { + private static final String TAG = GetEpisodesParser.class.getName(); + + ArrayList parse(List tmdbEpisodes) { + try { + ArrayList episodes = new ArrayList<>(tmdbEpisodes.size()); + for (TvEpisode episode : tmdbEpisodes) { + Episode e = new Episode(); + e.setId(episode.id); + e.setTmdbId(episode.id); + if (episode.external_ids != null) { + if (episode.external_ids.tvdb_id != null) { + e.setTvdbId(episode.external_ids.tvdb_id); + } + if (episode.external_ids.imdb_id != null) { + e.setImdbId(episode.external_ids.imdb_id); + } + } + e.setName(episode.name != null ? episode.name : ""); + e.setOverview(episode.overview); + e.setSeasonNumber(episode.season_number); + e.setEpisodeNumber(episode.episode_number); + e.setFirstAired(episode.air_date); + episodes.add(e); + } + return episodes; + } catch (Exception ex) { + Log.w(TAG, ex); + return null; + } + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/tvdb/GetShowParser.java b/app/src/main/java/com/redcoracle/episodes/tvdb/GetShowParser.java new file mode 100644 index 00000000..16875d33 --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/tvdb/GetShowParser.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012-2014 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes.tvdb; + +import android.util.Log; + +import com.uwetrottmann.tmdb2.entities.TvShow; + +class GetShowParser { + private static final String TAG = "GetShowParser"; + + Show parse(TvShow series, String language) { + Show show; + try { + show = new Show(); + if (series.id != null) { + show.setId(series.id); + show.setTmdbId(series.id); + } else { + Log.w(TAG, String.format("Show does not have an ID: %s", series.name)); + return null; + } + if (series.external_ids != null) { + if (series.external_ids.tvdb_id != null) { + show.setTvdbId(series.external_ids.tvdb_id); + } + if (series.external_ids.imdb_id != null) { + show.setImdbId(series.external_ids.imdb_id); + } + } + show.setName(series.name); + show.setLanguage(language); + show.setOverview(series.overview); + show.setFirstAired(series.first_air_date); + show.setBannerPath(series.backdrop_path); + show.setPosterPath(series.poster_path); + } catch (Exception e) { + Log.w(TAG, e); + return null; + } + return show; + } +} diff --git a/app/src/main/java/com/redcoracle/episodes/tvdb/SearchShowsParser.java b/app/src/main/java/com/redcoracle/episodes/tvdb/SearchShowsParser.java new file mode 100644 index 00000000..767bde3b --- /dev/null +++ b/app/src/main/java/com/redcoracle/episodes/tvdb/SearchShowsParser.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2012 Jamie Nicol + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.redcoracle.episodes.tvdb; + +import android.util.Log; + +import com.uwetrottmann.tmdb2.entities.BaseTvShow; +import com.uwetrottmann.tmdb2.entities.TvShowResultsPage; + +import java.util.LinkedList; +import java.util.List; + +class SearchShowsParser { + private static final String TAG = SearchShowsParser.class.getName(); + + private List parsed; + + List parse(TvShowResultsPage results, String language) { + try { + List series = results.results; + parsed = new LinkedList<>(); + for(BaseTvShow s : series) { + Show show = new Show(); + show.setId(s.id); + show.setTmdbId(s.id); + show.setName(s.name); + show.setLanguage(language); + show.setOverview(s.overview); + show.setFirstAired(s.first_air_date); + parsed.add(show); + } + } catch (Exception e) { + Log.w(TAG, e); + } + return parsed; + } + +} diff --git a/app/src/main/java/org/jamienicol/episodes/tvdb/Show.java b/app/src/main/java/com/redcoracle/episodes/tvdb/Show.java similarity index 72% rename from app/src/main/java/org/jamienicol/episodes/tvdb/Show.java rename to app/src/main/java/com/redcoracle/episodes/tvdb/Show.java index afbafd86..9b29f8ce 100644 --- a/app/src/main/java/org/jamienicol/episodes/tvdb/Show.java +++ b/app/src/main/java/com/redcoracle/episodes/tvdb/Show.java @@ -15,15 +15,18 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes.tvdb; +package com.redcoracle.episodes.tvdb; import java.util.Date; import java.util.List; -public class Show -{ +public class Show { private int id; + private int tvdbId; + private int tmdbId; + private String imdbId; private String name; + private String language; private String overview; private Date firstAired; private String bannerPath; @@ -32,8 +35,8 @@ public class Show private List episodes; public Show() { - id = 0; name = ""; + language = ""; overview = ""; firstAired = null; bannerPath = ""; @@ -50,6 +53,30 @@ public void setId(int id) { this.id = id; } + public int getTvdbId() { + return tvdbId; + } + + public void setTvdbId(int tvdbId) { + this.tvdbId = tvdbId; + } + + public int getTmdbId() { + return this.tmdbId; + } + + public void setTmdbId(int tmdbId) { + this.tmdbId = tmdbId; + } + + public String getImdbId() { + return this.imdbId; + } + + public void setImdbId(String imdbId) { + this.imdbId = imdbId; + } + public String getName() { return name; } @@ -58,6 +85,14 @@ public void setName(String name) { this.name = name; } + public String getLanguage() { + return language; + } + + void setLanguage(String language) { + this.language = language; + } + public String getOverview() { return overview; } @@ -70,7 +105,7 @@ public Date getFirstAired() { return firstAired; } - public void setFirstAired(Date firstAired) { + void setFirstAired(Date firstAired) { this.firstAired = firstAired; } @@ -78,7 +113,7 @@ public String getBannerPath() { return bannerPath; } - public void setBannerPath(String bannerPath) { + void setBannerPath(String bannerPath) { this.bannerPath = bannerPath; } @@ -86,7 +121,7 @@ public String getFanartPath() { return fanartPath; } - public void setFanartPath(String fanartPath) { + void setFanartPath(String fanartPath) { this.fanartPath = fanartPath; } diff --git a/app/src/main/java/org/jamienicol/episodes/widget/ObservableScrollView.java b/app/src/main/java/com/redcoracle/episodes/widget/ObservableScrollView.java similarity index 97% rename from app/src/main/java/org/jamienicol/episodes/widget/ObservableScrollView.java rename to app/src/main/java/com/redcoracle/episodes/widget/ObservableScrollView.java index ae126497..5c303451 100644 --- a/app/src/main/java/org/jamienicol/episodes/widget/ObservableScrollView.java +++ b/app/src/main/java/com/redcoracle/episodes/widget/ObservableScrollView.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes.widget; +package com.redcoracle.episodes.widget; import android.content.Context; import android.util.AttributeSet; diff --git a/app/src/main/java/org/jamienicol/episodes/widget/WrapContentListView.java b/app/src/main/java/com/redcoracle/episodes/widget/WrapContentListView.java similarity index 97% rename from app/src/main/java/org/jamienicol/episodes/widget/WrapContentListView.java rename to app/src/main/java/com/redcoracle/episodes/widget/WrapContentListView.java index 03487af8..6b23bec4 100644 --- a/app/src/main/java/org/jamienicol/episodes/widget/WrapContentListView.java +++ b/app/src/main/java/com/redcoracle/episodes/widget/WrapContentListView.java @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes.widget; +package com.redcoracle.episodes.widget; import android.content.Context; import android.util.AttributeSet; diff --git a/app/src/main/java/org/jamienicol/episodes/widget/WrapContentViewPager.java b/app/src/main/java/com/redcoracle/episodes/widget/WrapContentViewPager.java similarity index 91% rename from app/src/main/java/org/jamienicol/episodes/widget/WrapContentViewPager.java rename to app/src/main/java/com/redcoracle/episodes/widget/WrapContentViewPager.java index 97bcdf6c..7e093ef3 100644 --- a/app/src/main/java/org/jamienicol/episodes/widget/WrapContentViewPager.java +++ b/app/src/main/java/com/redcoracle/episodes/widget/WrapContentViewPager.java @@ -15,15 +15,16 @@ * along with this program. If not, see . */ -package org.jamienicol.episodes.widget; +package com.redcoracle.episodes.widget; import android.content.Context; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.view.View; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; + public class WrapContentViewPager extends ViewPager { diff --git a/app/src/main/java/org/jamienicol/episodes/EpisodesApplication.java b/app/src/main/java/org/jamienicol/episodes/EpisodesApplication.java deleted file mode 100644 index c224c0ef..00000000 --- a/app/src/main/java/org/jamienicol/episodes/EpisodesApplication.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes; - -import android.app.Application; -import android.preference.PreferenceManager; -import android.util.Log; -import com.nostra13.universalimageloader.core.ImageLoader; -import com.nostra13.universalimageloader.core.ImageLoaderConfiguration; -import com.squareup.okhttp.Cache; -import com.squareup.okhttp.OkHttpClient; -import java.io.IOException; - -public class EpisodesApplication - extends Application -{ - private static final String TAG = EpisodesApplication.class.getName(); - - private static EpisodesApplication instance; - - private AutoRefreshHelper autoRefreshHelper; - private OkHttpClient httpClient; - - @Override - public void onCreate() { - super.onCreate(); - - instance = this; - - // ensure the default settings are initialised at first launch, - // rather than waiting for the settings screen to be opened. - // do this before anything that needs these settings is instantiated. - PreferenceManager.setDefaultValues(this, R.xml.preferences, false); - - autoRefreshHelper = AutoRefreshHelper.getInstance(this); - - httpClient = new OkHttpClient(); - try { - Cache httpCache = new Cache(getCacheDir(), 1024 * 1024); - httpClient.setCache(httpCache); - } catch (IOException e) { - Log.w(TAG, "Error initialising okhttp cache", e); - } - - final ImageLoaderConfiguration imageLoaderConfig = - new ImageLoaderConfiguration.Builder(this) - .build(); - - ImageLoader.getInstance().init(imageLoaderConfig); - } - - public static EpisodesApplication getInstance() { - return instance; - } - - public OkHttpClient getHttpClient() { - return httpClient; - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/MainActivity.java b/app/src/main/java/org/jamienicol/episodes/MainActivity.java deleted file mode 100644 index 91e7ca76..00000000 --- a/app/src/main/java/org/jamienicol/episodes/MainActivity.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2012-2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import android.support.v4.app.FragmentManager; -import android.support.v4.view.MenuItemCompat; -import android.support.v7.app.ActionBarActivity; -import android.support.v7.widget.SearchView; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import org.jamienicol.episodes.db.BackUpRestoreHelper; - -public class MainActivity - extends ActionBarActivity - implements ShowsListFragment.OnShowSelectedListener, - SelectBackupDialog.OnBackupSelectedListener -{ - @Override - public void onCreate(Bundle savedInstanceState) - { - super.onCreate(savedInstanceState); - setContentView(R.layout.main_activity); - - // ensure that the auto-refresh alarm is scheduled. - // this should mainly be useful the first time the app is ran. - AutoRefreshHelper.getInstance(getApplicationContext()) - .rescheduleAlarm(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - final MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.main, menu); - - final MenuItem menuItem = menu.findItem(R.id.menu_add_new_show); - final SearchView addShow = - (SearchView)MenuItemCompat.getActionView(menuItem); - addShow.setQueryHint(getString(R.string.menu_add_show_search_hint)); - addShow.setOnQueryTextListener(new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextChange(String query) { - return true; - } - - @Override - public boolean onQueryTextSubmit(String query) { - final Intent intent = - new Intent(MainActivity.this, - AddShowSearchActivity.class); - intent.putExtra("query", query); - startActivity(intent); - MenuItemCompat.collapseActionView(menuItem); - return true; - } - }); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_back_up: - back_up(); - return true; - - case R.id.menu_restore: - restore(); - return true; - - case R.id.menu_settings: - showSettings(); - return true; - - case R.id.menu_about: - showAbout(); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public void onShowSelected(int showId) { - final Intent intent = new Intent(this, ShowActivity.class); - intent.putExtra("showId", showId); - startActivity(intent); - } - - private void back_up() { - BackUpRestoreHelper.backUp(getApplicationContext()); - } - - private void restore() { - final FragmentManager fm = getSupportFragmentManager(); - final SelectBackupDialog dialog = new SelectBackupDialog(); - dialog.show(fm, "select_backup_dialog"); - } - - @Override - public void onBackupSelected(String backupFilename) { - BackUpRestoreHelper.restore(getApplicationContext(), backupFilename); - } - - private void showSettings() { - final Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); - } - - private void showAbout() { - final Intent intent = new Intent(this, AboutActivity.class); - startActivity(intent); - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/RefreshShowUtil.java b/app/src/main/java/org/jamienicol/episodes/RefreshShowUtil.java deleted file mode 100644 index 0a32097c..00000000 --- a/app/src/main/java/org/jamienicol/episodes/RefreshShowUtil.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright (C) 2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes; - -import android.content.ContentResolver; -import android.content.ContentValues; -import android.database.Cursor; -import android.net.Uri; -import android.util.Log; -import java.util.List; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsTable; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.tvdb.Client; -import org.jamienicol.episodes.tvdb.Episode; -import org.jamienicol.episodes.tvdb.Show; - -public class RefreshShowUtil -{ - private static final String TAG = RefreshShowUtil.class.getName(); - - public static void refreshShow(int showId, - ContentResolver contentResolver) { - final Client tvdbClient = new Client("25B864A8BC56AFAD"); - - Log.i(TAG, String.format("Refreshing show %d", showId)); - - final int showTvdbId = getShowTvdbId(showId, contentResolver); - // fetch full show + episode information from tvdb - final Show show = tvdbClient.getShow(showTvdbId); - - if (show != null) { - updateShow(showId, show, contentResolver); - updateExistingEpisodes(showId, show.getEpisodes(), contentResolver); - addNewEpisodes(showId, show.getEpisodes(), contentResolver); - } - } - - private static int getShowTvdbId(int showId, - ContentResolver contentResolver) { - final Uri showUri = - Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, - String.valueOf(showId)); - final String[] projection = { - ShowsTable.COLUMN_TVDB_ID - }; - - final Cursor showCursor = contentResolver.query(showUri, - projection, - null, - null, - null); - - final int tvdbIdColumnIndex = - showCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_TVDB_ID); - showCursor.moveToFirst(); - - return showCursor.getInt(tvdbIdColumnIndex); - } - - private static void updateShow(int showId, - Show show, - ContentResolver contentResolver) { - - final ContentValues showValues = new ContentValues(); - showValues.put(ShowsTable.COLUMN_TVDB_ID, show.getId()); - showValues.put(ShowsTable.COLUMN_NAME, show.getName()); - showValues.put(ShowsTable.COLUMN_OVERVIEW, show.getOverview()); - if (show.getFirstAired() != null) { - showValues.put(ShowsTable.COLUMN_FIRST_AIRED, - show.getFirstAired().getTime() / 1000); - } - showValues.put(ShowsTable.COLUMN_BANNER_PATH, show.getBannerPath()); - showValues.put(ShowsTable.COLUMN_FANART_PATH, show.getFanartPath()); - showValues.put(ShowsTable.COLUMN_POSTER_PATH, show.getPosterPath()); - - final Uri showUri = - Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, - String.valueOf(showId)); - contentResolver.update(showUri, showValues, null, null); - } - - private static void updateExistingEpisodes(int showId, - List episodes, - ContentResolver contentResolver) { - final Cursor episodesCursor = - getEpisodesCursor(showId, contentResolver); - - while (episodesCursor.moveToNext()) { - - final int idColumnIndex = - episodesCursor.getColumnIndexOrThrow(EpisodesTable.COLUMN_ID); - final int episodeId = episodesCursor.getInt(idColumnIndex); - final Uri episodeUri = - Uri.withAppendedPath(ShowsProvider.CONTENT_URI_EPISODES, - String.valueOf(episodeId)); - - final int tvdbIdColumnIndex = - episodesCursor.getColumnIndexOrThrow(EpisodesTable.COLUMN_TVDB_ID); - final int episodeTvdbId = episodesCursor.getInt(tvdbIdColumnIndex); - final Episode episode = findEpisodeWithTvdbId(episodes, - episodeTvdbId); - - if (episode == null) { - /* the episode no longer exists in tvdb. delete */ - Log.i(TAG, String.format("Deleting episode %d: no longer exists in tvdb.", episodeId)); - contentResolver.delete(episodeUri, null, null); - - } else { - /* update the episode row with the new values */ - final ContentValues epValues = new ContentValues(); - epValues.put(EpisodesTable.COLUMN_TVDB_ID, episode.getId()); - epValues.put(EpisodesTable.COLUMN_SHOW_ID, showId); - epValues.put(EpisodesTable.COLUMN_NAME, episode.getName()); - epValues.put(EpisodesTable.COLUMN_OVERVIEW, - episode.getOverview()); - epValues.put(EpisodesTable.COLUMN_EPISODE_NUMBER, - episode.getEpisodeNumber()); - epValues.put(EpisodesTable.COLUMN_SEASON_NUMBER, - episode.getSeasonNumber()); - if (episode.getFirstAired() != null) { - epValues.put(EpisodesTable.COLUMN_FIRST_AIRED, - episode.getFirstAired().getTime() / 1000); - } - - Log.i(TAG, String.format("Updating episode %d.", episodeId)); - contentResolver.update(episodeUri, epValues, null, null); - - /* remove episode from list of episodes - * returned by tvdb. by the end of this function - * this list will only contain new episodes */ - episodes.remove(episode); - } - } - } - - private static Cursor getEpisodesCursor(int showId, - ContentResolver contentResolver) { - final String[] projection = { - EpisodesTable.COLUMN_ID, - EpisodesTable.COLUMN_TVDB_ID - }; - final String selection = String.format("%s=?", - EpisodesTable.COLUMN_SHOW_ID); - final String[] selectionArgs = { - String.valueOf(showId) - }; - - final Cursor cursor = - contentResolver.query(ShowsProvider.CONTENT_URI_EPISODES, - projection, - selection, - selectionArgs, - null); - - return cursor; - } - - private static Episode findEpisodeWithTvdbId(List episodes, - int episodeTvdbId) { - for (Episode ep : episodes) { - if (ep.getId() == episodeTvdbId) { - return ep; - } - } - - return null; - } - - private static void addNewEpisodes(int showId, - List episodes, - ContentResolver contentResolver) { - - for (Episode episode : episodes) { - final ContentValues epValues = new ContentValues(); - epValues.put(EpisodesTable.COLUMN_TVDB_ID, episode.getId()); - epValues.put(EpisodesTable.COLUMN_SHOW_ID, showId); - epValues.put(EpisodesTable.COLUMN_NAME, episode.getName()); - epValues.put(EpisodesTable.COLUMN_OVERVIEW, - episode.getOverview()); - epValues.put(EpisodesTable.COLUMN_EPISODE_NUMBER, - episode.getEpisodeNumber()); - epValues.put(EpisodesTable.COLUMN_SEASON_NUMBER, - episode.getSeasonNumber()); - if (episode.getFirstAired() != null) { - epValues.put(EpisodesTable.COLUMN_FIRST_AIRED, - episode.getFirstAired().getTime() / 1000); - } - - Log.i(TAG, "Adding new episode."); - contentResolver.insert(ShowsProvider.CONTENT_URI_EPISODES, - epValues); - } - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/ShowsListFragment.java b/app/src/main/java/org/jamienicol/episodes/ShowsListFragment.java deleted file mode 100644 index 4ad1feea..00000000 --- a/app/src/main/java/org/jamienicol/episodes/ShowsListFragment.java +++ /dev/null @@ -1,495 +0,0 @@ -/* - * Copyright (C) 2012-2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes; - -import android.app.Activity; -import android.content.AsyncQueryHandler; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.support.v4.app.ListFragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.CursorLoader; -import android.support.v4.content.Loader; -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 android.widget.BaseAdapter; -import android.widget.CompoundButton; -import android.widget.ImageView; -import android.widget.ListView; -import android.widget.ProgressBar; -import android.widget.TextView; -import android.widget.ToggleButton; -import com.nostra13.universalimageloader.core.DisplayImageOptions; -import com.nostra13.universalimageloader.core.ImageLoader; -import java.util.ArrayList; -import java.util.List; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.db.ShowsTable; -import org.jamienicol.episodes.services.RefreshShowService; - -public class ShowsListFragment - extends ListFragment - implements LoaderManager.LoaderCallbacks -{ - private static final int LOADER_ID_SHOWS = 0; - private static final int LOADER_ID_EPISODES = 1; - - private static final String KEY_PREF_SHOWS_FILTER = "pref_shows_filter"; - - private static final int SHOWS_FILTER_ALL = 0; - private static final int SHOWS_FILTER_STARRED = 1; - private static final int SHOWS_FILTER_UNCOMPLETED = 2; - - private ShowsListAdapter listAdapter; - private Cursor showsData; - private Cursor episodesData; - - public interface OnShowSelectedListener { - public void onShowSelected(int showId); - } - private OnShowSelectedListener onShowSelectedListener; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - - try { - onShowSelectedListener = (OnShowSelectedListener)activity; - } catch (ClassCastException e) { - final String message = - String.format("%s must implement OnShowSelectedListener", - activity.toString()); - throw new ClassCastException(message); - } - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setHasOptionsMenu(true); - } - - public View onCreateView(LayoutInflater inflater, - ViewGroup container, - Bundle savedInstanceState) { - return inflater.inflate(R.layout.shows_list_fragment, container, false); - } - - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - - listAdapter = new ShowsListAdapter(getActivity(), - null, - null); - setListAdapter(listAdapter); - - getLoaderManager().initLoader(LOADER_ID_SHOWS, null, this); - getLoaderManager().initLoader(LOADER_ID_EPISODES, null, this); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.shows_list_fragment, menu); - } - - @Override - public void onPrepareOptionsMenu(Menu menu) { - - // hide refresh all option if no shows exist - final boolean showsExist = - (showsData != null && showsData.moveToFirst()); - - menu.findItem(R.id.menu_refresh_all_shows).setVisible(showsExist); - - /* set the currently selected filter's menu item as checked */ - final SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(getActivity()); - final int filter = - prefs.getInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_ALL); - - switch (filter) { - case SHOWS_FILTER_ALL: - menu.findItem(R.id.menu_filter_all).setChecked(true); - break; - case SHOWS_FILTER_STARRED: - menu.findItem(R.id.menu_filter_starred).setChecked(true); - break; - case SHOWS_FILTER_UNCOMPLETED: - menu.findItem(R.id.menu_filter_uncompleted).setChecked(true); - break; - } - - super.onPrepareOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_refresh_all_shows: - refreshAllShows(); - return true; - - case R.id.menu_filter_all: - case R.id.menu_filter_starred: - case R.id.menu_filter_uncompleted: - if (!item.isChecked()) { - item.setChecked(true); - } - - final SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(getActivity()); - final SharedPreferences.Editor editor = prefs.edit(); - if (item.getItemId() == R.id.menu_filter_all) { - editor.putInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_ALL); - } else if (item.getItemId() == R.id.menu_filter_starred) { - editor.putInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_STARRED); - } else if (item.getItemId() == R.id.menu_filter_uncompleted) { - editor.putInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_UNCOMPLETED); - } - editor.apply(); - - return true; - - default: - return super.onOptionsItemSelected(item); - } - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - if (id == LOADER_ID_SHOWS) { - final String[] projection = { - ShowsTable.COLUMN_ID, - ShowsTable.COLUMN_NAME, - ShowsTable.COLUMN_STARRED, - ShowsTable.COLUMN_BANNER_PATH - }; - return new CursorLoader(getActivity(), - ShowsProvider.CONTENT_URI_SHOWS, - projection, - null, - null, - ShowsTable.COLUMN_STARRED + " DESC," + - ShowsTable.COLUMN_NAME + " ASC"); - - } else if (id == LOADER_ID_EPISODES) { - final String[] projection = { - EpisodesTable.COLUMN_SHOW_ID, - EpisodesTable.COLUMN_SEASON_NUMBER, - EpisodesTable.COLUMN_FIRST_AIRED, - EpisodesTable.COLUMN_WATCHED - }; - final String selection = - String.format("%s!=?", EpisodesTable.COLUMN_SEASON_NUMBER); - final String[] selectionArgs = { - "0" - }; - return new CursorLoader(getActivity(), - ShowsProvider.CONTENT_URI_EPISODES, - projection, - selection, - selectionArgs, - null); - - } else { - throw new IllegalArgumentException("invalid loader id"); - } - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - switch (loader.getId()) { - case LOADER_ID_SHOWS: - showsData = data; - listAdapter.swapShowsCursor(data); - break; - - case LOADER_ID_EPISODES: - episodesData = data; - listAdapter.swapEpisodesCursor(data); - break; - } - - getActivity().supportInvalidateOptionsMenu(); - } - - @Override - public void onLoaderReset(Loader loader) { - onLoadFinished(loader, null); - } - - @Override - public void onListItemClick(ListView l, View v, int position, long id) { - onShowSelectedListener.onShowSelected((int)id); - } - - private void refreshAllShows() { - if (showsData != null && showsData.moveToFirst()) { - do { - final int idColumnIndex = - showsData.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); - - final int id = showsData.getInt(idColumnIndex); - - final Intent intent = new Intent(getActivity(), - RefreshShowService.class); - intent.putExtra("showId", id); - - getActivity().startService(intent); - - } while (showsData.moveToNext()); - } - } - - private static class ShowsListAdapter - extends BaseAdapter - implements SharedPreferences.OnSharedPreferenceChangeListener - { - private Context context; - private Cursor showsCursor; - private int filter; - private EpisodesCounter episodesCounter; - - // list of shows to be displayed with current filter. maps from - // the show's position in the list to its position in the cursor. - private List filteredShows; - - public ShowsListAdapter(Context context, - Cursor showsCursor, - Cursor episodesCursor) { - this.context = context; - - episodesCounter = new EpisodesCounter(EpisodesTable.COLUMN_SHOW_ID); - episodesCounter.swapCursor(episodesCursor); - - final SharedPreferences prefs = - PreferenceManager.getDefaultSharedPreferences(context); - prefs.registerOnSharedPreferenceChangeListener(this); - filter = prefs.getInt(KEY_PREF_SHOWS_FILTER, SHOWS_FILTER_ALL); - - filteredShows = new ArrayList(); - - swapShowsCursor(showsCursor); - } - - public void swapShowsCursor(Cursor showsCursor) { - this.showsCursor = showsCursor; - - updateFilter(); - notifyDataSetChanged(); - } - - public void swapEpisodesCursor(Cursor episodesCursor) { - episodesCounter.swapCursor(episodesCursor); - - if (showsCursor != null) { - updateFilter(); - notifyDataSetChanged(); - } - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { - if (key.equals(KEY_PREF_SHOWS_FILTER)) { - filter = sharedPreferences.getInt(KEY_PREF_SHOWS_FILTER, - SHOWS_FILTER_ALL); - - if (showsCursor != null) { - updateFilter(); - notifyDataSetChanged(); - } - } - } - - private void updateFilter() { - filteredShows.clear(); - - if (showsCursor == null || !showsCursor.moveToFirst()) { - return; - } - - do { - switch (filter) { - case SHOWS_FILTER_STARRED: - final int starredColumnIndex = - showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_STARRED); - if (showsCursor.getInt(starredColumnIndex) > 0) { - filteredShows.add(showsCursor.getPosition()); - } - break; - - case SHOWS_FILTER_UNCOMPLETED: - final int idColumnIndex = - showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); - final int id = showsCursor.getInt(idColumnIndex); - - if (episodesCounter.getNumWatchedEpisodes(id) < - episodesCounter.getNumAiredEpisodes(id)) { - filteredShows.add(showsCursor.getPosition()); - } - break; - - default: - filteredShows.add(showsCursor.getPosition()); - break; - } - } while (showsCursor.moveToNext()); - } - - @Override - public int getCount() { - if (showsCursor == null) { - return 0; - } else { - return filteredShows.size(); - } - } - - @Override - public Object getItem(int position) { - return null; - } - - @Override - public long getItemId(int position) { - showsCursor.moveToPosition(filteredShows.get(position)); - - final int idColumnIndex = - showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); - return showsCursor.getInt(idColumnIndex); - } - - @Override - public View getView(int position, - View convertView, - ViewGroup parent) { - - final LayoutInflater inflater = LayoutInflater.from(context); - if(convertView == null) { - convertView = inflater.inflate(R.layout.shows_list_item, - parent, - false); - } - - showsCursor.moveToPosition(filteredShows.get(position)); - - final int idColumnIndex = - showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_ID); - final int id = showsCursor.getInt(idColumnIndex); - - final ContentResolver contentResolver = - context.getContentResolver(); - - final TextView nameView = - (TextView)convertView.findViewById(R.id.show_name_view); - final int nameColumnIndex = - showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_NAME); - final String name = showsCursor.getString(nameColumnIndex); - nameView.setText(name); - - final ImageView bannerView = - (ImageView)convertView.findViewById(R.id.banner_view); - final int bannerPathColumnIndex = - showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_BANNER_PATH); - final String bannerPath = showsCursor.getString(bannerPathColumnIndex); - - bannerView.setImageResource(R.drawable.blank_show_banner); - if (bannerPath != null && !bannerPath.equals("")) { - final String bannerUrl = - String.format("http://thetvdb.com/banners/%s", bannerPath); - - final DisplayImageOptions options = - new DisplayImageOptions.Builder() - .cacheInMemory(true) - .cacheOnDisk(true) - .build(); - ImageLoader.getInstance().displayImage(bannerUrl, - bannerView, - options); - } - - final ToggleButton starredToggle = - (ToggleButton)convertView.findViewById(R.id.show_starred_toggle); - final int starredColumnIndex = - showsCursor.getColumnIndexOrThrow(ShowsTable.COLUMN_STARRED); - final boolean starred = - showsCursor.getInt(starredColumnIndex) > 0 ? true : false; - - starredToggle.setOnCheckedChangeListener(null); - starredToggle.setChecked(starred); - - starredToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - public void onCheckedChanged(CompoundButton buttonView, - boolean isChecked) { - final AsyncQueryHandler handler = - new AsyncQueryHandler(contentResolver) {}; - final ContentValues showValues = new ContentValues(); - showValues.put(ShowsTable.COLUMN_STARRED, isChecked); - - final Uri showUri = - Uri.withAppendedPath(ShowsProvider.CONTENT_URI_SHOWS, - String.valueOf(id)); - handler.startUpdate(0, - null, - showUri, - showValues, - null, - null); - } - }); - - final int numAired = episodesCounter.getNumAiredEpisodes(id); - final int numWatched = episodesCounter.getNumWatchedEpisodes(id); - final int numUpcoming = episodesCounter.getNumUpcomingEpisodes(id); - - final ProgressBar progressBar = - (ProgressBar)convertView.findViewById(R.id.show_progress_bar); - progressBar.setMax(numAired); - progressBar.setProgress(numWatched); - - final TextView watchedCountView = - (TextView)convertView.findViewById(R.id.watched_count_view); - String watchedCountText = context.getString(R.string.watched_count, - numWatched, - numAired); - if (numUpcoming != 0) { - watchedCountText += " " + - context.getString(R.string.upcoming_count, - numUpcoming); - } - watchedCountView.setText(watchedCountText); - - return convertView; - } - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/db/BackUpRestoreHelper.java b/app/src/main/java/org/jamienicol/episodes/db/BackUpRestoreHelper.java deleted file mode 100644 index c6a08437..00000000 --- a/app/src/main/java/org/jamienicol/episodes/db/BackUpRestoreHelper.java +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Copyright (C) 2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.db; - -import android.content.Context; -import android.os.AsyncTask; -import android.os.Environment; -import android.util.Log; -import android.widget.Toast; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.channels.FileChannel; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import org.jamienicol.episodes.R; - -public class BackUpRestoreHelper -{ - private final static String TAG = BackUpRestoreHelper.class.getName(); - - public static void backUp(Context context) { - final BackUpTask task = new BackUpTask(context); - task.execute(); - } - - public static void restore(Context context, String filename) { - final RestoreTask task = new RestoreTask(context); - task.execute(filename); - } - - public static File getBackupDir() { - return new File(Environment.getExternalStorageDirectory(), "episodes"); - } - - private static class BackUpTask - extends AsyncTask - { - private final Context context; - private String destFilePath; - - public BackUpTask(Context context) { - this.context = context; - } - - @Override - protected Boolean doInBackground(Void... params) { - Log.i(TAG, "Backing up library."); - - final File srcFile = - context.getDatabasePath(DatabaseOpenHelper.getDbName()); - if (!srcFile.canRead()) { - Log.e(TAG, String.format("Cannot read database file: '%s'.", - srcFile.getPath())); - return false; - } - - final File destDir = getBackupDir(); - destDir.mkdirs(); - if (!destDir.isDirectory()) { - Log.e(TAG, - String.format("Error creating backup directory '%s'.", - destDir.getPath())); - return false; - } - - final File destFile = new File(destDir, getBackupFilename()); - destFilePath = destFile.getPath(); - - try { - FileChannel src = new FileInputStream(srcFile).getChannel(); - FileChannel dest = new FileOutputStream(destFile).getChannel(); - - dest.transferFrom(src, 0, src.size()); - - Log.i(TAG, String.format("Library backed up successfully: '%s'.", - destFile.getPath())); - - return true; - - } catch (IOException e) { - Log.e(TAG, String.format("Error backing up library: %s", - e.toString())); - - return false; - } - } - - @Override - protected void onPostExecute(Boolean success) { - if (success) { - final String message = - context.getString(R.string.back_up_success_message, - destFilePath); - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(context, - R.string.back_up_error_message, - Toast.LENGTH_SHORT).show(); - } - } - - private static String getBackupFilename() { - final Date today = new Date(); - final DateFormat sdf = - new SimpleDateFormat("yyyyMMdd_HHmm", Locale.US); - - return "episodes_" + sdf.format(today) + ".db"; - } - } - - private static class RestoreTask - extends AsyncTask - { - private final Context context; - - public RestoreTask(Context context) { - this.context = context; - } - - @Override - protected Boolean doInBackground(String... filename) { - final File srcFile = new File(filename[0]); - final File destFile = - context.getDatabasePath(DatabaseOpenHelper.getDbName()); - - try { - FileChannel src = new FileInputStream(srcFile).getChannel(); - FileChannel dest = new FileOutputStream(destFile).getChannel(); - - dest.transferFrom(src, 0, src.size()); - - Log.i(TAG, String.format("Library restored successfully.", - destFile.getPath())); - - return true; - - } catch (IOException e) { - Log.e(TAG, String.format("Error restoring library: %s", - e.toString())); - return false; - } - } - - @Override - protected void onPostExecute(Boolean success) { - if (success) { - Toast.makeText(context, - R.string.restore_success_message, - Toast.LENGTH_SHORT).show(); - - ShowsProvider.reloadDatabase(context); - } else { - Toast.makeText(context, - R.string.restore_error_message, - Toast.LENGTH_SHORT).show(); - } - } - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/db/DatabaseOpenHelper.java b/app/src/main/java/org/jamienicol/episodes/db/DatabaseOpenHelper.java deleted file mode 100644 index 45f972e4..00000000 --- a/app/src/main/java/org/jamienicol/episodes/db/DatabaseOpenHelper.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2012-2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.db; - -import android.content.Context; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.util.Log; - -public class DatabaseOpenHelper extends SQLiteOpenHelper -{ - private static final String name = "episodes.db"; - private static final int version = 5; - - private static final String TAG = "DatabaseOpenHelper"; - - DatabaseOpenHelper(Context context) { - super(context, name, null, version); - } - - @Override - public void onCreate(SQLiteDatabase db) { - Log.d(TAG, "creating database"); - ShowsTable.onCreate(db); - EpisodesTable.onCreate(db); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { - Log.d(TAG, String.format("upgrading database from version %d to %d", - oldVersion, newVersion)); - ShowsTable.onUpgrade(db, oldVersion, newVersion); - EpisodesTable.onUpgrade(db, oldVersion, newVersion); - } - - @Override - public void onOpen(SQLiteDatabase db) { - Log.d(TAG, "opening database."); - } - - public static String getDbName() { - return name; - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/db/EpisodesTable.java b/app/src/main/java/org/jamienicol/episodes/db/EpisodesTable.java deleted file mode 100644 index 9498d482..00000000 --- a/app/src/main/java/org/jamienicol/episodes/db/EpisodesTable.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (C) 2012-2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.db; - -import android.database.sqlite.SQLiteDatabase; -import android.provider.BaseColumns; -import android.util.Log; - -public class EpisodesTable -{ - private static final String TAG = "EpisodesTable"; - - public static final String TABLE_NAME = "episodes"; - - public static final String COLUMN_ID = BaseColumns._ID; - public static final String COLUMN_TVDB_ID = "tvdb_id"; - public static final String COLUMN_SHOW_ID = "show_id"; - public static final String COLUMN_NAME = "name"; - public static final String COLUMN_OVERVIEW = "overview"; - public static final String COLUMN_EPISODE_NUMBER = "episode_number"; - public static final String COLUMN_SEASON_NUMBER = "season_number"; - public static final String COLUMN_FIRST_AIRED = "first_aired"; - public static final String COLUMN_WATCHED = "watched"; - - public static void onCreate(SQLiteDatabase db) { - String create = - String.format("CREATE TABLE %s (" + - " %s INTEGER PRIMARY KEY," + - " %s INTEGER UNIQUE NOT NULL," + - " %s INTEGER NOT NULL," + - " %s VARCHAR(200) NOT NULL," + - " %s TEXT," + - " %s INTEGER," + - " %s INTEGER," + - " %s DATE," + - " %s BOOLEAN" + - ");", - TABLE_NAME, - COLUMN_ID, - COLUMN_TVDB_ID, - COLUMN_SHOW_ID, - COLUMN_NAME, - COLUMN_OVERVIEW, - COLUMN_EPISODE_NUMBER, - COLUMN_SEASON_NUMBER, - COLUMN_FIRST_AIRED, - COLUMN_WATCHED); - - Log.d(TAG, String.format("creating episodes table: %s", create)); - - db.execSQL(create); - } - - public static void onUpgrade(SQLiteDatabase db, - int oldVersion, - int newVersion) { - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/db/ShowsProvider.java b/app/src/main/java/org/jamienicol/episodes/db/ShowsProvider.java deleted file mode 100644 index 84d74b30..00000000 --- a/app/src/main/java/org/jamienicol/episodes/db/ShowsProvider.java +++ /dev/null @@ -1,336 +0,0 @@ -/* - * Copyright (C) 2012 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.db; - -import android.content.ContentProvider; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteConstraintException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.util.Log; -import org.jamienicol.episodes.BuildConfig; - -public class ShowsProvider extends ContentProvider -{ - private static final String TAG = "ShowsProvider"; - - private static final String URI_AUTHORITY = - BuildConfig.APPLICATION_ID + ".db.ShowsProvider"; - - private static final Uri CONTENT_URI_BASE = - Uri.parse(ContentResolver.SCHEME_CONTENT + - "://" + - ShowsProvider.URI_AUTHORITY); - - public static final Uri CONTENT_URI_SHOWS = - Uri.parse(ContentResolver.SCHEME_CONTENT + - "://" + - ShowsProvider.URI_AUTHORITY + - "/" + - ShowsTable.TABLE_NAME); - - public static final Uri CONTENT_URI_EPISODES = - Uri.parse(ContentResolver.SCHEME_CONTENT + - "://" + - ShowsProvider.URI_AUTHORITY + - "/" + - EpisodesTable.TABLE_NAME); - - public static final String CONTENT_TYPE_SHOW_DIR = - ContentResolver.CURSOR_DIR_BASE_TYPE + "/show"; - public static final String CONTENT_TYPE_SHOW_ITEM = - ContentResolver.CURSOR_ITEM_BASE_TYPE + "/show"; - public static final String CONTENT_TYPE_EPISODE_DIR = - ContentResolver.CURSOR_DIR_BASE_TYPE + "/episode"; - public static final String CONTENT_TYPE_EPISODE_ITEM = - ContentResolver.CURSOR_ITEM_BASE_TYPE + "/episode"; - - private static final int URI_TYPE_SHOWS = 1; - private static final int URI_TYPE_SHOWS_ID = 2; - private static final int URI_TYPE_EPISODES = 3; - private static final int URI_TYPE_EPISODES_ID = 4; - - private static final UriMatcher uriMatcher = - new UriMatcher(UriMatcher.NO_MATCH); - static { - uriMatcher.addURI(URI_AUTHORITY, - ShowsTable.TABLE_NAME, - URI_TYPE_SHOWS); - uriMatcher.addURI(URI_AUTHORITY, - ShowsTable.TABLE_NAME + "/#", - URI_TYPE_SHOWS_ID); - uriMatcher.addURI(URI_AUTHORITY, - EpisodesTable.TABLE_NAME, - URI_TYPE_EPISODES); - uriMatcher.addURI(URI_AUTHORITY, - EpisodesTable.TABLE_NAME + "/#", - URI_TYPE_EPISODES_ID); - } - - private DatabaseOpenHelper databaseOpenHelper; - - @Override - public Cursor query(Uri uri, - String[] projection, - String selection, - String[] selectionArgs, - String sortOrder) { - String table; - String sel; - - switch (uriMatcher.match(uri)) { - case URI_TYPE_SHOWS: - table = ShowsTable.TABLE_NAME; - sel = selection; - break; - - case URI_TYPE_SHOWS_ID: - table = ShowsTable.TABLE_NAME; - sel = String.format("%s=%s", - ShowsTable.COLUMN_ID, - uri.getLastPathSegment()); - if (selection != null) { - sel += " AND " + selection; - } - break; - - case URI_TYPE_EPISODES: - table = EpisodesTable.TABLE_NAME; - sel = selection; - break; - - case URI_TYPE_EPISODES_ID: - table = EpisodesTable.TABLE_NAME; - sel = String.format("%s=%s", - EpisodesTable.COLUMN_ID, - uri.getLastPathSegment()); - if (selection != null) { - sel += " AND " + selection; - } - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - SQLiteDatabase db = databaseOpenHelper.getReadableDatabase(); - Cursor cursor = db.query(table, - projection, - sel, - selectionArgs, - null, - null, - sortOrder, - null); - - cursor.setNotificationUri(getContext().getContentResolver(), uri); - - return cursor; - } - - @Override - public Uri insert(Uri uri, ContentValues values) { - - String table; - Uri contentUri; - if (uriMatcher.match(uri) == URI_TYPE_SHOWS) { - table = ShowsTable.TABLE_NAME; - contentUri = CONTENT_URI_SHOWS; - } else if (uriMatcher.match(uri) == URI_TYPE_EPISODES) { - table = EpisodesTable.TABLE_NAME; - contentUri = CONTENT_URI_EPISODES; - } else { - throw new IllegalArgumentException("Unknown URI " + uri); - } - - SQLiteDatabase db = databaseOpenHelper.getWritableDatabase(); - try { - long rowId = db.insertOrThrow(table, null, values); - Log.i(TAG, String.format("succesfully inserted row. id: %d", - rowId)); - Uri rowUri = ContentUris.withAppendedId(contentUri, - rowId); - getContext().getContentResolver().notifyChange(rowUri, null); - - return rowUri; - } catch (SQLiteConstraintException e) { - Log.i(TAG, String.format("constraint error inserting row: %s", - e.toString())); - return null; - } - } - - @Override - public int delete(Uri uri, - String selection, - String[] selectionArgs) { - String table; - String sel; - - switch (uriMatcher.match(uri)) { - case URI_TYPE_SHOWS: - table = ShowsTable.TABLE_NAME; - sel = selection; - break; - - case URI_TYPE_SHOWS_ID: - table = ShowsTable.TABLE_NAME; - sel = String.format("%s=%s", - ShowsTable.COLUMN_ID, - uri.getLastPathSegment()); - if (selection != null) { - sel += " AND " + selection; - } - - break; - - case URI_TYPE_EPISODES: - table = EpisodesTable.TABLE_NAME; - sel = selection; - break; - - case URI_TYPE_EPISODES_ID: - table = EpisodesTable.TABLE_NAME; - sel = String.format("%s=%s", - EpisodesTable.COLUMN_ID, - uri.getLastPathSegment()); - if (selection != null) { - sel += " AND " + selection; - } - - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - SQLiteDatabase db = databaseOpenHelper.getWritableDatabase(); - int count = db.delete(table, - sel, - selectionArgs); - - getContext().getContentResolver().notifyChange(uri, null); - - return count; - } - - @Override - public int update(Uri uri, - ContentValues values, - String selection, - String[] selectionArgs) { - String table; - String sel; - - switch (uriMatcher.match(uri)) { - case URI_TYPE_SHOWS: - table = ShowsTable.TABLE_NAME; - sel = selection; - break; - - case URI_TYPE_SHOWS_ID: - table = ShowsTable.TABLE_NAME; - sel = String.format("%s=%s", - ShowsTable.COLUMN_ID, - uri.getLastPathSegment()); - if (selection != null) { - sel += " AND " + selection; - } - break; - - case URI_TYPE_EPISODES: - table = EpisodesTable.TABLE_NAME; - sel = selection; - break; - - case URI_TYPE_EPISODES_ID: - table = EpisodesTable.TABLE_NAME; - sel = String.format("%s=%s", - EpisodesTable.COLUMN_ID, - uri.getLastPathSegment()); - if (selection != null) { - sel += " AND " + selection; - } - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - SQLiteDatabase db = databaseOpenHelper.getWritableDatabase(); - int count = db.update(table, - values, - sel, - selectionArgs); - - getContext().getContentResolver().notifyChange(uri, null); - - return count; - } - - @Override - public String getType(Uri uri) { - switch (uriMatcher.match(uri)) { - case URI_TYPE_SHOWS: - return CONTENT_TYPE_SHOW_DIR; - - case URI_TYPE_SHOWS_ID: - return CONTENT_TYPE_SHOW_ITEM; - - case URI_TYPE_EPISODES: - return CONTENT_TYPE_EPISODE_DIR; - - case URI_TYPE_EPISODES_ID: - return CONTENT_TYPE_EPISODE_ITEM; - - default: - return null; - } - } - - @Override - public boolean onCreate() { - databaseOpenHelper = new DatabaseOpenHelper(getContext()); - - return true; - } - - public static void reloadDatabase(Context context) { - final ContentResolver resolver = context.getContentResolver(); - final ContentProviderClient client = - resolver.acquireContentProviderClient(URI_AUTHORITY); - final ShowsProvider provider = - (ShowsProvider)client.getLocalContentProvider(); - - provider.databaseOpenHelper.close(); - provider.databaseOpenHelper = - new DatabaseOpenHelper(provider.getContext()); - - resolver.notifyChange(CONTENT_URI_BASE, null); - - client.release(); - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/db/ShowsTable.java b/app/src/main/java/org/jamienicol/episodes/db/ShowsTable.java deleted file mode 100644 index a9b73764..00000000 --- a/app/src/main/java/org/jamienicol/episodes/db/ShowsTable.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright (C) 2012-2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.db; - -import android.database.sqlite.SQLiteDatabase; -import android.provider.BaseColumns; -import android.util.Log; - -public class ShowsTable -{ - private static final String TAG = "ShowsTable"; - - public static final String TABLE_NAME = "shows"; - - public static final String COLUMN_ID = BaseColumns._ID; - public static final String COLUMN_TVDB_ID = "tvdb_id"; - public static final String COLUMN_NAME = "name"; - public static final String COLUMN_OVERVIEW = "overview"; - public static final String COLUMN_FIRST_AIRED = "first_aired"; - public static final String COLUMN_STARRED = "starred"; - public static final String COLUMN_BANNER_PATH = "banner_path"; - public static final String COLUMN_FANART_PATH = "fanart_path"; - public static final String COLUMN_POSTER_PATH = "poster_path"; - public static final String COLUMN_NOTES = "notes"; - - public static final String COLUMN_TYPE_ID = "INTEGER PRIMARY KEY"; - public static final String COLUMN_TYPE_TVDB_ID = "INTEGER UNIQUE NOT NULL"; - public static final String COLUMN_TYPE_NAME = "TEXT NOT NULL"; - public static final String COLUMN_TYPE_OVERVIEW = "TEXT"; - public static final String COLUMN_TYPE_FIRST_AIRED = "DATE"; - public static final String COLUMN_TYPE_STARRED = "BOOLEAN DEFAULT 0"; - public static final String COLUMN_TYPE_BANNER_PATH = "TEXT"; - public static final String COLUMN_TYPE_FANART_PATH = "TEXT"; - public static final String COLUMN_TYPE_POSTER_PATH = "TEXT"; - public static final String COLUMN_TYPE_NOTES = "TEXT"; - - public static void onCreate(SQLiteDatabase db) { - String create = - String.format("CREATE TABLE %s (" + - " %s %s," + - " %s %s," + - " %s %s," + - " %s %s," + - " %s %s," + - " %s %s," + - " %s %s," + - " %s %s," + - " %s %s," + - " %s %s" + - ");", - TABLE_NAME, - COLUMN_ID, COLUMN_TYPE_ID, - COLUMN_TVDB_ID, COLUMN_TYPE_TVDB_ID, - COLUMN_NAME, COLUMN_TYPE_NAME, - COLUMN_OVERVIEW, COLUMN_TYPE_OVERVIEW, - COLUMN_FIRST_AIRED, COLUMN_TYPE_FIRST_AIRED, - COLUMN_STARRED, COLUMN_TYPE_STARRED, - COLUMN_BANNER_PATH, COLUMN_TYPE_BANNER_PATH, - COLUMN_FANART_PATH, COLUMN_TYPE_FANART_PATH, - COLUMN_POSTER_PATH, COLUMN_TYPE_POSTER_PATH, - COLUMN_NOTES, COLUMN_TYPE_NOTES); - - Log.d(TAG, String.format("creating shows table: %s", create)); - - db.execSQL(create); - } - - public static void onUpgrade(SQLiteDatabase db, - int oldVersion, - int newVersion) { - switch (oldVersion) { - case 1: - // Add starred column - Log.d(TAG, "upgrading shows table: adding starred column"); - db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", - TABLE_NAME, - COLUMN_STARRED, - COLUMN_TYPE_STARRED)); - - // fall through - case 2: - // Add banner path column - Log.d(TAG, "upgrading shows table: adding banner path column"); - db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", - TABLE_NAME, - COLUMN_BANNER_PATH, - COLUMN_TYPE_BANNER_PATH)); - - // fall through - case 3: - // Add fanart path and poster path columns - Log.d(TAG, "upgrading shows table: adding fanart path column"); - db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", - TABLE_NAME, - COLUMN_FANART_PATH, - COLUMN_TYPE_FANART_PATH)); - - Log.d(TAG, "upgrading shows table: adding poster path column"); - db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", - TABLE_NAME, - COLUMN_POSTER_PATH, - COLUMN_TYPE_POSTER_PATH)); - - // fall through - case 4: - // Add notes column - Log.d(TAG, "upgrading shows table: adding notes column"); - db.execSQL(String.format("ALTER TABLE %s ADD COLUMN %s %s", - TABLE_NAME, - COLUMN_NOTES, - COLUMN_TYPE_NOTES)); - // fall through - } - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/services/AddShowService.java b/app/src/main/java/org/jamienicol/episodes/services/AddShowService.java deleted file mode 100644 index e68f6e39..00000000 --- a/app/src/main/java/org/jamienicol/episodes/services/AddShowService.java +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (C) 2012-2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.services; - -import android.app.IntentService; -import android.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.database.Cursor; -import android.net.Uri; -import android.os.Handler; -import android.util.Log; -import android.widget.Toast; -import org.jamienicol.episodes.db.EpisodesTable; -import org.jamienicol.episodes.db.ShowsTable; -import org.jamienicol.episodes.db.ShowsProvider; -import org.jamienicol.episodes.tvdb.Client; -import org.jamienicol.episodes.tvdb.Episode; -import org.jamienicol.episodes.tvdb.Show; -import org.jamienicol.episodes.R; - -public class AddShowService extends IntentService -{ - private static final String TAG = "AddShowService"; - - private Handler handler; - - public AddShowService() { - super("AddShowService"); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - handler = new Handler(); - - return super.onStartCommand(intent, flags, startId); - } - - @Override - protected void onHandleIntent(Intent intent) { - final Client tvdbClient = new Client("25B864A8BC56AFAD"); - - final int tvdbId = intent.getIntExtra("tvdbId", 0); - final String showName = intent.getStringExtra("showName"); - - if (isShowAlreadyAdded(tvdbId) == false) { - - showMessage(getString(R.string.adding_show, showName)); - - // fetch full show + episode information from tvdb - final Show show = tvdbClient.getShow(tvdbId); - - if (show != null) { - // add show and episodes to database - final int showId = insertShow(show); - for (Episode episode : show.getEpisodes()) { - insertEpisode(episode, showId); - } - - showMessage(getString(R.string.show_added, showName)); - } else { - showMessage(getString(R.string.error_adding_show, showName)); - } - } else { - showMessage(getString(R.string.show_already_added, showName)); - } - } - - private boolean isShowAlreadyAdded(int tvdbId) { - final String[] projection = { - }; - final String selection = String.format("%s=?", - ShowsTable.COLUMN_TVDB_ID); - final String[] selectionArgs = { - Integer.valueOf(tvdbId).toString() - }; - final Cursor cursor = - getContentResolver().query(ShowsProvider.CONTENT_URI_SHOWS, - projection, - selection, - selectionArgs, - null); - - return cursor.moveToFirst(); - } - - private int insertShow(Show show) { - // fill in information about the show - final ContentValues showValues = new ContentValues(); - showValues.put(ShowsTable.COLUMN_TVDB_ID, show.getId()); - showValues.put(ShowsTable.COLUMN_NAME, show.getName()); - showValues.put(ShowsTable.COLUMN_OVERVIEW, show.getOverview()); - if (show.getFirstAired() != null) { - showValues.put(ShowsTable.COLUMN_FIRST_AIRED, - show.getFirstAired().getTime() / 1000); - } - showValues.put(ShowsTable.COLUMN_BANNER_PATH, show.getBannerPath()); - showValues.put(ShowsTable.COLUMN_FANART_PATH, show.getFanartPath()); - showValues.put(ShowsTable.COLUMN_POSTER_PATH, show.getPosterPath()); - - // insert the show into the database - final Uri showUri = - getContentResolver().insert(ShowsProvider.CONTENT_URI_SHOWS, - showValues); - - // need to obtain the ID of the inserted show. - // the ID is just the final segment of the URI - final int showId = Integer.parseInt(showUri.getLastPathSegment()); - - Log.i(TAG, String.format("show %s successfully added to database as row %d. adding episodes", - show.getName(), - showId)); - - return showId; - } - - private void insertEpisode(Episode episode, int showId) { - final ContentValues episodeValues = new ContentValues(); - episodeValues.put(EpisodesTable.COLUMN_TVDB_ID, episode.getId()); - episodeValues.put(EpisodesTable.COLUMN_SHOW_ID, showId); - episodeValues.put(EpisodesTable.COLUMN_NAME, episode.getName()); - episodeValues.put(EpisodesTable.COLUMN_OVERVIEW, - episode.getOverview()); - episodeValues.put(EpisodesTable.COLUMN_EPISODE_NUMBER, - episode.getEpisodeNumber()); - episodeValues.put(EpisodesTable.COLUMN_SEASON_NUMBER, - episode.getSeasonNumber()); - if (episode.getFirstAired() != null) { - episodeValues.put(EpisodesTable.COLUMN_FIRST_AIRED, - episode.getFirstAired().getTime() / 1000); - } - - getContentResolver().insert(ShowsProvider.CONTENT_URI_EPISODES, - episodeValues); - } - - private void showMessage(String message) { - final Context context = this; - final String text = message; - final int duration = Toast.LENGTH_SHORT; - - handler.post(new Runnable() { - @Override - public void run() { - final Toast toast = Toast.makeText(context, text, duration); - toast.show(); - } - }); - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/tvdb/Client.java b/app/src/main/java/org/jamienicol/episodes/tvdb/Client.java deleted file mode 100644 index 3d0612a4..00000000 --- a/app/src/main/java/org/jamienicol/episodes/tvdb/Client.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2012-2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.tvdb; - -import android.util.Log; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Request; -import com.squareup.okhttp.Response; -import java.io.IOException; -import java.net.URLEncoder; -import java.util.List; -import java.util.Locale; -import org.jamienicol.episodes.EpisodesApplication; - -public class Client -{ - private static final String TAG = Client.class.getName(); - private static final String baseUrl = "http://thetvdb.com/api"; - - private final String apiKey; - private final OkHttpClient http; - - public Client(String apiKey) { - this.apiKey = apiKey; - http = EpisodesApplication.getInstance().getHttpClient(); - } - - public List searchShows(String query) { - - try { - final String escapedQuery = URLEncoder.encode(query, "UTF-8"); - final String url = String.format("%s/GetSeries.php?seriesname=%s", - baseUrl, - escapedQuery); - Log.d(TAG, String.format("Sending request to %s", url)); - - final Request request = new Request.Builder().url(url).build(); - - final Response response = http.newCall(request).execute(); - - Log.d(TAG, String.format("Received response %d: %s", - response.code(), - response.message())); - - if (response.isSuccessful()) { - final SearchShowsParser parser = new SearchShowsParser(); - - return parser.parse(response.body().byteStream()); - } else { - return null; - } - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } - - public Show getShow(int id) { - try { - final String url = String.format(Locale.US, - "%s/%s/series/%d/all/en.xml", - baseUrl, - apiKey, - id); - Log.d(TAG, String.format("Sending request to %s", url)); - - final Request request = new Request.Builder().url(url).build(); - - final Response response = http.newCall(request).execute(); - - Log.d(TAG, String.format("Received response %d: %s", - response.code(), - response.message())); - - if (response.isSuccessful()) { - final GetShowParser parser = new GetShowParser(); - - return parser.parse(response.body().byteStream()); - } else { - return null; - } - } catch (IOException e) { - Log.w(TAG, e); - return null; - } - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/tvdb/GetShowParser.java b/app/src/main/java/org/jamienicol/episodes/tvdb/GetShowParser.java deleted file mode 100644 index ae92e790..00000000 --- a/app/src/main/java/org/jamienicol/episodes/tvdb/GetShowParser.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2012-2014 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.tvdb; - -import android.sax.Element; -import android.sax.EndElementListener; -import android.sax.EndTextElementListener; -import android.sax.RootElement; -import android.sax.StartElementListener; -import android.util.Log; -import java.io.InputStream; -import java.io.IOException; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import org.xml.sax.Attributes; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; - -class GetShowParser -{ - private static final String TAG = "GetShowParser"; - - // show which is being parsed - Show show; - - // episode which is currently being parsed - Episode episode; - - public Show parse(InputStream inputStream) { - - try { - InputSource inputSource = new InputSource(inputStream); - XMLReader xmlReader = XMLReaderFactory.createXMLReader("org.xmlpull.v1.sax2.Driver"); - - RootElement rootElement = new RootElement("Data"); - - Element seriesElement = rootElement.requireChild("Series"); - - Element idElement = seriesElement.requireChild("id"); - idElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - int id = Integer.parseInt(body); - - Log.i(TAG, String.format("Parsed show ID: %d", id)); - show.setId(id); - } - }); - - Element nameElement = seriesElement.requireChild("SeriesName"); - nameElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, String.format("Parsed show name: %s", body)); - show.setName(body); - } - }); - - Element overviewElement = seriesElement.getChild("Overview"); - overviewElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, - String.format("Parsed show overview: %s", body)); - show.setOverview(body); - } - }); - - Element firstAiredElement = seriesElement.getChild("FirstAired"); - firstAiredElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - try { - DateFormat df = new SimpleDateFormat("yyyy-MM-dd", - Locale.US); - Date firstAired = df.parse(body); - - Log.i(TAG, - String.format("Parsed show first aired date: %s", - firstAired.toString())); - show.setFirstAired(firstAired); - - } catch (ParseException e) { - Log.w(TAG, "Error parsing first aired date: " + e.toString()); - show.setFirstAired(null); - } - } - }); - - Element bannerPathElement = seriesElement.getChild("banner"); - bannerPathElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, - String.format("Parsed show banner path: %s", body)); - show.setBannerPath(body); - } - }); - - final Element fanartPathElement = seriesElement.getChild("fanart"); - fanartPathElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, - String.format("Parsed show fanart path: %s", body)); - show.setFanartPath(body); - } - }); - - final Element posterPathElement = seriesElement.getChild("poster"); - posterPathElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, - String.format("Parsed show poster path: %s", body)); - show.setPosterPath(body); - } - }); - - Element episodeElement = rootElement.getChild("Episode"); - episodeElement.setStartElementListener(new StartElementListener() { - public void start(Attributes attributes) { - Log.i(TAG, "Begin parsing episode"); - episode = new Episode(); - } - }); - episodeElement.setEndElementListener(new EndElementListener() { - public void end() { - Log.i(TAG, "End parsing episode"); - show.getEpisodes().add(episode); - episode = null; - } - }); - - Element episodeIdElement = episodeElement.requireChild("id"); - episodeIdElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - int id = Integer.parseInt(body); - - Log.i(TAG, String.format("Parsed episode ID: %d", id)); - episode.setId(id); - } - }); - - Element episodeNameElement = episodeElement.getChild("EpisodeName"); - episodeNameElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, String.format("Parsed episode name: %s", body)); - episode.setName(body); - } - }); - - Element episodeOverviewElement = episodeElement.getChild("Overview"); - episodeOverviewElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, - String.format("Parsed episode overview: %s", body)); - episode.setOverview(body); - } - }); - - Element episodeEpisodeNumberElement = episodeElement.getChild("EpisodeNumber"); - episodeEpisodeNumberElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - int episodeNumber = Integer.parseInt(body); - - Log.i(TAG, - String.format("Parsed episode episode number: %d", - episodeNumber)); - episode.setEpisodeNumber(episodeNumber); - } - }); - - Element episodeSeasonNumberElement = episodeElement.getChild("SeasonNumber"); - episodeSeasonNumberElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - int seasonNumber = Integer.parseInt(body); - - Log.i(TAG, - String.format("Parsed episode season number: %d", - seasonNumber)); - episode.setSeasonNumber(seasonNumber); - } - }); - - Element episodeFirstAiredElement = episodeElement.getChild("FirstAired"); - episodeFirstAiredElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - try { - DateFormat df = new SimpleDateFormat("yyyy-MM-dd", - Locale.US); - Date firstAired = df.parse(body); - - Log.i(TAG, - String.format("Parsed episode first aired date: %s", - firstAired.toString())); - episode.setFirstAired(firstAired); - - } catch (ParseException e) { - Log.w(TAG, "Error parsing first aired date: " + e.toString()); - episode.setFirstAired(null); - } - } - }); - - xmlReader.setContentHandler(rootElement.getContentHandler()); - - show = new Show(); - show.setEpisodes(new LinkedList()); - xmlReader.parse(inputSource); - - return show; - - } catch (SAXException e) { - Log.w(TAG, "SAXException - parse: " + e.toString()); - return null; - } catch (IOException e) { - Log.w(TAG, "IOException - parse: " + e.toString()); - return null; - } - } -} diff --git a/app/src/main/java/org/jamienicol/episodes/tvdb/SearchShowsParser.java b/app/src/main/java/org/jamienicol/episodes/tvdb/SearchShowsParser.java deleted file mode 100644 index 0bac4dce..00000000 --- a/app/src/main/java/org/jamienicol/episodes/tvdb/SearchShowsParser.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2012 Jamie Nicol - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.jamienicol.episodes.tvdb; - -import android.sax.Element; -import android.sax.EndElementListener; -import android.sax.EndTextElementListener; -import android.sax.RootElement; -import android.sax.StartElementListener; -import android.util.Log; -import java.io.InputStream; -import java.io.IOException; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Locale; -import org.xml.sax.Attributes; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.XMLReaderFactory; - -class SearchShowsParser -{ - private static final String TAG = "SearchShowsParser"; - - // show which is currently being parsed - Show current; - - // shows which have finished being parsed - List parsed; - - public List parse(InputStream inputStream) { - - try { - InputSource inputSource = new InputSource(inputStream); - XMLReader xmlReader = XMLReaderFactory.createXMLReader("org.xmlpull.v1.sax2.Driver"); - - RootElement rootElement = new RootElement("Data"); - Element seriesElement = rootElement.getChild("Series"); - seriesElement.setStartElementListener(new StartElementListener() { - public void start(Attributes attributes) { - Log.i(TAG, "Begin parsing show"); - current = new Show(); - } - }); - seriesElement.setEndElementListener(new EndElementListener() { - public void end() { - Log.i(TAG, "End parsing show"); - parsed.add(current); - current = null; - } - }); - - Element idElement = seriesElement.requireChild("id"); - idElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - int id = Integer.parseInt(body); - - Log.i(TAG, String.format("Parsed ID: %d", id)); - current.setId(id); - } - }); - - Element nameElement = seriesElement.requireChild("SeriesName"); - nameElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, String.format("Parsed name: %s", body)); - current.setName(body); - } - }); - - Element overviewElement = seriesElement.getChild("Overview"); - overviewElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - Log.i(TAG, String.format("Parsed overview: %s", body)); - current.setOverview(body); - } - }); - - Element firstAiredElement = seriesElement.getChild("FirstAired"); - firstAiredElement.setEndTextElementListener(new EndTextElementListener() { - public void end(String body) { - try { - DateFormat df = new SimpleDateFormat("yyyy-MM-dd", - Locale.US); - Date firstAired = df.parse(body); - - Log.i(TAG, String.format("Parsed first aired date: %s", - firstAired.toString())); - current.setFirstAired(firstAired); - - } catch (ParseException e) { - Log.w(TAG, "Error parsing first aired date: " + e.toString()); - current.setFirstAired(null); - } - } - }); - - xmlReader.setContentHandler(rootElement.getContentHandler()); - - current = null; - parsed = new LinkedList(); - xmlReader.parse(inputSource); - - return parsed; - - } catch (SAXException e) { - Log.w(TAG, "SAXException - parse: " + e.toString()); - return null; - } catch (IOException e) { - Log.w(TAG, "IOException - parse: " + e.toString()); - return null; - } - } -} diff --git a/app/src/main/res/drawable-hdpi/ic_show_starred.png b/app/src/main/res/drawable-hdpi/ic_show_starred.png deleted file mode 100644 index 86674448..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_show_starred.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_show_unstarred.png b/app/src/main/res/drawable-hdpi/ic_show_unstarred.png deleted file mode 100644 index 0fefae71..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_show_unstarred.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_shows_list_starred.png b/app/src/main/res/drawable-hdpi/ic_shows_list_starred.png deleted file mode 100644 index a593e6a7..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_shows_list_starred.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_shows_list_unstarred.png b/app/src/main/res/drawable-hdpi/ic_shows_list_unstarred.png deleted file mode 100644 index f6670329..00000000 Binary files a/app/src/main/res/drawable-hdpi/ic_shows_list_unstarred.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/shows_list_gradient.xml b/app/src/main/res/drawable-hdpi/shows_list_gradient.xml new file mode 100644 index 00000000..398c2358 --- /dev/null +++ b/app/src/main/res/drawable-hdpi/shows_list_gradient.xml @@ -0,0 +1,8 @@ + + + diff --git a/app/src/main/res/drawable-mdpi/ic_show_starred.png b/app/src/main/res/drawable-mdpi/ic_show_starred.png deleted file mode 100644 index 603691e9..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_show_starred.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_show_unstarred.png b/app/src/main/res/drawable-mdpi/ic_show_unstarred.png deleted file mode 100644 index f4609b98..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_show_unstarred.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_shows_list_starred.png b/app/src/main/res/drawable-mdpi/ic_shows_list_starred.png deleted file mode 100644 index b5638942..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_shows_list_starred.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_shows_list_unstarred.png b/app/src/main/res/drawable-mdpi/ic_shows_list_unstarred.png deleted file mode 100644 index 2e6d1e50..00000000 Binary files a/app/src/main/res/drawable-mdpi/ic_shows_list_unstarred.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_show_starred.png b/app/src/main/res/drawable-xhdpi/ic_show_starred.png deleted file mode 100644 index 4d24afe7..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_show_starred.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_show_unstarred.png b/app/src/main/res/drawable-xhdpi/ic_show_unstarred.png deleted file mode 100644 index ad3b4efe..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_show_unstarred.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_shows_list_starred.png b/app/src/main/res/drawable-xhdpi/ic_shows_list_starred.png deleted file mode 100644 index 2a8273e7..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_shows_list_starred.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_shows_list_unstarred.png b/app/src/main/res/drawable-xhdpi/ic_shows_list_unstarred.png deleted file mode 100644 index 0f91e22e..00000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_shows_list_unstarred.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_show_starred.png b/app/src/main/res/drawable-xxhdpi/ic_show_starred.png deleted file mode 100644 index f516d813..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_show_starred.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_show_unstarred.png b/app/src/main/res/drawable-xxhdpi/ic_show_unstarred.png deleted file mode 100644 index 2efab925..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_show_unstarred.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_shows_list_starred.png b/app/src/main/res/drawable-xxhdpi/ic_shows_list_starred.png deleted file mode 100644 index 4395ee39..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_shows_list_starred.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_shows_list_unstarred.png b/app/src/main/res/drawable-xxhdpi/ic_shows_list_unstarred.png deleted file mode 100644 index 2603f8f5..00000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_shows_list_unstarred.png and /dev/null differ diff --git a/app/src/main/res/drawable/archived_toggle.xml b/app/src/main/res/drawable/archived_toggle.xml new file mode 100644 index 00000000..fea721ea --- /dev/null +++ b/app/src/main/res/drawable/archived_toggle.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_show_archived.xml b/app/src/main/res/drawable/ic_show_archived.xml new file mode 100644 index 00000000..f18c6a1b --- /dev/null +++ b/app/src/main/res/drawable/ic_show_archived.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_show_starred.xml b/app/src/main/res/drawable/ic_show_starred.xml new file mode 100644 index 00000000..3c3df63c --- /dev/null +++ b/app/src/main/res/drawable/ic_show_starred.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_show_unarchived.xml b/app/src/main/res/drawable/ic_show_unarchived.xml new file mode 100644 index 00000000..9f89aa54 --- /dev/null +++ b/app/src/main/res/drawable/ic_show_unarchived.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_show_unstarred.xml b/app/src/main/res/drawable/ic_show_unstarred.xml new file mode 100644 index 00000000..88dd5a3c --- /dev/null +++ b/app/src/main/res/drawable/ic_show_unstarred.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/starred_toggle.xml b/app/src/main/res/drawable/starred_toggle.xml index 00eb4ba6..82ea142f 100644 --- a/app/src/main/res/drawable/starred_toggle.xml +++ b/app/src/main/res/drawable/starred_toggle.xml @@ -3,8 +3,8 @@ xmlns:android="http://schemas.android.com/apk/res/android" > + android:drawable="@drawable/ic_show_starred" /> + android:drawable="@drawable/ic_show_unstarred" /> diff --git a/app/src/main/res/layout/about_activity.xml b/app/src/main/res/layout/about_activity.xml index bf804d72..92695331 100644 --- a/app/src/main/res/layout/about_activity.xml +++ b/app/src/main/res/layout/about_activity.xml @@ -34,7 +34,14 @@ + diff --git a/app/src/main/res/layout/add_show_search_activity.xml b/app/src/main/res/layout/add_show_search_activity.xml index 4e411c1a..95bba716 100644 --- a/app/src/main/res/layout/add_show_search_activity.xml +++ b/app/src/main/res/layout/add_show_search_activity.xml @@ -3,7 +3,27 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" - android:layout_height="match_parent" > + android:layout_height="match_parent"> + + + + + - diff --git a/app/src/main/res/layout/episode_details_fragment.xml b/app/src/main/res/layout/episode_details_fragment.xml index c36770d2..f4f9e5a1 100644 --- a/app/src/main/res/layout/episode_details_fragment.xml +++ b/app/src/main/res/layout/episode_details_fragment.xml @@ -1,5 +1,5 @@ - - + diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 9607d557..33a7518b 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" > diff --git a/app/src/main/res/layout/seasons_list_fragment.xml b/app/src/main/res/layout/seasons_list_fragment.xml index a843a9f5..baf2ecc6 100644 --- a/app/src/main/res/layout/seasons_list_fragment.xml +++ b/app/src/main/res/layout/seasons_list_fragment.xml @@ -1,5 +1,5 @@ - + android:layout_height="match_parent" + android:id="@+id/settings_layout"> diff --git a/app/src/main/res/layout/show_activity.xml b/app/src/main/res/layout/show_activity.xml index 87db6ed7..67109e84 100644 --- a/app/src/main/res/layout/show_activity.xml +++ b/app/src/main/res/layout/show_activity.xml @@ -1,17 +1,17 @@ - - - - - + - - + - - + diff --git a/app/src/main/res/layout/show_details_fragment.xml b/app/src/main/res/layout/show_details_fragment.xml index b4fe18bd..7649e9c5 100644 --- a/app/src/main/res/layout/show_details_fragment.xml +++ b/app/src/main/res/layout/show_details_fragment.xml @@ -1,5 +1,5 @@ - @@ -20,4 +20,4 @@ android:textAppearance="?android:attr/textAppearance" android:padding="8dp"/> - + diff --git a/app/src/main/res/layout/show_notes_fragment.xml b/app/src/main/res/layout/show_notes_fragment.xml index 520ac494..33f957e1 100644 --- a/app/src/main/res/layout/show_notes_fragment.xml +++ b/app/src/main/res/layout/show_notes_fragment.xml @@ -1,5 +1,5 @@ - @@ -11,4 +11,4 @@ android:autoLink="all" android:gravity="top" android:padding="4dp"/> - + diff --git a/app/src/main/res/layout/shows_list_fragment.xml b/app/src/main/res/layout/shows_list_fragment.xml index a9f0473a..a6514dcb 100644 --- a/app/src/main/res/layout/shows_list_fragment.xml +++ b/app/src/main/res/layout/shows_list_fragment.xml @@ -8,8 +8,7 @@ android:id="@id/android:list" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="2dp" - android:clipToPadding="false" + android:clipToPadding="true" android:divider="@null" android:scrollbarStyle="outsideOverlay" /> diff --git a/app/src/main/res/layout/shows_list_item.xml b/app/src/main/res/layout/shows_list_item.xml index c03a31af..c298ab3a 100644 --- a/app/src/main/res/layout/shows_list_item.xml +++ b/app/src/main/res/layout/shows_list_item.xml @@ -2,25 +2,37 @@ + android:paddingLeft="-1dp" + android:paddingRight="-1dp"> - + + + + + android:layout_below="@id/name_banner_frame"> diff --git a/app/src/main/res/menu/main.xml b/app/src/main/res/menu/main.xml index 28845e46..709f7bb8 100644 --- a/app/src/main/res/menu/main.xml +++ b/app/src/main/res/menu/main.xml @@ -6,8 +6,8 @@ android:id="@+id/menu_add_new_show" android:icon="@drawable/ic_menu_add_show" android:title="@string/menu_add_new_show" - episodes:showAsAction="ifRoom|withText|collapseActionView" - episodes:actionViewClass="android.support.v7.widget.SearchView" /> + episodes:showAsAction="ifRoom|collapseActionView" + episodes:actionViewClass="androidx.appcompat.widget.SearchView" /> + + + 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 100755 index 00000000..e2c420d6 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ 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 old mode 100644 new mode 100755 index ff10861e..5f9ed3e3 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100755 index 00000000..0a673ba4 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..90722c92 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png old mode 100644 new mode 100755 index afc48238..a12cde89 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100755 index 00000000..afd4c770 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..fd92fee8 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png old mode 100644 new mode 100755 index 834b1025..4be4a27e Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100755 index 00000000..e4a51344 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..5d7626d1 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png old mode 100644 new mode 100755 index a998b1bf..e2aa383b Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100755 index 00000000..78bf8153 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..d86cb12f Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.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 100755 index 00000000..90ead600 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_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100755 index 00000000..713157c5 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100755 index 00000000..af231731 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 00000000..74542c2a --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,63 @@ + + + Филтърът показва + Звезден + Относно + Изтриване на шоуто + Епизоди + Премахване на звезди + Звездна спектакция + Настройки + Възстановяване от архивиране + Обновяване на шоуто + Опресняване на всички предавания + Маркирай показването като гледано + Маркирай като неподредено + Марк сезон, както беше наблюдаван + Маркиране на сезон като необгледан + Предстоящи + Ход + Архивирани + Всички + Добавяне на показване към библиотеката + Добавяне на ново шоу към библиотеката + Търсене на ново шоу + За + Първо излъчено: %s + Не може да се добави %s в библиотеката + Архивирането е завършено: \'%s\" + Не може да се архивира библиотеката + Версия %s (Къмит на %s) + Интерфейс + Серия за автоматично обновяване + Уведомяване при синхронизиране показва + Показване на синхронизирането + %1$d от %2$d епизоди, наблюдавани + Гледани + (+ %d предстоящи) + Преглед + Бележки + Следващата + %s вече в библиотека + %s добавен към библиотеката + Настройки + Специални + Сезон %d + "%1$02dx%2$02d - " + Библиотеката е възстановена + Не може да се възстанови библиотеката + Възстановяване от архив + Не са намерени архиви в папка \'%s\'. + Показва най-новия сезон + Обратен ред на сортиране + Език + Само при неомерени връзки + Автоматично обновяване + Интервал + Премахни показването от архива + Архивно шоу + Епизодите са софтуер за GPLv3+. Моля, пиши bugs, писане на код, превеждайте или направете всичко друго, което можете да направите, за да помогнете! + Следете кои епизоди сте гледали от любимите си предавания + Архивиране на библиотека + Добавяне %s към библиотеката + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 00000000..dc6699f2 --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,5 @@ + + + Sledujte, které epizody jste sledovali z vašich oblíbených pořadů + Lägg till %s i biblioteket + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..e04987fd --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,63 @@ + + + Verfolge, welche Episoden Deiner Lieblingssendungen Du gesehen hast + Benutzeroberfläche + Serien automatisch aktualisieren + %1$d von %2$d Folgen gesehen + Gesehen + (+ %d angekündigt) + Übersicht + Nächste + Folgen + %s bereits in der Bibliothek + %s zur Bibliothek hinzugefügt + Einstellungen + Specials + Staffel %d + "%1$02dx%2$02d - " + Bibliothek wiederhergestellt + Bibliothek konnte nicht wiederhergestellt werden + Aus einer Sicherung wiederherstellen + Keine Datensicherungen im Ordner „%s“ gefunden + Neueste Staffel zuerst + Reihenfolge umkehren + Sprache + Nur bei nicht gemessenen Verbindungen + Automatisch aktualisieren + Intervall + Serie aus Archiv entfernen + Serie archivieren + Serie aus Favoriten entfernen + Serie favorisieren + Einstellungen + Datensicherung wiederherstellen + Serie aktualisieren + Serien aktualisieren + Folge als gesehen markieren + Folge als ungesehen markieren + Staffel als gesehen markieren + Staffel als ungesehen markieren + Angekündigte + Angefangene + Archivierte + Favoriten + Alle + Serien filtern + Serie löschen + Datensicherung erstellen + Serie hinzufügen + Serie hinzufügen + Neue Serie suchen + Über + Erstausstrahlung: %s + %s konnte nicht zur Bibliothek hinzugefügt werden + Datensicherung fertiggestellt: „%s“ + Datensicherung der Bibliothek konnte nicht erstellt werden + %s wird zur Bibliothek hinzugefügt + Über + Episodes ist freie Software unter GPLv3+. Hilf gern mit, indem Du Fehler meldest, Code oder Übersetzungen schreibst! + Synchronisierung anzeigen + Benachrichtigung beim Synchronisieren von Sendungen + Anmerkungen + Version %s (Commit %s) + \ No newline at end of file diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml new file mode 100644 index 00000000..33f2a923 --- /dev/null +++ b/app/src/main/res/values-eo/strings.xml @@ -0,0 +1,63 @@ + + + Fasado + %1$d el %2$d epizodoj spektiĝis + Spektita + Superrigardo + Notoj + Epizodoj + %s jam en biblioteko + %s aldonita al biblioteko + Agordoj + Sezono %d + Biblioteko restaŭriĝis + Ne povis restaŭri bibliotekon + Restaŭri savkopion + Neniu savkopio troviĝis en la dosierujo \'%s\'. + Lingvo + Elarkivigi spektaĵon + Enarkivigi spektaĵon + Malmarki kiel plej ŝatatan + Marki kiel plej ŝatatan + Agordoj + Marki spektaĵon kiel spektitan + Marki spektaĵon kiel nespektitan + Marki sezonon kiel spektita + Marki sezonon kiel nespektita + En arkivo + Plej ŝatataj + Ĉiuj + Forigi spektaĵon + Savkopii bibliotekon + Aldoni spektaĵon al biblioteko + Aldoni novan spektaĵon al biblioteko + Serĉi novan spektaĵon + Pri + Ne povis aldoni %s al biblioteko + Savkopio kompleta: \'%s\' + Ne povis savkopii bibliotekon + Aldonante %s al biblioteko + Pri + Filtri spektaĵojn + Versio %s (Enmeto %s) + Specialaĵoj + "%1$02d×%2$02d - " + Intervalo + Prezentata + Prezentota + (+ %d prezentota(j)) + Unua Prezentado: %s + Sciigo pri sinkronigo de spektaĵoj + Reŝargi spektaĵon + Reŝargi ĉiujn spektaĵojn + Episodes estas kopilasita libera programo sub la permesiloj GPLv3+. Bonvolu raporti cimojn, skribi kodojn, traduki aŭ helpi ĉiel ajn! + Marku la spektitajn epizodojn de viaj plej ŝatataj spektaĵoj + Aŭtomate reŝargi spektaĵon + Sekva + Sciigi pri sinkronigo + Nur per nemezurataj retkonektoj + Aŭtomate reŝargata + Montri unue la plej novan sezonon + Mala ordo + Restaŭri savkopion + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 00000000..f9a15986 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,63 @@ + + + (+ %d próximamente) + Descripción + Sólo en conexiones no medidas + Próximamente + En emisión + Emitida por primera vez: %s + Episodes es software libre copyleft bajo licencia GPLv3+. ¡Por favor registra bugs, escribe código, traduce o haz cualquier cosa que puedas para ayudar! + Mantén un registro de qué episodios de tus series favoritas has visto + Versión %s (Commit %s) + Interfaz + Actualizar series automáticamente + Notificación cuando se sincronizan series + Sincronizar serie + %1$d de %2$d episodios vistos + Visto + Notas + Siguiente + Episodios + %s ya está en la biblioteca + %s añadida a la biblioteca + Ajustes + Especiales + Temporada %d + "%1$02dx%2$02d - " + Biblioteca restaurada + No se ha podido restaurar la biblioteca + Restaurar desde copia de seguridad + No se han encontrado copias de seguridad en la carpeta \'%s\'. + Muestra primero la temporada más nueva + Orden inverso + Lenguaje + Actualización automática activada + Intervalo + Quitar serie del archivo + Archivar serie + No destacar serie + Destacar serie + Ajustes + Restaurar desde copia de seguridad + Actualizar serie + Actualizar todas las series + Marcar serie como vista + Marcar serie como no vista + Marcar temporada como vista + Marcar temporada como no vista + Archivadas + Destacadas + Todas + Filtrar series + Borrar serie + Realizar copia de seguridad de la biblioteca + Añadir serie a la biblioteca + Añadir nueva serie a la biblioteca + Buscar una nueva serie + Acerca de + No se ha podido añadir %s a la biblioteca + Copia de seguridad completada: \'%s\' + No se ha podido realizar la copia de seguridad de la biblioteca + Añadiendo %s a la biblioteca + Acerca de + \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml new file mode 100644 index 00000000..34d0f397 --- /dev/null +++ b/app/src/main/res/values-eu/strings.xml @@ -0,0 +1,63 @@ + + + Bertsioa %s (Egin %s) + Interfazea + Automatikoki freskatu serieak + Ikuskizunak sinkronizatzean jakinarazpena + Erakutsi sinkronizazioa + %1$d of %2$d ikusitako pasarteak + Ikusita + (+ %d datozenak) + Ikuspegi orokorra + Oharrak + Hurrengoa + Episodes + %s dagoeneko liburutegian + %s liburutegian gehitu da + Ezarpenak + Bereziak + Denboraldia %d + "%1$02dx%2$02d - " + Liburutegia zaharberritu da + Ezin izan da liburutegia leheneratu + Leheneratu segurtasun kopiatik + Ez da babeskopiarik aurkitu \'%s\' karpetan. + Denboraldi berriena erakusten du lehenengo + Alderantziz ordenatzeko ordena + Hizkuntza + Meditu gabeko konexioetan soilik + Freskatze automatikoa aktibatuta + Tartea + Kendu ikuskizuna artxibotik + Artxibo ikuskizuna + Erakutsi izarra + Izar ikuskizuna + Ezarpenak + Leheneratu segurtasun kopiatik + Freskatu ikuskizuna + Freskatu ikuskizun guztiak + Markatu ikuskizuna ikusi bezala + Markatu ikuskizuna ikusi gabeko moduan + Markatu denboraldia ikusitako moduan + Markatu denboraldia ikusi gabeko moduan + Datozenak + Martxan + Artxibatuta + Izarrak + Guztiak + Iragazkien ikuskizunak + Ezabatu ikuskizuna + Egin liburutegiaren babeskopia + Gehitu ikuskizuna liburutegian + Gehitu ikuskizun berria liburutegian + Bilatu ikuskizun berria + Buruz + Lehenengo igorpena: %s + Ezin izan da %s gehitu liburutegian + Babeskopia osatua: \'%s\' + Ezin izan da liburutegiaren segurtasun kopia egin + liburutegian %s gehitzen + Buruz + Episodes copylefted libre software lizentziadun GPLv3 + da. Mesedez, bidali akatsak, idatzi kodea, itzuli edo egin ezazu laguntzeko duzun guztia! + Jarrai itzazu zure saio gogokoenen atalak ikusi dituzunak + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 00000000..d113f502 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,63 @@ + + + À propos + Première diffusion : %s + À propos + Ajout de %s à la bibliothèque + Episodes est un logiciel libre sous licence GPLv3+. Vous pouvez signaler des erreurs, écrire du code, traduire ou faire quoi que ce soit d\'autre pour contribuer ! + Gardez une trace des épisodes que vous avez regardés dans vos séries préférées + Ajouter la série à la bibliothèque + Ajouter une nouvelle série à la bibliothèque + Rechercher une nouvelle série + Impossible d\'ajouter %s à la bibliothèque + Sauvegarde terminée : « %s » + Impossible de sauvegarder la bibliothèque + Interface + Actualiser automatiquement les séries + Notification lors de la synchronisation des séries + Synchronisation de la série + %1$d épisode sur %2$d regardé + Regardé + (+ %d à venir) + Aperçu + Notes + Suivant + Épisodes + %s déjà dans la bibliothèque + %s ajouté à la bibliothèque + Paramètres + Spécial + Saison %d + "%1$02dx%2$02d - " + Bibliothèque restaurée + Impossible de restaurer la bibliothèque + Restaurer à partir d\'une sauvegarde + Aucune sauvegarde trouvée dans le dossier « %s ». + Affiche la dernière saison en premier + Inverser l\'ordre de tri + Langue + Seulement sur les connexions non limitées + Actualisation auto activée + Intervale + Retirer la série des archives + Archiver la série + Retirer la série des favoris + Ajouter la série aux favoris + Paramètres + Restaurer une sauvegarde + Actualiser la série + Actualiser toutes les séries + Marquer la série comme regardée + Marquer la série comme non regardée + Marquer la saison comme regardée + Marquer la saison comme non regardée + À venir + En cours + Archivé + Favoris + Tout + Filtrer les séries + Supprimer la série + Restaurer la bibliothèque + %s verzió (Commit %s) + \ No newline at end of file diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml new file mode 100644 index 00000000..36847e2b --- /dev/null +++ b/app/src/main/res/values-hr/strings.xml @@ -0,0 +1,63 @@ + + + Prikaži sinkronizaciju + Označi sezonu kao pogledanu + Filtriraj emisije + Odznači emisiju + Obnovi iz sigurnosne kopije + Sezona %d + Automatsko aktualiziranje uključeno + Označi emisiju + Označi emisiju kao pogledanu + Prvi put emitirano: %s + (+ %d predstojeće) + Spremi sigurnosnu kopiju biblioteke + Ukloni emisjiu iz arhive + Prati stanja gledanja epizoda tvojih omiljenih emisija + Emisija „%s” je dodana u biblioteku + Dodaj novu emisiju u biblioteku + Obrni redoslijed + Pogledano + Epizode + Obnovi iz sigurnosne kopije + Aktualiziraj emisiju + Predstojeće + Neuspjelo spremanje sigurnosne kopije biblioteke + „Epizode” je copyleft slobodni softver s licencom GPLv3+. Prijavi greške, programiraj, prevodi ili pomogni na bilo koji drugi način! + Pregled + Emisija „%s” se već nalazi u biblioteci + Prikazuje najnoviju sezonu na prvom mjestu + Arhivirane + Napomene + Sljedeća + Postavke + Neuspjelo obnavljanje biblioteke + Jezik + Traži novu emisiju + Informacije + Nadolazeće + Označi emisiju kao nepogledanu + Obavijesti pri sinkronizaciji emisija + Aktualiziraj sve emisije + Informacije + Pogledanih epizoda: %1$d od %2$d + Biblioteka obnovljena + Postavke + Označene + Označi sezonu kao nepogledanu + Izbriši emisiju + Samo na vezama bez ograničenja + Interval + Arhiviraj emisiju + Sučelje + Emisija „%s” se dodaje u biblioteku + Neuspjelo dodavanje emisije „%s” u biblioteku + Sve + Dodaj emisiju u biblioteku + Automatsko aktualiziranje serija + Specijalne + Sigurnosna kopija gotova: „%s” + U mapi „%s” nema sigurnosnih kopija. + "%1$02d × %2$02d - " + Verzija %s (Izmjena %s) + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 00000000..3fefd3df --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,63 @@ + + + Aggiunta di %s alla libreria + Informazioni + Episodes è un software libero con copyleft di licenza GPLv3+. Per favore, segnalate errori, scrivete codice, traducete o fate qualsiasi altra cosa per aiutare! + Tieni traccia di quali episodi hai guardato dei tuoi programmi preferiti + Versione %s (Commit %s) + Interfaccia + Aggiornamento automatico serie + Notifica quando le serie si sincronizzano + Sincronizzazione della serie + %1$d di %2$d episodi guardati + Guardato + (+ %d prossimamente) + Panoramica + Note + Successivo + Episodes + %s già nella raccolta + %s aggiunto alla raccolta + Impostazioni + Speciali + Stagione %d + "%1$02dx%2$02d - " + Raccolta ripristinata + Non è stato possibile ripristinare la raccolta + Ripristina da un backup + Nessun backup trovato nella cartella «%s». + Visualizza prima la stagione più recente + Invertisci l\'ordine di ordinamento + Lingua + Solo con connessioni non misurate + Aggiornamento automatico abilitato + Periodicità + Rimuovi la serie dall\'archivio + Archivia la serie + Rimuovi la serie dai favoriti + Aggiungi la serie ai favoriti + Impostazioni + Ripristino da backup + Aggiorna la serie + Aggiorna tutte le serie + Segna la serie come guardata + Segna come non guardato + Segna la stagione come guardato + Segna la stagione come non guardato + Prossimamente + In corso + Archiviato + Stellato + Tutto + Filtra le serie + Elimina la serie + Backup della raccolta + Aggiungi la serie alla raccolta + Aggiungi una nuova serie alla raccolta + Cerca una nuova serie + Informazioni + Prima messa in onda: %s + Impossibile aggiungere %s alla raccolta + Backup completato: «%s» + Impossibile eseguire il backup della raccolta + \ No newline at end of file diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml new file mode 100644 index 00000000..e51e86fc --- /dev/null +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,63 @@ + + + Grensesnitt + Auto-gjenoppfrisk serier + Merknad ved synkronisering av programmer + Vis synkronisering + %1$d av %2$d episoder sett + Sett + (+ %d kommende) + Oversikt + Notater + Neste + Episoder + %s allerede i bibliotek + %s lagt til i bibliotek + Innstillinger + Sesong %d + Spesialsendinger + "%1$02dx%2$02d - " + Bibliotek gjenopprettet + Klarte ikke å gjenopprette bibliotek + Gjenopprett fra sikkehetskopi + Fant ingen sikkerhetskopier i «%s»-mappen. + Vis nyeste sesong først + Reverser sorteringsrekkefølge + Språk + Kun på ubegrensede tilkoblinger + Auto-gjenoppfrisking påskrudd + Intervall + Fjern program fra arkiv + Arkiver program + Fjern stjernemerking + Stjernemerk program + Innstillinger + Gjenopprett fra sikkerhetskopi + Gjenoppfrisk program + Gjenoppfrisk alle programmer + Marker program som sett + Marker program som usett + Marker sesong som sett + Marker sesong som usett + Kommende + Underveis + Arkivert + Stjernemerkede + Alle + Filtrer programmer + Slett program + Sikkerhetskopier bibliotek + Legg til program i bibliotek + Legg til nytt program i bibliotek + Søk etter nytt program + Om + Først sendt: %s + Klarte ikke å legge til %s i biblioteket + Sikkerhetskopi utført: «%s» + Klarte ikke å sikkerhetskopiere bibliotek + Legger til %s i biblioteket + Om + Episodes er gemenlig fri programvare lisensiert GPLv3+. Send inn feil, skriv kode, oversett og gjør hva du kan for å hjelpe til. + Hold orden på hvilke episoder du har sett av dine favoritt-programmer + Versjon %s (innsendelse %s) + \ 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 new file mode 100644 index 00000000..9dbe58a8 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,63 @@ + + + Интерфейс + Автообновление серий + Уведомление при синхронизации шоу + Показать синхронизацию + %1$d из %2$d эпизодов просмотрено + Просмотрено + (+ %d предстоящие) + Обзор + Примечания + Следующий + Эпизоды + %s уже в библиотеке + %s добавлен в библиотеку + Настройки + Specials + Сезон %d + "%1$02dx%2$02d - " + Библиотека восстановлена + Не удалось восстановить библиотеку + Восстановить из резервной копии + В папке \'%s\' нет резервных копий. + Отображать сначала новейший сезон + Обратный порядок сортировки + Язык + Только на безлимитных соединениях + Автоматическое обновление + Интервал + Удалить из избранного + Удалить шоу из архива + Добавить в архив + Избранные + Добавить в избранное + Настройки + Восстановить из резервной копии + Обновить шоу + Обновить все шоу + Отметить шоу как просмотренное + Отметить шоу как непросмотренное + Отметить сезон как просмотренный + Отметить сезон как непросмотренный + Предстоящие + В процессе + Архивные + Все + Фильтр шоу + Удалить шоу + Резервное копирование библиотеки + Добавить шоу в библиотеку + Добавить новое шоу в библиотеку + Поиск нового шоу + О приложении + Первый эфир: %s + Не удалось добавить %s в библиотеку + Резервное копирование завершено: \'%s\' + Не удалось создать резервную копию библиотеки + Добавление %s в библиотеку + О приложении + Episodes - это бесплатное программное обеспечение с авторским левом под лицензией GPLv3 +. Пожалуйста, сообщайте об ошибках, пишите код, переводите или делайте что-нибудь ещё, чтобы помочь! + Следите за тем, какие серии любимых шоу вы смотрели + Версия %s (Изменение %s) + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 00000000..4c859348 --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,63 @@ + + + Version %s (åtagande %s) + Gränssnitt + %1$d av %2$d avsnitt sedda + "%1$02dx%2$02d - " + Avisering när program synkroniseras + Programsynkronisering + Sedda + Överblick + Anteckningar + Nästa + Avsnitt + %s redan i biblioteket + %s lades till i biblioteket + Inställningar + Säsong %d + Biblioteket återställdes + Kunde inte återställa bibliotek + Återställ från säkerhetskopia + Inga säkerhetskopior hittades i mappen \'%s\'. + Visar den nyaste säsongen först + Omvänd sorteringsordning + Språk + Endast på obegränsade anslutningar + Automatisk uppdatering på + Intervall + Ta bort program från arkiv + Arkivera program + Sluta stjärnmarkera program + Stjärnmarkera program + Inställningar + Återställ från säkerhetskopia + Uppdatera program + Uppdatera alla program + Kommande + Pågående + Arkiverade + Stjärnmärkta + Alla + Filtrera program + Ta bort program + Säkerhetskopiera biblioteket + Lägg till program i biblioteket + Lägg till program i biblioteket + Sök efter nytt program + Om + Sändes först: %s + Kunde inte lägga till %s i biblioteket + Säkerhetskopieringen färdig: \'%s\' + Kunde inte säkerhetskopiera biblioteket + Lägger till %s i biblioteket + Om + (+ %d kommande) + Håll koll på vilka avsnitt du har tittat på av dina favoritprogram + Episodes är copylefted libre software licensierad GPLv3+. Var snäll och skicka in felrapporter, skriv kod, översätt eller gör vad du kan för att hjälpa till! + Markera säsong som osedd + Markera säsong som sedd + Markera program som osett + Markera program som sett + Specialavsnitt + Automatisk uppdatering av serier + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 00000000..0cc73f69 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,63 @@ + + + "%1$02dx%2$02d - " + Sürüm %s (Kaydetme %s) + Arayüz + Dizileri otomatik yenile + Şovları senkronize ederken bildirim + Senkronizasyonu göster + %2$d bölümden %1$d görüldü + İzlendi + (+%d yakında) + Genel Bakış + Notlar + Sıradaki + Bölümler + %s zaten kitaplıkta + %s kitaplığa eklendi + Ayarlar + Özel + Sezon %d + Kitaplık geri yüklendi + Kitaplık geri yüklenemedi + Yedekten geri yükle + \'%s\' klasöründe yedek bulunamadı. + Önce en yeni sezonu görüntüler + Ters sıralama düzeni + Dil + Yalnızca ölçülmemiş bağlantılarda + Otomatik yenileme açık + Aralık + Diziyi arşivden kaldır + Diziyi arşivle + Gösterinin yıldızını kaldır + Gösteriyi yıldızla + Ayarlar + Yedekten geri yükle + Diziyi yenile + Tüm dizileri yenile + Diziyi izlenmiş olarak işaretle + Diziyi izlenmemiş olarak işaretle + Sezonu izlendi olarak işaretle + Sezonu izlenmemiş olarak işaretle + Yakında + Yolda + Arşivlendi + Yıldızlı + Hepsi + Filtrele + Diziyi sil + Kitaplığı yedekle + Kitaplığa dizi ekle + Kitaplığa yeni dizi ekle + Yeni dizi ara + Hakkında + İlk Yayınlanma Tarihi: %s + %s kitaplığa eklenemedi + Yedekleme tamamlandı: \'%s\' + Kitaplık yedeklenemedi + %s kitaplığa ekleniyor + Hakkında + Episodes, GPLv3+ lisanslı copyleft özgür yazılımdır. Lütfen hataları bildirin, kod yazın, tercüme edin veya yardım etmek için elinizden gelen her şeyi yapın! + En sevdiğiniz dizilerin hangi bölümlerini izlediğinizi takip edin + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 8eec19ea..c90e7af3 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -12,4 +12,54 @@ 168 336 + + Chinese + Croation + Czech + Dansk + Deutsch + English + Español + Français + Greek + Hebrew + Italiano + Japanese + Korean + Magyar + Nederlands + Norsk + Polski + Portuguese + Russian + Slovenian + Suomeksi + Svenska + Turkish + + + zh + hr + cs + da + de + en + es + fr + el + he + it + ja + ko + hu + nl + no + pl + pt + ru + sl + fi + sv + tr + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 70ad0eda..8f47cfa1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,15 +1,15 @@ - Keep track of which episodes you\'ve watched of your favourite TV shows - Episodes is free software licensed under the GNU GPL (version 3 or later). Please file bugs, write code, or do anything else you can to help! + Keep track of which episodes you\'ve watched of your favourite shows + Episodes is copylefted libre software licensed GPLv3+. Please file bugs, write code, translate or do anything else you can to help! About - TV show information is supplied by TheTVDB.com, and is licensed under the Creative Commons Attribution license. Please contribute information or artwork to TheTVDB.com if you can. - http://github.com/jamienicol/episodes - Episodes + This product uses the TMDB API but is not endorsed or certified by TMDB. + https://github.com/red-coracle/episodes + Episodes Adding %s to library - Error backing up library - Back up complete: \'%s\' - Error adding %s to library + Could not back up library + Back-up complete: \'%s\' + Could not add %s to library First Aired: %s About Search for new show @@ -17,13 +17,15 @@ Add show to library Back up library Delete show - All shows Filter shows - Starred shows - Uncompleted shows - Mark season as not watched + All + Starred + Archived + Underway + Upcoming + Mark season as unwatched Mark season as watched - Mark show as not watched + Mark show as unwatched Mark show as watched Refresh all shows Refresh show @@ -31,24 +33,39 @@ Settings Star show Unstar show - Refresh frequency - Auto-refresh library - Refresh only via Wi-Fi - No backups found in folder \'%s\'. + Archive show + Remove show from archive + Interval + Auto-refresh on + Only on unmetered connections + Language + Reverse sorting order + Displays the newest season first + No backups found in \'%s\' folder. Restore from backup - Error restoring library + Could not restore library Library restored "%1$02dx%2$02d - " Season %d Specials Settings %s added to library - %s is already in library + %s already in library Episodes Next Notes Overview (+ %d upcoming) Watched - Watched %1$d of %2$d episodes + %1$d of %2$d episodes seen + Show sync + Notification when syncing shows + Auto-refresh series + Interface + Version %s (Commit %s) + No search results found + Metadata provider has switched to TMDB. We recommend backing up your database before refreshing all metadata. Press Continue to proceed with refreshing all shows or Cancel to return. + Metadata Provider Change + Continue + Cancel diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 2fc2c6ed..099e3ccf 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -1,21 +1,39 @@ - - - - + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 7130c34e..dc4fab06 100644 --- a/build.gradle +++ b/build.gradle @@ -1,14 +1,16 @@ buildscript { repositories { - jcenter() + mavenCentral() + google() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.0' + classpath 'com.android.tools.build:gradle:7.3.0' } } allprojects { repositories { - jcenter() + mavenCentral() + google() } } diff --git a/fastlane/metadata/android/en-US/changelogs/0014.txt b/fastlane/metadata/android/en-US/changelogs/0014.txt new file mode 100644 index 00000000..ca5b8a0e --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0014.txt @@ -0,0 +1,5 @@ +- Upgrade SDK and gradle versions +- Switch from support library to androidx +- Replace universal-image-loader with Glide +- Use newer OkHttp & TVDB client versions +- Fix sync notification/progress diff --git a/fastlane/metadata/android/en-US/changelogs/0015.txt b/fastlane/metadata/android/en-US/changelogs/0015.txt new file mode 100644 index 00000000..5155e7db --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0015.txt @@ -0,0 +1,6 @@ +- Replace deprecated AsyncTask +- Searching and adding shows now uses language preference +- Use poster image if fanart image is unavailable +- Display loading indicator when fetching fanart/poster image +- Add preference categories and use switches instead of checkboxes +- Update dependencies and gradle diff --git a/fastlane/metadata/android/en-US/changelogs/0016.txt b/fastlane/metadata/android/en-US/changelogs/0016.txt new file mode 100644 index 00000000..8a229bbd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0016.txt @@ -0,0 +1,4 @@ +- Add filter for upcoming shows +- Fix app crash when searching for shows +- Add language column to episodes table if it does not exist +- Update dependencies/gradle diff --git a/fastlane/metadata/android/en-US/changelogs/0017.txt b/fastlane/metadata/android/en-US/changelogs/0017.txt new file mode 100644 index 00000000..6532813a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0017.txt @@ -0,0 +1,4 @@ +- Added translations for Croatian, French, German, Norwegian Bokmål, Russian, and Spanish. Many thanks to the contributors on Weblate! +- Add version and commit info to the About screen +- Use the configured language when downloading show data +- Update dependencies/gradle diff --git a/fastlane/metadata/android/en-US/changelogs/0018.txt b/fastlane/metadata/android/en-US/changelogs/0018.txt new file mode 100644 index 00000000..3faf3084 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0018.txt @@ -0,0 +1,2 @@ +- New and updated translations + diff --git a/fastlane/metadata/android/en-US/changelogs/0019.txt b/fastlane/metadata/android/en-US/changelogs/0019.txt new file mode 100644 index 00000000..1da5869b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0019.txt @@ -0,0 +1 @@ +- Fix searching for shows to add diff --git a/fastlane/metadata/android/en-US/changelogs/0020.txt b/fastlane/metadata/android/en-US/changelogs/0020.txt new file mode 100644 index 00000000..b78efb71 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0020.txt @@ -0,0 +1,5 @@ +- Display a message when a search returns no results +- Show a loading spinner when searching for new shows +- When marking a season as watched, only mark aired episodes +- Updated dependencies +- Updated translations diff --git a/fastlane/metadata/android/en-US/changelogs/0021.txt b/fastlane/metadata/android/en-US/changelogs/0021.txt new file mode 100644 index 00000000..3c5fdd6c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0021.txt @@ -0,0 +1,3 @@ +- Fix back button on season details +- Update R8 configuration +- Fix crash on Android 12 diff --git a/fastlane/metadata/android/en-US/changelogs/0022.txt b/fastlane/metadata/android/en-US/changelogs/0022.txt new file mode 100644 index 00000000..47224f0f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0022.txt @@ -0,0 +1,3 @@ +- Update badges in README +- New launcher icon +- Use storage framework on API19+ diff --git a/fastlane/metadata/android/en-US/changelogs/0023.txt b/fastlane/metadata/android/en-US/changelogs/0023.txt new file mode 100644 index 00000000..b4df4144 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0023.txt @@ -0,0 +1,5 @@ +- Final release with TVDB as metadata provider +- Target version 33 (Android 13) +- Raise minimum version to 21 (Android L) +- Request notification permission on Android 13+ +- Update gradle version diff --git a/fastlane/metadata/android/en-US/changelogs/0024.txt b/fastlane/metadata/android/en-US/changelogs/0024.txt new file mode 100644 index 00000000..46cf4c5b --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0024.txt @@ -0,0 +1,4 @@ +- Switch to TMDB as metadata provider +- Add support for themed app icons +- Fix for null/zero TVDB ID +- Update setup-java CI action diff --git a/fastlane/metadata/android/en-US/changelogs/0025.txt b/fastlane/metadata/android/en-US/changelogs/0025.txt new file mode 100644 index 00000000..8a0540f8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/0025.txt @@ -0,0 +1,2 @@ +- Fix sorting in shows list +- Update star/archive icons to improve visibility diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 00000000..f60f6d49 --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,4 @@ +Keep track of which episodes you’ve watched of your favourite TV shows. +This product uses the TMDB API but is not endorsed or certified by TMDB. + +Anti-Feature: NonFreeNet - uses a non-free network service which is impossible, or not easy to replace without rebuilding the app diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100755 index 00000000..cb8a520a Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png new file mode 100644 index 00000000..30b60b1f Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png new file mode 100644 index 00000000..7cd07807 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png new file mode 100644 index 00000000..ecbfc1e5 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png new file mode 100644 index 00000000..b3c1673d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png new file mode 100644 index 00000000..ccd8ec2d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png new file mode 100644 index 00000000..e3cca5b0 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 00000000..d79ea21c --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Keep track of which episodes you’ve watched of your favourite TV shows. diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 00000000..c84917f3 --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +Episodes diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..5465fec0 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +android.enableJetifier=true +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 8c0fb64a..cc4fdc29 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0c71e760..f7189a77 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Apr 10 15:27:10 PDT 2013 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionSha256Sum=db9c8211ed63f61f60292c69e80d89196f9eb36665e369e7f00ac4cc841c2219 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/gradlew b/gradlew index 91a7e269..2fe81a7d 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## ## @@ -6,20 +22,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# 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='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +75,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# 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\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +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 @@ -110,10 +125,11 @@ 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 +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; 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` @@ -138,27 +154,30 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $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" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +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" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec99730..24467a14 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem 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="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +62,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +75,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line