com.battlelancer.seriesguide.dataliberation.JsonImportTask.java Source code

Java tutorial

Introduction

Here is the source code for com.battlelancer.seriesguide.dataliberation.JsonImportTask.java

Source

/*
 * Copyright 2014 Uwe Trottmann
 *
 * 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
 *
 *      http://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.
 */

package com.battlelancer.seriesguide.dataliberation;

import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.ParcelFileDescriptor;
import android.support.annotation.Nullable;
import android.widget.Toast;
import com.battlelancer.seriesguide.R;
import com.battlelancer.seriesguide.dataliberation.JsonExportTask.ListItemTypesExport;
import com.battlelancer.seriesguide.dataliberation.model.Episode;
import com.battlelancer.seriesguide.dataliberation.model.List;
import com.battlelancer.seriesguide.dataliberation.model.ListItem;
import com.battlelancer.seriesguide.dataliberation.model.Movie;
import com.battlelancer.seriesguide.dataliberation.model.Season;
import com.battlelancer.seriesguide.dataliberation.model.Show;
import com.battlelancer.seriesguide.enums.EpisodeFlags;
import com.battlelancer.seriesguide.interfaces.OnTaskFinishedListener;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.Episodes;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.ListItemTypes;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.ListItems;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.Lists;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.Seasons;
import com.battlelancer.seriesguide.provider.SeriesGuideContract.Shows;
import com.battlelancer.seriesguide.settings.BackupSettings;
import com.battlelancer.seriesguide.sync.SgSyncAdapter;
import com.battlelancer.seriesguide.util.DBUtils;
import com.battlelancer.seriesguide.util.TaskManager;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.stream.JsonReader;
import com.uwetrottmann.androidutils.AndroidUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import timber.log.Timber;

import static com.battlelancer.seriesguide.provider.SeriesGuideContract.Movies;

/**
 * Import a show database from a human-readable JSON file on external storage. By default meta-data
 * like descriptions, ratings, actors, etc. will not be included.
 */
public class JsonImportTask extends AsyncTask<Void, Integer, Integer> {

    private static final int SUCCESS = 1;
    private static final int ERROR_STORAGE_ACCESS = 0;
    private static final int ERROR = -1;
    private static final int ERROR_LARGE_DB_OP = -2;
    private static final int ERROR_FILE_ACCESS = -3;

    private Context context;
    private String[] languageCodes;
    private OnTaskFinishedListener finishedListener;
    private boolean isImportingAutoBackup;
    private boolean isUseDefaultFolders;
    private boolean isImportShows;
    private boolean isImportLists;
    private boolean isImportMovies;

    public JsonImportTask(Context context, OnTaskFinishedListener listener) {
        this(context);
        finishedListener = listener;
        isImportingAutoBackup = true;
        isImportShows = true;
        isImportLists = true;
        isImportMovies = true;
        // use Storage Access Framework on KitKat and up to select custom backup files,
        // on older versions use default folders
        // also auto backup by default uses default folders
        isUseDefaultFolders = !AndroidUtils.isKitKatOrHigher()
                || BackupSettings.isUseAutoBackupDefaultFiles(context);
    }

    public JsonImportTask(Context context, OnTaskFinishedListener listener, boolean importShows,
            boolean importLists, boolean importMovies) {
        this(context);
        finishedListener = listener;
        isImportingAutoBackup = false;
        isImportShows = importShows;
        isImportLists = importLists;
        isImportMovies = importMovies;
        // use Storage Access Framework on KitKat and up to select custom backup files,
        // on older versions use default folders
        // also auto backup by default uses default folders
        isUseDefaultFolders = !AndroidUtils.isKitKatOrHigher();
    }

    private JsonImportTask(Context context) {
        this.context = context.getApplicationContext();
        languageCodes = this.context.getResources().getStringArray(R.array.languageData);
    }

