/*
* Copyright 2015 - 2016 Hauke Oldsen
*
* 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 de.gebatzens.sia;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.os.Build;
import android.support.design.widget.Snackbar;
import android.support.v4.widget.SwipeRefreshLayout;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import com.google.firebase.messaging.FirebaseMessaging;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.zip.GZIPInputStream;
import de.gebatzens.sia.data.Exams;
import de.gebatzens.sia.data.Filter;
import de.gebatzens.sia.data.Subst;
import de.gebatzens.sia.data.Mensa;
import de.gebatzens.sia.data.News;
import de.gebatzens.sia.data.StaticData;
public class SiaAPI {
public static final String PREFS_NAME = "remoteprefs";
SharedPreferences prefs;
public SiaAPI() {
prefs = SIAApp.SIA_APP.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
public void showReloadSnackbar(final String msg) {
if(SIAApp.SIA_APP.activity == null)
return;
SIAApp.SIA_APP.activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Snackbar.make(SIAApp.SIA_APP.activity.getWindow().getDecorView().findViewById(R.id.coordinator_layout), msg, Snackbar.LENGTH_LONG)
.setAction(R.string.again, new View.OnClickListener() {
@Override
public void onClick(View v) {
final View rv = SIAApp.SIA_APP.activity.getWindow().getDecorView();
((SwipeRefreshLayout) rv.findViewById(R.id.refresh)).setRefreshing(true);
SIAApp.SIA_APP.refreshAsync(new Runnable() {
@Override
public void run() {
((SwipeRefreshLayout) rv.findViewById(R.id.refresh)).setRefreshing(false);
}
}, true, SIAApp.SIA_APP.school.fragments.get(SIAApp.SIA_APP.getFragmentIndex()));
}
}).show();
}
});
}
public void logout() {
final String token = getToken();
FirebaseMessaging.getInstance().unsubscribeFromTopic("sia_sid_" + SIAApp.SIA_APP.school.sid);
for(String name : SIAApp.SIA_APP.fileList())
if(name.startsWith("schedule"))
SIAApp.SIA_APP.deleteFile(name);
SIAApp.SIA_APP.deleteFile("news");
SIAApp.SIA_APP.deleteFile("mensa");
SIAApp.SIA_APP.deleteFile("exams");
SIAApp.SIA_APP.deleteFile("ggfilter");
SIAApp.SIA_APP.deleteFile("ggfilterV2");
SIAApp.SIA_APP.filters.clear();
SIAApp.SIA_APP.school = null;
prefs.edit().clear().apply();
SIAApp.SIA_APP.preferences.edit().remove("customTheme").remove("sid").apply();
new Thread() {
@Override
public void run() {
try {
APIResponse re = doRequest("/logout?token=" + token, null);
if(re.state != APIState.SUCCEEDED) {
Log.w("ggvp", "Warning: Logout received " + re.state);
}
} catch(Exception e) {
Log.w("ggvp", "Warning: Logout failed " + e.getMessage());
}
}
}.start();
}
public Subst.GGPlans getPlans(boolean toast) {
Subst.GGPlans plans = new Subst.GGPlans();
String snackMessage = "";
try {
APIResponse re = doRequest("/subst?token=" + getToken(), null);
if(re.state == APIState.SUCCEEDED) {
Iterator<String> days = ((JSONObject) re.data).keys();
while (days.hasNext()) {
String date = days.next();
JSONObject obj = ((JSONObject) re.data).getJSONObject(date);
Subst plan = new Subst();
plan.date = Subst.parseDate(date);
plans.add(plan);
JSONArray entries = obj.getJSONArray("entries");
getPlan(entries, plan);
JSONArray messages = obj.getJSONArray("messages");
for (int i = 0; i < messages.length(); i++) {
if(i < messages.length())
plan.special.add(messages.getString(i) + "\n");
else
plan.special.add(messages.getString(i));
}
}
} else {
throw new APIException(re.reason);
}
} catch(Exception e) {
if(e instanceof IOException || e instanceof APIException) {
Log.w("ggvp", "Failed to get plans " + e.getMessage());
snackMessage = e instanceof IOException ? SIAApp.SIA_APP.getString(R.string.no_internet_connection) : e.getMessage();
} else {
snackMessage = SIAApp.SIA_APP.getString(R.string.unknown_error);
e.printStackTrace();
}
plans.throwable = e;
}
Collections.sort(plans, new Comparator<Subst>() {
@Override
public int compare(Subst lhs, Subst rhs) {
return lhs.date.compareTo(rhs.date);
}
});
if(plans.throwable != null) {
if (plans.load()) {
final Throwable t = plans.throwable;
plans.throwable = null;
if(toast)
showReloadSnackbar(snackMessage);
}
} else {
SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy HH:mm");
plans.loadDate = new Date();
}
return plans;
}
private void getPlan(JSONArray array, Subst p) throws Exception {
for(int i = 0; i < array.length(); i++) {
Subst.Entry e = new Subst.Entry();
e.date = p.date;
p.add(e);
JSONObject entry = array.getJSONObject(i);
e.clazz = entry.getString("class");
e.lesson = "" + entry.getInt("lesson");
e.teacher = entry.getString("substitutor");
e.missing = entry.getString("missing");
e.subject = entry.getString("subject");
e.repsub = entry.getString("substitutionsubject");
e.type = entry.getString("substitutionplantype");
e.comment = entry.getString("comment");
e.room = entry.getString("room");
e.unify();
}
}
public News getNews(boolean toast) {
News n = new News();
try {
DateFormat parser = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
APIResponse re = doRequest("/news?token=" + getToken(), null);
if(re.state == APIState.SUCCEEDED) {
JSONArray entries = (JSONArray) re.data;
for(int i = 0; i < entries.length(); i++) {
JSONObject cobj = entries.getJSONObject(i);
News.Entry e = new News.Entry();
n.add(e);
e.id = cobj.getString("id");
e.date = parser.parse(cobj.getString("date"));
e.source = cobj.getString("source");
e.topic = cobj.getString("topic");
e.title = cobj.getString("title");
e.text = cobj.getString("text");
}
} else {
throw new APIException(re.reason);
}
} catch (final Exception e) {
e.printStackTrace();
if(toast)
showReloadSnackbar(e instanceof IOException ? SIAApp.SIA_APP.getString(R.string.no_internet_connection) : e.getMessage());
if(!n.load()) {
n.throwable = e;
}
}
return n;
}
public StaticData downloadStaticFile(String name, boolean snack) {
StaticData data = new StaticData();
data.name = name;
try {
APIResponse re = doRequest("/static?token=" + getToken() + "&file=" + URLEncoder.encode(name, "UTF-8"), null);
if(re.state == APIState.SUCCEEDED) {
data.data = Base64.decode((String) re.data, Base64.DEFAULT);
} else {
throw new APIException(re.reason);
}
} catch(Exception e) {
e.printStackTrace();
if(snack)
showReloadSnackbar(e instanceof IOException ? SIAApp.SIA_APP.getString(R.string.no_internet_connection) : e.getMessage());
if(!data.load()) {
data.throwable = e;
}
}
return data;
}
public Mensa getMensa(boolean toast) {
Mensa m = new Mensa();
try {
APIResponse re = doRequest("/cafeteria?token=" + getToken(), null);
if(re.state == APIState.SUCCEEDED) {
JSONArray entries = (JSONArray) re.data;
for(int i = 0; i < entries.length(); i++) {
JSONObject obj = entries.getJSONObject(i);
Mensa.MensaItem mi = new Mensa.MensaItem();
m.add(mi);
mi.date = obj.getString("date");
mi.id = obj.getString("id");
mi.meal = obj.getString("meal");
mi.garnish = obj.getString("garnish");
mi.dessert = obj.getString("dessert");
mi.vegetarian = obj.getString("vegetarian");
mi.image = obj.getString("image");
}
} else {
throw new APIException(re.reason);
}
} catch (final Exception e) {
e.printStackTrace();
if(toast)
showReloadSnackbar(e instanceof IOException ? SIAApp.SIA_APP.getString(R.string.no_internet_connection) : e.getMessage());
if(!m.load()) {
m.throwable = e;
}
}
return m;
}
public Bitmap getMensaImage(String filename) throws IOException {
//TODO not implemented yet
/*HttpsURLConnection con = openConnection("/infoapp/infoapp_provider_new.php?site=mensa_image&sessid=" + session.id + "&filename=" + filename, true);
con.setRequestMethod("GET");
if(con.getResponseCode() == 200) {
return BitmapFactory.decodeStream(con.getInputStream());
} else {
throw new IOException();
}*/
throw new IOException();
}
public Exams getExams(boolean toast) {
Exams exams = new Exams();
DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
try {
APIResponse re = doRequest("/exams?token=" + getToken(), null);
if(re.state == APIState.SUCCEEDED) {
JSONArray entries = (JSONArray) re.data;
for(int i = 0; i < entries.length(); i++) {
JSONObject obj = entries.getJSONObject(i);
Exams.ExamItem e = new Exams.ExamItem();
exams.add(e);
e.date = sdf.parse(obj.getString("date"));
e.clazz = obj.getString("class");
e.lesson = obj.getString("lesson");
e.length = obj.getString("length");
e.subject = obj.getString("subject");
e.teacher = obj.optString("teacher");
}
exams.sort();
} else {
throw new APIException(re.reason);
}
} catch (final Exception e) {
e.printStackTrace();
if(toast)
showReloadSnackbar(e instanceof IOException ? SIAApp.SIA_APP.getString(R.string.no_internet_connection) : e.getMessage());
if(!exams.load()) {
exams.throwable = e;
}
}
return exams;
}
/**
* Sends an authentication request
* @param user
* @param pass
* @return 0: ok, 1: invalid user/passwd, 2: no connection, 3: maintenance, 4: everything else
*/
public int login(String sid, String user, String pass) {
try {
JSONObject post = new JSONObject();
post.put("username", user);
post.put("passwd", pass);
post.put("sid", sid);
APIResponse re = doRequest("/auth", post);
switch(re.state) {
case SUCCEEDED:
JSONObject data = (JSONObject) re.data;
String group = prefs.getString("group", null);
Filter.FilterList filters = SIAApp.SIA_APP.filters;
if (group != null && !group.equals("lehrer")) {
filters.including.add(new Filter.IncludingFilter(Filter.FilterType.CLASS, group));
} else if (group != null) {
filters.including.add(new Filter.IncludingFilter(Filter.FilterType.TEACHER, user));
}
FilterActivity.saveFilter(SIAApp.SIA_APP.filters);
if(data.has("username")) {
if(!data.getString("username").equals(user) || !data.getString("sid").equals(sid))
return 3;
}
SharedPreferences.Editor edit = prefs.edit();
edit.putString("username", user);
edit.putString("token", data.getString("token"));
edit.apply();
APIResponse resp = doRequest("/schoolInfo?token=" + data.getString("token"), null);
if(resp.state == APIState.FAILED && resp.reason.equals(API_MAINTENANCE))
return 3;
else if(resp.state == APIState.FAILED)
return 4;
School.updateSchool((JSONObject) resp.data);
School.saveList();
SIAApp.SIA_APP.setSchool(sid);
FirebaseMessaging.getInstance().subscribeToTopic("sia_sid_" + sid);
return 0;
case FAILED:
switch(re.reason) {
case API_INVALID_TOKEN:
return 1;
case API_MAINTENANCE:
return 3;
default:
return 4;
}
}
return 4;
} catch (Exception e) {
if (e instanceof IOException) {
Log.w("ggvp", "Login failed " + e.getMessage());
return 2;
} else {
e.printStackTrace();
return 4;
}
}
}
public String getToken() {
return prefs.getString("token", null);
}
public String getUsername() {
return prefs.getString("username", isLoggedIn() ? SIAApp.SIA_APP.getString(R.string.anonymous) : null);
}
public boolean isLoggedIn() {
return getToken() != null;
}
public APIResponse doRequest(String url, JSONObject request) throws IOException {
HttpURLConnection con = (HttpURLConnection) new URL(BuildConfig.BACKEND_SERVER + url).openConnection();
con.setRequestProperty("User-Agent", "SchulinfoAPP/" + BuildConfig.VERSION_NAME + " (" +
BuildConfig.VERSION_CODE + " " + BuildConfig.BUILD_TYPE + " Android " + Build.VERSION.RELEASE + " " + Build.PRODUCT + ")");
con.setRequestProperty("Accept-Encoding", "gzip");
con.setConnectTimeout(3000);
con.setRequestMethod(request == null ? "GET" : "POST");
con.setInstanceFollowRedirects(false);
if(request != null) {
con.setDoOutput(true);
con.setRequestProperty("Content-Type", "application/json");
DataOutputStream wr = new DataOutputStream(con.getOutputStream());
wr.writeBytes(request.toString());
wr.flush();
wr.close();
}
if(BuildConfig.DEBUG)
Log.d("ggvp", "connection to " + con.getURL() + " established");
InputStream in = con.getResponseCode() != 200 ? con.getErrorStream() : con.getInputStream();
String encoding = con.getHeaderField("Content-Encoding");
if(encoding != null && encoding.equalsIgnoreCase("gzip")) {
in = new GZIPInputStream(in);
}
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String response = "";
String line = "";
while((line = reader.readLine()) != null)
response += line;
JSONObject json = null;
try {
json = new JSONObject(response);
String state = json.getString("state");
Object data = json.opt("data");
String reason = json.optString("reason", "");
Log.d("ggvp", "received state " + state + " " + con.getResponseCode() + " reason: " + reason);
return new APIResponse(state.equals("succeeded") ? APIState.SUCCEEDED : APIState.FAILED, data, reason);
} catch(JSONException e) {
Log.e("ggvp", e.toString());
e.printStackTrace();
return new APIResponse(APIState.FAILED);
}
}
public enum APIState {
SUCCEEDED, FAILED
}
public static final String API_TOKEN_EXPIRED = "token expired";
public static final String API_INVALID_TOKEN = "invalid token";
public static final String API_NOT_FOUND = "not found";
public static final String API_METHOD_NOT_ALLOWED = "method not allowed";
public static final String API_MAINTENANCE = "maintenance";
public static class APIResponse {
APIState state;
Object data;
String reason;
public APIResponse(APIState state) {
this(state, null, "");
}
public APIResponse(APIState state, Object data) {
this(state, data, "");
}
public APIResponse(APIState state, Object data, String reason) {
this.state = state;
this.data = data;
this.reason = reason;
}
}
}