mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-09-18 21:50:56 +00:00
#1399 Search for feeds
This commit is contained in:
parent
fd69353bc8
commit
ce0f403b59
13 changed files with 396 additions and 415 deletions
|
@ -133,19 +133,15 @@
|
|||
android:name=".activity.MuteConfig"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/mute_sites"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.SearchForFeeds" android:launchMode="singleTop" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/>
|
||||
</activity>
|
||||
|
||||
|
||||
<activity
|
||||
android:name=".activity.SearchForFeeds"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<activity
|
||||
android:name=".activity.SocialFeedReading"/>
|
||||
|
||||
<service
|
||||
<service
|
||||
android:name=".service.NBSyncService"
|
||||
android:permission="android.permission.BIND_JOB_SERVICE" />
|
||||
<service android:name=".widget.WidgetRemoteViewsService"
|
||||
|
|
9
clients/android/NewsBlur/res/drawable/ic_clear.xml
Normal file
9
clients/android/NewsBlur/res/drawable/ic_clear.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/gray55"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||
</vector>
|
|
@ -4,31 +4,97 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<include layout="@layout/toolbar_newsblur" />
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:animateLayoutChanges="true"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/toolbar_arrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:src="?attr/homeAsUpIndicator" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/toolbar_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:src="@drawable/logo" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/input_search_query"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:autofillHints="@null"
|
||||
android:background="@null"
|
||||
android:hint="@string/search_for_feed"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textCapSentences|textNoSuggestions"
|
||||
android:maxLines="1" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/clear_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:padding="6dp"
|
||||
android:src="@drawable/ic_clear"
|
||||
android:visibility="gone" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading_circle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
style="?listBackground"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerInParent="true"
|
||||
android:gravity="center"
|
||||
style="?defaultText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/empty_search_notice"
|
||||
android:textColor="@color/text"
|
||||
android:textSize="13sp"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
<ListView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/feed_result_list"
|
||||
android:name="com.newsblur.fragment.FolderListFragment"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="?layoutBackground"
|
||||
android:focusable="false">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent" >
|
||||
|
||||
<View
|
||||
android:id="@+id/row_result_favicon_color"
|
||||
android:layout_width="8dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/row_item_sidebar"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_toRightOf="@id/row_result_favicon_color" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/row_result_feedicon"
|
||||
android:layout_width="21dp"
|
||||
android:layout_height="21dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingLeft="2dp"
|
||||
android:paddingRight="2dp"
|
||||
android:layout_toRightOf="@id/row_item_sidebar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/row_result_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_toRightOf="@id/row_result_feedicon"
|
||||
android:layout_alignBottom="@id/row_result_feedicon"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textStyle="bold"
|
||||
style="?defaultText"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/row_result_tagline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignLeft="@id/row_result_feedicon"
|
||||
android:layout_below="@id/row_result_feedicon"
|
||||
android:layout_marginRight="8dp"
|
||||
android:paddingTop="8dp"
|
||||
style="?defaultText"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/row_result_subscribercount"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginRight="8dp"
|
||||
android:paddingTop="8dp"
|
||||
style="?defaultText"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/row_result_address"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignLeft="@id/row_result_feedicon"
|
||||
android:layout_below="@id/row_result_tagline"
|
||||
android:layout_marginRight="8dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="12dp"
|
||||
style="?defaultText"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</LinearLayout>
|
57
clients/android/NewsBlur/res/layout/view_feed_search_row.xml
Normal file
57
clients/android/NewsBlur/res/layout/view_feed_search_row.xml
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="?layoutBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:focusable="false"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_feed_icon"
|
||||
android:layout_width="21dp"
|
||||
android:layout_height="21dp"
|
||||
android:src="@drawable/world" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
style="?defaultText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_toLeftOf="@+id/text_subscription_count"
|
||||
android:layout_toRightOf="@id/img_feed_icon"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_tagline"
|
||||
style="?defaultText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/img_feed_icon"
|
||||
android:layout_alignLeft="@id/img_feed_icon"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_subscription_count"
|
||||
style="?defaultText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/row_result_address"
|
||||
style="?defaultText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/text_tagline"
|
||||
android:layout_alignLeft="@id/img_feed_icon"
|
||||
android:layout_marginTop="4dp"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:id="@+id/menu_search"
|
||||
android:icon="@drawable/ic_menu_search_gray55"
|
||||
android:title="@string/menu_search"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
|
@ -15,7 +15,7 @@
|
|||
<string name="login_retrieving_feeds">Retrieving feeds…</string>
|
||||
<string name="login_next">Next</string>
|
||||
|
||||
<string name="title_feed_search">Search for feeds</string>
|
||||
<string name="search_for_feed">Search for feeds</string>
|
||||
|
||||
<string name="loading">Loading…</string>
|
||||
<string name="orig_text_loading">Fetching story text…</string>
|
||||
|
|
|
@ -1,67 +0,0 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.FeedResult;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
public class FeedSearchResultAdapter extends ArrayAdapter<FeedResult>{
|
||||
|
||||
private LayoutInflater inflater;
|
||||
private Context context;
|
||||
|
||||
public FeedSearchResultAdapter(Context context, int resource, int textViewResourceId, List<FeedResult> items) {
|
||||
super(context, resource, textViewResourceId, items);
|
||||
this.context = context;
|
||||
inflater = ((Activity) context).getLayoutInflater();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View v;
|
||||
if (convertView != null) {
|
||||
v = convertView;
|
||||
} else {
|
||||
v = inflater.inflate(R.layout.row_feedresult, null);
|
||||
}
|
||||
|
||||
FeedResult result = getItem(position);
|
||||
ImageView favicon = (ImageView) v.findViewById(R.id.row_result_feedicon);
|
||||
Bitmap bitmap = null;
|
||||
if (!TextUtils.isEmpty(result.favicon)) {
|
||||
final byte[] data = Base64.decode(result.favicon, Base64.DEFAULT);
|
||||
bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
|
||||
}
|
||||
if (bitmap == null) {
|
||||
bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.world);
|
||||
}
|
||||
|
||||
favicon.setImageBitmap(bitmap);
|
||||
|
||||
((TextView) v.findViewById(R.id.row_result_title)).setText(result.label);
|
||||
((TextView) v.findViewById(R.id.row_result_tagline)).setText(result.tagline);
|
||||
if (!TextUtils.isEmpty(result.url)) {
|
||||
((TextView) v.findViewById(R.id.row_result_address)).setText(result.url);
|
||||
} else {
|
||||
v.findViewById(R.id.row_result_address).setVisibility(View.GONE);
|
||||
}
|
||||
((TextView) v.findViewById(R.id.row_result_subscribercount)).setText(result.numberOfSubscriber + " subscribers");
|
||||
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package com.newsblur.activity
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.text.TextUtils
|
||||
import android.util.Base64
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.newsblur.R
|
||||
import com.newsblur.databinding.ViewFeedSearchRowBinding
|
||||
import com.newsblur.domain.FeedResult
|
||||
|
||||
internal class FeedSearchAdapter(private val onClickListener: OnFeedSearchResultClickListener) : RecyclerView.Adapter<FeedSearchAdapter.ViewHolder>() {
|
||||
|
||||
private val resultsList: MutableList<FeedResult> = ArrayList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.view_feed_search_row, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val result = resultsList[position]
|
||||
var bitmap: Bitmap? = null
|
||||
if (!TextUtils.isEmpty(result.favicon)) {
|
||||
val data = Base64.decode(result.favicon, Base64.DEFAULT)
|
||||
bitmap = BitmapFactory.decodeByteArray(data, 0, data.size)
|
||||
}
|
||||
bitmap?.let {
|
||||
holder.binding.imgFeedIcon.setImageBitmap(bitmap)
|
||||
}
|
||||
|
||||
holder.binding.textTitle.text = result.label
|
||||
holder.binding.textTagline.text = result.tagline
|
||||
val subscribersCountText = holder.binding.root.context.resources.getString(R.string.feed_subscribers, result.numberOfSubscriber)
|
||||
holder.binding.textSubscriptionCount.text = subscribersCountText
|
||||
|
||||
if (!TextUtils.isEmpty(result.url)) {
|
||||
holder.binding.rowResultAddress.text = result.url
|
||||
holder.binding.rowResultAddress.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.binding.rowResultAddress.visibility = View.GONE
|
||||
}
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
onClickListener.onFeedSearchResultClickListener(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = resultsList.size
|
||||
|
||||
fun replaceAll(results: Array<FeedResult>) {
|
||||
val newResultsList: List<FeedResult> = results.toList()
|
||||
val diffCallback = ResultDiffCallback(resultsList, newResultsList)
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||
resultsList.clear()
|
||||
resultsList.addAll(newResultsList)
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
}
|
||||
|
||||
internal class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val binding: ViewFeedSearchRowBinding = ViewFeedSearchRowBinding.bind(itemView)
|
||||
}
|
||||
|
||||
internal class ResultDiffCallback(private val oldList: List<FeedResult>,
|
||||
private val newList: List<FeedResult>) : DiffUtil.Callback() {
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldFeedResult = oldList[oldItemPosition]
|
||||
val newFeedResult = newList[newItemPosition]
|
||||
return oldFeedResult.label == newFeedResult.label &&
|
||||
oldFeedResult.numberOfSubscriber == newFeedResult.numberOfSubscriber
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldFeedResult = oldList[oldItemPosition]
|
||||
val newFeedResult = newList[newItemPosition]
|
||||
return oldFeedResult.label == newFeedResult.label
|
||||
&& oldFeedResult.tagline == newFeedResult.tagline
|
||||
}
|
||||
|
||||
override fun getOldListSize(): Int = oldList.size
|
||||
|
||||
override fun getNewListSize(): Int = newList.size
|
||||
}
|
||||
|
||||
interface OnFeedSearchResultClickListener {
|
||||
|
||||
fun onFeedSearchResultClickListener(result: FeedResult)
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
package com.newsblur.activity;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.newsblur.R;
|
||||
import com.newsblur.domain.FeedResult;
|
||||
import com.newsblur.fragment.AddFeedFragment;
|
||||
import com.newsblur.network.SearchAsyncTaskLoader;
|
||||
import com.newsblur.network.SearchLoaderResponse;
|
||||
import com.newsblur.util.UIUtils;
|
||||
|
||||
// TODO: this activity's use of the inbuilt activity search facility as well as an improper use of a loader to
|
||||
// make network requests makes it easily lose state, lack non-legacy progress indication, and generally
|
||||
// buggy. a normal layout and a proper use of sync for search results should be implemented.
|
||||
public class SearchForFeeds extends NbActivity implements LoaderCallbacks<SearchLoaderResponse>, OnItemClickListener, AddFeedFragment.AddFeedProgressListener {
|
||||
|
||||
private static final Set<String> SUPPORTED_URL_PROTOCOLS = new HashSet<String>();
|
||||
static {
|
||||
SUPPORTED_URL_PROTOCOLS.add("http");
|
||||
SUPPORTED_URL_PROTOCOLS.add("https");
|
||||
}
|
||||
|
||||
private ListView resultsList;
|
||||
private Loader<SearchLoaderResponse> searchLoader;
|
||||
private FeedSearchResultAdapter adapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle arg0) {
|
||||
super.onCreate(arg0);
|
||||
|
||||
setContentView(R.layout.activity_feed_search);
|
||||
UIUtils.setupToolbar(this, R.drawable.logo, getString(R.string.title_feed_search), true);
|
||||
|
||||
TextView emptyView = (TextView) findViewById(R.id.empty_view);
|
||||
resultsList = (ListView) findViewById(R.id.feed_result_list);
|
||||
resultsList.setEmptyView(emptyView);
|
||||
resultsList.setOnItemClickListener(this);
|
||||
resultsList.setItemsCanFocus(false);
|
||||
searchLoader = LoaderManager.getInstance(this).initLoader(0, new Bundle(), this);
|
||||
|
||||
onSearchRequested();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.search, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
String query = intent.getStringExtra(SearchManager.QUERY);
|
||||
|
||||
// test to see if a feed URL was passed rather than a search term
|
||||
if (tryAddByURL(query)) { return; }
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(SearchAsyncTaskLoader.SEARCH_TERM, query);
|
||||
searchLoader = LoaderManager.getInstance(this).restartLoader(0, bundle, this);
|
||||
|
||||
searchLoader.forceLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* See if the text entered in the query field was actually a URL so we can skip the
|
||||
* search step and just let users who know feed URLs directly subscribe.
|
||||
*/
|
||||
private boolean tryAddByURL(String s) {
|
||||
URL u = null;
|
||||
try {
|
||||
u = new URL(s);
|
||||
} catch (MalformedURLException mue) {
|
||||
; // this just signals that the string wasn't a URL, we will return
|
||||
}
|
||||
if (u == null) { return false; }
|
||||
if (! SUPPORTED_URL_PROTOCOLS.contains(u.getProtocol())) { return false; };
|
||||
if ((u.getHost() == null) || (u.getHost().trim().isEmpty())) { return false; }
|
||||
|
||||
DialogFragment addFeedFragment = AddFeedFragment.newInstance(s, s);
|
||||
addFeedFragment.show(getSupportFragmentManager(), "dialog");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_search) {
|
||||
onSearchRequested();
|
||||
return true;
|
||||
} else if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<SearchLoaderResponse> onCreateLoader(int loaderId, Bundle bundle) {
|
||||
String searchTerm = bundle.getString(SearchAsyncTaskLoader.SEARCH_TERM);
|
||||
return new SearchAsyncTaskLoader(this, searchTerm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<SearchLoaderResponse> loader, SearchLoaderResponse results) {
|
||||
if(!results.hasError()) {
|
||||
adapter = new FeedSearchResultAdapter(this, 0, 0, results.getResults());
|
||||
resultsList.setAdapter(adapter);
|
||||
} else {
|
||||
String message = results.getErrorMessage() == null ? "Error" : results.getErrorMessage();
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<SearchLoaderResponse> loader) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> arg0, View view, int position, long id) {
|
||||
FeedResult result = adapter.getItem(position);
|
||||
DialogFragment addFeedFragment = AddFeedFragment.newInstance(result.url, result.label);
|
||||
addFeedFragment.show(getSupportFragmentManager(), "dialog");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addFeedStarted() {
|
||||
runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
// TODO: this UI should offer some progress indication, since the add API call can block for several seconds
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package com.newsblur.activity
|
||||
|
||||
import android.os.AsyncTask
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.newsblur.activity.FeedSearchAdapter.OnFeedSearchResultClickListener
|
||||
import com.newsblur.databinding.ActivityFeedSearchBinding
|
||||
import com.newsblur.domain.FeedResult
|
||||
import com.newsblur.fragment.AddFeedFragment
|
||||
import com.newsblur.fragment.AddFeedFragment.AddFeedProgressListener
|
||||
import com.newsblur.network.APIManager
|
||||
import java.net.MalformedURLException
|
||||
import java.net.URL
|
||||
import java.util.*
|
||||
|
||||
class SearchForFeeds : NbActivity(), OnFeedSearchResultClickListener, AddFeedProgressListener {
|
||||
|
||||
private val supportedUrlProtocols: MutableSet<String> = HashSet(2)
|
||||
|
||||
init {
|
||||
supportedUrlProtocols.add("http")
|
||||
supportedUrlProtocols.add("https")
|
||||
}
|
||||
|
||||
private lateinit var adapter: FeedSearchAdapter
|
||||
private lateinit var binding: ActivityFeedSearchBinding
|
||||
private lateinit var apiManager: APIManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityFeedSearchBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupViews()
|
||||
setupListeners()
|
||||
apiManager = APIManager(this)
|
||||
binding.inputSearchQuery.requestFocus()
|
||||
}
|
||||
|
||||
override fun onFeedSearchResultClickListener(result: FeedResult) {
|
||||
showAddFeedDialog(result.url, result.label)
|
||||
}
|
||||
|
||||
override fun addFeedStarted() {
|
||||
// loading views handled by AddFeedFragment
|
||||
}
|
||||
|
||||
private fun setupViews() {
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
supportActionBar?.setDisplayShowHomeEnabled(false)
|
||||
|
||||
adapter = FeedSearchAdapter(this)
|
||||
binding.feedResultList.adapter = adapter
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.toolbarArrow.setOnClickListener { finish() }
|
||||
binding.toolbarIcon.setOnClickListener { finish() }
|
||||
binding.clearText.setOnClickListener { binding.inputSearchQuery.setText("") }
|
||||
|
||||
binding.inputSearchQuery.addTextChangedListener(object : TextWatcher {
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var searchQueryRunnable: Runnable? = null
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
searchQueryRunnable?.let { handler.removeCallbacks(it) }
|
||||
searchQueryRunnable = Runnable {
|
||||
if (tryAddByURL(s.toString())) {
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
syncClearIconVisibility(s)
|
||||
if (s.isNotEmpty()) searchQuery(s)
|
||||
else syncSearchResults(arrayOf())
|
||||
}
|
||||
handler.postDelayed(searchQueryRunnable!!, 350)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun searchQuery(query: Editable) {
|
||||
object : AsyncTask<Void?, Void?, Array<FeedResult>?>() {
|
||||
override fun onPreExecute() {
|
||||
super.onPreExecute()
|
||||
binding!!.loadingCircle.visibility = View.VISIBLE
|
||||
binding!!.clearText.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun doInBackground(vararg params: Void?): Array<FeedResult>? {
|
||||
return apiManager!!.searchForFeed(query.toString())
|
||||
}
|
||||
|
||||
override fun onPostExecute(result: Array<FeedResult>?) {
|
||||
binding!!.loadingCircle.visibility = View.GONE
|
||||
binding!!.clearText.visibility = View.VISIBLE
|
||||
syncSearchResults(result ?: arrayOf())
|
||||
}
|
||||
|
||||
|
||||
}.execute()
|
||||
}
|
||||
|
||||
private fun syncClearIconVisibility(query: Editable) {
|
||||
binding.clearText.visibility = if (query.isNotEmpty()) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun syncSearchResults(results: Array<FeedResult>) {
|
||||
adapter.replaceAll(results)
|
||||
}
|
||||
|
||||
/**
|
||||
* See if the text entered in the query field was actually a URL so we can skip the
|
||||
* search step and just let users who know feed URLs directly subscribe.
|
||||
*/
|
||||
private fun tryAddByURL(s: String): Boolean {
|
||||
var u: URL? = null
|
||||
try {
|
||||
u = URL(s)
|
||||
} catch (mue: MalformedURLException) {
|
||||
// this just signals that the string wasn't a URL, we will return
|
||||
}
|
||||
if (u == null) {
|
||||
return false
|
||||
}
|
||||
if (!supportedUrlProtocols.contains(u.protocol)) {
|
||||
return false
|
||||
}
|
||||
if (u.host == null || u.host.trim().isEmpty()) {
|
||||
return false
|
||||
}
|
||||
showAddFeedDialog(s, s)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showAddFeedDialog(feedUrl: String, feedLabel: String) {
|
||||
val addFeedFragment: DialogFragment = AddFeedFragment.newInstance(feedUrl, feedLabel)
|
||||
addFeedFragment.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package com.newsblur.network;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
import com.newsblur.domain.FeedResult;
|
||||
|
||||
public class SearchAsyncTaskLoader extends AsyncTaskLoader<SearchLoaderResponse> {
|
||||
|
||||
public static final String SEARCH_TERM = "searchTerm";
|
||||
|
||||
private String searchTerm;
|
||||
private APIManager apiManager;
|
||||
|
||||
public SearchAsyncTaskLoader(Context context, String searchTerm) {
|
||||
super(context);
|
||||
this.searchTerm = searchTerm;
|
||||
apiManager = new APIManager(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SearchLoaderResponse loadInBackground() {
|
||||
ArrayList<FeedResult> list = new ArrayList<FeedResult>();
|
||||
FeedResult[] results = apiManager.searchForFeed(searchTerm);
|
||||
if (results != null) {
|
||||
for (FeedResult result : results) {
|
||||
list.add(result);
|
||||
}
|
||||
}
|
||||
SearchLoaderResponse response = new SearchLoaderResponse(list);
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
package com.newsblur.network;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import com.newsblur.domain.FeedResult;
|
||||
|
||||
public class SearchLoaderResponse extends BaseLoaderResponse {
|
||||
|
||||
private ArrayList<FeedResult> results;
|
||||
|
||||
/**
|
||||
* Use to indicate there was a problem w/ the search
|
||||
*
|
||||
* @param errorMessage
|
||||
*/
|
||||
public SearchLoaderResponse(String errorMessage) {
|
||||
super(errorMessage);
|
||||
results = new ArrayList<FeedResult>(0);
|
||||
}
|
||||
|
||||
public SearchLoaderResponse(ArrayList<FeedResult> results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
public ArrayList<FeedResult> getResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Reference in a new issue