    @Override
    protected Integer doInBackground(Void... params) {
        // Ensure no large database ops are running
        TaskManager tm = TaskManager.getInstance(context);
        if (SgSyncAdapter.isSyncActive(context, false) || tm.isAddTaskRunning()) {
            return ERROR_LARGE_DB_OP;
        }

        File importPath = null;
        if (isUseDefaultFolders) {
            // Ensure external storage
            if (!AndroidUtils.isExtStorageAvailable()) {
                return ERROR_STORAGE_ACCESS;
            }
            importPath = JsonExportTask.getExportPath(isImportingAutoBackup);
        }

        // last chance to abort
        if (isCancelled()) {
            return ERROR;
        }

        int result;
        if (isImportShows) {
            result = importData(importPath, JsonExportTask.BACKUP_SHOWS);
            if (result != SUCCESS) {
                return result;
            }
            if (isCancelled()) {
                return ERROR;
            }
        }

        if (isImportLists) {
            result = importData(importPath, JsonExportTask.BACKUP_LISTS);
            if (result != SUCCESS) {
                return result;
            }
            if (isCancelled()) {
                return ERROR;
            }
        }

        if (isImportMovies) {
            result = importData(importPath, JsonExportTask.BACKUP_MOVIES);
            if (result != SUCCESS) {
                return result;
            }
            if (isCancelled()) {
                return ERROR;
            }
        }

        // Renew search table
        DBUtils.rebuildFtsTable(context);

        return SUCCESS;
    }

    @Override
    protected void onPostExecute(Integer result) {
        int messageId;
        switch (result) {
        case SUCCESS:
            messageId = R.string.import_success;
            break;
        case ERROR_STORAGE_ACCESS:
            messageId = R.string.import_failed_nosd;
            break;
        case ERROR_FILE_ACCESS:
            messageId = R.string.import_failed_nofile;
            break;
        case ERROR_LARGE_DB_OP:
            messageId = R.string.update_inprogress;
            break;
        default:
            messageId = R.string.import_failed;
            break;
        }
        Toast.makeText(context, messageId, Toast.LENGTH_LONG).show();

        if (finishedListener != null) {
            finishedListener.onTaskFinished();
        }
    }

    private int importData(File importPath, @JsonExportTask.BackupType int type) {
        // if using default files or non-user custom files the backup task will not create a file
        // if there is no data to export,
        // so make sure to not fail just because a default folder file is missing
        if (!isUseDefaultFolders) {
            // make sure we have a file uri...
            Uri backupFileUri = getDataBackupFile(type);
            if (backupFileUri == null) {
                return ERROR_FILE_ACCESS;
            }
            // ...and the file actually exists
            ParcelFileDescriptor pfd;
            try {
                pfd = context.getContentResolver().openFileDescriptor(backupFileUri, "r");
            } catch (FileNotFoundException | SecurityException e) {
                Timber.e(e, "Backup file not found.");
                return ERROR_FILE_ACCESS;
            }

            clearExistingData(type);

            // Access JSON from backup file and try to import data
            FileInputStream in = new FileInputStream(pfd.getFileDescriptor());
            try {
                importFromJson(type, in);

                // let the document provider know we're done.
                pfd.close();
            } catch (JsonParseException | IOException | IllegalStateException e) {
                // the given Json might not be valid or unreadable
                Timber.e(e, "JSON import failed");
                return ERROR;
            }
        } else {
            // make sure we can access the backup file
            File backupFile = null;
            if (type == JsonExportTask.BACKUP_SHOWS) {
                backupFile = new File(importPath, JsonExportTask.EXPORT_JSON_FILE_SHOWS);
            } else if (type == JsonExportTask.BACKUP_LISTS) {
                backupFile = new File(importPath, JsonExportTask.EXPORT_JSON_FILE_LISTS);
            } else if (type == JsonExportTask.BACKUP_MOVIES) {
                backupFile = new File(importPath, JsonExportTask.EXPORT_JSON_FILE_MOVIES);
            }
            if (backupFile == null || !backupFile.canRead()) {
                return ERROR_FILE_ACCESS;
            }
            if (!backupFile.exists()) {
                // no backup file, so nothing to restore, skip it
                return SUCCESS;
            }

            FileInputStream in;
            try {
                in = new FileInputStream(backupFile);
            } catch (FileNotFoundException e) {
                Timber.e(e, "Backup file not found.");
                return ERROR_FILE_ACCESS;
            }

            clearExistingData(type);

            // Access JSON from backup file and try to import data
            try {
                importFromJson(type, in);
            } catch (JsonParseException | IOException | IllegalStateException e) {
                // the given Json might not be valid or unreadable
                Timber.e(e, "JSON show import failed");
                return ERROR;
            }
        }

        return SUCCESS;
    }

