1
0
mirror of https://github.com/KokaKiwi/BarInsta synced 2024-11-22 14:47:29 +00:00

improve search

1. separate logged-in and anonymous endpoints
2. migrate to retrofit + gson, retire SuggestionsFetcher
3. prefixing search with @ or # will only return users or hashtags, respectively
4. add subtitles for locations (address) and hashtags (rough post count)
This commit is contained in:
Austin Huang 2021-03-22 16:48:35 -04:00
parent 0f996f88ba
commit 1cee2cd4c0
No known key found for this signature in database
GPG Key ID: 84C23AA04587A91F
10 changed files with 283 additions and 172 deletions

View File

@ -11,7 +11,6 @@ import android.content.ServiceConnection;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.database.MatrixCursor; import android.database.MatrixCursor;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@ -57,21 +56,22 @@ import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.adapters.SuggestionsAdapter; import awais.instagrabber.adapters.SuggestionsAdapter;
import awais.instagrabber.asyncs.PostFetcher; import awais.instagrabber.asyncs.PostFetcher;
import awais.instagrabber.asyncs.SuggestionsFetcher;
import awais.instagrabber.customviews.emoji.EmojiVariantManager; import awais.instagrabber.customviews.emoji.EmojiVariantManager;
import awais.instagrabber.databinding.ActivityMainBinding; import awais.instagrabber.databinding.ActivityMainBinding;
import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.fragments.PostViewV2Fragment;
import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections; import awais.instagrabber.fragments.directmessages.DirectMessageInboxFragmentDirections;
import awais.instagrabber.fragments.main.FeedFragment; import awais.instagrabber.fragments.main.FeedFragment;
import awais.instagrabber.fragments.settings.PreferenceKeys; import awais.instagrabber.fragments.settings.PreferenceKeys;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.IntentModel; import awais.instagrabber.models.IntentModel;
import awais.instagrabber.models.SuggestionModel; import awais.instagrabber.models.SuggestionModel;
import awais.instagrabber.models.enums.SuggestionType; import awais.instagrabber.models.enums.SuggestionType;
import awais.instagrabber.repositories.responses.search.SearchItem;
import awais.instagrabber.repositories.responses.search.SearchResponse;
import awais.instagrabber.services.ActivityCheckerService; import awais.instagrabber.services.ActivityCheckerService;
import awais.instagrabber.services.DMSyncAlarmReceiver; import awais.instagrabber.services.DMSyncAlarmReceiver;
import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.AppExecutors;
@ -83,6 +83,10 @@ import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
import awais.instagrabber.utils.emoji.EmojiParser; import awais.instagrabber.utils.emoji.EmojiParser;
import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.AppStateViewModel;
import awais.instagrabber.webservices.SearchService;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController; import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -105,6 +109,7 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
private SuggestionsAdapter suggestionAdapter; private SuggestionsAdapter suggestionAdapter;
private AutoCompleteTextView searchAutoComplete; private AutoCompleteTextView searchAutoComplete;
private SearchView searchView; private SearchView searchView;
private SearchService searchService;
private boolean showSearch = true; private boolean showSearch = true;
private Handler suggestionsFetchHandler; private Handler suggestionsFetchHandler;
private int firstFragmentGraphIndex; private int firstFragmentGraphIndex;
@ -175,6 +180,7 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
EmojiVariantManager.getInstance(); EmojiVariantManager.getInstance();
}); });
initEmojiCompat(); initEmojiCompat();
searchService = SearchService.getInstance();
// initDmService(); // initDmService();
} }
@ -338,51 +344,84 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
private boolean searchUser; private boolean searchUser;
private boolean searchHash; private boolean searchHash;
private AsyncTask<?, ?, ?> prevSuggestionAsync; private Call<SearchResponse> prevSuggestionAsync;
private final String[] COLUMNS = { private final String[] COLUMNS = {
BaseColumns._ID, BaseColumns._ID,
Constants.EXTRAS_USERNAME, Constants.EXTRAS_USERNAME,
Constants.EXTRAS_NAME, Constants.EXTRAS_NAME,
Constants.EXTRAS_TYPE, Constants.EXTRAS_TYPE,
"query",
"pfp", "pfp",
"verified" "verified"
}; };
private String currentSearchQuery; private String currentSearchQuery;
private final FetchListener<SuggestionModel[]> fetchListener = new FetchListener<SuggestionModel[]>() { private final Callback<SearchResponse> cb = new Callback<SearchResponse>() {
@Override @Override
public void doBefore() { public void onResponse(@NonNull final Call<SearchResponse> call,
suggestionAdapter.changeCursor(null); @NonNull final Response<SearchResponse> response) {
}
@Override
public void onResult(final SuggestionModel[] result) {
final MatrixCursor cursor; final MatrixCursor cursor;
if (result == null) cursor = null; final SearchResponse body = response.body();
if (body == null) {
cursor = null;
return;
}
final List<SearchItem> result = new ArrayList<SearchItem>();
if (isLoggedIn) {
if (body.getList() != null) result.addAll(searchHash ? body.getList()
.stream()
.filter(i -> i.getUser() == null)
.collect(Collectors.toList()) : body.getList());
}
else { else {
cursor = new MatrixCursor(COLUMNS, 0); if (body.getUsers() != null && !searchHash) result.addAll(body.getUsers());
for (int i = 0; i < result.length; i++) { if (body.getHashtags() != null) result.addAll(body.getHashtags());
final SuggestionModel suggestionModel = result[i]; if (body.getPlaces() != null) result.addAll(body.getPlaces());
if (suggestionModel != null) { }
final SuggestionType suggestionType = suggestionModel.getSuggestionType(); cursor = new MatrixCursor(COLUMNS, 0);
final Object[] objects = { for (int i = 0; i < result.size(); i++) {
i, final SearchItem suggestionModel = result.get(i);
suggestionType == SuggestionType.TYPE_LOCATION ? suggestionModel.getName() : suggestionModel.getUsername(), if (suggestionModel != null) {
suggestionType == SuggestionType.TYPE_LOCATION ? suggestionModel.getUsername() : suggestionModel.getName(), Object[] objects = null;
suggestionType, if (suggestionModel.getUser() != null)
suggestionModel.getProfilePic(), objects = new Object[]{
suggestionModel.isVerified()}; suggestionModel.getPosition(),
if (!searchHash && !searchUser) cursor.addRow(objects); suggestionModel.getUser().getUsername(),
else { suggestionModel.getUser().getFullName(),
final boolean isCurrHash = suggestionType == SuggestionType.TYPE_HASHTAG; SuggestionType.TYPE_USER,
if (searchHash && isCurrHash || !searchHash && !isCurrHash) suggestionModel.getUser().getUsername(),
cursor.addRow(objects); suggestionModel.getUser().getProfilePicUrl(),
} suggestionModel.getUser().isVerified()};
} else if (suggestionModel.getHashtag() != null)
objects = new Object[]{
suggestionModel.getPosition(),
suggestionModel.getHashtag().getName(),
suggestionModel.getHashtag().getSubtitle(),
SuggestionType.TYPE_HASHTAG,
suggestionModel.getHashtag().getName(),
"res:/" + R.drawable.ic_hashtag,
false};
else if (suggestionModel.getPlace() != null)
objects = new Object[]{
suggestionModel.getPosition(),
suggestionModel.getPlace().getTitle(),
suggestionModel.getPlace().getSubtitle(),
SuggestionType.TYPE_LOCATION,
suggestionModel.getPlace().getLocation().getPk(),
"res:/" + R.drawable.ic_location,
false};
cursor.addRow(objects);
} }
} }
suggestionAdapter.changeCursor(cursor); suggestionAdapter.changeCursor(cursor);
} }
@Override
public void onFailure(@NonNull final Call<SearchResponse> call,
Throwable t) {
if (!call.isCanceled() && t != null)
Log.e(TAG, "Exception on search:", t);
}
}; };
private final Runnable runnable = () -> { private final Runnable runnable = () -> {
@ -401,17 +440,19 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
if (searchAutoComplete != null) { if (searchAutoComplete != null) {
searchAutoComplete.setThreshold(1); searchAutoComplete.setThreshold(1);
} }
prevSuggestionAsync = new SuggestionsFetcher(fetchListener).executeOnExecutor( prevSuggestionAsync = searchService.search(isLoggedIn,
AsyncTask.THREAD_POOL_EXECUTOR, searchUser || searchHash ? currentSearchQuery.substring(1)
searchUser || searchHash ? currentSearchQuery.substring(1) : currentSearchQuery,
: currentSearchQuery); searchUser ? "user" : (searchHash ? "hashtag" : "blended"));
suggestionAdapter.changeCursor(null);
prevSuggestionAsync.enqueue(cb);
} }
}; };
private void cancelSuggestionsAsync() { private void cancelSuggestionsAsync() {
if (prevSuggestionAsync != null) if (prevSuggestionAsync != null)
try { try {
prevSuggestionAsync.cancel(true); prevSuggestionAsync.cancel();
} catch (final Exception ignored) {} } catch (final Exception ignored) {}
} }

