From ba6a3cd70d6b5cd1479cde3755c6deace734ba69 Mon Sep 17 00:00:00 2001 From: Eric Kok Date: Tue, 23 Jul 2013 19:51:28 +0200 Subject: [PATCH] Added search UI and functionality. --- core/AndroidManifest.xml | 4 +- core/res/layout-w600dp/activity_search.xml | 40 +++ core/res/layout/actionbar_searchsite.xml | 17 ++ core/res/layout/activity_search.xml | 29 ++ core/res/layout/fragment_searchresults.xml | 37 +++ core/res/layout/list_item_searchresult.xml | 58 ++++ core/res/layout/list_item_searchsite.xml | 28 ++ core/res/menu/activity_search.xml | 18 ++ core/res/menu/fragment_rssitems_cab.xml | 8 +- core/res/menu/fragment_searchresults_cab.xml | 9 + core/res/values/changelog.xml | 5 + core/res/values/strings.xml | 13 +- core/res/xml/searchable.xml | 2 +- .../core/app/search/SearchHelper.java | 6 +- .../core/app/search/SearchResult.java | 44 +++- .../core/app/search/SearchSite.java | 8 +- .../app/settings/ApplicationSettings.java | 93 ++++++- .../core/app/settings/WebsearchSetting.java | 3 +- .../core/gui/search/SearchActivity.java | 248 ++++++++++++++++++ .../{ => search}/SearchHistoryProvider.java | 4 +- .../core/gui/search/SearchResultView.java | 40 +++ .../core/gui/search/SearchResultsAdapter.java | 71 +++++ .../gui/search/SearchResultsFragment.java | 168 ++++++++++++ .../core/gui/search/SearchSetting.java | 19 ++ .../search/SearchSettingSelectionView.java | 28 ++ .../search/SearchSettingsDropDownAdapter.java | 43 +++ .../core/gui/search/SearchSiteView.java | 49 ++++ .../core/gui/search/SearchSitesAdapter.java | 72 +++++ .../core/gui/search/SendIntentHelper.java | 61 +++++ .../search/TorrentSearchHistoryProvider.java | 26 ++ full/AndroidManifest.xml | 32 +-- lite/AndroidManifest.xml | 4 +- 32 files changed, 1251 insertions(+), 36 deletions(-) create mode 100644 core/res/layout-w600dp/activity_search.xml create mode 100644 core/res/layout/actionbar_searchsite.xml create mode 100644 core/res/layout/activity_search.xml create mode 100644 core/res/layout/fragment_searchresults.xml create mode 100644 core/res/layout/list_item_searchresult.xml create mode 100644 core/res/layout/list_item_searchsite.xml create mode 100644 core/res/menu/activity_search.xml create mode 100644 core/res/menu/fragment_searchresults_cab.xml create mode 100644 core/src/org/transdroid/core/gui/search/SearchActivity.java rename core/src/org/transdroid/core/gui/{ => search}/SearchHistoryProvider.java (91%) create mode 100644 core/src/org/transdroid/core/gui/search/SearchResultView.java create mode 100644 core/src/org/transdroid/core/gui/search/SearchResultsAdapter.java create mode 100644 core/src/org/transdroid/core/gui/search/SearchResultsFragment.java create mode 100644 core/src/org/transdroid/core/gui/search/SearchSetting.java create mode 100644 core/src/org/transdroid/core/gui/search/SearchSettingSelectionView.java create mode 100644 core/src/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java create mode 100644 core/src/org/transdroid/core/gui/search/SearchSiteView.java create mode 100644 core/src/org/transdroid/core/gui/search/SearchSitesAdapter.java create mode 100644 core/src/org/transdroid/core/gui/search/SendIntentHelper.java create mode 100644 core/src/org/transdroid/core/gui/search/TorrentSearchHistoryProvider.java diff --git a/core/AndroidManifest.xml b/core/AndroidManifest.xml index e30ef514..d905a57a 100644 --- a/core/AndroidManifest.xml +++ b/core/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="2" + android:versionName="2.0-alpha2" > + + + + + + + + + \ No newline at end of file diff --git a/core/res/layout/actionbar_searchsite.xml b/core/res/layout/actionbar_searchsite.xml new file mode 100644 index 00000000..63244f61 --- /dev/null +++ b/core/res/layout/actionbar_searchsite.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/core/res/layout/activity_search.xml b/core/res/layout/activity_search.xml new file mode 100644 index 00000000..e26348e2 --- /dev/null +++ b/core/res/layout/activity_search.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/layout/fragment_searchresults.xml b/core/res/layout/fragment_searchresults.xml new file mode 100644 index 00000000..a3a69c8a --- /dev/null +++ b/core/res/layout/fragment_searchresults.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/core/res/layout/list_item_searchresult.xml b/core/res/layout/list_item_searchresult.xml new file mode 100644 index 00000000..c4d1c530 --- /dev/null +++ b/core/res/layout/list_item_searchresult.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/res/layout/list_item_searchsite.xml b/core/res/layout/list_item_searchsite.xml new file mode 100644 index 00000000..92b0838b --- /dev/null +++ b/core/res/layout/list_item_searchsite.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/core/res/menu/activity_search.xml b/core/res/menu/activity_search.xml new file mode 100644 index 00000000..dc460ae9 --- /dev/null +++ b/core/res/menu/activity_search.xml @@ -0,0 +1,18 @@ + + + + + + + \ No newline at end of file diff --git a/core/res/menu/fragment_rssitems_cab.xml b/core/res/menu/fragment_rssitems_cab.xml index 7be923f5..8e3fd55c 100644 --- a/core/res/menu/fragment_rssitems_cab.xml +++ b/core/res/menu/fragment_rssitems_cab.xml @@ -1,9 +1,15 @@ + + - + \ No newline at end of file diff --git a/core/res/menu/fragment_searchresults_cab.xml b/core/res/menu/fragment_searchresults_cab.xml new file mode 100644 index 00000000..7be923f5 --- /dev/null +++ b/core/res/menu/fragment_searchresults_cab.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/core/res/values/changelog.xml b/core/res/values/changelog.xml index a6b4dcbc..66fd568e 100644 --- a/core/res/values/changelog.xml +++ b/core/res/values/changelog.xml @@ -1,6 +1,11 @@ +Transdroid 2.0.0-alpha2\n +- Fixed Transmission adapter folder setting\n +- Fixed RSS feed screens\n +- UI tweaks\n +\n Transdroid 2.0.0-alpha1\n - Totally reworked Holo-style interface\n - Provide tablet interface on smaller tablets\n diff --git a/core/res/values/strings.xml b/core/res/values/strings.xml index c5092423..7a1b46c4 100644 --- a/core/res/values/strings.xml +++ b/core/res/values/strings.xml @@ -38,6 +38,7 @@ High Remote play in VLC Download using (S)FTP + Show details Remove settings Visit transdroid.org @@ -117,8 +118,18 @@ Torrent search Search for torrents + No results for your query + S: %1$s + L: %1$s + This feature requires a one-time installation of the Torrent Search module. Click download to get the install package (apk) from transdroid.org and restart your search. + Download module + Opening details for %1$s The Barcode Scanner could not be found. Would you like to install it from the Play Store? No compatible file manager could not be found. Would you like to install IO File Manager from the Play Store? + + %1$d result selected + %1$d results selected + RSS feeds You have not defined any RSS feeds yet to monitor. Torrent-specific RSS feeds keep you up to date with new releases and you are notified of new items. @@ -130,7 +141,7 @@ %1$d item selected %1$d items selected - + Servers Add new server Search sites diff --git a/core/res/xml/searchable.xml b/core/res/xml/searchable.xml index 9f811c8c..3ad2bc96 100644 --- a/core/res/xml/searchable.xml +++ b/core/res/xml/searchable.xml @@ -3,6 +3,6 @@ \ No newline at end of file diff --git a/core/src/org/transdroid/core/app/search/SearchHelper.java b/core/src/org/transdroid/core/app/search/SearchHelper.java index 1eca6cb5..d01c9d9c 100644 --- a/core/src/org/transdroid/core/app/search/SearchHelper.java +++ b/core/src/org/transdroid/core/app/search/SearchHelper.java @@ -76,14 +76,14 @@ public class SearchHelper { } /** - * Queries the Torrent Search module to search for torrents on the web. This method is synchornous and should always + * Queries the Torrent Search module to search for torrents on the web. This method is synchronous and should always * be called in a background thread. * @param query The search query to pass to the torrent site * @param site The site to search, as retrieved from the TorrentSitesProvider, or null if the Torrent Search package * @param sortBy.name() The sort order to request from the torrent site, if supported * @return A list of torrent search results as POJOs, or null if the Torrent Search package is not installed */ - public List search(String query, SearchSite site, SearchSortOrder sortBy) { + public ArrayList search(String query, SearchSite site, SearchSortOrder sortBy) { // Try to query the TorrentSearchProvider to search for torrents on the web Uri uri = Uri.parse("content://org.transdroid.search.torrentsearchprovider/search/" + query); @@ -96,7 +96,7 @@ public class SearchHelper { sortBy.name()); } if (cursor.moveToFirst()) { - List results = new ArrayList(); + ArrayList results = new ArrayList(); do { // Read the cursor fields into the SearchResult object results.add(new SearchResult(cursor.getInt(CURSOR_SEARCH_ID), cursor.getString(CURSOR_SEARCH_NAME), diff --git a/core/src/org/transdroid/core/app/search/SearchResult.java b/core/src/org/transdroid/core/app/search/SearchResult.java index 6bbfaa76..feff27c6 100644 --- a/core/src/org/transdroid/core/app/search/SearchResult.java +++ b/core/src/org/transdroid/core/app/search/SearchResult.java @@ -2,11 +2,14 @@ package org.transdroid.core.app.search; import java.util.Date; +import android.os.Parcel; +import android.os.Parcelable; + /** * Represents a search result as retrieved by querying the Torrent Search package. * @author Eric Kok */ -public class SearchResult { +public class SearchResult implements Parcelable { private final int id; private final String name; @@ -61,4 +64,43 @@ public class SearchResult { return leechers; } + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(id); + out.writeString(name); + out.writeString(torrentUrl); + out.writeString(detailsUrl); + out.writeString(size); + out.writeLong(addedOn == null ? -1 : addedOn.getTime()); + out.writeString(seeders); + out.writeString(leechers); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public SearchResult createFromParcel(Parcel in) { + return new SearchResult(in); + } + + public SearchResult[] newArray(int size) { + return new SearchResult[size]; + } + }; + + public SearchResult(Parcel in) { + id = in.readInt(); + name = in.readString(); + torrentUrl = in.readString(); + detailsUrl = in.readString(); + size = in.readString(); + long addedOnIn = in.readLong(); + addedOn = addedOnIn == -1 ? null : new Date(addedOnIn); + seeders = in.readString(); + leechers = in.readString(); + } + } diff --git a/core/src/org/transdroid/core/app/search/SearchSite.java b/core/src/org/transdroid/core/app/search/SearchSite.java index a30ad09e..7e051cff 100644 --- a/core/src/org/transdroid/core/app/search/SearchSite.java +++ b/core/src/org/transdroid/core/app/search/SearchSite.java @@ -1,12 +1,13 @@ package org.transdroid.core.app.search; import org.transdroid.core.gui.lists.SimpleListItem; +import org.transdroid.core.gui.search.SearchSetting; /** * Represents an available torrent site that can be searched using the Torrent Search package. * @author Eric Kok */ -public class SearchSite implements SimpleListItem { +public class SearchSite implements SimpleListItem, SearchSetting { private final int id; private final String key; @@ -36,5 +37,10 @@ public class SearchSite implements SimpleListItem { public String getRssFeedUrl() { return rssFeedUrl; } + + @Override + public String getBaseUrl() { + return rssFeedUrl; + } } diff --git a/core/src/org/transdroid/core/app/settings/ApplicationSettings.java b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java index b019572d..02104dc4 100644 --- a/core/src/org/transdroid/core/app/settings/ApplicationSettings.java +++ b/core/src/org/transdroid/core/app/settings/ApplicationSettings.java @@ -1,12 +1,17 @@ package org.transdroid.core.app.settings; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; +import org.androidannotations.annotations.Bean; import org.androidannotations.annotations.EBean; import org.androidannotations.annotations.EBean.Scope; import org.androidannotations.annotations.RootContext; +import org.transdroid.core.app.search.SearchHelper; +import org.transdroid.core.app.search.SearchSite; +import org.transdroid.core.gui.search.SearchSetting; import org.transdroid.daemon.Daemon; import org.transdroid.daemon.OS; import org.transdroid.daemon.TorrentsSortBy; @@ -26,6 +31,8 @@ public class ApplicationSettings { @RootContext protected Context context; private SharedPreferences prefs; + @Bean + protected SearchHelper searchHelper; protected ApplicationSettings(Context context) { prefs = PreferenceManager.getDefaultSharedPreferences(context); @@ -40,7 +47,7 @@ public class ApplicationSettings { for (int i = 0; i <= getMaxServer(); i++) { servers.add(getServerSetting(i)); } - return servers; + return Collections.unmodifiableList(servers); } /** @@ -208,7 +215,7 @@ public class ApplicationSettings { for (int i = 0; i <= getMaxWebsearch(); i++) { websearches.add(getWebsearchSetting(i)); } - return websearches; + return Collections.unmodifiableList(websearches); } /** @@ -271,7 +278,7 @@ public class ApplicationSettings { for (int i = 0; i <= getMaxRssfeed(); i++) { rssfeeds.add(getRssfeedSetting(i)); } - return rssfeeds; + return Collections.unmodifiableList(rssfeeds); } /** @@ -373,4 +380,84 @@ public class ApplicationSettings { return prefs.getBoolean("system_lastusedsortdirection", false); } + /** + * Returns the list of all available in-app search sites as well as all web searches that the user configured. + * @return A list of search settings, all of which are either a {@link SearchSite} or {@link WebsearchSetting} + */ + public List getSearchSettings() { + List all = new ArrayList(); + all.addAll(searchHelper.getAvailableSites()); + all.addAll(getWebsearchSettings()); + return Collections.unmodifiableList(all); + } + + /** + * Returns the settings of the search site that was last used by the user or was selected by the user as default + * site in the main settings. As opposed to getLastUsedSearchSiteKey(int), this method checks whether a site was + * already registered as being last used (or set as default) and checks whether the site still exists. It returns + * the first in-app search site if that fails. + * @return A site settings object of the last used server (or, if not known, the first server), or null if no + * servers exist + */ + public SearchSetting getLastUsedSearchSite() { + String lastKey = getLastUsedSearchSiteKey(); + List allsites = searchHelper.getAvailableSites(); + int lastWebsearch = -1; + try { + lastWebsearch = Integer.parseInt(lastKey); + } catch (Exception e) { + // Not an in-app search site, but probably an in-app search + } + + if (lastKey == null) { + // No site yet set specified; return the first in-app one, if available + if (allsites != null) { + return allsites.get(0); + } + return null; + } + + if (lastWebsearch >= 0) { + // The last used site should be a user-configured web search site + int max = getMaxWebsearch(); // Zero-based index, so with max == 0 there is 1 server + if (max < 0 || lastWebsearch > max) { + // No web search sites configured + return null; + } + return getWebsearchSetting(lastWebsearch); + } + + // Should be an in-app search key + if (allsites != null) { + for (SearchSite searchSite : allsites) { + if (searchSite.getKey().equals(lastKey)) { + return searchSite; + } + } + // Not found at all; probably a no longer existing web search; return the first in-app one + return allsites.get(0); + } + + return null; + } + + /** + * Returns the unique key of the site that the used last used or selected as default form the main settings; use + * with getLastUsedSearchSite directly. WARNING: the returned string may no longer refer to a known web search site + * or in-app search settings object. + * @return A string indicating the key of the last used search site, or null if no site was yet used or set as + * default + */ + private String getLastUsedSearchSiteKey() { + return prefs.getString("header_setsearchsite", null); + } + + /** + * Registers the unique key of some web search or in-app search site as being last used by the user + * @param order The key identifying the specific server + */ + public void setLastUsedSearchSite(String siteKey) { + prefs.edit().putString("header_setsearchsite", siteKey).commit(); + } + } diff --git a/core/src/org/transdroid/core/app/settings/WebsearchSetting.java b/core/src/org/transdroid/core/app/settings/WebsearchSetting.java index 8c6c673f..9d9a612f 100644 --- a/core/src/org/transdroid/core/app/settings/WebsearchSetting.java +++ b/core/src/org/transdroid/core/app/settings/WebsearchSetting.java @@ -1,6 +1,7 @@ package org.transdroid.core.app.settings; import org.transdroid.core.gui.lists.SimpleListItem; +import org.transdroid.core.gui.search.SearchSetting; import android.net.Uri; import android.text.TextUtils; @@ -9,7 +10,7 @@ import android.text.TextUtils; * Represents a user-specified website that can be searched (by starting the browser, rather than in-app) * @author Eric Kok */ -public class WebsearchSetting implements SimpleListItem { +public class WebsearchSetting implements SimpleListItem, SearchSetting { private static final String DEFAULT_NAME = "Default"; private static final String KEY_PREFIX = "websearch_"; diff --git a/core/src/org/transdroid/core/gui/search/SearchActivity.java b/core/src/org/transdroid/core/gui/search/SearchActivity.java new file mode 100644 index 00000000..e09d0f4d --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchActivity.java @@ -0,0 +1,248 @@ +package org.transdroid.core.gui.search; + +import java.util.List; + +import org.androidannotations.annotations.AfterViews; +import org.androidannotations.annotations.Bean; +import org.androidannotations.annotations.EActivity; +import org.androidannotations.annotations.FragmentById; +import org.androidannotations.annotations.OptionsItem; +import org.androidannotations.annotations.OptionsMenu; +import org.androidannotations.annotations.SystemService; +import org.androidannotations.annotations.ViewById; +import org.transdroid.core.R; +import org.transdroid.core.app.search.SearchHelper; +import org.transdroid.core.app.search.SearchSite; +import org.transdroid.core.app.settings.ApplicationSettings; +import org.transdroid.core.app.settings.SystemSettings_; +import org.transdroid.core.app.settings.WebsearchSetting; +import org.transdroid.core.gui.TorrentsActivity_; +import org.transdroid.core.gui.navigation.NavigationHelper; + +import android.annotation.TargetApi; +import android.app.SearchManager; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.provider.SearchRecentSuggestions; +import android.view.View; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.TextView; + +import com.actionbarsherlock.app.ActionBar; +import com.actionbarsherlock.app.ActionBar.OnNavigationListener; +import com.actionbarsherlock.app.SherlockFragmentActivity; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; +import com.actionbarsherlock.view.SherlockListView; +import com.actionbarsherlock.widget.SearchView; + +/** + * An activity that shows search results to the user (after a query was supplied by the standard Android search manager) + * and either shows the list of search sites on the left (e.g. on tablets) or allows switching between search sites via + * the action bar spinner. + * @author Eric Kok + */ +@EActivity(resName = "activity_search") +@OptionsMenu(resName = "activity_search") +public class SearchActivity extends SherlockFragmentActivity implements OnNavigationListener { + + @FragmentById(resName = "searchresults_list") + protected SearchResultsFragment fragmentResults; + @ViewById + protected SherlockListView searchsitesList; + @ViewById + protected TextView installmoduleText; + @Bean + protected ApplicationSettings applicationSettings; + @Bean + protected NavigationHelper navigationHelper; + @Bean + protected SearchHelper searchHelper; + @SystemService + protected SearchManager searchManager; + private SearchRecentSuggestions suggestions = new SearchRecentSuggestions(this, + TorrentSearchHistoryProvider.AUTHORITY, TorrentSearchHistoryProvider.MODE); + + private List searchSites; + private SearchSetting lastUsedSite; + private String lastUsedQuery; + + @Override + public void onCreate(Bundle savedInstanceState) { + // Set the theme according to the user preference + if (SystemSettings_.getInstance_(this).useDarkTheme()) { + setTheme(R.style.TransdroidTheme_Dark); + getSupportActionBar().setIcon(R.drawable.ic_activity_torrents); + } + super.onCreate(savedInstanceState); + } + + @AfterViews + protected void init() { + + // Get the user query, as coming from the standard SearchManager + handleIntent(getIntent()); + + if (!searchHelper.isTorrentSearchInstalled()) { + // The module install text will be shown instead (in onPrepareOptionsMenu) + return; + } + + // Load sites and find the last used (or set as default) search site + searchSites = applicationSettings.getSearchSettings(); + lastUsedSite = applicationSettings.getLastUsedSearchSite(); + int lastUsedPosition = -1; + if (lastUsedSite != null) { + for (int i = 0; i < searchSites.size(); i++) { + if (searchSites.get(i).getKey().equals(lastUsedSite.getKey())) + lastUsedPosition = i; + } + } + + // Allow site selection via list (on large screens) or action bar spinner + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + if (searchsitesList != null) { + // The current layout has a dedicated list view to select the search site + SearchSitesAdapter searchSitesAdapter = SearchSitesAdapter_.getInstance_(this); + searchSitesAdapter.update(searchSites); + searchsitesList.setAdapter(searchSitesAdapter); + searchsitesList.setOnItemClickListener(onSearchSiteClicked); + // Select the last used site; this also starts the search! + if (lastUsedPosition >= 0) + searchsitesList.setItemChecked(lastUsedPosition, true); + } else { + // Use the action bar spinner to select sites + getSupportActionBar().setNavigationMode(ActionBar.NAVIGATION_MODE_LIST); + getSupportActionBar().setDisplayShowTitleEnabled(false); + getSupportActionBar().setListNavigationCallbacks(new SearchSettingsDropDownAdapter(this, searchSites), this); + // Select the last used site; this also starts the search! + if (lastUsedPosition >= 0) + getSupportActionBar().setSelectedNavigationItem(lastUsedPosition); + } + + } + + @TargetApi(Build.VERSION_CODES.FROYO) + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + if (navigationHelper.enableSearchUi()) { + // For Android 2.1+, add an expandable SearchView to the action bar + MenuItem item = menu.findItem(R.id.action_search); + if (android.os.Build.VERSION.SDK_INT >= 8) { + final SearchView searchView = new SearchView(this); + searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); + searchView.setQueryRefinementEnabled(true); + item.setActionView(searchView); + } + } + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + super.onPrepareOptionsMenu(menu); + + boolean searchInstalled = searchHelper.isTorrentSearchInstalled(); + menu.findItem(R.id.action_search).setVisible(searchInstalled); + menu.findItem(R.id.action_refresh).setVisible(searchInstalled); + menu.findItem(R.id.action_downloadsearch).setVisible(!searchInstalled); + if (searchsitesList != null) + searchsitesList.setVisibility(searchInstalled ? View.VISIBLE : View.GONE); + if (searchInstalled) + getSupportFragmentManager().beginTransaction().show(fragmentResults).commit(); + else + getSupportFragmentManager().beginTransaction().hide(fragmentResults).commit(); + installmoduleText.setVisibility(searchInstalled ? View.GONE : View.VISIBLE); + + return true; + } + + @Override + protected void onNewIntent(Intent intent) { + handleIntent(intent); + refreshSearch(); + } + + private void handleIntent(Intent intent) { + lastUsedQuery = getQuery(intent); + getSupportActionBar().setTitle(NavigationHelper.buildCondensedFontString(lastUsedQuery)); + + // Is this actually a full HTTP URL? Then redirect this request to add the URL directly + if (lastUsedQuery != null + && (lastUsedQuery.startsWith("http") || lastUsedQuery.startsWith("https") + || lastUsedQuery.startsWith("magnet") || lastUsedQuery.startsWith("file"))) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(lastUsedQuery))); + finish(); + return; + } + + } + + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + @OptionsItem(android.R.id.home) + protected void navigateUp() { + TorrentsActivity_.intent(this).flags(Intent.FLAG_ACTIVITY_CLEAR_TOP).start(); + } + + private OnItemClickListener onSearchSiteClicked = new OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + lastUsedSite = searchSites.get(position); + refreshSearch(); + } + }; + + @Override + public boolean onNavigationItemSelected(int itemPosition, long itemId) { + lastUsedSite = searchSites.get(itemPosition); + refreshSearch(); + return true; + } + + /** + * Extracts the query string from the search {@link Intent} + * @return The query string that was entered by the user + */ + private String getQuery(Intent intent) { + + String query = null; + if (intent.getAction().equals(Intent.ACTION_SEARCH)) { + query = intent.getStringExtra(SearchManager.QUERY); + } else if (intent.getAction().equals(Intent.ACTION_SEND)) { + query = SendIntentHelper.cleanUpText(intent); + } + if (query != null && query.length() > 0) { + + // Remember this search query to later show as a suggestion + suggestions.saveRecentQuery(query, null); + return query; + + } + return null; + + } + + @OptionsItem(resName = "action_refresh") + protected void refreshSearch() { + if (lastUsedSite instanceof WebsearchSetting) { + // Start a browser page directly to the requested search results + WebsearchSetting websearch = (WebsearchSetting) lastUsedSite; + startActivity(new Intent(Intent.ACTION_VIEW, + Uri.parse(String.format(websearch.getBaseUrl(), lastUsedQuery)))); + } else if (lastUsedSite instanceof SearchSite) { + // Ask the resutls fragment to start a search for the specified query + fragmentResults.startSearch(lastUsedQuery, (SearchSite) lastUsedSite); + } + } + + @OptionsItem(resName = "action_downloadsearch") + protected void downloadSearchModule() { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.transdroid.org/latest-search"))); + } + +} diff --git a/core/src/org/transdroid/core/gui/SearchHistoryProvider.java b/core/src/org/transdroid/core/gui/search/SearchHistoryProvider.java similarity index 91% rename from core/src/org/transdroid/core/gui/SearchHistoryProvider.java rename to core/src/org/transdroid/core/gui/search/SearchHistoryProvider.java index 4a85ebfa..fc270f22 100644 --- a/core/src/org/transdroid/core/gui/SearchHistoryProvider.java +++ b/core/src/org/transdroid/core/gui/search/SearchHistoryProvider.java @@ -1,4 +1,4 @@ -package org.transdroid.core.gui; +package org.transdroid.core.gui.search; import android.content.Context; import android.content.SearchRecentSuggestionsProvider; @@ -10,7 +10,7 @@ import android.provider.SearchRecentSuggestions; */ public class SearchHistoryProvider extends SearchRecentSuggestionsProvider { - public final static String AUTHORITY = "org.transdroid.core.gui.SearchHistoryProvider"; + public final static String AUTHORITY = "org.transdroid.core.gui.search.SearchHistoryProvider"; public final static int MODE = DATABASE_MODE_QUERIES; public SearchHistoryProvider() { diff --git a/core/src/org/transdroid/core/gui/search/SearchResultView.java b/core/src/org/transdroid/core/gui/search/SearchResultView.java new file mode 100644 index 00000000..1fb7e4a8 --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchResultView.java @@ -0,0 +1,40 @@ +package org.transdroid.core.gui.search; + +import org.androidannotations.annotations.EViewGroup; +import org.androidannotations.annotations.ViewById; +import org.transdroid.core.R; +import org.transdroid.core.app.search.SearchResult; + +import android.content.Context; +import android.text.format.DateUtils; +import android.widget.TextView; +import fr.marvinlabs.widget.CheckableRelativeLayout; + +/** + * View that represents a {@link SearchResult} object from an in-app search + * @author Eric Kok + */ +@EViewGroup(resName = "list_item_searchresult") +public class SearchResultView extends CheckableRelativeLayout { + + // Views + @ViewById + protected TextView nameText, seedersText, leechersText, sizeText, dateText; + + public SearchResultView(Context context) { + super(context); + } + + public void bind(SearchResult result) { + + nameText.setText(result.getName()); + sizeText.setText(result.getSize()); + dateText.setText(result.getAddedOn() == null ? "" : DateUtils.getRelativeDateTimeString(getContext(), result + .getAddedOn().getTime(), DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, + DateUtils.FORMAT_ABBREV_MONTH)); + seedersText.setText(getContext().getString(R.string.search_seeders, result.getSeeders())); + leechersText.setText(getContext().getString(R.string.search_leechers, result.getLeechers())); + + } + +} diff --git a/core/src/org/transdroid/core/gui/search/SearchResultsAdapter.java b/core/src/org/transdroid/core/gui/search/SearchResultsAdapter.java new file mode 100644 index 00000000..20b37678 --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchResultsAdapter.java @@ -0,0 +1,71 @@ +package org.transdroid.core.gui.search; + +import java.util.List; + +import org.androidannotations.annotations.EBean; +import org.androidannotations.annotations.RootContext; +import org.transdroid.core.app.search.SearchResult; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +/** + * Adapter that contains a list of {@link SearchResult}s. + * @author Eric Kok + */ +@EBean +public class SearchResultsAdapter extends BaseAdapter { + + private List results = null; + + @RootContext + protected Context context; + + /** + * Allows updating the search results, replacing the old data + * @param newRssfeeds The new list of search results + */ + public void update(List results) { + this.results = results; + notifyDataSetChanged(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public int getCount() { + if (results == null) + return 0; + return results.size(); + } + + @Override + public SearchResult getItem(int position) { + if (results == null) + return null; + return results.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + SearchResultView rssitemView; + if (convertView == null) { + rssitemView = SearchResultView_.build(context); + } else { + rssitemView = (SearchResultView) convertView; + } + rssitemView.bind(getItem(position)); + return rssitemView; + } + +} diff --git a/core/src/org/transdroid/core/gui/search/SearchResultsFragment.java b/core/src/org/transdroid/core/gui/search/SearchResultsFragment.java new file mode 100644 index 00000000..4f7f6cf1 --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchResultsFragment.java @@ -0,0 +1,168 @@ +package org.transdroid.core.gui.search; + +import java.util.ArrayList; +import java.util.List; + +import org.androidannotations.annotations.AfterViews; +import org.androidannotations.annotations.Background; +import org.androidannotations.annotations.Bean; +import org.androidannotations.annotations.EFragment; +import org.androidannotations.annotations.InstanceState; +import org.androidannotations.annotations.ItemClick; +import org.androidannotations.annotations.UiThread; +import org.androidannotations.annotations.ViewById; +import org.transdroid.core.R; +import org.transdroid.core.app.search.SearchHelper; +import org.transdroid.core.app.search.SearchHelper.SearchSortOrder; +import org.transdroid.core.app.search.SearchResult; +import org.transdroid.core.app.search.SearchSite; +import org.transdroid.core.gui.navigation.SelectionManagerMode; + +import android.content.Intent; +import android.net.Uri; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.actionbarsherlock.app.SherlockFragment; +import com.actionbarsherlock.view.ActionMode; +import com.actionbarsherlock.view.Menu; +import com.actionbarsherlock.view.MenuItem; +import com.actionbarsherlock.view.SherlockListView; +import com.actionbarsherlock.view.SherlockListView.MultiChoiceModeListenerCompat; + +/** + * Fragment that lists the items in a specific RSS feed + * @author Eric Kok + */ +@EFragment(resName = "fragment_searchresults") +public class SearchResultsFragment extends SherlockFragment { + + @InstanceState + protected ArrayList results = null; + @Bean + protected SearchHelper searchHelper; + + // Views + @ViewById(resName = "searchresults_list") + protected SherlockListView resultsList; + @Bean + protected SearchResultsAdapter resultsAdapter; + @ViewById + protected TextView emptyText; + @ViewById + protected ProgressBar loadingProgress; + + @AfterViews + protected void init() { + + // Set up the list adapter, which allows multi-select + resultsList.setAdapter(resultsAdapter); + resultsList.setMultiChoiceModeListener(onItemsSelected); + if (results != null) + showResults(); + + } + + public void startSearch(String query, SearchSite site) { + loadingProgress.setVisibility(View.VISIBLE); + resultsList.setVisibility(View.GONE); + emptyText.setVisibility(View.GONE); + performSearch(query, site); + } + + @Background + protected void performSearch(String query, SearchSite site) { + results = searchHelper.search(query, site, SearchSortOrder.BySeeders); + showResults(); + } + + @UiThread + protected void showResults() { + loadingProgress.setVisibility(View.GONE); + if (results == null || results.size() == 0) { + resultsList.setVisibility(View.GONE); + emptyText.setVisibility(View.VISIBLE); + return; + } + resultsAdapter.update(results); + resultsList.setVisibility(View.VISIBLE); + emptyText.setVisibility(View.GONE); + } + + @ItemClick(resName = "searchresults_list") + protected void onItemClicked(SearchResult item) { + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(item.getTorrentUrl())); + i.putExtra("TORRENT_TITLE", item.getName()); + startActivity(i); + } + + private MultiChoiceModeListenerCompat onItemsSelected = new MultiChoiceModeListenerCompat() { + + SelectionManagerMode selectionManagerMode; + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + // Show contextual action bar to add items in batch mode + mode.getMenuInflater().inflate(R.menu.fragment_searchresults_cab, menu); + selectionManagerMode = new SelectionManagerMode(resultsList, R.plurals.search_resutlsselected); + selectionManagerMode.onCreateActionMode(mode, menu); + return true; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return selectionManagerMode.onPrepareActionMode(mode, menu); + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + + // Get checked torrents + List checked = new ArrayList(); + for (int i = 0; i < resultsList.getCheckedItemPositions().size(); i++) { + if (resultsList.getCheckedItemPositions().valueAt(i)) + checked.add(resultsAdapter.getItem(resultsList.getCheckedItemPositions().keyAt(i))); + } + + int itemId = item.getItemId(); + if (itemId == R.id.action_addall) { + // Start an Intent that adds multiple items at once, by supplying the urls and titles as string array + // extras and setting the Intent action to ADD_MULTIPLE + Intent intent = new Intent("org.transdroid.ADD_MULTIPLE"); + String[] urls = new String[checked.size()]; + String[] titles = new String[checked.size()]; + for (int i = 0; i < checked.size(); i++) { + urls[i] = checked.get(i).getTorrentUrl(); + titles[i] = checked.get(i).getName(); + } + intent.putExtra("TORRENT_URLS", urls); + intent.putExtra("TORRENT_TITLES", titles); + startActivity(intent); + mode.finish(); + return true; + } else if (itemId == R.id.action_showdetails) { + SearchResult first = checked.get(0); + // Open the torrent's web page in the browser + Toast.makeText(getActivity(), getString(R.string.search_openingdetails, first), Toast.LENGTH_LONG) + .show(); + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(first.getDetailsUrl()))); + return true; + } else { + return false; + } + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + selectionManagerMode.onItemCheckedStateChanged(mode, position, id, checked); + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + selectionManagerMode.onDestroyActionMode(mode); + } + + }; + +} diff --git a/core/src/org/transdroid/core/gui/search/SearchSetting.java b/core/src/org/transdroid/core/gui/search/SearchSetting.java new file mode 100644 index 00000000..b0fce14d --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchSetting.java @@ -0,0 +1,19 @@ +package org.transdroid.core.gui.search; + +import org.transdroid.core.gui.lists.SimpleListItem; + +public interface SearchSetting extends SimpleListItem { + + /** + * Should return a unique key for this search setting, so that it can be compared (using equals()) to other settings. + * @return A unique string identifying this search setting + */ + public String getKey(); + + /** + * Should return an URL (which may still be abstract and not the actual search URL) specific to the search site + * @return A clean URL directing to the search site, to, for example, get the favicon of the site + */ + public String getBaseUrl(); + +} diff --git a/core/src/org/transdroid/core/gui/search/SearchSettingSelectionView.java b/core/src/org/transdroid/core/gui/search/SearchSettingSelectionView.java new file mode 100644 index 00000000..0749399a --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchSettingSelectionView.java @@ -0,0 +1,28 @@ +package org.transdroid.core.gui.search; + +import org.androidannotations.annotations.EViewGroup; +import org.androidannotations.annotations.ViewById; + +import android.content.Context; +import android.widget.FrameLayout; +import android.widget.TextView; + +/** + * View that shows, as part of the action bar spinner, which {@link SearchSetting} is currently chosen. + * @author Eric Kok + */ +@EViewGroup(resName = "actionbar_searchsite") +public class SearchSettingSelectionView extends FrameLayout { + + @ViewById + protected TextView searchsiteText; + + public SearchSettingSelectionView(Context context) { + super(context); + } + + public void bind(SearchSetting searchSettingItem) { + searchsiteText.setText(searchSettingItem.getName()); + } + +} diff --git a/core/src/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java b/core/src/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java new file mode 100644 index 00000000..525d6b25 --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchSettingsDropDownAdapter.java @@ -0,0 +1,43 @@ +package org.transdroid.core.gui.search; + +import java.util.List; + +import org.transdroid.core.gui.lists.SimpleListItem; +import org.transdroid.core.gui.navigation.FilterListItemAdapter; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +/** + * List adapter that holds search settings, that is, web searches and in-app search sites, displayed as content to a + * Spinner instead of a ListView. + * @author Eric Kok + */ +public class SearchSettingsDropDownAdapter extends FilterListItemAdapter { + + private final Context context; + protected SearchSettingSelectionView searchSettingView = null; + + public SearchSettingsDropDownAdapter(Context context, List items) { + super(context, items); + this.context = context; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + // This returns the item to show in the action bar spinner + if (searchSettingView == null) { + searchSettingView = SearchSettingSelectionView_.build(context); + } + searchSettingView.bind((SearchSetting) getItem(position)); + return searchSettingView; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + // This returns the item to show in the drop down list + return super.getView(position, convertView, parent); + } + +} diff --git a/core/src/org/transdroid/core/gui/search/SearchSiteView.java b/core/src/org/transdroid/core/gui/search/SearchSiteView.java new file mode 100644 index 00000000..e28581b1 --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchSiteView.java @@ -0,0 +1,49 @@ +package org.transdroid.core.gui.search; + +import org.androidannotations.annotations.Bean; +import org.androidannotations.annotations.EViewGroup; +import org.androidannotations.annotations.ViewById; +import org.transdroid.core.app.settings.RssfeedSetting; +import org.transdroid.core.gui.navigation.NavigationHelper; + +import android.content.Context; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * View that represents some {@link RssfeedSetting} object and displays name as well as loads a favicon for the feed's + * site and can load how many new items are available. + * @author Eric Kok + */ +@EViewGroup(resName = "list_item_searchsite") +public class SearchSiteView extends LinearLayout { + + private static final String GETFVO_URL = "http://g.etfv.co/%1$s"; + + @Bean + protected NavigationHelper navigationHelper; + + // Views + @ViewById + protected ImageView faviconImage; + @ViewById + protected TextView nameText; + + public SearchSiteView(Context context) { + super(context); + } + + public void bind(SearchSetting rssfeedLoader) { + + // Show the RSS feed name and either a loading indicator or the number of new items + nameText.setText(rssfeedLoader.getName()); + // Clear and then asynchronously load the site's favicon + // Uses the g.etfv.co service to resolve the favicon of any URL + faviconImage.setImageDrawable(null); + navigationHelper.getImageCache().displayImage(String.format(GETFVO_URL, rssfeedLoader.getBaseUrl()), + faviconImage); + + } + +} diff --git a/core/src/org/transdroid/core/gui/search/SearchSitesAdapter.java b/core/src/org/transdroid/core/gui/search/SearchSitesAdapter.java new file mode 100644 index 00000000..eabf68b0 --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SearchSitesAdapter.java @@ -0,0 +1,72 @@ +package org.transdroid.core.gui.search; + +import java.util.List; + +import org.androidannotations.annotations.EBean; +import org.androidannotations.annotations.RootContext; +import org.transdroid.core.app.search.SearchSite; +import org.transdroid.core.app.settings.WebsearchSetting; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +/** + * Adapter that contains a list of {@link SearchSetting}s, either {@link SearchSite} or {@link WebsearchSetting}. + * @author Eric Kok + */ +@EBean +public class SearchSitesAdapter extends BaseAdapter { + + private List sites = null; + + @RootContext + protected Context context; + + /** + * Allows updating the full internal list of sites at once, replacing the old list + * @param sites The new list of search sites, either in-app or web search settings + */ + public void update(List sites) { + this.sites = sites; + notifyDataSetChanged(); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public int getCount() { + if (sites == null) + return 0; + return sites.size(); + } + + @Override + public SearchSetting getItem(int position) { + if (sites == null) + return null; + return sites.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + SearchSiteView rssfeedView; + if (convertView == null) { + rssfeedView = SearchSiteView_.build(context); + } else { + rssfeedView = (SearchSiteView) convertView; + } + rssfeedView.bind(getItem(position)); + return rssfeedView; + } + +} diff --git a/core/src/org/transdroid/core/gui/search/SendIntentHelper.java b/core/src/org/transdroid/core/gui/search/SendIntentHelper.java new file mode 100644 index 00000000..a82dfc46 --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/SendIntentHelper.java @@ -0,0 +1,61 @@ +package org.transdroid.core.gui.search; + +import android.content.Intent; + +/** + * Used to clean up text as received from a generic ACTION_SEND intent. This class is highly custom-based for known + * applications, i.e. the EXTRA_TEXT send by some known applications. + * @author Eric Kok + */ +public class SendIntentHelper { + + private static final String SOUNDHOUND1 = "Just used #SoundHound to find "; + private static final String SOUNDHOUND1_END = " http://"; + private static final String SHAZAM = "I just used Shazam to discover "; + private static final String SHAZAM_END = ". http://"; + private static final String YOUTUBE_ID = "Watch \""; + private static final String YOUTUBE_START = "\""; + private static final String YOUTUBE_END = "\""; + + /** + * Cleans a SEND intent text string by removing irrelevant parts, so that the remaining text can be used as search + * string. Typically deals with specific known applications such as Shazam and YouTube's SEND intents. + * @param intent The original SEND intent that was received + * @return A cleaned string to be used as search query + */ + public static String cleanUpText(Intent intent) { + + if (intent == null || !intent.hasExtra(Intent.EXTRA_TEXT)) { + return null; + } + String text = intent.getStringExtra(Intent.EXTRA_TEXT); + try { + + // Soundhound song/artist share + if (text.startsWith(SOUNDHOUND1)) { + return cutOut(text, SOUNDHOUND1, SOUNDHOUND1_END).replace(" by ", " "); + } + // Shazam song share + if (text.startsWith(SHAZAM)) { + return cutOut(text, SHAZAM, SHAZAM_END).replace(" by ", " "); + } + // YouTube app share (stores title in EXTRA_SUBJECT) + if (intent.hasExtra(Intent.EXTRA_SUBJECT)) { + String subject = intent.getStringExtra(Intent.EXTRA_SUBJECT); + if (subject.startsWith(YOUTUBE_ID)) { + return cutOut(subject, YOUTUBE_START, YOUTUBE_END); + } + } + + } catch (Exception e) { + // Ignore any errors in parsing; just return the raw text + } + return text; + } + + private static String cutOut(String text, String start, String end) { + int startAt = text.indexOf(start) + start.length(); + return text.substring(startAt, text.indexOf(end, startAt)); + } + +} diff --git a/core/src/org/transdroid/core/gui/search/TorrentSearchHistoryProvider.java b/core/src/org/transdroid/core/gui/search/TorrentSearchHistoryProvider.java new file mode 100644 index 00000000..548e0d34 --- /dev/null +++ b/core/src/org/transdroid/core/gui/search/TorrentSearchHistoryProvider.java @@ -0,0 +1,26 @@ +package org.transdroid.core.gui.search; + +import android.content.Context; +import android.content.SearchRecentSuggestionsProvider; +import android.provider.SearchRecentSuggestions; + +/** + * Provides a wrapper for the {@link SearchRecentSuggestionsProvider} to show the last torrent searches to the user. + * @author Eric Kok + */ +public class TorrentSearchHistoryProvider extends SearchRecentSuggestionsProvider { + + public static final String AUTHORITY = "org.transdroid.core.gui.search.TorrentSearchHistoryProvider"; + public static final int MODE = DATABASE_MODE_QUERIES; + + public TorrentSearchHistoryProvider() { + super(); + setupSuggestions(AUTHORITY, MODE); + } + + public static void clearHistory(Context context) { + SearchRecentSuggestions suggestions = new SearchRecentSuggestions(context, + TorrentSearchHistoryProvider.AUTHORITY, TorrentSearchHistoryProvider.MODE); + suggestions.clearHistory(); + } +} diff --git a/full/AndroidManifest.xml b/full/AndroidManifest.xml index 8df10586..6ac1d2f4 100644 --- a/full/AndroidManifest.xml +++ b/full/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="2" + android:versionName="2.0-alpha2" > - + android:value="org.transdroid.core.gui.search.SearchActivity_" /> + android:theme="@style/TransdroidTheme" + android:launchMode="singleTask" > @@ -185,14 +185,18 @@ - + - + + - - - \ No newline at end of file diff --git a/lite/AndroidManifest.xml b/lite/AndroidManifest.xml index 8eaffe64..068fd8dd 100644 --- a/lite/AndroidManifest.xml +++ b/lite/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="2" + android:versionName="2.0-alpha2" >