    @Nullable
    private Uri getDataBackupFile(@JsonExportTask.BackupType int type) {
        // use import URIs
        // if they are not set getFileUri will fall back to the export URI
        // for auto backup always use the URI data is configured to be exported to
        if (type == JsonExportTask.BACKUP_SHOWS) {
            return BackupSettings.getFileUri(context,
                    isImportingAutoBackup ? BackupSettings.KEY_AUTO_BACKUP_SHOWS_EXPORT_URI
                            : BackupSettings.KEY_SHOWS_IMPORT_URI);
        }
        if (type == JsonExportTask.BACKUP_LISTS) {
            return BackupSettings.getFileUri(context,
                    isImportingAutoBackup ? BackupSettings.KEY_AUTO_BACKUP_LISTS_EXPORT_URI
                            : BackupSettings.KEY_LISTS_IMPORT_URI);
        }
        if (type == JsonExportTask.BACKUP_MOVIES) {
            return BackupSettings.getFileUri(context,
                    isImportingAutoBackup ? BackupSettings.KEY_AUTO_BACKUP_MOVIES_EXPORT_URI
                            : BackupSettings.KEY_MOVIES_IMPORT_URI);
        }
        return null;
    }

    private void clearExistingData(@JsonExportTask.BackupType int type) {
        if (type == JsonExportTask.BACKUP_SHOWS) {
            context.getContentResolver().delete(Shows.CONTENT_URI, null, null);
            context.getContentResolver().delete(Seasons.CONTENT_URI, null, null);
            context.getContentResolver().delete(Episodes.CONTENT_URI, null, null);
        } else if (type == JsonExportTask.BACKUP_LISTS) {
            context.getContentResolver().delete(Lists.CONTENT_URI, null, null);
            context.getContentResolver().delete(ListItems.CONTENT_URI, null, null);
        } else if (type == JsonExportTask.BACKUP_MOVIES) {
            context.getContentResolver().delete(Movies.CONTENT_URI, null, null);
        }
    }

    private void importFromJson(@JsonExportTask.BackupType int type, FileInputStream in)
            throws JsonParseException, IOException, IllegalArgumentException {
        Gson gson = new Gson();
        JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
        reader.beginArray();

        if (type == JsonExportTask.BACKUP_SHOWS) {
            while (reader.hasNext()) {
                Show show = gson.fromJson(reader, Show.class);
                addShowToDatabase(show);
            }
        } else if (type == JsonExportTask.BACKUP_LISTS) {
            while (reader.hasNext()) {
                List list = gson.fromJson(reader, List.class);
                addListToDatabase(list);
            }
        } else if (type == JsonExportTask.BACKUP_MOVIES) {
            while (reader.hasNext()) {
                Movie movie = gson.fromJson(reader, Movie.class);
                addMovieToDatabase(movie);
            }
        }

        reader.endArray();
        reader.close();
    }

