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:
commit
1a5b60f654
@ -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>
|
||||||
|
@ -4,6 +4,12 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:id="@+id/category_wrapper"
|
||||||
|
android:layout_alignParentTop="true">
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
android:id="@+id/category_spinner"
|
android:id="@+id/category_spinner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -18,13 +24,15 @@
|
|||||||
android:layout_alignBottom="@id/category_spinner"
|
android:layout_alignBottom="@id/category_spinner"
|
||||||
android:background="@color/fdroid_green" />
|
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>
|
||||||
|
@ -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() {
|
||||||
|
21
F-Droid/src/org/fdroid/fdroid/compat/UriCompat.java
Normal file
21
F-Droid/src/org/fdroid/fdroid/compat/UriCompat.java
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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";
|
||||||
@ -447,6 +449,8 @@ public class AppProvider extends FDroidProvider {
|
|||||||
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))));
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
if (categorySpinner != null) {
|
||||||
for (int i = 0; i < categorySpinner.getCount(); i++) {
|
for (int i = 0; i < categorySpinner.getCount(); i++) {
|
||||||
if (currentCategory.equals(categorySpinner.getItemAtPosition(i).toString())) {
|
if (currentCategory.equals(categorySpinner.getItemAtPosition(i).toString())) {
|
||||||
categorySpinner.setSelection(i);
|
categorySpinner.setSelection(i);
|
||||||
break;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
46
F-Droid/tools/test-search-intents.sh
Executable file
46
F-Droid/tools/test-search-intents.sh
Executable 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
|
Loading…
x
Reference in New Issue
Block a user