package org.fdroid.fdroid.views.fragments;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.util.Pair;
import android.text.TextUtils;
import android.view.View;
import android.widget.AdapterView;
import android.widget.TextView;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.AppDetails2;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
import org.fdroid.fdroid.views.AppListAdapter;
public abstract class AppListFragment extends ListFragment implements
AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener,
Preferences.ChangeListener,
LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = "AppListFragment";
private static final int REQUEST_APPDETAILS = 0;
private static final String[] APP_PROJECTION = {
AppMetadataTable.Cols._ID, // Required for cursor loader to work.
AppMetadataTable.Cols.Package.PACKAGE_NAME,
AppMetadataTable.Cols.NAME,
AppMetadataTable.Cols.SUMMARY,
AppMetadataTable.Cols.IS_COMPATIBLE,
AppMetadataTable.Cols.LICENSE,
AppMetadataTable.Cols.ICON,
AppMetadataTable.Cols.ICON_URL,
AppMetadataTable.Cols.InstalledApp.VERSION_CODE,
AppMetadataTable.Cols.InstalledApp.VERSION_NAME,
AppMetadataTable.Cols.SuggestedApk.VERSION_NAME,
AppMetadataTable.Cols.SUGGESTED_VERSION_CODE,
AppMetadataTable.Cols.REQUIREMENTS, // Needed for filtering apps that require root.
AppMetadataTable.Cols.ANTI_FEATURES, // Needed for filtering apps that require anti-features.
};
private static final String APP_SORT = AppMetadataTable.Cols.NAME;
private AppListAdapter appAdapter;
@Nullable private String searchQuery;
protected abstract AppListAdapter getAppListAdapter();
protected abstract String getFromTitle();
protected abstract Uri getDataUri();
protected abstract Uri getDataUri(String query);
protected abstract int getEmptyMessage();
protected abstract int getNoSearchResultsMessage();
/**
* 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.
*/
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.
}
/**
* Utility function to set empty view text which should be different
* depending on whether search is active or not.
*/
private void setEmptyText(int resId) {
((TextView) getListView().getEmptyView()).setText(resId);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Can't do this in the onCreate view, because "onCreateView" which
// returns the list view is "called between onCreate and
// onActivityCreated" according to the docs.
getListView().setOnItemClickListener(this);
getListView().setOnItemLongClickListener(this);
}
@Override
public void onResume() {
super.onResume();
//Starts a new or restarts an existing Loader in this manager
getLoaderManager().initLoader(0, null, this);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
appAdapter = getAppListAdapter();
if (appAdapter.getCount() == 0) {
updateEmptyRepos();
}
setListAdapter(appAdapter);
}
/**
* The first time the app is run, we will have an empty app list.
* If this is the case, we will attempt to update with the default repo.
* However, if we have tried this at least once, then don't try to do
* it automatically again, because the repos or internet connection may
* be bad.
*/
private boolean updateEmptyRepos() {
final String triedEmptyUpdate = "triedEmptyUpdate";
SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE);
boolean hasTriedEmptyUpdate = prefs.getBoolean(triedEmptyUpdate, false);
if (!hasTriedEmptyUpdate) {
Utils.debugLog(TAG, "Empty app list, and we haven't done an update yet. Forcing repo update.");
prefs.edit().putBoolean(triedEmptyUpdate, true).apply();
UpdateService.updateNow(getActivity());
return true;
}
Utils.debugLog(TAG, "Empty app list, but it looks like we've had an update previously. Will not force repo update.");
return false;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
showItemDetails(view, position, false);
}
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
showItemDetails(view, position, true);
return true;
}
private void showItemDetails(View view, int position, boolean useNewDetailsActivity) {
// Cursor is null in the swap list when touching the first item.
Cursor cursor = (Cursor) getListView().getItemAtPosition(position);
if (cursor != null) {
final App app = new App(cursor);
Intent intent = getAppDetailsIntent(useNewDetailsActivity);
intent.putExtra(AppDetails.EXTRA_APPID, app.packageName);
intent.putExtra(AppDetails.EXTRA_FROM, getFromTitle());
if (Build.VERSION.SDK_INT >= 21) {
Pair<View, String> iconTransitionPair = Pair.create(view.findViewById(R.id.icon),
getString(R.string.transition_app_item_icon));
Bundle bundle = ActivityOptionsCompat
.makeSceneTransitionAnimation(getActivity(),
iconTransitionPair)
.toBundle();
startActivityForResult(intent, REQUEST_APPDETAILS, bundle);
} else {
startActivityForResult(intent, REQUEST_APPDETAILS);
}
}
}
private Intent getAppDetailsIntent(boolean useNewDetailsActivity) {
return new Intent(getActivity(), useNewDetailsActivity ? AppDetails2.class : AppDetails.class);
}
@Override
public void onPreferenceChange() {
getAppListAdapter().notifyDataSetChanged();
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
appAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
appAdapter.swapCursor(null);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = updateSearchStatus() ? getDataUri(searchQuery) : getDataUri();
return new CursorLoader(
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 and changes empty message
* appropriately.
*
* @return True if a user is searching.
*/
private boolean updateSearchStatus() {
if (TextUtils.isEmpty(searchQuery)) {
onSearchStopped();
setEmptyText(getEmptyMessage());
return false;
}
onSearch();
setEmptyText(getNoSearchResultsMessage());
return true;
}
public void updateSearchQuery(@Nullable String query) {
if (!TextUtils.equals(query, searchQuery)) {
searchQuery = query;
if (isAdded()) {
getLoaderManager().restartLoader(0, null, this);
}
}
}
}