    private void addShowToDatabase(Show show) {
        if (show.tvdbId <= 0) {
            // valid id required
            return;
        }

        // Insert the show
        ContentValues showValues = new ContentValues();
        showValues.put(Shows._ID, show.tvdbId);
        showValues.put(Shows.TITLE, show.title == null ? "" : show.title);
        showValues.put(Shows.TITLE_NOARTICLE, DBUtils.trimLeadingArticle(show.title));
        showValues.put(Shows.FAVORITE, show.favorite);
        showValues.put(Shows.HIDDEN, show.hidden);
        // only add the language, if we support it
        for (int i = 0, size = languageCodes.length; i < size; i++) {
            if (languageCodes[i].equals(show.language)) {
                showValues.put(Shows.LANGUAGE, show.language);
                break;
            }
        }
        showValues.put(Shows.RELEASE_TIME, show.release_time);
        if (show.release_weekday < -1 || show.release_weekday > 7) {
            show.release_weekday = -1;
        }
        showValues.put(Shows.RELEASE_WEEKDAY, show.release_weekday);
        showValues.put(Shows.RELEASE_TIMEZONE, show.release_timezone);
        showValues.put(Shows.RELEASE_COUNTRY, show.country);
        showValues.put(Shows.LASTWATCHEDID, show.lastWatchedEpisode);
        showValues.put(Shows.POSTER, show.poster);
        showValues.put(Shows.CONTENTRATING, show.contentRating);
        if (show.runtime < 0) {
            show.runtime = 0;
        }
        showValues.put(Shows.RUNTIME, show.runtime);
        showValues.put(Shows.NETWORK, show.network);
        showValues.put(Shows.IMDBID, show.imdbId);
        if (show.traktId != null && show.traktId > 0) {
            showValues.put(Shows.TRAKT_ID, show.traktId);
        }
        showValues.put(Shows.FIRST_RELEASE, show.firstAired);
        if (show.rating_user < 0 || show.rating_user > 10) {
            show.rating_user = 0;
        }
        showValues.put(Shows.RATING_USER, show.rating_user);
        showValues.put(Shows.STATUS, DataLiberationTools.encodeShowStatus(show.status));
        // Full dump values
        showValues.put(Shows.OVERVIEW, show.overview);
        if (show.rating < 0 || show.rating > 10) {
            show.rating = 0;
        }
        showValues.put(Shows.RATING_GLOBAL, show.rating);
        if (show.rating_votes < 0) {
            show.rating_votes = 0;
        }
        showValues.put(Shows.RATING_VOTES, show.rating_votes);
        showValues.put(Shows.GENRES, show.genres);
        showValues.put(Shows.ACTORS, show.actors);
        if (show.lastUpdated > System.currentTimeMillis()) {
            show.lastUpdated = 0;
        }
        showValues.put(Shows.LASTUPDATED, show.lastUpdated);
        showValues.put(Shows.LASTEDIT, show.lastEdited);

        context.getContentResolver().insert(Shows.CONTENT_URI, showValues);

        if (show.seasons == null || show.seasons.isEmpty()) {
            // no seasons (or episodes)
            return;
        }

        ContentValues[][] seasonsAndEpisodes = buildSeasonAndEpisodeBatches(show);
        if (seasonsAndEpisodes[0] != null && seasonsAndEpisodes[1] != null) {
            // Insert all seasons
            context.getContentResolver().bulkInsert(Seasons.CONTENT_URI, seasonsAndEpisodes[0]);
            // Insert all episodes
            context.getContentResolver().bulkInsert(Episodes.CONTENT_URI, seasonsAndEpisodes[1]);
        }
    }

