Merge branch 'fix-323--improved-search' into 'master'

Search as the user types

Fixes #323.

This does away with the separate `SearchResult` and instead applies the search to the currently viewed tab on the main screen (Available, Installed, Updates). When filtering the Available list, it filters the currently selected category.

Note however that there are still times when the old style `SearchDialog` will be shown over the top of the action bar rather than the `SearchView` within the action bar. These times include:
 * When a user with a hardware keyboard starts typing from the main screen.
 * On older devices with a "search" hardware button.
 * Probably some other cases (I think when there is not enough screen real estate, but haven't seen that happen).

In cases where this dialog is shown, filtering the lists as you type does not seem to be an option. I tried to figure out how to do that, but failed. If someone else figures it out, that would be great. However, when the search is submitted, it will hide the `SearchDialog` and populate the `SearchView`, focus it, and apply the search appropriately.

There is a script in the `F-Droid/tools/` subdirectory which will consecutively send various intents to F-Droid relating to search. This includes Play, market, Amazon search links. For good measure, I also made it send intents to do with viewing app details. This should probably be made into a proper instrumented test at some point, but I didn't have the time to figure out how to do that. Maybe a project for future @pserwylo.

One unknown is the performance implications. There is no problems on my Nexus 4 with Android 5.0. My Chinese/ebay/$30/Android 2.3.4 device seems good enough too.

See merge request !177
This commit is contained in:
Peter Serwylo 2015-12-16 11:50:30 +00:00
commit 1a5b60f654
11 changed files with 338 additions and 88 deletions

View File

@ -100,7 +100,7 @@
<meta-data <meta-data
android:name="android.app.default_searchable" android:name="android.app.default_searchable"
android:value=".SearchResults" /> android:value=".FDroid" />
<activity <activity
android:name=".FDroid" android:name=".FDroid"
@ -224,6 +224,14 @@
<data android:path="/store/search" /> <data android:path="/store/search" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
<!-- Handle NFC tags detected from outside our application --> <!-- Handle NFC tags detected from outside our application -->
<intent-filter> <intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" /> <action android:name="android.nfc.action.NDEF_DISCOVERED" />
@ -388,25 +396,6 @@
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<activity
android:name=".SearchResults"
android:label="@string/search_results"
android:exported="true"
android:launchMode="singleTop"
android:parentActivityName=".FDroid"
android:configChanges="layoutDirection|locale" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".FDroid" />
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
</activity>
<receiver android:name=".receiver.StartupReceiver" > <receiver android:name=".receiver.StartupReceiver" >
<intent-filter> <intent-filter>

View File

@ -4,27 +4,35 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<Spinner <RelativeLayout
android:id="@+id/category_spinner"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:id="@+id/category_wrapper"
android:layout_marginRight="8dp" android:layout_alignParentTop="true">
android:paddingBottom="1dp" />
<View <Spinner
android:layout_width="match_parent" android:id="@+id/category_spinner"
android:layout_height="2dp" android:layout_width="match_parent"
android:layout_alignBottom="@id/category_spinner" android:layout_height="48dp"
android:background="@color/fdroid_green" /> android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:paddingBottom="1dp" />
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_alignBottom="@id/category_spinner"
android:background="@color/fdroid_green" />
</RelativeLayout>
<ListView <ListView
style="@style/AppList" style="@style/AppList"
android:layout_below="@id/category_spinner" /> android:layout_below="@id/category_wrapper" />
<TextView <TextView
style="@style/AppListEmptyText" style="@style/AppListEmptyText"
android:layout_below="@id/category_spinner" android:layout_below="@id/category_wrapper"
android:text="@string/empty_available_app_list" /> android:text="@string/empty_available_app_list" />
</RelativeLayout> </RelativeLayout>

View File

@ -29,10 +29,12 @@ import android.content.res.Configuration;
import android.database.ContentObserver; import android.database.ContentObserver;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.MenuItemCompat; import android.support.v4.view.MenuItemCompat;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.SearchView; import android.support.v7.widget.SearchView;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -43,6 +45,7 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.fdroid.fdroid.compat.TabManager; import org.fdroid.fdroid.compat.TabManager;
import org.fdroid.fdroid.compat.UriCompat;
import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.NewRepoConfig; import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity; import org.fdroid.fdroid.privileged.install.InstallExtensionDialogActivity;
@ -50,7 +53,7 @@ import org.fdroid.fdroid.views.AppListFragmentPagerAdapter;
import org.fdroid.fdroid.views.ManageReposActivity; import org.fdroid.fdroid.views.ManageReposActivity;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity; import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
public class FDroid extends ActionBarActivity { public class FDroid extends AppCompatActivity implements SearchView.OnQueryTextListener {
private static final String TAG = "FDroid"; private static final String TAG = "FDroid";
@ -66,8 +69,17 @@ public class FDroid extends ActionBarActivity {
private ViewPager viewPager; private ViewPager viewPager;
@Nullable
private TabManager tabManager; private TabManager tabManager;
private AppListFragmentPagerAdapter adapter;
@Nullable
private MenuItem searchMenuItem;
@Nullable
private String pendingSearchQuery;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -84,10 +96,7 @@ public class FDroid extends ActionBarActivity {
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
Intent intent = getIntent(); Intent intent = getIntent();
handleSearchOrAppViewIntent(intent);
// If the intent can be handled via AppDetails or SearchResults, it
// will call finish() and the rest of the code won't execute
handleIntent(intent);
if (intent.hasExtra(EXTRA_TAB_UPDATE)) { if (intent.hasExtra(EXTRA_TAB_UPDATE)) {
boolean showUpdateTab = intent.getBooleanExtra(EXTRA_TAB_UPDATE, false); boolean showUpdateTab = intent.getBooleanExtra(EXTRA_TAB_UPDATE, false);
@ -109,6 +118,18 @@ public class FDroid extends ActionBarActivity {
// } // }
} }
private void performSearch(String query) {
if (searchMenuItem == null) {
// Store this for later when we do actually have a search menu ready to use.
pendingSearchQuery = query;
return;
}
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchMenuItem);
MenuItemCompat.expandActionView(searchMenuItem);
searchView.setQuery(query, true);
}
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
@ -117,11 +138,24 @@ public class FDroid extends ActionBarActivity {
checkForAddRepoIntent(); checkForAddRepoIntent();
} }
private void handleIntent(Intent intent) { @Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handleSearchOrAppViewIntent(intent);
}
private void handleSearchOrAppViewIntent(Intent intent) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
String query = intent.getStringExtra(SearchManager.QUERY);
performSearch(query);
return;
}
final Uri data = intent.getData(); final Uri data = intent.getData();
if (data == null) { if (data == null) {
return; return;
} }
final String scheme = data.getScheme(); final String scheme = data.getScheme();
final String path = data.getPath(); final String path = data.getPath();
String appId = null; String appId = null;
@ -133,11 +167,14 @@ public class FDroid extends ActionBarActivity {
} }
switch (host) { switch (host) {
case "f-droid.org": case "f-droid.org":
// http://f-droid.org/app/app.id
if (path.startsWith("/repository/browse")) { if (path.startsWith("/repository/browse")) {
// http://f-droid.org/repository/browse?fdfilter=search+query
query = UriCompat.getQueryParameter(data, "fdfilter");
// http://f-droid.org/repository/browse?fdid=app.id // http://f-droid.org/repository/browse?fdid=app.id
appId = data.getQueryParameter("fdid"); appId = UriCompat.getQueryParameter(data, "fdid");
} else if (path.startsWith("/app")) { } else if (path.startsWith("/app")) {
// http://f-droid.org/app/app.id
appId = data.getLastPathSegment(); appId = data.getLastPathSegment();
if ("app".equals(appId)) { if ("app".equals(appId)) {
appId = null; appId = null;
@ -146,28 +183,28 @@ public class FDroid extends ActionBarActivity {
break; break;
case "details": case "details":
// market://details?id=app.id // market://details?id=app.id
appId = data.getQueryParameter("id"); appId = UriCompat.getQueryParameter(data, "id");
break; break;
case "search": case "search":
// market://search?q=query // market://search?q=query
query = data.getQueryParameter("q"); query = UriCompat.getQueryParameter(data, "q");
break; break;
case "play.google.com": case "play.google.com":
if (path.startsWith("/store/apps/details")) { if (path.startsWith("/store/apps/details")) {
// http://play.google.com/store/apps/details?id=app.id // http://play.google.com/store/apps/details?id=app.id
appId = data.getQueryParameter("id"); appId = UriCompat.getQueryParameter(data, "id");
} else if (path.startsWith("/store/search")) { } else if (path.startsWith("/store/search")) {
// http://play.google.com/store/search?q=foo // http://play.google.com/store/search?q=foo
query = data.getQueryParameter("q"); query = UriCompat.getQueryParameter(data, "q");
} }
break; break;
case "apps": case "apps":
case "amazon.com": case "amazon.com":
case "www.amazon.com": case "www.amazon.com":
// amzn://apps/android?p=app.id // amzn://apps/android?p=app.id
// http://amazon.com/gp/mas/dl/android?p=app.id // http://amazon.com/gp/mas/dl/android?s=app.id
appId = data.getQueryParameter("p"); appId = UriCompat.getQueryParameter(data, "p");
query = data.getQueryParameter("s"); query = UriCompat.getQueryParameter(data, "s");
break; break;
} }
} else if ("fdroid.app".equals(scheme)) { } else if ("fdroid.app".equals(scheme)) {
@ -188,20 +225,14 @@ public class FDroid extends ActionBarActivity {
query = query.split(":")[1]; query = query.split(":")[1];
} }
Intent call = null;
if (!TextUtils.isEmpty(appId)) { if (!TextUtils.isEmpty(appId)) {
Utils.debugLog(TAG, "FDroid launched via app link for '" + appId + "'"); Utils.debugLog(TAG, "FDroid launched via app link for '" + appId + "'");
call = new Intent(this, AppDetails.class); Intent intentToInvoke = new Intent(this, AppDetails.class);
call.putExtra(AppDetails.EXTRA_APPID, appId); intentToInvoke.putExtra(AppDetails.EXTRA_APPID, appId);
startActivity(intentToInvoke);
} else if (!TextUtils.isEmpty(query)) { } else if (!TextUtils.isEmpty(query)) {
Utils.debugLog(TAG, "FDroid launched via search link for '" + query + "'"); Utils.debugLog(TAG, "FDroid launched via search link for '" + query + "'");
call = new Intent(this, SearchResults.class); performSearch(query);
call.setAction(Intent.ACTION_SEARCH);
call.putExtra(SearchManager.QUERY, query);
}
if (call != null) {
startActivity(call);
finish();
} }
} }
@ -243,11 +274,19 @@ public class FDroid extends ActionBarActivity {
} }
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
MenuItem searchItem = menu.findItem(R.id.action_search); searchMenuItem = menu.findItem(R.id.action_search);
SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchItem); SearchView searchView = (SearchView) MenuItemCompat.getActionView(searchMenuItem);
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName())); searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
// LayoutParams.MATCH_PARENT does not work, use a big value instead // LayoutParams.MATCH_PARENT does not work, use a big value instead
searchView.setMaxWidth(1000000); searchView.setMaxWidth(1000000);
searchView.setOnQueryTextListener(this);
// If we were asked to execute a search before getting around to building the options
// menu, then we should deal with that now that the options menu is all sorted out.
if (pendingSearchQuery != null) {
performSearch(pendingSearchQuery);
pendingSearchQuery = null;
}
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@ -335,8 +374,8 @@ public class FDroid extends ActionBarActivity {
private void createViews() { private void createViews() {
viewPager = (ViewPager) findViewById(R.id.main_pager); viewPager = (ViewPager) findViewById(R.id.main_pager);
AppListFragmentPagerAdapter viewPagerAdapter = new AppListFragmentPagerAdapter(this); adapter = new AppListFragmentPagerAdapter(this);
viewPager.setAdapter(viewPagerAdapter); viewPager.setAdapter(adapter);
viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override @Override
public void onPageSelected(int position) { public void onPageSelected(int position) {
@ -345,6 +384,7 @@ public class FDroid extends ActionBarActivity {
}); });
} }
@NonNull
private TabManager getTabManager() { private TabManager getTabManager() {
if (tabManager == null) { if (tabManager == null) {
tabManager = new TabManager(this, viewPager); tabManager = new TabManager(this, viewPager);
@ -363,6 +403,19 @@ public class FDroid extends ActionBarActivity {
nMgr.cancel(id); nMgr.cancel(id);
} }
@Override
public boolean onQueryTextSubmit(String query) {
// Do nothing, because we respond to the query being changed as it is updated
// via onQueryTextChange(...)
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
adapter.updateSearchQuery(newText);
return true;
}
private class AppObserver extends ContentObserver { private class AppObserver extends ContentObserver {
AppObserver() { AppObserver() {

View File

@ -0,0 +1,21 @@
package org.fdroid.fdroid.compat;
import android.net.Uri;
import android.os.Build;
public class UriCompat {
/**
* Uri#getQueryParameter(String) has the following warning:
*
* > Prior to Ice Cream Sandwich, this decoded the '+' character as '+' rather than ' '.
*/
public static String getQueryParameter(Uri uri, String key) {
String value = uri.getQueryParameter(key);
if (value != null && Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
value = value.replaceAll("\\+", " ");
}
return value;
}
}

View File

@ -425,6 +425,8 @@ public class AppProvider extends FDroidProvider {
private static final String PATH_INSTALLED = "installed"; private static final String PATH_INSTALLED = "installed";
private static final String PATH_CAN_UPDATE = "canUpdate"; private static final String PATH_CAN_UPDATE = "canUpdate";
private static final String PATH_SEARCH = "search"; private static final String PATH_SEARCH = "search";
private static final String PATH_SEARCH_INSTALLED = "searchInstalled";
private static final String PATH_SEARCH_CAN_UPDATE = "searchCanUpdate";
private static final String PATH_SEARCH_REPO = "searchRepo"; private static final String PATH_SEARCH_REPO = "searchRepo";
private static final String PATH_NO_APKS = "noApks"; private static final String PATH_NO_APKS = "noApks";
private static final String PATH_APPS = "apps"; private static final String PATH_APPS = "apps";
@ -435,18 +437,20 @@ public class AppProvider extends FDroidProvider {
private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex"; private static final String PATH_CALC_APP_DETAILS_FROM_INDEX = "calcDetailsFromIndex";
private static final String PATH_REPO = "repo"; private static final String PATH_REPO = "repo";
private static final int CAN_UPDATE = CODE_SINGLE + 1; private static final int CAN_UPDATE = CODE_SINGLE + 1;
private static final int INSTALLED = CAN_UPDATE + 1; private static final int INSTALLED = CAN_UPDATE + 1;
private static final int SEARCH = INSTALLED + 1; private static final int SEARCH = INSTALLED + 1;
private static final int NO_APKS = SEARCH + 1; private static final int NO_APKS = SEARCH + 1;
private static final int APPS = NO_APKS + 1; private static final int APPS = NO_APKS + 1;
private static final int RECENTLY_UPDATED = APPS + 1; private static final int RECENTLY_UPDATED = APPS + 1;
private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1; private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1;
private static final int CATEGORY = NEWLY_ADDED + 1; private static final int CATEGORY = NEWLY_ADDED + 1;
private static final int IGNORED = CATEGORY + 1; private static final int IGNORED = CATEGORY + 1;
private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1; private static final int CALC_APP_DETAILS_FROM_INDEX = IGNORED + 1;
private static final int REPO = CALC_APP_DETAILS_FROM_INDEX + 1; private static final int REPO = CALC_APP_DETAILS_FROM_INDEX + 1;
private static final int SEARCH_REPO = REPO + 1; private static final int SEARCH_REPO = REPO + 1;
private static final int SEARCH_INSTALLED = SEARCH_REPO + 1;
private static final int SEARCH_CAN_UPDATE = SEARCH_INSTALLED + 1;
static { static {
matcher.addURI(getAuthority(), null, CODE_LIST); matcher.addURI(getAuthority(), null, CODE_LIST);
@ -456,6 +460,8 @@ public class AppProvider extends FDroidProvider {
matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED); matcher.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED);
matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); matcher.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH); matcher.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH);
matcher.addURI(getAuthority(), PATH_SEARCH_INSTALLED + "/*", SEARCH_INSTALLED);
matcher.addURI(getAuthority(), PATH_SEARCH_CAN_UPDATE + "/*", SEARCH_CAN_UPDATE);
matcher.addURI(getAuthority(), PATH_SEARCH_REPO + "/*/*", SEARCH_REPO); matcher.addURI(getAuthority(), PATH_SEARCH_REPO + "/*/*", SEARCH_REPO);
matcher.addURI(getAuthority(), PATH_REPO + "/#", REPO); matcher.addURI(getAuthority(), PATH_REPO + "/#", REPO);
matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE); matcher.addURI(getAuthority(), PATH_CAN_UPDATE, CAN_UPDATE);
@ -536,7 +542,23 @@ public class AppProvider extends FDroidProvider {
public static Uri getSearchUri(String query) { public static Uri getSearchUri(String query) {
return getContentUri().buildUpon() return getContentUri().buildUpon()
.appendPath(PATH_SEARCH) .appendPath(PATH_SEARCH)
.appendPath(query) .appendEncodedPath(query)
.build();
}
public static Uri getSearchInstalledUri(String query) {
return getContentUri()
.buildUpon()
.appendPath(PATH_SEARCH_INSTALLED)
.appendEncodedPath(query)
.build();
}
public static Uri getSearchCanUpdateUri(String query) {
return getContentUri()
.buildUpon()
.appendPath(PATH_SEARCH_CAN_UPDATE)
.appendEncodedPath(query)
.build(); .build();
} }
@ -544,7 +566,7 @@ public class AppProvider extends FDroidProvider {
return getContentUri().buildUpon() return getContentUri().buildUpon()
.appendPath(PATH_SEARCH_REPO) .appendPath(PATH_SEARCH_REPO)
.appendPath(repo.id + "") .appendPath(repo.id + "")
.appendPath(query) .appendEncodedPath(query)
.build(); .build();
} }
@ -597,7 +619,7 @@ public class AppProvider extends FDroidProvider {
getTableName() + ".description", getTableName() + ".description",
}; };
// Remove duplicates, surround in % for case insensitive searching // Remove duplicates, surround in % for wildcard searching
final Set<String> keywordSet = new HashSet<>(Arrays.asList(query.split("\\s"))); final Set<String> keywordSet = new HashSet<>(Arrays.asList(query.split("\\s")));
final String[] keywords = new String[keywordSet.size()]; final String[] keywords = new String[keywordSet.size()];
int iKeyword = 0; int iKeyword = 0;
@ -735,6 +757,16 @@ public class AppProvider extends FDroidProvider {
includeSwap = false; includeSwap = false;
break; break;
case SEARCH_INSTALLED:
selection = querySearch(uri.getLastPathSegment()).add(queryInstalled());
includeSwap = false;
break;
case SEARCH_CAN_UPDATE:
selection = querySearch(uri.getLastPathSegment()).add(queryCanUpdate());
includeSwap = false;
break;
case SEARCH_REPO: case SEARCH_REPO:
selection = selection.add(querySearch(uri.getPathSegments().get(2))); selection = selection.add(querySearch(uri.getPathSegments().get(2)));
selection = selection.add(queryRepo(Long.parseLong(uri.getPathSegments().get(1)))); selection = selection.add(queryRepo(Long.parseLong(uri.getPathSegments().get(1))));

View File

@ -1,5 +1,7 @@
package org.fdroid.fdroid.views; package org.fdroid.fdroid.views;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentPagerAdapter;
@ -7,6 +9,7 @@ import org.fdroid.fdroid.FDroid;
import org.fdroid.fdroid.R; import org.fdroid.fdroid.R;
import org.fdroid.fdroid.compat.TabManager; import org.fdroid.fdroid.compat.TabManager;
import org.fdroid.fdroid.data.AppProvider; import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.views.fragments.AppListFragment;
import org.fdroid.fdroid.views.fragments.AvailableAppsFragment; import org.fdroid.fdroid.views.fragments.AvailableAppsFragment;
import org.fdroid.fdroid.views.fragments.CanUpdateAppsFragment; import org.fdroid.fdroid.views.fragments.CanUpdateAppsFragment;
import org.fdroid.fdroid.views.fragments.InstalledAppsFragment; import org.fdroid.fdroid.views.fragments.InstalledAppsFragment;
@ -17,11 +20,19 @@ import org.fdroid.fdroid.views.fragments.InstalledAppsFragment;
*/ */
public class AppListFragmentPagerAdapter extends FragmentPagerAdapter { public class AppListFragmentPagerAdapter extends FragmentPagerAdapter {
private final FDroid parent; @NonNull private final FDroid parent;
public AppListFragmentPagerAdapter(FDroid parent) { @NonNull private final AppListFragment availableFragment;
@NonNull private final AppListFragment installedFragment;
@NonNull private final AppListFragment canUpdateFragment;
public AppListFragmentPagerAdapter(@NonNull FDroid parent) {
super(parent.getSupportFragmentManager()); super(parent.getSupportFragmentManager());
this.parent = parent; this.parent = parent;
availableFragment = new AvailableAppsFragment();
installedFragment = new InstalledAppsFragment();
canUpdateFragment = new CanUpdateAppsFragment();
} }
private String getInstalledTabTitle() { private String getInstalledTabTitle() {
@ -34,15 +45,21 @@ public class AppListFragmentPagerAdapter extends FragmentPagerAdapter {
return parent.getString(R.string.tab_updates_count, updateCount); return parent.getString(R.string.tab_updates_count, updateCount);
} }
public void updateSearchQuery(@Nullable String query) {
availableFragment.updateSearchQuery(query);
installedFragment.updateSearchQuery(query);
canUpdateFragment.updateSearchQuery(query);
}
@Override @Override
public Fragment getItem(int i) { public Fragment getItem(int i) {
switch (i) { switch (i) {
case TabManager.INDEX_AVAILABLE: case TabManager.INDEX_AVAILABLE:
return new AvailableAppsFragment(); return availableFragment;
case TabManager.INDEX_INSTALLED: case TabManager.INDEX_INSTALLED:
return new InstalledAppsFragment(); return installedFragment;
default: default:
return new CanUpdateAppsFragment(); return canUpdateFragment;
} }
} }

View File

@ -6,10 +6,12 @@ import android.content.SharedPreferences;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.ListFragment; import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.widget.AdapterView; import android.widget.AdapterView;
@ -52,12 +54,36 @@ public abstract class AppListFragment extends ListFragment implements
protected AppListAdapter appAdapter; protected AppListAdapter appAdapter;
@Nullable private String searchQuery;
protected abstract AppListAdapter getAppListAdapter(); protected abstract AppListAdapter getAppListAdapter();
protected abstract String getFromTitle(); protected abstract String getFromTitle();
protected abstract Uri getDataUri(); protected abstract Uri getDataUri();
protected abstract Uri getDataUri(String query);
/**
* Subclasses can choose to do different things based on when a user begins searching.
* For example, the "Available" tab chooses to hide its category spinner to make it clear
* that it is searching all apps, not the current category.
* NOTE: This will get called <em>multiple</em> times, every time the user changes the
* search query.
*/
protected void onSearch() {
// Do nothing by default.
}
/**
* Alerts the child class that the user is no longer performing a search.
* This is triggered every time the search query is blank.
* @see AppListFragment#onSearch()
*/
protected void onSearchStopped() {
// Do nothing by default.
}
@Override @Override
public void onActivityCreated(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
@ -144,9 +170,30 @@ public abstract class AppListFragment extends ListFragment implements
@Override @Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) { public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = getDataUri(); Uri uri = updateSearchStatus() ? getDataUri(searchQuery) : getDataUri();
return new CursorLoader( return new CursorLoader(
getActivity(), uri, APP_PROJECTION, null, null, APP_SORT); getActivity(), uri, APP_PROJECTION, null, null, APP_SORT);
} }
/**
* Notifies the subclass via {@link AppListFragment#onSearch()} and {@link AppListFragment#onSearchStopped()}
* about whether or not a search is taking place.
* @return True if a user is searching.
*/
private boolean updateSearchStatus() {
if (TextUtils.isEmpty(searchQuery)) {
onSearchStopped();
return false;
} else {
onSearch();
return true;
}
}
public void updateSearchQuery(@Nullable String query) {
searchQuery = query;
if (isAdded()) {
getLoaderManager().restartLoader(0, null, this);
}
}
} }

View File

@ -8,6 +8,7 @@ import android.database.ContentObserver;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@ -37,6 +38,11 @@ public class AvailableAppsFragment extends AppListFragment implements
private static String defaultCategory; private static String defaultCategory;
private List<String> categories; private List<String> categories;
@Nullable
private View categoryWrapper;
@Nullable
private Spinner categorySpinner; private Spinner categorySpinner;
private String currentCategory; private String currentCategory;
private AppListAdapter adapter; private AppListAdapter adapter;
@ -147,6 +153,7 @@ public class AvailableAppsFragment extends AppListFragment implements
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.available_app_list, container, false); View view = inflater.inflate(R.layout.available_app_list, container, false);
categoryWrapper = view.findViewById(R.id.category_wrapper);
setupCategorySpinner((Spinner) view.findViewById(R.id.category_spinner)); setupCategorySpinner((Spinner) view.findViewById(R.id.category_spinner));
defaultCategory = AppProvider.Helper.getCategoryWhatsNew(getActivity()); defaultCategory = AppProvider.Helper.getCategoryWhatsNew(getActivity());
@ -164,6 +171,11 @@ public class AvailableAppsFragment extends AppListFragment implements
return AppProvider.getCategoryUri(currentCategory); return AppProvider.getCategoryUri(currentCategory);
} }
@Override
protected Uri getDataUri(String query) {
return AppProvider.getSearchUri(query);
}
private void setCurrentCategory(String category) { private void setCurrentCategory(String category) {
currentCategory = category; currentCategory = category;
Utils.debugLog(TAG, "Category '" + currentCategory + "' selected."); Utils.debugLog(TAG, "Category '" + currentCategory + "' selected.");
@ -175,15 +187,18 @@ public class AvailableAppsFragment extends AppListFragment implements
super.onResume(); super.onResume();
/* restore the saved Category Spinner position */ /* restore the saved Category Spinner position */
Activity activity = getActivity(); Activity activity = getActivity();
SharedPreferences p = activity.getSharedPreferences(PREFERENCES_FILE, SharedPreferences p = activity.getSharedPreferences(PREFERENCES_FILE, Context.MODE_PRIVATE);
Context.MODE_PRIVATE);
currentCategory = p.getString(CATEGORY_KEY, defaultCategory); currentCategory = p.getString(CATEGORY_KEY, defaultCategory);
for (int i = 0; i < categorySpinner.getCount(); i++) {
if (currentCategory.equals(categorySpinner.getItemAtPosition(i).toString())) { if (categorySpinner != null) {
categorySpinner.setSelection(i); for (int i = 0; i < categorySpinner.getCount(); i++) {
break; if (currentCategory.equals(categorySpinner.getItemAtPosition(i).toString())) {
categorySpinner.setSelection(i);
break;
}
} }
} }
setCurrentCategory(currentCategory); setCurrentCategory(currentCategory);
} }
@ -197,4 +212,18 @@ public class AvailableAppsFragment extends AppListFragment implements
e.putString(CATEGORY_KEY, currentCategory); e.putString(CATEGORY_KEY, currentCategory);
e.commit(); e.commit();
} }
@Override
protected void onSearch() {
if (categoryWrapper != null) {
categoryWrapper.setVisibility(View.GONE);
}
}
@Override
protected void onSearchStopped() {
if (categoryWrapper != null) {
categoryWrapper.setVisibility(View.VISIBLE);
}
}
} }

