Skip to content

Commit f9ec0e5

Browse files
committed
Added thread thumbnail feature using OP image
1 parent a1f7148 commit f9ec0e5

9 files changed

Lines changed: 251 additions & 40 deletions

File tree

theadwatch/src/main/java/honkhonk/threadwatch/adapters/ThreadListAdapter.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package honkhonk.threadwatch.adapters;
22

33
import android.content.Context;
4+
import android.content.SharedPreferences;
5+
import android.graphics.Bitmap;
6+
import android.graphics.BitmapFactory;
7+
import android.preference.PreferenceManager;
8+
import android.util.Base64;
49
import android.util.Log;
510
import android.view.View;
611
import android.view.ViewGroup;
712
import android.widget.ArrayAdapter;
13+
import android.widget.ImageView;
814
import android.widget.TextView;
915

1016
import androidx.annotation.IdRes;
@@ -66,6 +72,23 @@ public View getView(final int position, View convertView, @NonNull ViewGroup par
6672
final TextView boardName = view.findViewById(R.id.boardTitle);
6773
boardName.setText("/" + thread.board + "/");
6874

75+
final SharedPreferences appSettings =
76+
PreferenceManager.getDefaultSharedPreferences((Context) this.context);
77+
78+
final boolean showThumbnails = appSettings.getBoolean("pref_view_thumbnails", true);
79+
80+
final ImageView thumbnailView = view.findViewById(R.id.thumbnailView);
81+
if (thread.thumbnail != null && showThumbnails) {
82+
byte[] decodedString = Base64.decode(thread.thumbnail, Base64.DEFAULT);
83+
Bitmap decodedImage = BitmapFactory.decodeByteArray(decodedString, 0,
84+
decodedString.length);
85+
thumbnailView.setImageBitmap(decodedImage);
86+
thumbnailView.setVisibility(View.VISIBLE);
87+
88+
} else {
89+
thumbnailView.setVisibility(View.GONE);
90+
}
91+
6992
final TextView title = view.findViewById(R.id.threadTitle);
7093
final String titleText = thread.getTitle();
7194

theadwatch/src/main/java/honkhonk/threadwatch/models/PostModel.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ public class PostModel {
6969
@SerializedName("time")
7070
public int time;
7171

72+
/**
73+
* The UNIX timestamp (including microtime) of when an image was uploaded.
74+
* Used as the id for an attachment/image.
75+
*/
76+
@SerializedName("tim")
77+
public long attachmentId;
78+
7279
/**
7380
* Post does not exist or was deleted
7481
*/

theadwatch/src/main/java/honkhonk/threadwatch/models/ThreadModel.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,16 @@ public class ThreadModel {
116116
*/
117117
public long lastPostId;
118118

119+
/**
120+
* The UNIX timestamp + microtime of the OP attachment/image
121+
*/
122+
public long attachmentId;
123+
124+
/**
125+
* Base64 encoded string of OP's image as a JPEG thumbnail, if it exists
126+
*/
127+
public String thumbnail;
128+
119129
/**
120130
* List of reply ids to track using the post number as the key and the value
121131
* as a list of comment strings that reference it
@@ -188,7 +198,6 @@ public boolean isAvailable() {
188198
return !(closed || archived || disabled || notFound);
189199
}
190200

191-
192201
@Override
193202
public int hashCode()
194203
{

theadwatch/src/main/java/honkhonk/threadwatch/retrievers/PostsRetriever.java

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,27 @@
2525
* Created by Gunbard on 10/11/2016.
2626
*/
2727

28-
public class PostsRetriever {
28+
public class PostsRetriever implements ThumbnailRetriever.ThumbnailRetrieverListener {
2929
/**
3030
* List of listeners to notify about retrieval events
3131
*/
3232
private ArrayList<PostsRetrieverListener> listeners = new ArrayList<>();
3333

34+
/**
35+
* Cached context
36+
*/
37+
private Context context;
38+
39+
/**
40+
* Cached response
41+
*/
42+
private PostsResponse response;
43+
44+
/**
45+
* The current thread this post retrieval is for
46+
*/
47+
private ThreadModel thread;
48+
3449
/**
3550
* Retrieval events
3651
*/
@@ -67,6 +82,9 @@ public void addListener(final PostsRetrieverListener listener) {
6782
* @param thread The thread to retrieve posts from. Must have the board and id set.
6883
*/
6984
public void retrievePosts(final Context context, final ThreadModel thread) {
85+
this.context = context;
86+
this.thread = thread;
87+
7088
ConnectivityManager cm =
7189
(ConnectivityManager) context
7290
.getSystemService(Context.CONNECTIVITY_SERVICE);
@@ -90,17 +108,21 @@ public void retrievePosts(final Context context, final ThreadModel thread) {
90108
(Request.Method.GET, url, null, new Response.Listener<JSONObject>() {
91109
@Override
92110
public void onResponse(JSONObject response) {
93-
thread.notFound = false;
94-
95-
final PostsResponse postsResponse =
111+
PostsRetriever.this.thread.notFound = false;
112+
PostsRetriever.this.response =
96113
(new Gson()).fromJson(response.toString(), PostsResponse.class);
97114

98-
for (final PostsRetrieverListener listener : listeners) {
99-
if (postsResponse.posts != null) {
100-
listener.postsRetrieved(context, thread, postsResponse.posts);
101-
} else {
102-
listener.postsRetrievalFailed(context, thread);
103-
}
115+
PostsRetriever.this.thread.attachmentId =
116+
PostsRetriever.this.response.posts.get(0).attachmentId;
117+
118+
// Try to get OP thumbnail, too, if needed
119+
if (PostsRetriever.this.thread.thumbnail == null) {
120+
ThumbnailRetriever thumbnailRetriever = new ThumbnailRetriever();
121+
thumbnailRetriever.addListener(PostsRetriever.this);
122+
thumbnailRetriever.retrieveThumbnail(context, PostsRetriever.this.thread);
123+
} else {
124+
thumbnailRetrievalFinished(PostsRetriever.this.thread,
125+
PostsRetriever.this.thread.thumbnail);
104126
}
105127
}
106128
}, new Response.ErrorListener() {
@@ -109,7 +131,7 @@ public void onErrorResponse(VolleyError error) {
109131
if (error != null &&
110132
error.networkResponse != null &&
111133
error.networkResponse.statusCode == 404) {
112-
thread.notFound = true;
134+
PostsRetriever.this.thread.notFound = true;
113135
}
114136

115137
for (final PostsRetrieverListener listener : listeners) {
@@ -120,4 +142,20 @@ public void onErrorResponse(VolleyError error) {
120142

121143
ThreadWatch.getInstance(context).addToRequestQueue(retrieveRequest);
122144
}
145+
146+
/****************************************
147+
* ThumbnailRetriever.ThumbnailRetrieverListener
148+
****************************************/
149+
@Override
150+
public void thumbnailRetrievalFinished(final ThreadModel thread, final String encodedImage) {
151+
this.thread.thumbnail = encodedImage;
152+
153+
for (final PostsRetrieverListener listener : listeners) {
154+
if (response.posts != null) {
155+
listener.postsRetrieved(context, this.thread, response.posts);
156+
} else {
157+
listener.postsRetrievalFailed(context, this.thread);
158+
}
159+
}
160+
}
123161
}

theadwatch/src/main/java/honkhonk/threadwatch/retrievers/ThreadsRetriever.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public void postsRetrieved(final Context context,
9696
thread.comment = op.comment;
9797
thread.subject = op.subject;
9898
thread.time = op.time;
99+
thread.attachmentId = op.attachmentId;
99100
thread.newReplyCount = op.replyCount - thread.replyCount;
100101
thread.replyCountDelta += (thread.firstRefresh) ? 0 : thread.newReplyCount;
101102
thread.replyCount = op.replyCount;
@@ -189,7 +190,7 @@ private boolean hasNewRepliesToYou(ThreadModel thread, final ArrayList<PostModel
189190
* @param thread The thread to check
190191
* @param posts The posts in the thread
191192
*/
192-
private void retrieveReplyComments(Context context, ThreadModel thread, final ArrayList<PostModel> posts) {
193+
private void retrieveReplyComments(final Context context, ThreadModel thread, final ArrayList<PostModel> posts) {
193194
if (thread.replyIds.isEmpty()) {
194195
return;
195196
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package honkhonk.threadwatch.retrievers;
2+
3+
import android.content.Context;
4+
import android.graphics.Bitmap;
5+
import android.net.ConnectivityManager;
6+
import android.net.NetworkInfo;
7+
import android.util.Base64;
8+
import android.util.Log;
9+
import android.widget.ImageView;
10+
11+
import com.android.volley.Response;
12+
import com.android.volley.VolleyError;
13+
import com.android.volley.toolbox.ImageRequest;
14+
15+
import java.io.ByteArrayOutputStream;
16+
import java.util.ArrayList;
17+
18+
import honkhonk.threadwatch.ThreadWatch;
19+
import honkhonk.threadwatch.models.ThreadModel;
20+
21+
/**
22+
* Retrieves the thumbnail version of an image from 4chan's CDN
23+
* Created by Gunbard on 6/17/2021
24+
*/
25+
public class ThumbnailRetriever {
26+
// Max 48x48 pixels
27+
final static int MAX_THUMBNAIL_DIMENSION = 48;
28+
29+
// List of listeners to notify
30+
private ArrayList<ThumbnailRetrieverListener> listeners = new ArrayList<>();
31+
32+
public interface ThumbnailRetrieverListener {
33+
/**
34+
* Callback for when the retrieval finishes
35+
* @param thread The thread the retrieval was based on
36+
* @param encodedThumbnail A base 64 encoded string of the thumbnail JPEG or null
37+
* if thread did not have an image/attachment for OP
38+
*/
39+
void thumbnailRetrievalFinished(final ThreadModel thread, final String encodedThumbnail);
40+
}
41+
42+
/**
43+
* Adds to the list of listeners
44+
* @param listener ThumbnailRetrieverListener to notify about events
45+
*/
46+
public void addListener(final ThumbnailRetrieverListener listener) {
47+
listeners.add(listener);
48+
}
49+
50+
/**
51+
* Makes a request
52+
* @param context Context for the retrieval
53+
* @param thread The thread to get the thumbnail from
54+
*/
55+
public void retrieveThumbnail(final Context context, final ThreadModel thread) {
56+
if (thread.attachmentId == 0) {
57+
Log.i(this.getClass().getSimpleName(),
58+
"Thread does not have OP image, so didn't retrieve!");
59+
for (ThumbnailRetrieverListener listener : listeners) {
60+
listener.thumbnailRetrievalFinished(thread, null);
61+
}
62+
return;
63+
}
64+
65+
ConnectivityManager cm =
66+
(ConnectivityManager) context
67+
.getSystemService(Context.CONNECTIVITY_SERVICE);
68+
69+
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
70+
boolean isConnected = activeNetwork != null &&
71+
activeNetwork.isConnectedOrConnecting();
72+
73+
if (!isConnected) {
74+
Log.i(this.getClass().getSimpleName(), "No network, so didn't retrieve!");
75+
for (ThumbnailRetrieverListener listener : listeners) {
76+
listener.thumbnailRetrievalFinished(thread, null);
77+
}
78+
return;
79+
}
80+
81+
final String url =
82+
"https://is2.4chan.org/"+ thread.board + "/" + thread.attachmentId + "s.jpg";
83+
84+
final ImageRequest retrieveRequest = new ImageRequest(url,
85+
new Response.Listener<Bitmap>() {
86+
@Override
87+
public void onResponse(Bitmap responseImage) {
88+
// Base64 encode image to string. This should make it
89+
// easier to cache/backup locally.
90+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
91+
responseImage.compress(Bitmap.CompressFormat.JPEG,
92+
90, byteArrayOutputStream);
93+
94+
for (ThumbnailRetrieverListener listener : listeners) {
95+
listener.thumbnailRetrievalFinished(thread,
96+
Base64.encodeToString(byteArrayOutputStream.toByteArray(),
97+
Base64.DEFAULT));
98+
}
99+
}
100+
}, MAX_THUMBNAIL_DIMENSION, MAX_THUMBNAIL_DIMENSION,
101+
ImageView.ScaleType.FIT_CENTER,
102+
Bitmap.Config.ARGB_8888,
103+
new Response.ErrorListener() {
104+
@Override
105+
public void onErrorResponse(VolleyError error) {
106+
for (ThumbnailRetrieverListener listener : listeners) {
107+
listener.thumbnailRetrievalFinished(thread, null);
108+
}
109+
}
110+
});
111+
112+
ThreadWatch.getInstance(context).addToRequestQueue(retrieveRequest);
113+
}
114+
}

0 commit comments

Comments
 (0)