package de.robv.android.xposed.installer; import android.app.Fragment; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.v4.view.MenuItemCompat; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.SearchView; import android.text.TextUtils; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.CursorAdapter; import android.widget.FilterQueryProvider; import android.widget.TextView; import com.afollestad.materialdialogs.MaterialDialog; import java.text.DateFormat; import java.util.Date; import de.robv.android.xposed.installer.repo.RepoDb; import de.robv.android.xposed.installer.repo.RepoDbDefinitions.OverviewColumnsIndexes; import de.robv.android.xposed.installer.util.Loader; import de.robv.android.xposed.installer.util.ModuleUtil; import de.robv.android.xposed.installer.util.ModuleUtil.InstalledModule; import de.robv.android.xposed.installer.util.ModuleUtil.ModuleListener; import de.robv.android.xposed.installer.util.RepoLoader; import de.robv.android.xposed.installer.util.ThemeUtil; import se.emilsjolander.stickylistheaders.StickyListHeadersAdapter; import se.emilsjolander.stickylistheaders.StickyListHeadersListView; public class DownloadFragment extends Fragment implements Loader.Listener<RepoLoader>, ModuleListener { private SharedPreferences mPref; private DownloadsAdapter mAdapter; private String mFilterText; private RepoLoader mRepoLoader; private ModuleUtil mModuleUtil; private int mSortingOrder; private SearchView mSearchView; private StickyListHeadersListView mListView; private View mRefreshHint; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPref = XposedApp.getPreferences(); mRepoLoader = RepoLoader.getInstance(); mModuleUtil = ModuleUtil.getInstance(); mAdapter = new DownloadsAdapter(getActivity()); mAdapter.setFilterQueryProvider(new FilterQueryProvider() { @Override public Cursor runQuery(CharSequence constraint) { return RepoDb.queryModuleOverview(mSortingOrder, constraint); } }); mSortingOrder = mPref.getInt("download_sorting_order", RepoDb.SORT_STATUS); setHasOptionsMenu(true); } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (mAdapter != null && mListView != null) { mListView.setAdapter(mAdapter); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.tab_downloader, container, false); mRefreshHint = v.findViewById(R.id.refresh_hint); final SwipeRefreshLayout refreshLayout = (SwipeRefreshLayout) v.findViewById(R.id.swiperefreshlayout); refreshLayout.setColorSchemeColors(getResources().getColor(R.color.colorPrimary)); mRepoLoader.addListener(this); mRepoLoader.setSwipeRefreshLayout(refreshLayout); mModuleUtil.addListener(this); mListView = (StickyListHeadersListView) v.findViewById(R.id.listModules); mListView.setAdapter(mAdapter); mListView.setOnScrollListener(new AbsListView.OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { if (view.getChildAt(0) != null) { refreshLayout.setEnabled(view.getFirstVisiblePosition() == 0 && view.getChildAt(0).getTop() == 0); } } }); reloadItems(); mListView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Cursor cursor = (Cursor) mAdapter.getItem(position); String packageName = cursor.getString(OverviewColumnsIndexes.PKGNAME); Intent detailsIntent = new Intent(getActivity(), DownloadDetailsActivity.class); detailsIntent.setData(Uri.fromParts("package", packageName, null)); startActivity(detailsIntent); } }); mListView.setOnKeyListener(new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { // Expand the search view when the SEARCH key is triggered if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getAction() == KeyEvent.ACTION_UP && (event.getFlags() & KeyEvent.FLAG_CANCELED) == 0) { if (mSearchView != null) mSearchView.setIconified(false); return true; } return false; } }); setHasOptionsMenu(true); return v; } @Override public void onDestroyView() { super.onDestroyView(); mRepoLoader.removeListener(this); mRepoLoader.setSwipeRefreshLayout(null); mModuleUtil.removeListener(this); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.menu_download, menu); // Setup search button final MenuItem searchItem = menu.findItem(R.id.menu_search); mSearchView = (SearchView) searchItem.getActionView(); mSearchView.setIconifiedByDefault(true); mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { setFilter(query); mSearchView.clearFocus(); return true; } @Override public boolean onQueryTextChange(String newText) { setFilter(newText); return true; } }); MenuItemCompat.setOnActionExpandListener(searchItem, new MenuItemCompat.OnActionExpandListener() { @Override public boolean onMenuItemActionCollapse(MenuItem item) { setFilter(null); return true; // Return true to collapse action view } @Override public boolean onMenuItemActionExpand(MenuItem item) { return true; // Return true to expand action view } }); } private void setFilter(String filterText) { mFilterText = filterText; reloadItems(); mRefreshHint.setVisibility(TextUtils.isEmpty(filterText) ? View.VISIBLE : View.GONE); } private void reloadItems() { mAdapter.getFilter().filter(mFilterText); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_sort: new MaterialDialog.Builder(getActivity()) .title(R.string.download_sorting_title) .items(R.array.download_sort_order) .itemsCallbackSingleChoice(mSortingOrder, new MaterialDialog.ListCallbackSingleChoice() { @Override public boolean onSelection(MaterialDialog materialDialog, View view, int i, CharSequence charSequence) { mSortingOrder = i; mPref.edit().putInt("download_sorting_order", mSortingOrder).apply(); reloadItems(); materialDialog.dismiss(); return true; } }) .show(); return true; } return super.onOptionsItemSelected(item); } @Override public void onReloadDone(final RepoLoader loader) { reloadItems(); } @Override public void onSingleInstalledModuleReloaded(ModuleUtil moduleUtil, String packageName, InstalledModule module) { reloadItems(); } @Override public void onInstalledModulesReloaded(ModuleUtil moduleUtil) { reloadItems(); } private class DownloadsAdapter extends CursorAdapter implements StickyListHeadersAdapter { private final Context mContext; private final DateFormat mDateFormatter = DateFormat.getDateInstance(DateFormat.SHORT); private final LayoutInflater mInflater; private String[] sectionHeadersStatus; private String[] sectionHeadersDate; public DownloadsAdapter(Context context) { super(context, null, 0); mContext = context; mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); Resources res = context.getResources(); sectionHeadersStatus = new String[]{ res.getString(R.string.download_section_framework), res.getString(R.string.download_section_update_available), res.getString(R.string.download_section_installed), res.getString(R.string.download_section_not_installed),}; sectionHeadersDate = new String[]{ res.getString(R.string.download_section_24h), res.getString(R.string.download_section_7d), res.getString(R.string.download_section_30d), res.getString(R.string.download_section_older)}; } @Override public View getHeaderView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = mInflater.inflate(R.layout.list_sticky_header_download, parent, false); } long section = getHeaderId(position); TextView tv = (TextView) convertView.findViewById(android.R.id.title); tv.setText(mSortingOrder == RepoDb.SORT_STATUS ? sectionHeadersStatus[(int) section] : sectionHeadersDate[(int) section]); return convertView; } @Override public long getHeaderId(int position) { Cursor cursor = (Cursor) getItem(position); long created = cursor.getLong(OverviewColumnsIndexes.CREATED); long updated = cursor.getLong(OverviewColumnsIndexes.UPDATED); boolean isFramework = cursor.getInt(OverviewColumnsIndexes.IS_FRAMEWORK) > 0; boolean isInstalled = cursor.getInt(OverviewColumnsIndexes.IS_INSTALLED) > 0; boolean hasUpdate = cursor.getInt(OverviewColumnsIndexes.HAS_UPDATE) > 0; if (mSortingOrder != RepoDb.SORT_STATUS) { long timestamp = (mSortingOrder == RepoDb.SORT_UPDATED) ? updated : created; long age = System.currentTimeMillis() - timestamp; final long mSecsPerDay = 24 * 60 * 60 * 1000L; if (age < mSecsPerDay) return 0; if (age < 7 * mSecsPerDay) return 1; if (age < 30 * mSecsPerDay) return 2; return 3; } else { if (isFramework) return 0; if (hasUpdate) return 1; else if (isInstalled) return 2; else return 3; } } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return mInflater.inflate(R.layout.list_item_download, parent, false); } @Override public void bindView(View view, Context context, Cursor cursor) { String title = cursor.getString(OverviewColumnsIndexes.TITLE); String summary = cursor.getString(OverviewColumnsIndexes.SUMMARY); String installedVersion = cursor.getString(OverviewColumnsIndexes.INSTALLED_VERSION); String latestVersion = cursor.getString(OverviewColumnsIndexes.LATEST_VERSION); long created = cursor.getLong(OverviewColumnsIndexes.CREATED); long updated = cursor.getLong(OverviewColumnsIndexes.UPDATED); boolean isInstalled = cursor.getInt(OverviewColumnsIndexes.IS_INSTALLED) > 0; boolean hasUpdate = cursor.getInt(OverviewColumnsIndexes.HAS_UPDATE) > 0; TextView txtTitle = (TextView) view.findViewById(android.R.id.text1); txtTitle.setText(title); TextView txtSummary = (TextView) view.findViewById(android.R.id.text2); txtSummary.setText(summary); TextView txtStatus = (TextView) view.findViewById(R.id.downloadStatus); if (hasUpdate) { txtStatus.setText(mContext.getString( R.string.download_status_update_available, installedVersion, latestVersion)); txtStatus.setTextColor(getResources().getColor(R.color.download_status_update_available)); txtStatus.setVisibility(View.VISIBLE); } else if (isInstalled) { txtStatus.setText(mContext.getString( R.string.download_status_installed, installedVersion)); txtStatus.setTextColor(ThemeUtil.getThemeColor(mContext, R.attr.download_status_installed)); txtStatus.setVisibility(View.VISIBLE); } else { txtStatus.setVisibility(View.GONE); } String creationDate = mDateFormatter.format(new Date(created)); String updateDate = mDateFormatter.format(new Date(updated)); ((TextView) view.findViewById(R.id.timestamps)).setText(getString(R.string.download_timestamps, creationDate, updateDate)); } } }