View File

@ -35,12 +35,12 @@ public final class SuggestionsAdapter extends CursorAdapter {
@Override @Override
public void bindView(@NonNull final View view, final Context context, @NonNull final Cursor cursor) { public void bindView(@NonNull final View view, final Context context, @NonNull final Cursor cursor) {
// i, username, fullname, type, picUrl, verified // i, username, fullname, type, query, picUrl, verified
// 0, 1 , 2 , 3 , 4 , 5 // 0, 1 , 2 , 3 , 4 , 5 , 6
final String fullName = cursor.getString(2); final String fullName = cursor.getString(2);
String username = cursor.getString(1); String username = cursor.getString(1);
String picUrl = cursor.getString(4); String picUrl = cursor.getString(5);
final boolean verified = cursor.getString(5).charAt(0) == 't'; final boolean verified = cursor.getString(6).charAt(0) == 't';
final String type = cursor.getString(3); final String type = cursor.getString(3);
SuggestionType suggestionType = null; SuggestionType suggestionType = null;
@ -50,22 +50,14 @@ public final class SuggestionsAdapter extends CursorAdapter {
Log.e(TAG, "Unknown suggestion type: " + type, e); Log.e(TAG, "Unknown suggestion type: " + type, e);
} }
if (suggestionType == null) return; if (suggestionType == null) return;
final String query; String query = cursor.getString(4);
switch (suggestionType) { switch (suggestionType) {
case TYPE_USER: case TYPE_USER:
username = '@' + username; username = '@' + username;
query = username;
break; break;
case TYPE_HASHTAG: case TYPE_HASHTAG:
username = '#' + username; username = '#' + username;
query = username;
break; break;
case TYPE_LOCATION:
query = fullName;
picUrl = "res:/" + R.drawable.ic_location;
break;
default:
return; // will never come here
} }
if (onSuggestionClickListener != null) { if (onSuggestionClickListener != null) {
@ -75,12 +67,8 @@ public final class SuggestionsAdapter extends CursorAdapter {
final ItemSuggestionBinding binding = ItemSuggestionBinding.bind(view); final ItemSuggestionBinding binding = ItemSuggestionBinding.bind(view);
binding.isVerified.setVisibility(verified ? View.VISIBLE : View.GONE); binding.isVerified.setVisibility(verified ? View.VISIBLE : View.GONE);
binding.tvUsername.setText(username); binding.tvUsername.setText(username);
if (suggestionType.equals(SuggestionType.TYPE_LOCATION)) { binding.tvFullName.setVisibility(View.VISIBLE);
binding.tvFullName.setVisibility(View.GONE); binding.tvFullName.setText(fullName);
} else {
binding.tvFullName.setVisibility(View.VISIBLE);
binding.tvFullName.setText(fullName);
}
binding.ivProfilePic.setImageURI(picUrl); binding.ivProfilePic.setImageURI(picUrl);
} }

View File

@ -1,113 +0,0 @@
package awais.instagrabber.asyncs;
import android.os.AsyncTask;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.InterruptedIOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import awais.instagrabber.BuildConfig;
import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.SuggestionModel;
import awais.instagrabber.models.enums.SuggestionType;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.NetworkUtils;
import awais.instagrabber.utils.UrlEncoder;
public final class SuggestionsFetcher extends AsyncTask<String, String, SuggestionModel[]> {
private final FetchListener<SuggestionModel[]> fetchListener;
public SuggestionsFetcher(final FetchListener<SuggestionModel[]> fetchListener) {
this.fetchListener = fetchListener;
}
@Override
protected void onPreExecute() {
if (fetchListener != null) fetchListener.doBefore();
}
@Override
protected SuggestionModel[] doInBackground(final String... params) {
SuggestionModel[] result = null;
try {
final HttpURLConnection conn = (HttpURLConnection) new URL("https://www.instagram.com/web/search/topsearch/?context=blended&count=50&query="
+ UrlEncoder.encodeUrl(params[0])).openConnection();
conn.setUseCaches(false);
conn.connect();
if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
final JSONObject jsonObject = new JSONObject(NetworkUtils.readFromConnection(conn));
conn.disconnect();
final JSONArray usersArray = jsonObject.getJSONArray("users");
final JSONArray hashtagsArray = jsonObject.getJSONArray("hashtags");
final JSONArray placesArray = jsonObject.getJSONArray("places");
final int usersLen = usersArray.length();
final int hashtagsLen = hashtagsArray.length();
final int placesLen = placesArray.length();
final ArrayList<SuggestionModel> suggestionModels = new ArrayList<>(usersLen + hashtagsLen);
for (int i = 0; i < hashtagsLen; i++) {
final JSONObject hashtagsArrayJSONObject = hashtagsArray.getJSONObject(i);
final JSONObject hashtag = hashtagsArrayJSONObject.getJSONObject("hashtag");
suggestionModels.add(new SuggestionModel(false,
hashtag.getString(Constants.EXTRAS_NAME),
null,
hashtag.optString("profile_pic_url", Constants.DEFAULT_HASH_TAG_PIC),
SuggestionType.TYPE_HASHTAG,
hashtagsArrayJSONObject.optInt("position", suggestionModels.size() - 1)));
}
for (int i = 0; i < placesLen; i++) {
final JSONObject placesArrayJSONObject = placesArray.getJSONObject(i);
final JSONObject place = placesArrayJSONObject.getJSONObject("place");
// name
suggestionModels.add(new SuggestionModel(false,
place.getJSONObject("location").getString("pk"), // +"/"+place.getString("slug"),
place.getString("title"),
place.optString("profile_pic_url"),
SuggestionType.TYPE_LOCATION,
placesArrayJSONObject.optInt("position", suggestionModels.size() - 1)));
}
for (int i = 0; i < usersLen; i++) {
final JSONObject usersArrayJSONObject = usersArray.getJSONObject(i);
final JSONObject user = usersArrayJSONObject.getJSONObject(Constants.EXTRAS_USER);
suggestionModels.add(new SuggestionModel(user.getBoolean("is_verified"),
user.getString(Constants.EXTRAS_USERNAME),
user.getString("full_name"),
user.getString("profile_pic_url"),
SuggestionType.TYPE_USER,
usersArrayJSONObject.optInt("position", suggestionModels.size() - 1)));
}
suggestionModels.trimToSize();
Collections.sort(suggestionModels);
result = suggestionModels.toArray(new SuggestionModel[0]);
}
} catch (final Exception e) {
if (BuildConfig.DEBUG && !(e instanceof InterruptedIOException)) Log.e("AWAISKING_APP", "", e);
}
return result;
}
@Override
protected void onPostExecute(final SuggestionModel[] result) {
if (fetchListener != null) fetchListener.onResult(result);
}
}

View File

@ -0,0 +1,15 @@
package awais.instagrabber.repositories;
import java.util.Map;
import awais.instagrabber.repositories.responses.search.SearchResponse;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.QueryMap;
import retrofit2.http.Url;
public interface SearchRepository {
@GET
Call<SearchResponse> search(@Url String url, @QueryMap(encoded = true) Map<String, String> queryParams);
}

View File

@ -5,19 +5,22 @@ import java.io.Serializable;
import awais.instagrabber.models.enums.FollowingType; import awais.instagrabber.models.enums.FollowingType;
public final class Hashtag implements Serializable { public final class Hashtag implements Serializable {
private final FollowingType following; // 0 false 1 true private final FollowingType following; // 0 false 1 true; not on search results
private final long mediaCount; private final long mediaCount;
private final String id; private final String id;
private final String name; private final String name;
private final String searchResultSubtitle; // shows how many posts there are on search results
public Hashtag(final String id, public Hashtag(final String id,
final String name, final String name,
final long mediaCount, final long mediaCount,
final FollowingType following) { final FollowingType following,
final String searchResultSubtitle) {
this.id = id; this.id = id;
this.name = name; this.name = name;
this.mediaCount = mediaCount; this.mediaCount = mediaCount;
this.following = following; this.following = following;
this.searchResultSubtitle = searchResultSubtitle;
} }
public String getId() { public String getId() {
@ -35,4 +38,8 @@ public final class Hashtag implements Serializable {
public FollowingType getFollowing() { public FollowingType getFollowing() {
return following; return following;
} }
public String getSubtitle() {
return searchResultSubtitle;
}
} }

View File

@ -0,0 +1,36 @@
package awais.instagrabber.repositories.responses.search;
import awais.instagrabber.repositories.responses.Location;
public class Place {
private final Location location;
private final String title; // those are repeated within location
private final String subtitle; // address
private final String slug; // browser only; for end of address
public Place(final Location location,
final String title,
final String subtitle,
final String slug) {
this.location = location;
this.title = title;
this.subtitle = subtitle;
this.slug = slug;
}
public Location getLocation() {
return location;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public String getSlug() {
return slug;
}
}

View File

@ -0,0 +1,39 @@
package awais.instagrabber.repositories.responses.search;
import java.util.List;
import awais.instagrabber.repositories.responses.Hashtag;
import awais.instagrabber.repositories.responses.User;
public class SearchItem {
private final User user;
private final Place place;
private final Hashtag hashtag;
private final int position;
public SearchItem(final User user,
final Place place,
final Hashtag hashtag,
final int position) {
this.user = user;
this.place = place;
this.hashtag = hashtag;
this.position = position;
}
public User getUser() {
return user;
}
public Place getPlace() {
return place;
}
public Hashtag getHashtag() {
return hashtag;
}
public int getPosition() {
return position;
}
}

View File

@ -0,0 +1,48 @@
package awais.instagrabber.repositories.responses.search;
import java.util.List;
import awais.instagrabber.repositories.responses.User;
public class SearchResponse {
// app
private final List<SearchItem> list;
// browser
private final List<SearchItem> users;
private final List<SearchItem> places;
private final List<SearchItem> hashtags;
// universal
private final String status;
public SearchResponse(final List<SearchItem> list,
final List<SearchItem> users,
final List<SearchItem> places,
final List<SearchItem> hashtags,
final String status) {
this.list = list;
this.users = users;
this.places = places;
this.hashtags = hashtags;
this.status = status;
}
public List<SearchItem> getList() {
return list;
}
public List<SearchItem> getUsers() {
return users;
}
public List<SearchItem> getPlaces() {
return places;
}
public List<SearchItem> getHashtags() {
return hashtags;
}
public String getStatus() {
return status;
}
}

View File

@ -394,9 +394,9 @@ public class GraphQLService extends BaseService {
callback.onSuccess(new Hashtag( callback.onSuccess(new Hashtag(
body.getString(Constants.EXTRAS_ID), body.getString(Constants.EXTRAS_ID),
body.getString("name"), body.getString("name"),
body.getString("profile_pic_url"),
timelineMedia.getLong("count"), timelineMedia.getLong("count"),
body.optBoolean("is_following") ? FollowingType.FOLLOWING : FollowingType.NOT_FOLLOWING)); body.optBoolean("is_following") ? FollowingType.FOLLOWING : FollowingType.NOT_FOLLOWING,
null));
} catch (JSONException e) { } catch (JSONException e) {
Log.e(TAG, "onResponse", e); Log.e(TAG, "onResponse", e);
if (callback != null) { if (callback != null) {

View File

@ -0,0 +1,50 @@
package awais.instagrabber.webservices;
import androidx.annotation.NonNull;
import com.google.common.collect.ImmutableMap;
import awais.instagrabber.repositories.SearchRepository;
import awais.instagrabber.repositories.responses.search.SearchResponse;
import awais.instagrabber.utils.TextUtils;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
public class SearchService extends BaseService {
private static final String TAG = "LocationService";
private final SearchRepository repository;
private static SearchService instance;
private SearchService() {
final Retrofit retrofit = getRetrofitBuilder()
.baseUrl("https://www.instagram.com")
.build();
repository = retrofit.create(SearchRepository.class);
}
public static SearchService getInstance() {
if (instance == null) {
instance = new SearchService();
}
return instance;
}
public Call<SearchResponse> search(final boolean isLoggedIn,
final String query,
final String context) {
final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder();
builder.put("query", query);
// context is one of: "blended", "user", "place", "hashtag"
// note that "place" and "hashtag" can contain ONE user result, who knows why
builder.put("context", context);
builder.put("count", "50");
return repository.search(isLoggedIn
? "https://i.instagram.com/api/v1/fbsearch/topsearch_flat/"
: "https://www.instagram.com/web/search/topsearch/",
builder.build());
}
}