Java tutorial
/* * Copyright (C) 2012 - 2013 jonas.oreland@gmail.com * * 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 <http://www.gnu.org/licenses/>. */ package org.runnerup.export; import android.annotation.TargetApi; import android.app.Activity; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.sqlite.SQLiteDatabase; import android.os.Build; import android.preference.PreferenceManager; import android.util.Log; import org.apache.http.HttpStatus; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.runnerup.R; import org.runnerup.common.util.Constants; import org.runnerup.common.util.Constants.DB; import org.runnerup.common.util.Constants.DB.FEED; import org.runnerup.db.entities.ActivityEntity; import org.runnerup.export.format.RunKeeper; import org.runnerup.export.oauth2client.OAuth2Activity; import org.runnerup.export.oauth2client.OAuth2Server; import org.runnerup.export.util.FormValues; import org.runnerup.export.util.SyncHelper; import org.runnerup.feed.FeedList.FeedUpdater; import org.runnerup.util.Formatter; import org.runnerup.util.SyncActivityItem; import org.runnerup.workout.Sport; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.math.BigDecimal; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; @TargetApi(Build.VERSION_CODES.FROYO) public class RunKeeperSynchronizer extends DefaultSynchronizer implements Synchronizer, OAuth2Server { public static final String NAME = "RunKeeper"; private static Context context = null; /** * @todo register OAuth2Server */ private static String CLIENT_ID = null; private static String CLIENT_SECRET = null; private static final String AUTH_URL = "https://runkeeper.com/apps/authorize"; private static final String TOKEN_URL = "https://runkeeper.com/apps/token"; private static final String REDIRECT_URI = "http://localhost:8080/runnerup/runkeeper"; private static String REST_URL = "https://api.runkeeper.com"; private static final String FEED_TOKEN_URL = "https://fitnesskeeperapi.com/RunKeeper/deviceApi/login"; private static final String FEED_URL = "https://fitnesskeeperapi.com/RunKeeper/deviceApi/getFeedItems"; private static final String FEED_ITEM_TYPES = "[ 0 ]"; // JSON array private long id = 0; private String access_token = null; private String fitnessActivitiesUrl = null; private String feed_username = null; private String feed_password = null; private String feed_access_token = null; public static final Map<String, Sport> runkeeper2sportMap = new HashMap<String, Sport>(); public static final Map<Sport, String> sport2runkeeperMap = new HashMap<Sport, String>(); static { //sports list can be found at http://developer.runkeeper.com/healthgraph/fitness-activities#past /** * Running, Cycling, Mountain Biking, Walking, Hiking, Downhill Skiing, Cross-Country Skiing, * Snowboarding, Skating, Swimming, Wheelchair, Rowing, Elliptical, Other, Yoga, Pilates, * CrossFit, Spinning, Zumba, Barre, Group Workout, Dance, Bootcamp, Boxing / MMA, Meditation, * Strength Training, Circuit Training, Core Strengthening, Arc Trainer, Stairmaster / Stepwell, * Sports, Nordic Walking */ runkeeper2sportMap.put("Running", Sport.RUNNING); runkeeper2sportMap.put("Cycling", Sport.BIKING); runkeeper2sportMap.put("Mountain Biking", Sport.BIKING); runkeeper2sportMap.put("Other", Sport.OTHER); runkeeper2sportMap.put("Walking", Sport.WALKING); for (String i : runkeeper2sportMap.keySet()) { if (!sport2runkeeperMap.containsKey(runkeeper2sportMap.get(i))) { sport2runkeeperMap.put(runkeeper2sportMap.get(i), i); } } } public static final Map<String, Integer> POINT_TYPE = new HashMap<String, Integer>(); static { POINT_TYPE.put("start", DB.LOCATION.TYPE_START); POINT_TYPE.put("end", DB.LOCATION.TYPE_END); POINT_TYPE.put("gps", DB.LOCATION.TYPE_GPS); POINT_TYPE.put("pause", DB.LOCATION.TYPE_PAUSE); POINT_TYPE.put("resume", DB.LOCATION.TYPE_RESUME); POINT_TYPE.put("manual", DB.LOCATION.TYPE_GPS); } public RunKeeperSynchronizer(SyncManager syncManager) { if (CLIENT_ID == null || CLIENT_SECRET == null) { try { JSONObject tmp = new JSONObject(syncManager.loadData(this)); CLIENT_ID = tmp.getString("CLIENT_ID"); CLIENT_SECRET = tmp.getString("CLIENT_SECRET"); } catch (Exception ex) { Log.e(Constants.LOG, ex.getMessage()); } } context = syncManager.getContext(); } @Override public String getClientId() { return CLIENT_ID; } @Override public String getRedirectUri() { return REDIRECT_URI; } @Override public String getClientSecret() { return CLIENT_SECRET; } @Override public String getAuthUrl() { return AUTH_URL; } @Override public String getAuthExtra() { return null; } @Override public String getTokenUrl() { return TOKEN_URL; } @Override public String getRevokeUrl() { return null; } @Override public long getId() { return id; } @Override public String getName() { return NAME; } @Override public void init(ContentValues config) { String authConfig = config.getAsString(DB.ACCOUNT.AUTH_CONFIG); id = config.getAsLong("_id"); if (authConfig != null) { try { JSONObject tmp = new JSONObject(authConfig); access_token = tmp.optString("access_token", null); feed_access_token = tmp.optString("feed_access_token", null); if (feed_access_token == null) { feed_username = tmp.optString("username", null); feed_password = tmp.optString("password", null); } else { feed_username = null; feed_password = null; } } catch (Exception e) { Log.e(Constants.LOG, e.getMessage()); } } } @Override public boolean isConfigured() { return access_token != null; } @Override public String getAuthConfig() { JSONObject tmp = new JSONObject(); try { tmp.put("access_token", access_token); tmp.put("feed_access_token", feed_access_token); if (feed_access_token == null) { tmp.put("username", feed_username); tmp.put("password", feed_password); } else { tmp.put("username", null); tmp.put("password", null); } } catch (JSONException e) { Log.e(Constants.LOG, e.getMessage()); } return tmp.toString(); } @Override public Intent getAuthIntent(Activity activity) { return OAuth2Activity.getIntent(activity, this); } @Override public Status getAuthResult(int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK) { String authConfig = data.getStringExtra(DB.ACCOUNT.AUTH_CONFIG); try { access_token = new JSONObject(authConfig).getString("access_token"); return Status.OK; } catch (JSONException e) { Log.e(Constants.LOG, e.getMessage()); } } return Status.ERROR; } @Override public void reset() { access_token = null; feed_access_token = null; } @Override public Status connect() { Status s = Status.NEED_AUTH; s.authMethod = AuthMethod.OAUTH2; if (access_token == null) { return s; } if (feed_access_token == null && (feed_username != null && feed_password != null)) { return getFeedAccessToken(); } if (fitnessActivitiesUrl != null) { return Synchronizer.Status.OK; } /** * Get the fitnessActivities end-point */ String uri = null; HttpURLConnection conn = null; Exception ex = null; do { try { URL newurl = new URL(REST_URL + "/user"); conn = (HttpURLConnection) newurl.openConnection(); conn.setRequestProperty("Authorization", "Bearer " + access_token); InputStream in = new BufferedInputStream(conn.getInputStream()); uri = SyncHelper.parse(in).getString("fitness_activities"); } catch (MalformedURLException e) { ex = e; } catch (IOException e) { if (REST_URL.contains("https")) { REST_URL = REST_URL.replace("https", "http"); Log.e(Constants.LOG, e.getMessage()); Log.e(Constants.LOG, " => retry with REST_URL: " + REST_URL); continue; // retry } ex = e; } catch (JSONException e) { ex = e; } break; } while (true); if (conn != null) { conn.disconnect(); } if (ex != null) { Log.e(Constants.LOG, ex.getMessage()); } if (uri != null) { fitnessActivitiesUrl = uri; return Synchronizer.Status.OK; } s = Synchronizer.Status.ERROR; s.ex = ex; return s; } public Status listActivities(List<SyncActivityItem> list) { Status s = connect(); if (s != Status.OK) { return s; } String requestUrl = REST_URL + fitnessActivitiesUrl; while (requestUrl != null) { try { URL nextUrl = new URL(requestUrl); HttpURLConnection conn = (HttpURLConnection) nextUrl.openConnection(); conn.setDoInput(true); conn.setRequestMethod(RequestMethod.GET.name()); conn.addRequestProperty("Authorization", "Bearer " + access_token); conn.addRequestProperty("Content-Type", "application/vnd.com.runkeeper.FitnessActivityFeed+json"); InputStream input = new BufferedInputStream(conn.getInputStream()); if (conn.getResponseCode() == HttpStatus.SC_OK) { JSONObject resp = SyncHelper.parse(input); requestUrl = parseForNext(resp, list); s = Status.OK; } else { s = Status.ERROR; } input.close(); conn.disconnect(); } catch (IOException e) { Log.e(Constants.LOG, e.getMessage()); requestUrl = null; s = Status.ERROR; } catch (JSONException e) { Log.e(Constants.LOG, e.getMessage()); requestUrl = null; s = Status.ERROR; } } return s; } private String parseForNext(JSONObject resp, List<SyncActivityItem> items) throws JSONException { if (resp.has("items")) { JSONArray activities = resp.getJSONArray("items"); for (int i = 0; i < activities.length(); i++) { JSONObject item = activities.getJSONObject(i); SyncActivityItem ai = new SyncActivityItem(); String startTime = item.getString("start_time"); SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US); try { ai.setStartTime(TimeUnit.MILLISECONDS.toSeconds(format.parse(startTime).getTime())); } catch (ParseException e) { Log.e(Constants.LOG, e.getMessage()); return null; } Float time = Float.parseFloat(item.getString("duration")); ai.setDuration(time.longValue()); BigDecimal dist = new BigDecimal(Float.parseFloat(item.getString("total_distance"))); dist = dist.setScale(2, BigDecimal.ROUND_UP); ai.setDistance(dist.floatValue()); ai.setURI(REST_URL + item.getString("uri")); ai.setId((long) items.size()); String sport = item.getString("type"); if (runkeeper2sportMap.containsKey(sport)) { ai.setSport(runkeeper2sportMap.get(sport).getDbValue()); } else { ai.setSport(Sport.OTHER.getDbValue()); } items.add(ai); } } if (resp.has("next")) { return REST_URL + resp.getString("next"); } return null; } @Override public Status upload(SQLiteDatabase db, final long mID) { Status s; if ((s = connect()) != Status.OK) { return s; } /** * Get the fitnessActivities end-point */ HttpURLConnection conn = null; Exception ex; try { URL newurl = new URL(REST_URL + fitnessActivitiesUrl); Log.e(Constants.LOG, "url: " + newurl.toString()); conn = (HttpURLConnection) newurl.openConnection(); conn.setDoOutput(true); conn.setRequestMethod(RequestMethod.POST.name()); conn.addRequestProperty("Authorization", "Bearer " + access_token); conn.addRequestProperty("Content-type", "application/vnd.com.runkeeper.NewFitnessActivity+json"); RunKeeper rk = new RunKeeper(db); BufferedWriter w = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream())); rk.export(mID, w); w.flush(); int responseCode = conn.getResponseCode(); String amsg = conn.getResponseMessage(); conn.disconnect(); conn = null; if (responseCode >= HttpStatus.SC_OK && responseCode < HttpStatus.SC_MULTIPLE_CHOICES) { s = Status.OK; s.activityId = mID; return s; } ex = new Exception(amsg); } catch (Exception e) { ex = e; } if (ex != null) { Log.e(getName(), "Failed to upload: " + ex.getMessage()); } if (conn != null) { conn.disconnect(); } s = Synchronizer.Status.ERROR; s.ex = ex; s.activityId = mID; return s; } @Override public boolean checkSupport(Synchronizer.Feature f) { switch (f) { case FEED: case UPLOAD: case ACTIVITY_LIST: case GET_ACTIVITY: return true; case GET_WORKOUT: case WORKOUT_LIST: case LIVE: case SKIP_MAP: break; } return false; } @Override public ActivityEntity download(SyncActivityItem item) { ActivityEntity activity = new ActivityEntity(); Status s = connect(); if (s != Status.OK) { return null; } HttpURLConnection conn = null; try { URL activityUrl = new URL(item.getURI()); conn = (HttpURLConnection) activityUrl.openConnection(); conn.setDoInput(true); conn.setRequestMethod(RequestMethod.GET.name()); conn.addRequestProperty("Authorization", "Bearer " + access_token); conn.addRequestProperty("Content-type", "application/vnd.com.runkeeper.FitnessActivity+json"); if (conn.getResponseCode() == HttpStatus.SC_OK) { BufferedInputStream input = new BufferedInputStream(conn.getInputStream()); JSONObject resp = SyncHelper.parse(input); activity = RunKeeper.parseToActivity(resp, getLapLength()); } } catch (IOException e) { Log.e(Constants.LOG, e.getMessage()); return activity; } catch (JSONException e) { Log.e(Constants.LOG, e.getMessage()); return activity; } return activity; } private double getLapLength() { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); Resources res = context.getResources(); double lapLength = Formatter.getUnitMeters(context); if (prefs.getBoolean(res.getString(R.string.pref_autolap_active), false)) { String autoLap = prefs.getString(res.getString(R.string.pref_autolap), String.valueOf(lapLength)); try { lapLength = Double.parseDouble(autoLap); } catch (NumberFormatException e) { return lapLength; } return lapLength; } return lapLength; } @Override public void logout() { this.fitnessActivitiesUrl = null; } private Status getFeedAccessToken() { Synchronizer.Status s = Status.OK; HttpURLConnection conn = null; try { URL newurl = new URL(FEED_TOKEN_URL); conn = (HttpURLConnection) newurl.openConnection(); conn.setDoOutput(true); conn.setRequestMethod(RequestMethod.POST.name()); FormValues kv = new FormValues(); kv.put("email", feed_username); kv.put("password", feed_password); { OutputStream wr = new BufferedOutputStream(conn.getOutputStream()); kv.write(wr); wr.flush(); wr.close(); } InputStream in = new BufferedInputStream(conn.getInputStream()); JSONObject obj = SyncHelper.parse(in); conn.disconnect(); feed_access_token = obj.getString("accessToken"); return s; } catch (MalformedURLException e) { s = Status.ERROR; s.ex = e; } catch (ProtocolException e) { s = Status.ERROR; s.ex = e; } catch (IOException e) { s = Status.ERROR; s.ex = e; } catch (JSONException e) { s = Status.NEED_AUTH; s.authMethod = AuthMethod.USER_PASS; s.ex = e; } Log.e(Constants.LOG, s.ex.getMessage()); return s; } @Override public Status getFeed(FeedUpdater feedUpdater) { Status s = Status.NEED_AUTH; s.authMethod = AuthMethod.USER_PASS; if (feed_access_token == null) { return s; } List<ContentValues> reply = new ArrayList<ContentValues>(); long from = System.currentTimeMillis(); final int MAX_ITER = 5; for (int iter = 0; iter < MAX_ITER && reply.size() < 25; iter++) { try { JSONObject feed = requestFeed(from); JSONArray arr = feed.getJSONArray("feedItems"); for (int i = 0; i < arr.length(); i++) { JSONObject e = arr.getJSONObject(i); try { if (e.getInt("type") != 0) { continue; } ContentValues c = new ContentValues(); c.put(FEED.ACCOUNT_ID, getId()); c.put(FEED.EXTERNAL_ID, e.getString("id")); c.put(FEED.FEED_TYPE, FEED.FEED_TYPE_ACTIVITY); JSONObject d = e.getJSONObject("data"); Sport sport = runkeeper2sportMap.get(d.getInt("activityType")); if (sport != null) { c.put(FEED.FEED_SUBTYPE, sport.getDbValue()); } else { Log.w(getName(), "Unknown sport with id " + d.getInt("activityType")); c.put(FEED.FEED_SUBTYPE, DB.ACTIVITY.SPORT_OTHER); break; } c.put(FEED.START_TIME, e.getLong("posttime")); c.put(FEED.FLAGS, "brokenStartTime"); // BUH!! if (e.has("data")) { JSONObject p = e.getJSONObject("data"); if (p.has("duration")) { c.put(FEED.DURATION, p.getLong("duration")); } if (p.has("distance")) { c.put(FEED.DISTANCE, p.getDouble("distance")); } if (p.has("notes") && p.getString("notes") != null && !p.getString("notes").equals("null")) { c.put(FEED.NOTES, p.getString("notes")); } } SyncHelper.setName(c, e.getString("sourceUserDisplayName")); if (e.has("sourceUserAvatarUrl") && e.getString("sourceUserAvatarUrl").length() > 0) { c.put(FEED.USER_IMAGE_URL, e.getString("sourceUserAvatarUrl")); } reply.add(c); from = e.getLong("posttime"); } catch (Exception ex) { Log.e(Constants.LOG, ex.getMessage()); iter = MAX_ITER; } } } catch (IOException e) { Log.e(Constants.LOG, e.getMessage()); break; } catch (JSONException e) { Log.e(Constants.LOG, e.getMessage()); break; } } feedUpdater.addAll(reply); return Status.OK; } JSONObject requestFeed(long from) throws IOException, JSONException { URL newurl = new URL(FEED_URL); HttpURLConnection conn = (HttpURLConnection) newurl.openConnection(); conn.setDoOutput(true); conn.setRequestMethod(RequestMethod.POST.name()); conn.addRequestProperty("Authorization", "Bearer " + feed_access_token); FormValues kv = new FormValues(); kv.put("lastPostTime", Long.toString(from)); kv.put("feedItemTypes", FEED_ITEM_TYPES); { OutputStream wr = new BufferedOutputStream(conn.getOutputStream()); kv.write(wr); wr.flush(); wr.close(); } int responseCode = conn.getResponseCode(); String amsg = conn.getResponseMessage(); InputStream in = new BufferedInputStream(conn.getInputStream()); JSONObject obj = SyncHelper.parse(in); conn.disconnect(); if (responseCode == HttpStatus.SC_OK) { return obj; } throw new IOException(amsg); } }