diff --git a/app/src/main/java/com/keylesspalace/tusky/FetchTimelineListener.java b/app/src/main/java/com/keylesspalace/tusky/FetchTimelineListener.java
deleted file mode 100644
index 0c612814..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/FetchTimelineListener.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.keylesspalace.tusky;
-
-import java.io.IOException;
-import java.util.List;
-
-public interface FetchTimelineListener {
- void onFetchTimelineSuccess(List statuses, boolean added);
- void onFetchTimelineFailure(IOException e);
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/FetchTimelineTask.java b/app/src/main/java/com/keylesspalace/tusky/FetchTimelineTask.java
deleted file mode 100644
index abc7959f..00000000
--- a/app/src/main/java/com/keylesspalace/tusky/FetchTimelineTask.java
+++ /dev/null
@@ -1,235 +0,0 @@
-package com.keylesspalace.tusky;
-
-import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.text.Html;
-import android.text.Spanned;
-import android.util.JsonReader;
-import android.util.JsonToken;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.UnsupportedEncodingException;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.net.ssl.HttpsURLConnection;
-
-public class FetchTimelineTask extends AsyncTask {
- private Context context;
- private FetchTimelineListener fetchTimelineListener;
- private String domain;
- private String accessToken;
- private String fromId;
- private List statuses;
- private IOException ioException;
-
- public FetchTimelineTask(
- Context context, FetchTimelineListener listener, String domain, String accessToken,
- String fromId) {
- super();
- this.context = context;
- fetchTimelineListener = listener;
- this.domain = domain;
- this.accessToken = accessToken;
- this.fromId = fromId;
- }
-
- private Date parseDate(String dateTime) {
- Date date;
- String s = dateTime.replace("Z", "+00:00");
- SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
- try {
- date = format.parse(s);
- } catch (ParseException e) {
- e.printStackTrace();
- return null;
- }
- return date;
- }
-
- private CharSequence trimTrailingWhitespace(CharSequence s) {
- int i = s.length();
- do {
- i--;
- } while (i >= 0 && Character.isWhitespace(s.charAt(i)));
- return s.subSequence(0, i + 1);
- }
-
- private Spanned compatFromHtml(String html) {
- Spanned result;
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
- } else {
- result = Html.fromHtml(html);
- }
- /* Html.fromHtml returns trailing whitespace if the html ends in a
tag, which
- * all status contents do, so it should be trimmed. */
- return (Spanned) trimTrailingWhitespace(result);
- }
-
- private com.keylesspalace.tusky.Status readStatus(JsonReader reader, boolean isReblog)
- throws IOException {
- JsonToken check = reader.peek();
- if (check == JsonToken.NULL) {
- reader.skipValue();
- return null;
- }
- String id = null;
- String displayName = null;
- String username = null;
- com.keylesspalace.tusky.Status reblog = null;
- String content = null;
- String avatar = null;
- Date createdAt = null;
- reader.beginObject();
- while (reader.hasNext()) {
- String name = reader.nextName();
- switch (name) {
- case "id": {
- id = reader.nextString();
- break;
- }
- case "account": {
- reader.beginObject();
- while (reader.hasNext()) {
- name = reader.nextName();
- switch (name) {
- case "acct": {
- username = reader.nextString();
- break;
- }
- case "display_name": {
- displayName = reader.nextString();
- break;
- }
- case "avatar": {
- avatar = reader.nextString();
- break;
- }
- default: {
- reader.skipValue();
- break;
- }
- }
- }
- reader.endObject();
- break;
- }
- case "reblog": {
- /* This case shouldn't be hit after the first recursion at all. But if this
- * method is passed unusual data this check will prevent extra recursion */
- if (!isReblog) {
- assert(false);
- reblog = readStatus(reader, true);
- }
- break;
- }
- case "content": {
- content = reader.nextString();
- break;
- }
- case "created_at": {
- createdAt = parseDate(reader.nextString());
- break;
- }
- default: {
- reader.skipValue();
- break;
- }
- }
- }
- reader.endObject();
- assert(username != null);
- com.keylesspalace.tusky.Status status;
- if (reblog != null) {
- status = reblog;
- status.setRebloggedByUsername(username);
- } else {
- assert(content != null);
- Spanned contentPlus = compatFromHtml(content);
- status = new com.keylesspalace.tusky.Status(
- id, displayName, username, contentPlus, avatar, createdAt);
- }
- return status;
- }
-
- private String parametersToQuery(Map parameters)
- throws UnsupportedEncodingException {
- StringBuilder s = new StringBuilder();
- String between = "";
- for (Map.Entry entry : parameters.entrySet()) {
- s.append(between);
- s.append(URLEncoder.encode(entry.getKey(), "UTF-8"));
- s.append("=");
- s.append(URLEncoder.encode(entry.getValue(), "UTF-8"));
- between = "&";
- }
- String urlParameters = s.toString();
- return "?" + urlParameters;
- }
-
- @Override
- protected Boolean doInBackground(String... data) {
- Boolean successful = true;
- HttpsURLConnection connection = null;
- try {
- String endpoint = context.getString(R.string.endpoint_timelines_home);
- String query = "";
- if (fromId != null) {
- Map parameters = new HashMap<>();
- if (fromId != null) {
- parameters.put("max_id", fromId);
- }
- query = parametersToQuery(parameters);
- }
- URL url = new URL("https://" + domain + endpoint + query);
- connection = (HttpsURLConnection) url.openConnection();
- connection.setRequestMethod("GET");
- connection.setRequestProperty("Authorization", "Bearer " + accessToken);
- connection.connect();
-
- statuses = new ArrayList<>(20);
- JsonReader reader = new JsonReader(
- new InputStreamReader(connection.getInputStream(), "UTF-8"));
- reader.beginArray();
- while (reader.hasNext()) {
- statuses.add(readStatus(reader, false));
- }
- reader.endArray();
- reader.close();
- } catch (IOException e) {
- ioException = e;
- successful = false;
- } finally {
- if (connection != null) {
- connection.disconnect();
- }
- }
- return successful;
- }
-
- @Override
- protected void onPostExecute(Boolean wasSuccessful) {
- super.onPostExecute(wasSuccessful);
- if (fetchTimelineListener != null) {
- if (wasSuccessful) {
- fetchTimelineListener.onFetchTimelineSuccess(statuses, fromId != null);
- } else {
- assert(ioException != null);
- fetchTimelineListener.onFetchTimelineFailure(ioException);
- }
- }
- }
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
index 898591df..24a15975 100644
--- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.java
@@ -7,7 +7,6 @@ import android.net.Uri;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
-import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
index 5ad3998b..c43112d1 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java
@@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
@@ -12,14 +13,30 @@ import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
+import android.text.Html;
+import android.text.Spanned;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
-import java.io.IOException;
-import java.util.List;
+import com.android.volley.AuthFailureError;
+import com.android.volley.Response;
+import com.android.volley.VolleyError;
+import com.android.volley.toolbox.JsonArrayRequest;
-public class MainActivity extends AppCompatActivity implements FetchTimelineListener,
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MainActivity extends AppCompatActivity implements
SwipeRefreshLayout.OnRefreshListener {
private String domain = null;
@@ -72,8 +89,117 @@ public class MainActivity extends AppCompatActivity implements FetchTimelineList
sendFetchTimelineRequest();
}
- private void sendFetchTimelineRequest(String fromId) {
- new FetchTimelineTask(this, this, domain, accessToken, fromId).execute();
+ private Date parseDate(String dateTime) {
+ Date date;
+ String s = dateTime.replace("Z", "+00:00");
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
+ try {
+ date = format.parse(s);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ return null;
+ }
+ return date;
+ }
+
+ private CharSequence trimTrailingWhitespace(CharSequence s) {
+ int i = s.length();
+ do {
+ i--;
+ } while (i >= 0 && Character.isWhitespace(s.charAt(i)));
+ return s.subSequence(0, i + 1);
+ }
+
+ private Spanned compatFromHtml(String html) {
+ Spanned result;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
+ } else {
+ result = Html.fromHtml(html);
+ }
+ /* Html.fromHtml returns trailing whitespace if the html ends in a tag, which
+ * all status contents do, so it should be trimmed. */
+ return (Spanned) trimTrailingWhitespace(result);
+ }
+
+ private Status parseStatus(JSONObject object, boolean isReblog) throws JSONException {
+ String id = object.getString("id");
+ String content = object.getString("content");
+ Date createdAt = parseDate(object.getString("created_at"));
+
+ JSONObject account = object.getJSONObject("account");
+ String displayName = account.getString("display_name");
+ String username = account.getString("acct");
+ String avatar = account.getString("avatar");
+
+ Status reblog = null;
+ /* This case shouldn't be hit after the first recursion at all. But if this method is
+ * passed unusual data this check will prevent extra recursion */
+ if (!isReblog) {
+ JSONObject reblogObject = object.optJSONObject("reblog");
+ if (reblogObject != null) {
+ reblog = parseStatus(reblogObject, true);
+ }
+ }
+
+ Status status;
+ if (reblog != null) {
+ status = reblog;
+ status.setRebloggedByUsername(username);
+ } else {
+ Spanned contentPlus = compatFromHtml(content);
+ status = new Status(id, displayName, username, contentPlus, avatar, createdAt);
+ }
+ return status;
+ }
+
+ private List parseStatuses(JSONArray array) throws JSONException {
+ List statuses = new ArrayList<>();
+ for (int i = 0; i < array.length(); i++) {
+ JSONObject object = array.getJSONObject(i);
+ statuses.add(parseStatus(object, false));
+ }
+ return statuses;
+ }
+
+ private void sendFetchTimelineRequest(final String fromId) {
+ String endpoint = getString(R.string.endpoint_timelines_home);
+ String url = "https://" + domain + endpoint;
+ JsonArrayRequest request = new JsonArrayRequest(url,
+ new Response.Listener() {
+ @Override
+ public void onResponse(JSONArray response) {
+ List statuses = null;
+ try {
+ statuses = parseStatuses(response);
+ } catch (JSONException e) {
+ onFetchTimelineFailure(e);
+ }
+ if (statuses != null) {
+ onFetchTimelineSuccess(statuses, fromId != null);
+ }
+ }
+ }, new Response.ErrorListener() {
+ @Override
+ public void onErrorResponse(VolleyError error) {
+ onFetchTimelineFailure(error);
+ }
+ }) {
+ @Override
+ public Map getHeaders() throws AuthFailureError {
+ Map headers = new HashMap<>();
+ headers.put("Authorization", "Bearer " + accessToken);
+ return headers;
+ }
+
+ @Override
+ protected Map getParams() throws AuthFailureError {
+ Map parameters = new HashMap<>();
+ parameters.put("max_id", fromId);
+ return parameters;
+ }
+ };
+ VolleySingleton.getInstance(this).addToRequestQueue(request);
}
private void sendFetchTimelineRequest() {
@@ -89,7 +215,7 @@ public class MainActivity extends AppCompatActivity implements FetchTimelineList
swipeRefreshLayout.setRefreshing(false);
}
- public void onFetchTimelineFailure(IOException exception) {
+ public void onFetchTimelineFailure(Exception exception) {
Toast.makeText(this, R.string.error_fetching_timeline, Toast.LENGTH_SHORT).show();
swipeRefreshLayout.setRefreshing(false);
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java
index b641fdce..5d4506ee 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/TimelineAdapter.java
@@ -2,6 +2,7 @@ package com.keylesspalace.tusky;
import android.content.Context;
import android.graphics.Bitmap;
+import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.text.Spanned;
import android.view.LayoutInflater;
@@ -169,11 +170,16 @@ public class TimelineAdapter extends RecyclerView.Adapter {
return prefix + span + unit;
}
- public void setCreatedAt(Date createdAt) {
- long then = createdAt.getTime();
- long now = new Date().getTime();
- String since = getRelativeTimeSpanString(then, now);
- sinceCreated.setText(since);
+ public void setCreatedAt(@Nullable Date createdAt) {
+ String readout;
+ if (createdAt != null) {
+ long then = createdAt.getTime();
+ long now = new Date().getTime();
+ readout = getRelativeTimeSpanString(then, now);
+ } else {
+ readout = "?m";
+ }
+ sinceCreated.setText(readout);
}
public void setRebloggedByUsername(String name) {