    /**
     * Returns all seasons and episodes of this show in neat {@link ContentValues} packages put into
     * arrays. The first array returned includes all seasons, the second array all episodes.
     */
    private static ContentValues[][] buildSeasonAndEpisodeBatches(Show show) {
        ArrayList<ContentValues> seasonBatch = new ArrayList<>();
        ArrayList<ContentValues> episodeBatch = new ArrayList<>();

        // Populate arrays...
        for (Season season : show.seasons) {
            if (season.tvdbId <= 0) {
                // valid id is required
                continue;
            }
            if (season.episodes == null || season.episodes.isEmpty()) {
                // episodes required
                continue;
            }

            // add the season...
            ContentValues seasonValues = new ContentValues();
            seasonValues.put(Seasons._ID, season.tvdbId);
            seasonValues.put(Shows.REF_SHOW_ID, show.tvdbId);
            if (season.season < 0) {
                season.season = 0;
            }
            seasonValues.put(Seasons.COMBINED, season.season);

            seasonBatch.add(seasonValues);

            // ...and its episodes
            for (Episode episode : season.episodes) {
                if (episode.tvdbId <= 0) {
                    // valid id is required
                    continue;
                }

                ContentValues episodeValues = new ContentValues();
                episodeValues.put(Episodes._ID, episode.tvdbId);
                episodeValues.put(Shows.REF_SHOW_ID, show.tvdbId);
                episodeValues.put(Seasons.REF_SEASON_ID, season.tvdbId);
                if (episode.episode < 0) {
                    episode.episode = 0;
                }
                episodeValues.put(Episodes.NUMBER, episode.episode);
                if (episode.episodeAbsolute < 0) {
                    episode.episodeAbsolute = 0;
                }
                episodeValues.put(Episodes.ABSOLUTE_NUMBER, episode.episodeAbsolute);
                episodeValues.put(Episodes.SEASON, season.season);
                episodeValues.put(Episodes.TITLE, episode.title);
                // watched/skipped represented internally in watched flag
                if (episode.skipped) {
                    episodeValues.put(Episodes.WATCHED, EpisodeFlags.SKIPPED);
                } else {
                    episodeValues.put(Episodes.WATCHED,
                            episode.watched ? EpisodeFlags.WATCHED : EpisodeFlags.UNWATCHED);
                }
                episodeValues.put(Episodes.COLLECTED, episode.collected);
                episodeValues.put(Episodes.FIRSTAIREDMS, episode.firstAired);
                episodeValues.put(Episodes.IMDBID, episode.imdbId);
                if (episode.rating_user < 0 || episode.rating_user > 10) {
                    episode.rating_user = 0;
                }
                episodeValues.put(Episodes.RATING_USER, episode.rating_user);
                // Full dump values
                if (episode.episodeDvd < 0) {
                    episode.episodeDvd = 0;
                }
                episodeValues.put(Episodes.DVDNUMBER, episode.episodeDvd);
                episodeValues.put(Episodes.OVERVIEW, episode.overview);
                episodeValues.put(Episodes.IMAGE, episode.image);
                episodeValues.put(Episodes.WRITERS, episode.writers);
                episodeValues.put(Episodes.GUESTSTARS, episode.gueststars);
                episodeValues.put(Episodes.DIRECTORS, episode.directors);
                if (episode.rating < 0 || episode.rating > 10) {
                    episode.rating = 0;
                }
                episodeValues.put(Episodes.RATING_GLOBAL, episode.rating);
                if (episode.rating_votes < 0) {
                    episode.rating_votes = 0;
                }
                episodeValues.put(Episodes.RATING_VOTES, episode.rating_votes);
                episodeValues.put(Episodes.LAST_EDITED, episode.lastEdited);

                episodeBatch.add(episodeValues);
            }
        }

        return new ContentValues[][] {
                seasonBatch.size() == 0 ? null : seasonBatch.toArray(new ContentValues[seasonBatch.size()]),
                episodeBatch.size() == 0 ? null : episodeBatch.toArray(new ContentValues[episodeBatch.size()]) };
    }

