diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index 3c90f0f9..4a13bff4 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -1,15 +1,24 @@ package awais.instagrabber.activities; +import android.annotation.SuppressLint; +import android.database.MatrixCursor; +import android.os.AsyncTask; import android.os.Bundle; +import android.os.Handler; +import android.provider.BaseColumns; import android.view.Menu; +import android.view.MenuItem; import android.view.View; +import android.widget.AutoCompleteTextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.coordinatorlayout.widget.CoordinatorLayout; import androidx.lifecycle.LiveData; +import androidx.navigation.NavBackStackEntry; import androidx.navigation.NavController; import androidx.navigation.ui.NavigationUI; @@ -19,11 +28,17 @@ import com.google.android.material.appbar.CollapsingToolbarLayout; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Deque; import java.util.List; import awais.instagrabber.R; +import awais.instagrabber.adapters.SuggestionsAdapter; +import awais.instagrabber.asyncs.SuggestionsFetcher; import awais.instagrabber.customviews.helpers.CustomHideBottomViewOnScrollBehavior; import awais.instagrabber.databinding.ActivityMainBinding; +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.Utils; @@ -55,6 +70,12 @@ public class MainActivity extends BaseLanguageActivity { private static final List REMOVE_COLLAPSING_TOOLBAR_SCROLL_DESTINATIONS = Collections.singletonList(R.id.commentsViewerFragment); private ActivityMainBinding binding; private LiveData currentNavControllerLiveData; + private MenuItem searchMenuItem; + private SuggestionsAdapter suggestionAdapter; + private AutoCompleteTextView searchAutoComplete; + private SearchView searchView; + private boolean showSearch = true; + private Handler suggestionsFetchHandler; @Override protected void onCreate(@Nullable final Bundle savedInstanceState) { @@ -63,15 +84,39 @@ public class MainActivity extends BaseLanguageActivity { final String cookie = settingsHelper.getString(Constants.COOKIE); Utils.setupCookies(cookie); setContentView(binding.getRoot()); - final Toolbar toolbar = binding.toolbar; setSupportActionBar(toolbar); - if (savedInstanceState == null) { setupBottomNavigationBar(); } - setupScrollingListener(); + setupSuggestions(); + } + + private void setupSuggestions() { + suggestionsFetchHandler = new Handler(); + suggestionAdapter = new SuggestionsAdapter(this, (type, query) -> { + if (searchMenuItem != null) searchMenuItem.collapseActionView(); + if (searchView != null && !searchView.isIconified()) searchView.setIconified(true); + if (currentNavControllerLiveData != null && currentNavControllerLiveData.getValue() != null) { + final NavController navController = currentNavControllerLiveData.getValue(); + final Bundle bundle = new Bundle(); + switch (type) { + case TYPE_LOCATION: + bundle.putString("locationId", query); + navController.navigate(R.id.action_global_locationFragment, bundle); + break; + case TYPE_HASHTAG: + bundle.putString("hashtag", query); + navController.navigate(R.id.action_global_hashTagFragment, bundle); + break; + case TYPE_USER: + bundle.putString("username", query); + navController.navigate(R.id.action_global_profileFragment, bundle); + break; + } + } + }); } private void setupScrollingListener() { @@ -83,11 +128,132 @@ public class MainActivity extends BaseLanguageActivity { @Override public boolean onCreateOptionsMenu(final Menu menu) { getMenuInflater().inflate(R.menu.main_menu, menu); + searchMenuItem = menu.findItem(R.id.search); + if (!showSearch) { + searchMenuItem.setVisible(false); + return true; + } + return setupSearchView(); + } + + private boolean setupSearchView() { + final View actionView = searchMenuItem.getActionView(); + if (!(actionView instanceof SearchView)) return false; + searchView = (SearchView) actionView; + searchView.setSuggestionsAdapter(suggestionAdapter); + searchView.setMaxWidth(Integer.MAX_VALUE); + final View searchText = searchView.findViewById(R.id.search_src_text); + if (searchText instanceof AutoCompleteTextView) { + searchAutoComplete = (AutoCompleteTextView) searchText; + searchAutoComplete.setTextColor(getResources().getColor(android.R.color.white)); + } + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + private boolean searchUser; + private boolean searchHash; + private AsyncTask prevSuggestionAsync; + private final String[] COLUMNS = { + BaseColumns._ID, + Constants.EXTRAS_USERNAME, + Constants.EXTRAS_NAME, + Constants.EXTRAS_TYPE, + "pfp", + "verified" + }; + private String currentSearchQuery; + + private final FetchListener fetchListener = new FetchListener() { + @Override + public void doBefore() { + suggestionAdapter.changeCursor(null); + } + + @Override + public void onResult(final SuggestionModel[] result) { + final MatrixCursor cursor; + if (result == null) cursor = null; + else { + cursor = new MatrixCursor(COLUMNS, 0); + for (int i = 0; i < result.length; i++) { + final SuggestionModel suggestionModel = result[i]; + if (suggestionModel != null) { + final SuggestionType suggestionType = suggestionModel.getSuggestionType(); + final Object[] objects = { + i, + suggestionType == SuggestionType.TYPE_LOCATION ? suggestionModel.getName() : suggestionModel.getUsername(), + suggestionType == SuggestionType.TYPE_LOCATION ? suggestionModel.getUsername() : suggestionModel.getName(), + suggestionType, + suggestionModel.getProfilePic(), + suggestionModel.isVerified()}; + if (!searchHash && !searchUser) cursor.addRow(objects); + else { + final boolean isCurrHash = suggestionType == SuggestionType.TYPE_HASHTAG; + if (searchHash && isCurrHash || !searchHash && !isCurrHash) + cursor.addRow(objects); + } + } + } + } + suggestionAdapter.changeCursor(cursor); + } + }; + + private final Runnable runnable = () -> { + cancelSuggestionsAsync(); + if (Utils.isEmpty(currentSearchQuery)) { + suggestionAdapter.changeCursor(null); + return; + } + searchUser = currentSearchQuery.charAt(0) == '@'; + searchHash = currentSearchQuery.charAt(0) == '#'; + if (currentSearchQuery.length() == 1 && (searchHash || searchUser)) { + if (searchAutoComplete != null) { + searchAutoComplete.setThreshold(2); + } + } else { + if (searchAutoComplete != null) { + searchAutoComplete.setThreshold(1); + } + prevSuggestionAsync = new SuggestionsFetcher(fetchListener).executeOnExecutor( + AsyncTask.THREAD_POOL_EXECUTOR, + searchUser || searchHash ? currentSearchQuery.substring(1) + : currentSearchQuery); + } + }; + + private void cancelSuggestionsAsync() { + if (prevSuggestionAsync != null) + try { + prevSuggestionAsync.cancel(true); + } catch (final Exception ignored) {} + } + + @Override + public boolean onQueryTextSubmit(final String query) { + return onQueryTextChange(query); + // menu.findItem(R.id.action_about).setVisible(true); + // menu.findItem(R.id.action_settings).setVisible(true); + // closeAnyOpenDrawer(); + // addToStack(); + // userQuery = (query.contains("@") || query.contains("#")) ? query : ("@" + query); + // searchAction.collapseActionView(); + // searchView.setIconified(true); + // searchView.setIconified(true); + // mainHelper.onRefresh(); + } + + @Override + public boolean onQueryTextChange(final String query) { + suggestionsFetchHandler.removeCallbacks(runnable); + currentSearchQuery = query; + suggestionsFetchHandler.postDelayed(runnable, 800); + return true; + } + }); return true; } private void setupBottomNavigationBar() { - final List navList = new ArrayList<>(Arrays.asList( + final List mainNavList = new ArrayList<>(Arrays.asList( R.navigation.direct_messages_nav_graph, R.navigation.feed_nav_graph, R.navigation.profile_nav_graph, @@ -96,7 +262,7 @@ public class MainActivity extends BaseLanguageActivity { )); final LiveData navControllerLiveData = setupWithNavController( binding.bottomNavView, - navList, + mainNavList, getSupportFragmentManager(), R.id.main_nav_host, getIntent(), @@ -108,8 +274,11 @@ public class MainActivity extends BaseLanguageActivity { private void setupNavigation(final NavController navController) { NavigationUI.setupWithNavController(binding.toolbar, navController); navController.addOnDestinationChangedListener((controller, destination, arguments) -> { + // below is a hack to check if we are at the end of the current stack, to setup the search view binding.appBarLayout.setExpanded(true, true); final int destinationId = destination.getId(); + @SuppressLint("RestrictedApi") final Deque backStack = navController.getBackStack(); + setupMenu(backStack.size(), destinationId); binding.bottomNavView.setVisibility(SHOW_BOTTOM_VIEW_DESTINATIONS.contains(destinationId) ? View.VISIBLE : View.GONE); if (KEEP_SCROLL_BEHAVIOUR_DESTINATIONS.contains(destinationId)) { setScrollingBehaviour(); @@ -124,6 +293,17 @@ public class MainActivity extends BaseLanguageActivity { }); } + private void setupMenu(final int backStackSize, final int destinationId) { + if (searchMenuItem == null) return; + if (backStackSize == 2 && destinationId != R.id.morePreferencesFragment) { + showSearch = true; + searchMenuItem.setVisible(true); + return; + } + showSearch = false; + searchMenuItem.setVisible(false); + } + private void setScrollingBehaviour() { final CoordinatorLayout.LayoutParams layoutParams = (CoordinatorLayout.LayoutParams) binding.mainNavHost.getLayoutParams(); layoutParams.setBehavior(new AppBarLayout.ScrollingViewBehavior()); diff --git a/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java index 826b8767..19412e37 100755 --- a/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java +++ b/app/src/main/java/awais/instagrabber/adapters/SuggestionsAdapter.java @@ -2,60 +2,87 @@ package awais.instagrabber.adapters; import android.content.Context; import android.database.Cursor; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; import androidx.annotation.NonNull; import androidx.cursoradapter.widget.CursorAdapter; -import com.bumptech.glide.Glide; -import com.bumptech.glide.RequestManager; -import com.bumptech.glide.request.RequestOptions; - -import awais.instagrabber.R; +import awais.instagrabber.databinding.ItemSuggestionBinding; +import awais.instagrabber.models.enums.SuggestionType; public final class SuggestionsAdapter extends CursorAdapter { - private final LayoutInflater layoutInflater; - private final View.OnClickListener onClickListener; - private final RequestManager glideRequestManager; + private static final String TAG = "SuggestionsAdapter"; - public SuggestionsAdapter(final Context context, final View.OnClickListener onClickListener) { + private final OnSuggestionClickListener onSuggestionClickListener; + + public SuggestionsAdapter(final Context context, + final OnSuggestionClickListener onSuggestionClickListener) { super(context, null, FLAG_REGISTER_CONTENT_OBSERVER); - this.glideRequestManager = Glide.with(context); - this.layoutInflater = LayoutInflater.from(context); - this.onClickListener = onClickListener; + this.onSuggestionClickListener = onSuggestionClickListener; } @Override public View newView(final Context context, final Cursor cursor, final ViewGroup parent) { - return layoutInflater.inflate(R.layout.item_suggestion, parent, false); + final LayoutInflater layoutInflater = LayoutInflater.from(context); + final ItemSuggestionBinding binding = ItemSuggestionBinding.inflate(layoutInflater, parent, false); + return binding.getRoot(); + // return layoutInflater.inflate(R.layout.item_suggestion, parent, false); } @Override public void bindView(@NonNull final View view, final Context context, @NonNull final Cursor cursor) { // i, username, fullname, type, picUrl, verified // 0, 1 , 2 , 3 , 4 , 5 - - final String fullname = cursor.getString(2); + final String fullName = cursor.getString(2); String username = cursor.getString(1); final String picUrl = cursor.getString(4); final boolean verified = cursor.getString(5).charAt(0) == 't'; - if ("TYPE_HASHTAG".equals(cursor.getString(3))) username = '#' + username; - else if ("TYPE_USER".equals(cursor.getString(3))) username = '@' + username; + final String type = cursor.getString(3); + SuggestionType suggestionType = null; + try { + suggestionType = SuggestionType.valueOf(type); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Unknown suggestion type: " + type, e); + } + if (suggestionType == null) return; + final String query; + switch (suggestionType) { + case TYPE_USER: + username = '@' + username; + query = username; + break; + case TYPE_HASHTAG: + username = '#' + username; + query = username; + break; + case TYPE_LOCATION: + query = fullName; + break; + default: + return; // will never come here + } - view.setOnClickListener(onClickListener); - view.setTag("TYPE_LOCATION".equals(cursor.getString(3)) ? fullname : username); + if (onSuggestionClickListener != null) { + final SuggestionType finalSuggestionType = suggestionType; + view.setOnClickListener(v -> onSuggestionClickListener.onSuggestionClick(finalSuggestionType, query)); + } + final ItemSuggestionBinding binding = ItemSuggestionBinding.bind(view); + binding.isVerified.setVisibility(verified ? View.VISIBLE : View.GONE); + binding.tvUsername.setText(username); + if (suggestionType.equals(SuggestionType.TYPE_LOCATION)) { + binding.tvFullName.setVisibility(View.GONE); + } else { + binding.tvFullName.setVisibility(View.VISIBLE); + binding.tvFullName.setText(fullName); + } + binding.ivProfilePic.setImageURI(picUrl); + } - view.findViewById(R.id.isVerified).setVisibility(verified ? View.VISIBLE : View.GONE); - - ((TextView) view.findViewById(R.id.tvUsername)).setText(username); - ((TextView) view.findViewById(R.id.tvFullName)).setText(fullname); - - glideRequestManager.applyDefaultRequestOptions(new RequestOptions().skipMemoryCache(true)) - .load(picUrl == null ? R.drawable.ic_location : picUrl).into((ImageView) view.findViewById(R.id.ivProfilePic)); + public interface OnSuggestionClickListener { + void onSuggestionClick(final SuggestionType type, final String query); } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java b/app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java index 0954103e..c4587ce7 100755 --- a/app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java +++ b/app/src/main/java/awais/instagrabber/asyncs/SuggestionsFetcher.java @@ -75,9 +75,9 @@ public final class SuggestionsFetcher extends AsyncTask { diff --git a/app/src/main/res/drawable/ic_search_black_24dp.xml b/app/src/main/res/drawable/ic_search_black_24dp.xml new file mode 100644 index 00000000..affc7ba2 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2966a155..d1fc054f 100755 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -25,12 +25,13 @@ + app:popupTheme="@style/Widget.AppTheme.Toolbar.PrimarySurface" + app:title="@string/app_name" + tools:menu="@menu/main_menu" /> diff --git a/app/src/main/res/layout/item_suggestion.xml b/app/src/main/res/layout/item_suggestion.xml index 1c016709..92e59e86 100755 --- a/app/src/main/res/layout/item_suggestion.xml +++ b/app/src/main/res/layout/item_suggestion.xml @@ -1,52 +1,67 @@ - + android:padding="8dp"> - + app:actualImageScaleType="centerCrop" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/tvUsername" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:roundAsCircle="true" + tools:src="@mipmap/ic_launcher" /> - + - - - - + - \ No newline at end of file + app:layout_constraintBaseline_toBaselineOf="@id/tvUsername" + app:layout_constraintBottom_toTopOf="@id/tvFullName" + app:layout_constraintStart_toEndOf="@id/tvUsername" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/verified" + tools:visibility="visible" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index 640dc82a..2ad8da42 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -1,5 +1,6 @@ - + @@ -14,4 +15,11 @@ + \ No newline at end of file diff --git a/app/src/main/res/navigation/direct_messages_nav_graph.xml b/app/src/main/res/navigation/direct_messages_nav_graph.xml index f128d28c..11903a11 100644 --- a/app/src/main/res/navigation/direct_messages_nav_graph.xml +++ b/app/src/main/res/navigation/direct_messages_nav_graph.xml @@ -16,6 +16,28 @@ app:nullable="false" /> + + + + + + + + + + + + + -