converted timeline fetching to use volley, for consistency
This commit is contained in:
parent
ce88450ee6
commit
b78ccb1b49
@ -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);
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
Loading…
Reference in New Issue
Block a user