Java tutorial
/* * Copyright 2017 Gurupad Mamadapur * * 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 io.github.protino.codewatch.remote; import android.content.ContentValues; import android.content.Context; import android.net.Uri; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.util.ArrayList; import java.util.List; import javax.net.ssl.HttpsURLConnection; import io.github.protino.codewatch.BuildConfig; import io.github.protino.codewatch.data.LeaderContract; import io.github.protino.codewatch.model.AccessToken; import io.github.protino.codewatch.utils.CacheUtils; import io.github.protino.codewatch.utils.Constants; import timber.log.Timber; /** * Fetches leaderboard data from wakatime API endpoint, parses it * and stores in database * <p> * Implementation - Instead of using Retrofit and gson, {@link HttpsURLConnection} is way * better to download the data and do JSON{@link org.json} parsing which is much faster. * Retrofit took almost 45s just to deserialize the data. With JSON parsing it just takes around * 2-3s \_( )_/. Overall there is 600% improvement in downloading-parsing-storing. * </p> * * @author Gurupad Mamadapur */ public class FetchLeaderBoardData { private final Context context; public FetchLeaderBoardData(Context context) { this.context = context; } /** * todo : Change boolean result to an error code * * @return true, on successful completion else false */ public boolean execute() { long start = System.currentTimeMillis(); List<String> jsonDataList = fetchLeaderBoardJson(); if (jsonDataList == null || jsonDataList.isEmpty()) { return false; } Timber.d("Data downloaded successfully - " + (System.currentTimeMillis() - start)); try { ContentValues[] contentValues = parseLeaderDataFromJson(jsonDataList); storeToDb(contentValues); } catch (JSONException e) { Timber.e(e); return false; } return true; } /* * A major change to the API_ENDPOINT had been made. It now takes a parameter - "page" * and returns about 100 items for every query. There about 3000 users. * * I could load each page dynamically but it also needs to be stored in the database * (because there is requirement in rubric to use the loaders), which creates more problems * and there is less time now. * * todo: Remove all this when revamping the app for play store release */ private List<String> fetchLeaderBoardJson() { HttpsURLConnection urlConnection = null; BufferedReader reader = null; List<String> jsonDataList = new ArrayList<>(); try { final String LEADER_PATH = "leaders"; final String API_SUFFIX = "api/v1"; final String API_KEY = "api_key"; final String PAGE = "page"; Uri.Builder builder; String jsonStr; int totalPages = -1; int page = 1; do { builder = Uri.parse(Constants.WAKATIME_BASE_URL).buildUpon(); builder.appendPath(API_SUFFIX).appendPath(LEADER_PATH) .appendQueryParameter(API_KEY, BuildConfig.API_KEY) .appendQueryParameter(PAGE, String.valueOf(page)).build(); URL url = new URL(builder.toString()); // Create the request to Wakatime.com, and open the connection urlConnection = (HttpsURLConnection) url.openConnection(); urlConnection.setRequestMethod("GET"); urlConnection.connect(); // Read the input stream into a string InputStream inputStream = urlConnection.getInputStream(); StringBuilder buffer = new StringBuilder(); if (inputStream == null) { return null; } reader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = reader.readLine()) != null) { buffer.append(line); } if (buffer.length() == 0) { return null; } jsonStr = buffer.toString(); jsonDataList.add(jsonStr); //parse totalpages if (totalPages == -1) { totalPages = new JSONObject(jsonStr).getInt("total_pages"); } page++; } while (totalPages != page); } catch (IOException e) { Timber.e(e, "IO Error"); } catch (JSONException e) { Timber.e(e, "JSON error"); } finally { if (urlConnection != null) { urlConnection.disconnect(); } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); Timber.e(e, "Error closing stream"); } } } return jsonDataList; } private ContentValues[] parseLeaderDataFromJson(List<String> dataList) throws JSONException { final long start = System.currentTimeMillis(); final String ROOT_DATA = "data"; final String RANK = "rank"; final String ROOT_USER_DATA = "user"; final String ROOT_STATS = "running_total"; final String DAILY_AVERAGE = "daily_average"; final String TOTAL_SECONDS = "total_seconds"; final String LANGUAGE_LIST = "languages"; //keys under languages section final String LANGUAGE_NAME = "name"; //keys under user section final String DISPLAY_NAME = "display_name"; final String EMAIL = "email"; final String FULL_NAME = "full_name"; //most of time this is empty, ignoring it final String USER_ID = "id"; final String LOCATION = "location"; final String PHOTO_URL = "photo"; final String USERNAME = "username"; final String WEBSITE = "website"; List<ContentValues> leaderValues = new ArrayList<>(); for (String dataJsonStr : dataList) { JSONObject rootObject = new JSONObject(dataJsonStr); JSONArray dataArray = rootObject.getJSONArray(ROOT_DATA); ContentValues values; for (int i = 0; i < dataArray.length(); i++) { values = new ContentValues(); JSONObject rootUserData = dataArray.getJSONObject(i); values.put(LeaderContract.LeaderEntry.COLUMN_RANK, rootUserData.getInt(RANK)); /* User stats*/ JSONObject runningTotal = rootUserData.getJSONObject(ROOT_STATS); values.put(LeaderContract.LeaderEntry.COLUMN_TOTAL_SECONDS, runningTotal.getInt(TOTAL_SECONDS)); values.put(LeaderContract.LeaderEntry.COLUMN_DAILY_AVERAGE, runningTotal.getInt(DAILY_AVERAGE)); // transform language list to a simple name-seconds map // Doing so simplifies the structure and takes less storage space JSONArray languageList = runningTotal.getJSONArray(LANGUAGE_LIST); JSONObject languageMap = new JSONObject(); for (int j = 0; j < languageList.length(); j++) { JSONObject language = languageList.getJSONObject(j); languageMap.put(language.getString(LANGUAGE_NAME), language.getInt(TOTAL_SECONDS)); } values.put(LeaderContract.LeaderEntry.COLUMN_LANGUAGE_STATS, languageMap.toString()); /* User data*/ JSONObject userData = rootUserData.getJSONObject(ROOT_USER_DATA); values.put(LeaderContract.LeaderEntry.COLUMN_USER_ID, userData.getString(USER_ID)); values.put(LeaderContract.LeaderEntry.COLUMN_PHOTO, userData.getString(PHOTO_URL)); values.put(LeaderContract.LeaderEntry.COLUMN_USER_NAME, userData.getString(USERNAME)); values.put(LeaderContract.LeaderEntry.COLUMN_DISPLAY_NAME, userData.getString(DISPLAY_NAME)); values.put(LeaderContract.LeaderEntry.COLUMN_LOCATION, userData.getString(LOCATION)); values.put(LeaderContract.LeaderEntry.COLUMN_EMAIL, userData.getString(EMAIL)); values.put(LeaderContract.LeaderEntry.COLUMN_WEBSITE, userData.getString(WEBSITE)); leaderValues.add(values); } } Timber.d("Data parsed - " + (System.currentTimeMillis() - start) + "ms"); ContentValues[] contentValuesArray = new ContentValues[leaderValues.size()]; return leaderValues.toArray(contentValuesArray); } private void storeToDb(ContentValues[] leaderValues) { final long start = System.currentTimeMillis(); //delete earlier data and then bulkInsert context.getContentResolver().delete(LeaderContract.LeaderEntry.CONTENT_URI, null, null); int rows = context.getContentResolver().bulkInsert(LeaderContract.LeaderEntry.CONTENT_URI, leaderValues); Timber.d("Successfully inserted " + rows + " rows - " + (System.currentTimeMillis() - start)); } public int fetchUserRank() { AccessToken accessToken = CacheUtils.getAccessToken(context); if (accessToken == null) { return -1; } HttpsURLConnection urlConnection = null; BufferedReader reader = null; String jsonStr; try { final String LEADER_PATH = "leaders"; final String API_SUFFIX = "api/v1"; final String CLIENT_SECRET = "secret"; final String APP_SECRET = "app_secret"; final String ACCESS_TOKEN = "token"; Uri.Builder builder; builder = Uri.parse(Constants.WAKATIME_BASE_URL).buildUpon(); builder.appendPath(API_SUFFIX).appendPath(LEADER_PATH) .appendQueryParameter(APP_SECRET, BuildConfig.CLIENT_ID) .appendQueryParameter(CLIENT_SECRET, BuildConfig.CLIENT_SECRET) .appendQueryParameter(ACCESS_TOKEN, accessToken.getAccessToken()).build(); URL url = new URL(builder.toString()); // Create the request to Wakatime.com, and open the connection urlConnection = (HttpsURLConnection) url.openConnection(); urlConnection.setRequestMethod("GET"); urlConnection.connect(); // Read the input stream into a string InputStream inputStream = urlConnection.getInputStream(); StringBuilder buffer = new StringBuilder(); if (inputStream == null) { return -1; } reader = new BufferedReader(new InputStreamReader(inputStream)); String line; while ((line = reader.readLine()) != null) { buffer.append(line); } if (buffer.length() == 0) { return -1; } jsonStr = buffer.toString(); JSONObject currentUser = new JSONObject(jsonStr).getJSONObject("current_user"); if (currentUser == null) { return -1; } else { //if is a new user, it'll result throw JSONException, because rank=null return currentUser.getInt("rank"); } } catch (IOException e) { Timber.e(e, "IO Error"); return -1; } catch (JSONException e) { Timber.e(e, "JSON error"); return -1; } finally { if (urlConnection != null) { urlConnection.disconnect(); } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); Timber.e(e, "Error closing stream"); } } } } }