View File

@ -28,6 +28,10 @@ public class CanUpdateAppsFragment extends AppListFragment {
return AppProvider.getCanUpdateUri(); return AppProvider.getCanUpdateUri();
} }
protected Uri getDataUri(String query) {
return AppProvider.getSearchCanUpdateUri(query);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.can_update_app_list, container, false); return inflater.inflate(R.layout.can_update_app_list, container, false);

View File

@ -28,6 +28,10 @@ public class InstalledAppsFragment extends AppListFragment {
return AppProvider.getInstalledUri(); return AppProvider.getInstalledUri();
} }
protected Uri getDataUri(String query) {
return AppProvider.getSearchInstalledUri(query);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.installed_app_list, container, false); return inflater.inflate(R.layout.installed_app_list, container, false);

View File

@ -0,0 +1,46 @@
#!/bin/sh
echo "A helper script to send all of the various intents that F-droid should be able to handle via ADB."
echo "Use this to ensure that things which should trigger searches, do trigger searches, and those which should bring up the app details screen, do bring it up."
echo ""
function view {
DESCRIPTION=$1
DATA=$2
wait "$DESCRIPTION"
CMD="adb shell am start -a android.intent.action.VIEW -d $DATA"
$CMD
echo ""
sleep 1
}
function wait {
DESCRIPTION=$1
echo "$DESCRIPTION [Y/n]"
read -n 1 RESULT
# Lower case the result.
RESULT=`echo "$RESULT" | tr '[:upper:]' '[:lower:]'`
echo ""
if [ "$RESULT" != 'y' ]; then
exit;
fi
}
APP_TO_SHOW=org.fdroid.fdroid
SEARCH_QUERY=book+reader
view "Search for '$SEARCH_QUERY' (fdroid web)" http://f-droid.org/repository/browse?fdfilter=$SEARCH_QUERY
view "Search for '$SEARCH_QUERY' (market)" market://search?q=$SEARCH_QUERY
view "Search for '$SEARCH_QUERY' (play)" http://play.google.com/store/search?q=$SEARCH_QUERY
view "Search for '$SEARCH_QUERY' (amazon)" http://amazon.com/gp/mas/dl/android?s=$SEARCH_QUERY
view "Search for '$SEARCH_QUERY' (fdroid)" fdroid.search:$SEARCH_QUERY
view "View '$APP_TO_SHOW' (fdroid web fdid)" http://f-droid.org/repository/browse?fdid=$APP_TO_SHOW
view "View '$APP_TO_SHOW' (fdroid web /app/ path)" http://f-droid.org/app/$APP_TO_SHOW
view "View '$APP_TO_SHOW' (market)" market://details?id=$APP_TO_SHOW
view "View '$APP_TO_SHOW' (play)" http://play.google.com/store/apps/details?id=$APP_TO_SHOW
view "View '$APP_TO_SHOW' (amazon)" amzn://apps/android?p=$APP_TO_SHOW
view "View '$APP_TO_SHOW' (fdroid)" fdroid.app:$APP_TO_SHOW