package com.thebluealliance.androidclient.background.firstlaunch; import com.thebluealliance.androidclient.BuildConfig; import com.thebluealliance.androidclient.Constants; import com.thebluealliance.androidclient.R; import com.thebluealliance.androidclient.TbaLogger; import com.thebluealliance.androidclient.Utilities; import com.thebluealliance.androidclient.api.ApiConstants; import com.thebluealliance.androidclient.api.call.TbaApiV3; import com.thebluealliance.androidclient.config.AppConfig; import com.thebluealliance.androidclient.database.Database; import com.thebluealliance.androidclient.database.writers.DistrictListWriter; import com.thebluealliance.androidclient.database.writers.EventListWriter; import com.thebluealliance.androidclient.database.writers.TeamListWriter; import com.thebluealliance.androidclient.datafeed.maps.AddDistrictKeys; import com.thebluealliance.androidclient.datafeed.status.TBAStatusController; import com.thebluealliance.androidclient.helpers.AnalyticsHelper; import com.thebluealliance.androidclient.models.ApiStatus; import com.thebluealliance.androidclient.models.District; import com.thebluealliance.androidclient.models.Event; import com.thebluealliance.androidclient.models.Team; import android.content.Context; import android.content.SharedPreferences; import android.os.AsyncTask; import android.preference.PreferenceManager; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutionException; import retrofit2.Call; import retrofit2.Response; import rx.schedulers.Schedulers; public class LoadTBAData extends AsyncTask<Short, LoadTBAData.LoadProgressInfo, Void> { public static final String DATA_TO_LOAD = "data_to_load"; public static final short LOAD_TEAMS = 0, LOAD_EVENTS = 1, LOAD_DISTRICTS = 2; private TbaApiV3 datafeed; private AppConfig config; private LoadTBADataCallbacks callbacks; private Context context; private long startTime; private Database mDb; private TeamListWriter mTeamWriter; private EventListWriter mEventWriter; private DistrictListWriter mDistrictWriter; public LoadTBAData(TbaApiV3 datafeed, AppConfig config, LoadTBADataCallbacks callbacks, Context c, Database db, TeamListWriter teamWriter, EventListWriter eventWriter, DistrictListWriter districtWriter) { this.datafeed = datafeed; this.config = config; this.callbacks = callbacks; this.context = c.getApplicationContext(); this.startTime = System.currentTimeMillis(); this.mDb = db; this.mTeamWriter = teamWriter; this.mEventWriter = eventWriter; this.mDistrictWriter = districtWriter; } @Override protected Void doInBackground(Short... params) { if (callbacks == null) { throw new IllegalArgumentException("callbacks must not be null!"); } TbaLogger.d("Input: " + Arrays.deepToString(params)); Short[] dataToLoad; if (params == null) { dataToLoad = new Short[]{LOAD_TEAMS, LOAD_EVENTS, LOAD_DISTRICTS}; } else { dataToLoad = params; } TbaLogger.d("Loading: " + Arrays.deepToString(dataToLoad)); /* We need to download and cache every team and event into the database. To avoid * unexpected behavior caused by changes in network connectivity, we will load all * teams into memory first. Once we have loaded everything, only then will we wipe the * database and insert all the new teams and events */ try { /* First, do a blocking update of Remote Config */ publishProgress(new LoadProgressInfo(LoadProgressInfo.STATE_LOADING, context.getString(R.string.loading_config))); config.updateRemoteDataBlocking(); Call<ApiStatus> statusCall = datafeed.fetchApiStatus(); Response<ApiStatus> statusResponse = statusCall.execute(); if (!statusResponse.isSuccessful() || statusResponse.body() == null) { onConnectionError(); return null; } int maxCompYear = statusResponse.body().getMaxSeason(); List<Team> allTeams = new ArrayList<>(); int maxPageNum = 0; if (Arrays.binarySearch(dataToLoad, LOAD_TEAMS) != -1) { mDb.getTeamsTable().deleteAllRows(); // First we will load all the teams for (int pageNum = 0; pageNum < 20; pageNum++) { // limit to 20 pages to prevent potential infinite loop if (isCancelled()) { return null; } int start = pageNum * Constants.API_TEAM_LIST_PAGE_SIZE; int end = start + Constants.API_TEAM_LIST_PAGE_SIZE - 1; start = start == 0 ? 1 : start; publishProgress(new LoadProgressInfo(LoadProgressInfo.STATE_LOADING, String.format(context.getString(R.string.loading_teams), start, end))); Call<List<Team>> teamListCall = datafeed.fetchTeamPage(pageNum, ApiConstants.TBA_CACHE_WEB); Response<List<Team>> teamListResponse = teamListCall.execute(); if (teamListResponse == null || !teamListResponse.isSuccessful()) { onConnectionError(); return null; } if (teamListResponse.body() == null || teamListResponse.body().isEmpty()) { // No teams found for a page; we are done break; } Date lastModified = teamListResponse.headers().getDate("Last-Modified"); List<Team> responseBody = teamListResponse.body(); if (lastModified != null) { long lastModifiedTimestamp = lastModified.getTime(); for (int i = 0; i < responseBody.size(); i++) { responseBody.get(i).setLastModified(lastModifiedTimestamp); } } allTeams.addAll(responseBody); maxPageNum = Math.max(maxPageNum, pageNum); } } List<Event> allEvents = new ArrayList<>(); if (Arrays.binarySearch(dataToLoad, LOAD_EVENTS) != -1) { mDb.getEventsTable().deleteAllRows(); // Now we load all events for (int year = Constants.FIRST_COMP_YEAR; year <= maxCompYear; year++) { if (isCancelled()) { return null; } publishProgress(new LoadProgressInfo(LoadProgressInfo.STATE_LOADING, String.format(context.getString(R.string.loading_events), Integer.toString(year)))); Call<List<Event>> eventListCall = datafeed.fetchEventsInYear(year, ApiConstants.TBA_CACHE_WEB); Response<List<Event>> eventListResponse = eventListCall.execute(); if (eventListResponse == null || !eventListResponse.isSuccessful()) { onConnectionError(); return null; } if (eventListResponse.body() == null) continue; Date lastModified = eventListResponse.headers().getDate("Last-Modified"); List<Event> responseBody = eventListResponse.body(); if (lastModified != null) { long lastModifiedTimestamp = lastModified.getTime(); for (int i = 0; i < responseBody.size(); i++) { responseBody.get(i).setLastModified(lastModifiedTimestamp); } } allEvents.addAll(responseBody); TbaLogger.i(String.format("Loaded %1$d events in %2$d", eventListResponse.body().size(), year)); } } List<District> allDistricts = new ArrayList<>(); if (Arrays.binarySearch(dataToLoad, LOAD_DISTRICTS) != -1) { mDb.getDistrictsTable().deleteAllRows(); //load all districts for (int year = Constants.FIRST_DISTRICT_YEAR; year <= maxCompYear; year++) { if (isCancelled()) { return null; } publishProgress(new LoadProgressInfo(LoadProgressInfo.STATE_LOADING, String.format(context.getString(R.string.loading_districts), year))); AddDistrictKeys keyAdder = new AddDistrictKeys(year); Call<List<District>> districtListCall = datafeed.fetchDistrictList(year, ApiConstants.TBA_CACHE_WEB); Response<List<District>> districtListResponse = districtListCall.execute(); if (districtListResponse == null || !districtListResponse.isSuccessful() || districtListResponse.body() == null) { onConnectionError(); return null; } List<District> newDistrictList = districtListResponse.body(); keyAdder.call(newDistrictList); Date lastModified = districtListResponse.headers().getDate("Last-Modified"); if (lastModified != null) { long lastModifiedTimestamp = lastModified.getTime(); for (int i = 0; i < newDistrictList.size(); i++) { newDistrictList.get(i).setLastModified(lastModifiedTimestamp); } } allDistricts.addAll(newDistrictList); TbaLogger.i(String.format("Loaded %1$d districts in %2$d", newDistrictList.size(), year)); } } if (isCancelled()) { return null; } // If no exception has been thrown at this point, we have all the data. We can now // insert it into the database. Pass a 0 as the last-modified time here, because we set // it individually above publishProgress(new LoadProgressInfo(LoadProgressInfo.STATE_LOADING, context.getString(R.string.loading_almost_finished))); TbaLogger.i("Writing " + allTeams.size() + " teams"); Schedulers.io().createWorker().schedule(() -> mTeamWriter.write(allTeams, 0L)); TbaLogger.i("Writing " + allEvents.size() + " events"); Schedulers.io().createWorker().schedule(() -> mEventWriter.write(allEvents, 0L)); TbaLogger.i("Writing " + allDistricts.size() + " districts"); Schedulers.io().createWorker().schedule(() -> mDistrictWriter.write(allDistricts, 0L)); SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); // Write TBA Status editor.putString(TBAStatusController.STATUS_PREF_KEY, statusResponse.body().getJsonBlob()); editor.putInt(Constants.LAST_YEAR_KEY, statusResponse.body().getMaxSeason()); // Loop through all pages for (int pageNum = 0; pageNum <= maxPageNum; pageNum++) { editor.putBoolean(Database.ALL_TEAMS_LOADED_TO_DATABASE_FOR_PAGE + pageNum, true); } // Loop through all years for (int year = Constants.FIRST_COMP_YEAR; year <= maxCompYear; year++) { editor.putBoolean(Database.ALL_EVENTS_LOADED_TO_DATABASE_FOR_YEAR + year, true); } // Loop through years for districts for (int year = Constants.FIRST_DISTRICT_YEAR; year <= maxCompYear; year++) { editor.putBoolean(Database.ALL_DISTRICTS_LOADED_TO_DATABASE_FOR_YEAR + year, true); } editor.putInt(Constants.APP_VERSION_KEY, BuildConfig.VERSION_CODE); editor.apply(); publishProgress(new LoadProgressInfo(LoadProgressInfo.STATE_FINISHED, context.getString(R.string.loading_finished))); } catch (RuntimeException ex) { // This is bad, probably an error in the response from the server ex.printStackTrace(); // Alert the user that there was a problem publishProgress(new LoadProgressInfo(LoadProgressInfo.STATE_ERROR, Utilities.exceptionStacktraceToString(ex))); } catch (IOException | InterruptedException e) { /* Some sort of network error */ e.printStackTrace(); onConnectionError(); } catch (ExecutionException e) { e.printStackTrace(); } return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); if (context != null) { AnalyticsHelper.sendTimingUpdate(context, System.currentTimeMillis() - startTime, "load all data", ""); } } private void onConnectionError() { publishProgress(new LoadProgressInfo(LoadProgressInfo.STATE_NO_CONNECTION, context.getString(R.string.connection_lost))); } @Override protected void onProgressUpdate(LoadProgressInfo... values) { callbacks.onProgressUpdate(values[0]); } public class LoadProgressInfo { public static final int STATE_LOADING = 0; public static final int STATE_FINISHED = 1; public static final int STATE_NO_CONNECTION = 2; public static final int STATE_ERROR = 3; public int state = -1; public String message = ""; public LoadProgressInfo(int state, String message) { this.state = state; this.message = message; } } public interface LoadTBADataCallbacks { void onProgressUpdate(LoadProgressInfo info); } }