converted timeline fetching to use volley, for consistency

This commit is contained in:
Vavassor 2017-01-03 19:23:57 -05:00
parent ce88450ee6
commit b78ccb1b49
5 changed files with 143 additions and 256 deletions

View File

@ -1,9 +0,0 @@
package com.keylesspalace.tusky;
import java.io.IOException;
import java.util.List;
public interface FetchTimelineListener {
void onFetchTimelineSuccess(List<Status> statuses, boolean added);
void onFetchTimelineFailure(IOException e);
}

View File

@ -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<String, Void, Boolean> {
private Context context;
private FetchTimelineListener fetchTimelineListener;
private String domain;
private String accessToken;
private String fromId;
private List<com.keylesspalace.tusky.Status> 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 </p> 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<String, String> parameters)
throws UnsupportedEncodingException {
StringBuilder s = new StringBuilder();
String between = "";
for (Map.Entry<String, String> 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<String, String> 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);
}
}
}
}

View File

@ -7,7 +7,6 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;

View File

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity; 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.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.text.Html;
import android.text.Spanned;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.widget.Toast; import android.widget.Toast;
import java.io.IOException; import com.android.volley.AuthFailureError;
import java.util.List; 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 { SwipeRefreshLayout.OnRefreshListener {
private String domain = null; private String domain = null;
@ -72,8 +89,117 @@ public class MainActivity extends AppCompatActivity implements FetchTimelineList
sendFetchTimelineRequest(); sendFetchTimelineRequest();
} }
private void sendFetchTimelineRequest(String fromId) { private Date parseDate(String dateTime) {
new FetchTimelineTask(this, this, domain, accessToken, fromId).execute(); 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 </p> 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<Status> parseStatuses(JSONArray array) throws JSONException {
List<Status> 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<JSONArray>() {
@Override
public void onResponse(JSONArray response) {
List<Status> 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<String, String> getHeaders() throws AuthFailureError {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
return headers;
}
@Override
protected Map<String, String> getParams() throws AuthFailureError {
Map<String, String> parameters = new HashMap<>();
parameters.put("max_id", fromId);
return parameters;
}
};
VolleySingleton.getInstance(this).addToRequestQueue(request);
} }
private void sendFetchTimelineRequest() { private void sendFetchTimelineRequest() {
@ -89,7 +215,7 @@ public class MainActivity extends AppCompatActivity implements FetchTimelineList
swipeRefreshLayout.setRefreshing(false); 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(); Toast.makeText(this, R.string.error_fetching_timeline, Toast.LENGTH_SHORT).show();
swipeRefreshLayout.setRefreshing(false); swipeRefreshLayout.setRefreshing(false);
} }

View File

@ -2,6 +2,7 @@ package com.keylesspalace.tusky;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.Spanned; import android.text.Spanned;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -169,11 +170,16 @@ public class TimelineAdapter extends RecyclerView.Adapter {
return prefix + span + unit; return prefix + span + unit;
} }
public void setCreatedAt(Date createdAt) { public void setCreatedAt(@Nullable Date createdAt) {
long then = createdAt.getTime(); String readout;
long now = new Date().getTime(); if (createdAt != null) {
String since = getRelativeTimeSpanString(then, now); long then = createdAt.getTime();
sinceCreated.setText(since); long now = new Date().getTime();
readout = getRelativeTimeSpanString(then, now);
} else {
readout = "?m";
}
sinceCreated.setText(readout);
} }
public void setRebloggedByUsername(String name) { public void setRebloggedByUsername(String name) {