    private int importLists(File importPath) {
        File backupLists = new File(importPath, JsonExportTask.EXPORT_JSON_FILE_LISTS);
        if (!backupLists.exists() || !backupLists.canRead()) {
            // Skip lists if the file is not accessible
            return SUCCESS;
        }

        // Access JSON from backup folder to create new database
        try {
            InputStream in = new FileInputStream(backupLists);

            Gson gson = new Gson();

            JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
            reader.beginArray();

            while (reader.hasNext()) {
                List list = gson.fromJson(reader, List.class);
                addListToDatabase(list);
            }

            reader.endArray();
            reader.close();
        } catch (JsonParseException | IOException | IllegalStateException e) {
            // the given Json might not be valid or unreadable
            Timber.e(e, "JSON lists import failed");
            return ERROR;
        }

        return SUCCESS;
    }

    private void addListToDatabase(List list) {
        // Insert the list
        ContentValues values = new ContentValues();
        values.put(Lists.LIST_ID, list.listId);
        values.put(Lists.NAME, list.name);
        values.put(Lists.ORDER, list.order);
        context.getContentResolver().insert(Lists.CONTENT_URI, values);

        if (list.items == null || list.items.isEmpty()) {
            return;
        }

        // Insert the lists items
        ArrayList<ContentValues> items = new ArrayList<>();
        for (ListItem item : list.items) {
            int type;
            if (ListItemTypesExport.SHOW.equals(item.type)) {
                type = ListItemTypes.SHOW;
            } else if (ListItemTypesExport.SEASON.equals(item.type)) {
                type = ListItemTypes.SEASON;
            } else if (ListItemTypesExport.EPISODE.equals(item.type)) {
                type = ListItemTypes.EPISODE;
            } else {
                // Unknown item type, skip
                continue;
            }
            ContentValues itemValues = new ContentValues();
            itemValues.put(ListItems.LIST_ITEM_ID, item.listItemId);
            itemValues.put(Lists.LIST_ID, list.listId);
            itemValues.put(ListItems.ITEM_REF_ID, item.tvdbId);
            itemValues.put(ListItems.TYPE, type);

            items.add(itemValues);
        }

        ContentValues[] itemsArray = new ContentValues[items.size()];
        context.getContentResolver().bulkInsert(ListItems.CONTENT_URI, items.toArray(itemsArray));
    }

    private int importMovies(File importPath) {
        context.getContentResolver().delete(Movies.CONTENT_URI, null, null);
        File backupMovies = new File(importPath, JsonExportTask.EXPORT_JSON_FILE_MOVIES);
        if (!backupMovies.exists() || !backupMovies.canRead()) {
            // Skip movies if the file is not available
            return SUCCESS;
        }

        // Access JSON from backup folder to create new database
        try {
            InputStream in = new FileInputStream(backupMovies);

            Gson gson = new Gson();

            JsonReader reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
            reader.beginArray();

            while (reader.hasNext()) {
                Movie movie = gson.fromJson(reader, Movie.class);
                addMovieToDatabase(movie);
            }

            reader.endArray();
            reader.close();
        } catch (JsonParseException | IOException | IllegalStateException e) {
            // the given Json might not be valid or unreadable
            Timber.e(e, "JSON movies import failed");
            return ERROR;
        }

        return SUCCESS;
    }

    private void addMovieToDatabase(Movie movie) {
        ContentValues values = new ContentValues();
        values.put(Movies.TMDB_ID, movie.tmdbId);
        values.put(Movies.IMDB_ID, movie.imdbId);
        values.put(Movies.TITLE, movie.title);
        values.put(Movies.TITLE_NOARTICLE, DBUtils.trimLeadingArticle(movie.title));
        values.put(Movies.RELEASED_UTC_MS, movie.releasedUtcMs);
        values.put(Movies.RUNTIME_MIN, movie.runtimeMin);
        values.put(Movies.POSTER, movie.poster);
        values.put(Movies.IN_COLLECTION, movie.inCollection);
        values.put(Movies.IN_WATCHLIST, movie.inWatchlist);
        values.put(Movies.WATCHED, movie.watched);
        // full dump values
        values.put(Movies.OVERVIEW, movie.overview);

        context.getContentResolver().insert(Movies.CONTENT_URI, values);
    }
}