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) {