package org.edx.mobile.http;
import android.content.Context;
import android.os.Bundle;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.edx.mobile.R;
import org.edx.mobile.core.IEdxEnvironment;
import org.edx.mobile.http.model.CourseIdObject;
import org.edx.mobile.http.model.EnrollmentRequestBody;
import org.edx.mobile.interfaces.SectionItemInterface;
import org.edx.mobile.logger.Logger;
import org.edx.mobile.model.api.AnnouncementsModel;
import org.edx.mobile.model.api.EnrolledCoursesResponse;
import org.edx.mobile.model.api.HandoutModel;
import org.edx.mobile.model.api.SectionEntry;
import org.edx.mobile.model.api.SyncLastAccessedSubsectionResponse;
import org.edx.mobile.model.api.VideoResponseModel;
import org.edx.mobile.model.course.CourseComponent;
import org.edx.mobile.model.course.CourseStructureJsonHandler;
import org.edx.mobile.model.course.CourseStructureV1Model;
import org.edx.mobile.module.prefs.LoginPrefs;
import org.edx.mobile.module.registration.model.RegistrationDescription;
import org.edx.mobile.services.CourseManager;
import org.edx.mobile.util.DateUtil;
import org.edx.mobile.util.NetworkUtil;
import org.json.JSONObject;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpCookie;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.CacheControl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
/**
* DESIGN NOTES -
* retrofit uses annotation approach, which can be handy for simple cases.
* there are some challenges n our case,
* 1. for the same endpoint, we can return different type of objects,
* 2. the cache behavior in okhttp is controlled by http header, but in our case, it is totally controlled by our client logic.
* 3. in okhttp document, cache is not thread safe, so it recommend singleton pattern, on the other hand, intercept is not individual request based.
*/
@Singleton
public class RestApiManager implements IApi {
protected final Logger logger = new Logger(getClass().getName());
@Inject
IEdxEnvironment environment;
@Inject
LoginPrefs loginPrefs;
private final OkHttpClient oauthBasedClient;
private final OauthRestApi oauthRestApi;
private final OkHttpClient client;
private final Gson gson = new Gson();
private Context context;
@Inject
public RestApiManager(Context context) {
this.context = context;
this.oauthBasedClient = OkHttpUtil.getOAuthBasedClientWithOfflineCache(context);
Retrofit retrofit = new Retrofit.Builder()
.client(oauthBasedClient)
.baseUrl(getBaseUrl())
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
oauthRestApi = retrofit.create(OauthRestApi.class);
client = OkHttpUtil.getClient(context);
}
public final OkHttpClient getClient() {
return client;
}
public final OkHttpClient createSpeedTestClient() {
OkHttpClient.Builder builder = OkHttpUtil.getClient(context).newBuilder();
int timeoutMillis = context.getResources().getInteger(R.integer.speed_test_timeout_in_milliseconds);
return builder.connectTimeout(timeoutMillis, TimeUnit.MILLISECONDS).build();
}
public String getBaseUrl() {
return environment.getConfig().getApiHostURL();
}
@Override
public List<EnrolledCoursesResponse> getEnrolledCourses() throws Exception {
return getEnrolledCourses(false);
}
@Override
public EnrolledCoursesResponse getCourseById(String courseId) {
try {
for (EnrolledCoursesResponse r : getEnrolledCourses(true)) {
if (r.getCourse().getId().equals(courseId)) {
return r;
}
}
} catch (Exception ex) {
logger.error(ex);
}
return null;
}
@Override
public List<EnrolledCoursesResponse> getEnrolledCourses(boolean fetchFromCache) throws Exception {
String orgCode = environment.getConfig().getOrganizationCode();
if (!NetworkUtil.isConnected(context)) {
return oauthRestApi.getEnrolledCourses(loginPrefs.getUsername(), orgCode).execute().body();
} else if (fetchFromCache) {
return oauthRestApi.getEnrolledCourses(loginPrefs.getUsername(), orgCode).execute().body();
} else {
return oauthRestApi.getEnrolledCoursesNoCache(loginPrefs.getUsername(), orgCode).execute().body();
}
}
@Override
public HandoutModel getHandout(String url, boolean prefCache) throws Exception {
Bundle p = new Bundle();
p.putString("format", "json");
String urlWithAppendedParams = OkHttpUtil.toGetUrl(url, p);
Request.Builder builder = new Request.Builder().url(urlWithAppendedParams);
if (NetworkUtil.isConnected(context) || !prefCache) {
builder.cacheControl(CacheControl.FORCE_NETWORK);
}
Request request = builder.build();
Response response = oauthBasedClient.newCall(request).execute();
if (!response.isSuccessful()) throw new Exception("Unexpected code " + response);
return gson.fromJson(response.body().charStream(), HandoutModel.class);
}
@Override
public List<AnnouncementsModel> getAnnouncement(String url, boolean preferCache) throws Exception {
Bundle p = new Bundle();
p.putString("format", "json");
String urlWithAppendedParams = OkHttpUtil.toGetUrl(url, p);
Request.Builder builder = new Request.Builder().url(urlWithAppendedParams);
if (NetworkUtil.isConnected(context) && !preferCache) {
builder.cacheControl(CacheControl.FORCE_NETWORK);
}
Request request = builder.build();
Response response = oauthBasedClient.newCall(request).execute();
if (!response.isSuccessful()) throw new Exception("Unexpected code " + response);
TypeToken<List<AnnouncementsModel>> t = new TypeToken<List<AnnouncementsModel>>() {
};
return gson.fromJson(response.body().charStream(), t.getType());
}
@Override
public String downloadTranscript(String url) throws Exception {
if (url != null) {
Request.Builder builder = new Request.Builder().url(url);
if (NetworkUtil.isConnected(context)) {
builder.cacheControl(CacheControl.FORCE_NETWORK);
}
Request request = builder.build();
Response response = oauthBasedClient.newCall(request).execute();
if (!response.isSuccessful()) throw new Exception("Unexpected code " + response);
return response.body().string();
}
return null;
}
@Override
public SyncLastAccessedSubsectionResponse syncLastAccessedSubsection(String courseId, String lastVisitedModuleId) throws Exception {
String date = DateUtil.getCurrentTimeStamp();
EnrollmentRequestBody.LastAccessRequestBody body = new EnrollmentRequestBody.LastAccessRequestBody();
body.last_visited_module_id = lastVisitedModuleId;
body.modification_date = date;
retrofit2.Response<SyncLastAccessedSubsectionResponse> response =
oauthRestApi.syncLastAccessedSubsection(body, loginPrefs.getUsername(), courseId).execute();
if (!response.isSuccessful()) {
throw new HttpResponseStatusException(response.code());
}
return response.body();
}
@Override
public SyncLastAccessedSubsectionResponse getLastAccessedSubsection(String courseId) throws Exception {
retrofit2.Response<SyncLastAccessedSubsectionResponse> response =
oauthRestApi.getLastAccessedSubsection(loginPrefs.getUsername(), courseId).execute();
if (!response.isSuccessful()) {
throw new HttpResponseStatusException(response.code());
}
return response.body();
}
@Override
public RegistrationDescription getRegistrationDescription() throws Exception {
Gson gson = new Gson();
InputStream in = context.getAssets().open("config/registration_form.json");
RegistrationDescription form = gson.fromJson(new InputStreamReader(in), RegistrationDescription.class);
logger.debug("picking up registration description (form) from assets, not from cache");
return form;
}
@Override
public Boolean enrollInACourse(String courseId, boolean email_opt_in) throws Exception {
String enrollUrl = getBaseUrl() + "/api/enrollment/v1/enrollment";
logger.debug("POST url for enrolling in a Course: " + enrollUrl);
CourseIdObject idObject = new CourseIdObject();
idObject.email_opt_in = Boolean.toString(email_opt_in);
idObject.course_id = courseId;
EnrollmentRequestBody body = new EnrollmentRequestBody();
body.course_details = idObject;
retrofit2.Response<String> response = oauthRestApi.enrollACourse(body).execute();
if (response.isSuccessful()) {
String json = response.body();
if (json != null && !json.isEmpty()) {
JSONObject resultJson = new JSONObject(json);
if (resultJson.has("error")) {
return false;
} else {
return true;
}
}
}
return false;
}
@Override
public List<HttpCookie> getSessionExchangeCookie() throws Exception {
String url = getBaseUrl() + "/oauth2/login/";
return OkHttpUtil.getCookies(context, url, false);
}
public CourseComponent getCourseStructure(String courseId, boolean preferCache) throws Exception {
String username = URLEncoder.encode(loginPrefs.getUsername(), "UTF-8");
String block_counts = URLEncoder.encode("video", "UTF-8");
String requested_fields = URLEncoder.encode("graded,format,student_view_multi_device", "UTF-8");
String student_view_data = URLEncoder.encode("video,discussion", "UTF-8");
String response;
if (!NetworkUtil.isConnected(context)) {
response = oauthRestApi.getCourseOutline(courseId, username, requested_fields, student_view_data, block_counts).execute().body();
} else if (preferCache) {
response = oauthRestApi.getCourseOutline(courseId, username, requested_fields, student_view_data, block_counts).execute().body();
} else {
response = oauthRestApi.getCourseOutlineNoCache(courseId, username, requested_fields, student_view_data, block_counts).execute().body();
}
CourseStructureV1Model model = new CourseStructureJsonHandler().processInput(response);
return (CourseComponent) CourseManager.normalizeCourseStructure(model, courseId);
}
@Override
public VideoResponseModel getVideoById(String courseId, String videoId) throws Exception {
return null;
}
@Override
public Map<String, SectionEntry> getCourseHierarchy(String courseId, boolean preferCache) throws Exception {
return null;
}
@Override
public ArrayList<SectionItemInterface> getLiveOrganizedVideosByChapter(String courseId, String chapter) {
return null;
}
@Override
public HttpManager.HttpResult getCourseStructure(HttpRequestDelegate delegate) throws Exception {
return null;
}
}