package org.wordpress.android.ui.main; import android.content.Context; import android.graphics.Typeface; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.AsyncTask; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; import org.wordpress.android.R; import org.wordpress.android.WordPress; import org.wordpress.android.fluxc.model.SiteModel; import org.wordpress.android.fluxc.store.AccountStore; import org.wordpress.android.fluxc.store.SiteStore; import org.wordpress.android.ui.prefs.AppPrefs; import org.wordpress.android.util.AppLog; import org.wordpress.android.util.SiteUtils; import org.wordpress.android.util.StringUtils; import org.wordpress.android.widgets.WPNetworkImageView; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.inject.Inject; public class SitePickerAdapter extends RecyclerView.Adapter<SitePickerAdapter.SiteViewHolder> { interface OnSiteClickListener { void onSiteClick(SiteRecord site); boolean onSiteLongClick(SiteRecord site); } interface OnSelectedCountChangedListener { void onSelectedCountChanged(int numSelected); } interface OnDataLoadedListener { void onBeforeLoad(boolean isEmpty); void onAfterLoad(); } private final int mTextColorNormal; private final int mTextColorHidden; private static int mBlavatarSz; private SiteList mSites = new SiteList(); private final int mCurrentLocalId; private final Drawable mSelectedItemBackground; private final LayoutInflater mInflater; private final HashSet<Integer> mSelectedPositions = new HashSet<>(); private boolean mIsMultiSelectEnabled; private final boolean mIsInSearchMode; private boolean mShowHiddenSites = false; private boolean mShowSelfHostedSites = true; private String mLastSearch; private SiteList mAllSites; private OnSiteClickListener mSiteSelectedListener; private OnSelectedCountChangedListener mSelectedCountListener; private OnDataLoadedListener mDataLoadedListener; // show recently picked first if there are at least this many blogs private static final int RECENTLY_PICKED_THRESHOLD = 15; @Inject AccountStore mAccountStore; @Inject SiteStore mSiteStore; class SiteViewHolder extends RecyclerView.ViewHolder { private final ViewGroup layoutContainer; private final TextView txtTitle; private final TextView txtDomain; private final WPNetworkImageView imgBlavatar; private final View divider; private Boolean isSiteHidden; public SiteViewHolder(View view) { super(view); layoutContainer = (ViewGroup) view.findViewById(R.id.layout_container); txtTitle = (TextView) view.findViewById(R.id.text_title); txtDomain = (TextView) view.findViewById(R.id.text_domain); imgBlavatar = (WPNetworkImageView) view.findViewById(R.id.image_blavatar); divider = view.findViewById(R.id.divider); isSiteHidden = null; } } public SitePickerAdapter(Context context, int currentLocalBlogId, String lastSearch, boolean isInSearchMode, OnDataLoadedListener dataLoadedListener) { super(); ((WordPress) context.getApplicationContext()).component().inject(this); setHasStableIds(true); mLastSearch = StringUtils.notNullStr(lastSearch); mAllSites = new SiteList(); mIsInSearchMode = isInSearchMode; mCurrentLocalId = currentLocalBlogId; mInflater = LayoutInflater.from(context); mDataLoadedListener = dataLoadedListener; mBlavatarSz = context.getResources().getDimensionPixelSize(R.dimen.blavatar_sz); mTextColorNormal = context.getResources().getColor(R.color.grey_dark); mTextColorHidden = context.getResources().getColor(R.color.grey); mSelectedItemBackground = new ColorDrawable(context.getResources().getColor(R.color.grey_lighten_20_translucent_50)); loadSites(); } @Override public int getItemCount() { return mSites.size(); } @Override public long getItemId(int position) { return getItem(position).localId; } private SiteRecord getItem(int position) { return mSites.get(position); } void setOnSelectedCountChangedListener(OnSelectedCountChangedListener listener) { mSelectedCountListener = listener; } public void setOnSiteClickListener(OnSiteClickListener listener) { mSiteSelectedListener = listener; } @Override public SiteViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = mInflater.inflate(R.layout.site_picker_listitem, parent, false); return new SiteViewHolder(itemView); } @Override public void onBindViewHolder(final SiteViewHolder holder, int position) { SiteRecord site = getItem(position); holder.txtTitle.setText(site.getBlogNameOrHomeURL()); holder.txtDomain.setText(site.homeURL); holder.imgBlavatar.setImageUrl(site.blavatarUrl, WPNetworkImageView.ImageType.BLAVATAR); if (site.localId == mCurrentLocalId || (mIsMultiSelectEnabled && isItemSelected(position))) { holder.layoutContainer.setBackgroundDrawable(mSelectedItemBackground); } else { holder.layoutContainer.setBackgroundDrawable(null); } // different styling for visible/hidden sites if (holder.isSiteHidden == null || holder.isSiteHidden != site.isHidden) { holder.isSiteHidden = site.isHidden; holder.txtTitle.setTextColor(site.isHidden ? mTextColorHidden : mTextColorNormal); holder.txtTitle.setTypeface(holder.txtTitle.getTypeface(), site.isHidden ? Typeface.NORMAL : Typeface.BOLD); holder.imgBlavatar.setAlpha(site.isHidden ? 0.5f : 1f); } // only show divider after last recent pick boolean showDivider = site.isRecentPick && !mIsInSearchMode && position < getItemCount() - 1 && !getItem(position + 1).isRecentPick; holder.divider.setVisibility(showDivider ? View.VISIBLE : View.GONE); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { int clickedPosition = holder.getAdapterPosition(); if (isValidPosition(clickedPosition)) { if (mIsMultiSelectEnabled) { toggleSelection(clickedPosition); } else if (mSiteSelectedListener != null) { mSiteSelectedListener.onSiteClick(getItem(clickedPosition)); } } else { AppLog.w(AppLog.T.MAIN, "site picker > invalid clicked position " + clickedPosition); } } }); holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { int clickedPosition = holder.getAdapterPosition(); if (isValidPosition(clickedPosition)) { if (mIsMultiSelectEnabled) { toggleSelection(clickedPosition); return true; } else if (mSiteSelectedListener != null) { boolean result = mSiteSelectedListener.onSiteLongClick(getItem(clickedPosition)); setItemSelected(clickedPosition, true); return result; } } else { AppLog.w(AppLog.T.MAIN, "site picker > invalid clicked position " + clickedPosition); } return false; } }); } public String getLastSearch() { return mLastSearch; } public void setLastSearch(String lastSearch) { mLastSearch = lastSearch; } public boolean getIsInSearchMode() { return mIsInSearchMode; } public void searchSites(String searchText) { mLastSearch = searchText; mSites = filteredSitesByText(mAllSites); notifyDataSetChanged(); } private boolean isValidPosition(int position) { return (position >= 0 && position < mSites.size()); } /* * called when the user chooses to edit the visibility of wp.com blogs */ void setEnableEditMode(boolean enable) { if (mIsMultiSelectEnabled == enable) return; if (enable) { mShowHiddenSites = true; mShowSelfHostedSites = false; } else { mShowHiddenSites = false; mShowSelfHostedSites = true; } mIsMultiSelectEnabled = enable; mSelectedPositions.clear(); loadSites(); } int getNumSelected() { return mSelectedPositions.size(); } int getNumHiddenSelected() { int numHidden = 0; for (Integer i: mSelectedPositions) { if (mSites.get(i).isHidden) { numHidden++; } } return numHidden; } int getNumVisibleSelected() { int numVisible = 0; for (Integer i: mSelectedPositions) { if (!mSites.get(i).isHidden) { numVisible++; } } return numVisible; } private void toggleSelection(int position) { setItemSelected(position, !isItemSelected(position)); } private boolean isItemSelected(int position) { return mSelectedPositions.contains(position); } private void setItemSelected(int position, boolean isSelected) { if (isItemSelected(position) == isSelected) { return; } if (isSelected) { mSelectedPositions.add(position); } else { mSelectedPositions.remove(position); } notifyItemChanged(position); if (mSelectedCountListener != null) { mSelectedCountListener.onSelectedCountChanged(getNumSelected()); } } void selectAll() { if (mSelectedPositions.size() == mSites.size()) return; mSelectedPositions.clear(); for (int i = 0; i < mSites.size(); i++) { mSelectedPositions.add(i); } notifyDataSetChanged(); if (mSelectedCountListener != null) { mSelectedCountListener.onSelectedCountChanged(getNumSelected()); } } void deselectAll() { if (mSelectedPositions.size() == 0) return; mSelectedPositions.clear(); notifyDataSetChanged(); if (mSelectedCountListener != null) { mSelectedCountListener.onSelectedCountChanged(getNumSelected()); } } private SiteList getSelectedSites() { SiteList sites = new SiteList(); if (!mIsMultiSelectEnabled) { return sites; } for (Integer position : mSelectedPositions) { if (isValidPosition(position)) sites.add(mSites.get(position)); } return sites; } SiteList getHiddenSites() { SiteList hiddenSites = new SiteList(); for (SiteRecord site: mSites) { if (site.isHidden) { hiddenSites.add(site); } } return hiddenSites; } Set<SiteRecord> setVisibilityForSelectedSites(boolean makeVisible) { SiteList sites = getSelectedSites(); Set<SiteRecord> siteRecordSet = new HashSet<>(); if (sites != null && sites.size() > 0) { for (SiteRecord site: sites) { int index = mAllSites.indexOfSite(site); if (index > -1) { SiteRecord siteRecord = mAllSites.get(index); if (siteRecord.isHidden == makeVisible) { // add it to change set siteRecordSet.add(siteRecord); } siteRecord.isHidden = !makeVisible; } } } notifyDataSetChanged(); return siteRecordSet; } void loadSites() { new LoadSitesTask().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } private SiteList filteredSitesByTextIfInSearchMode(SiteList sites) { if (!mIsInSearchMode) { return sites; } else { return filteredSitesByText(sites); } } private SiteList filteredSitesByText(SiteList sites) { SiteList filteredSiteList = new SiteList(); for (int i = 0; i < sites.size(); i++) { SiteRecord record = sites.get(i); String siteNameLowerCase = record.blogName.toLowerCase(); String hostNameLowerCase = record.homeURL.toLowerCase(); if (siteNameLowerCase.contains(mLastSearch.toLowerCase()) || hostNameLowerCase.contains(mLastSearch.toLowerCase())) { filteredSiteList.add(record); } } return filteredSiteList; } /* * AsyncTask which loads sites from database and populates the adapter */ private boolean mIsTaskRunning; private class LoadSitesTask extends AsyncTask<Void, Void, Void> { @Override protected void onPreExecute() { super.onPreExecute(); mIsTaskRunning = true; if (mDataLoadedListener != null) { boolean isEmpty = mSites == null || mSites.size() == 0; mDataLoadedListener.onBeforeLoad(isEmpty); } } @Override protected void onCancelled() { super.onCancelled(); mIsTaskRunning = false; } @Override protected Void doInBackground(Void... params) { List<SiteModel> siteModels; if (mIsInSearchMode) { siteModels = mSiteStore.getSites(); } else { siteModels = getBlogsForCurrentView(); } SiteList sites = new SiteList(siteModels); // sort primary blog to the top, otherwise sort by blog/host final long primaryBlogId = mAccountStore.getAccount().getPrimarySiteId(); Collections.sort(sites, new Comparator<SiteRecord>() { public int compare(SiteRecord site1, SiteRecord site2) { if (primaryBlogId > 0 && !mIsInSearchMode) { if (site1.siteId == primaryBlogId) { return -1; } else if (site2.siteId == primaryBlogId) { return 1; } } return site1.getBlogNameOrHomeURL().compareToIgnoreCase(site2.getBlogNameOrHomeURL()); } }); // flag recently-picked sites and move them to the top if there are enough sites and // the user isn't searching if (!mIsInSearchMode && sites.size() >= RECENTLY_PICKED_THRESHOLD) { ArrayList<Integer> pickedIds = AppPrefs.getRecentlyPickedSiteIds(); for (int i = pickedIds.size() - 1; i > -1; i--) { int thisId = pickedIds.get(i); int indexOfSite = sites.indexOfSiteId(thisId); if (indexOfSite > -1) { SiteRecord site = sites.remove(indexOfSite); site.isRecentPick = true; sites.add(0, site); } } } if (mSites == null || !mSites.isSameList(sites)) { mAllSites = (SiteList) sites.clone(); mSites = filteredSitesByTextIfInSearchMode(sites); } return null; } @Override protected void onPostExecute(Void results) { notifyDataSetChanged(); mIsTaskRunning = false; if (mDataLoadedListener != null) { mDataLoadedListener.onAfterLoad(); } } private List<SiteModel> getBlogsForCurrentView() { if (mShowHiddenSites) { if (mShowSelfHostedSites) { return mSiteStore.getSites(); } else { return mSiteStore.getSitesAccessedViaWPComRest(); } } else { if (mShowSelfHostedSites) { List<SiteModel> out = mSiteStore.getVisibleSitesAccessedViaWPCom(); out.addAll(mSiteStore.getSitesAccessedViaXMLRPC()); return out; } else { return mSiteStore.getVisibleSitesAccessedViaWPCom(); } } } } /** * SiteRecord is a simplified version of the full account (blog) record */ static class SiteRecord { final int localId; final long siteId; final String blogName; final String homeURL; final String url; final String blavatarUrl; boolean isHidden; boolean isRecentPick; SiteRecord(SiteModel siteModel) { localId = siteModel.getId(); siteId = siteModel.getSiteId(); blogName = SiteUtils.getSiteNameOrHomeURL(siteModel); homeURL = SiteUtils.getHomeURLOrHostName(siteModel); url = siteModel.getUrl(); blavatarUrl = SiteUtils.getSiteIconUrl(siteModel, mBlavatarSz); isHidden = !siteModel.isVisible(); } String getBlogNameOrHomeURL() { if (TextUtils.isEmpty(blogName)) { return homeURL; } return blogName; } } static class SiteList extends ArrayList<SiteRecord> { SiteList() { } SiteList(List<SiteModel> siteModels) { if (siteModels != null) { for (SiteModel siteModel : siteModels) { add(new SiteRecord(siteModel)); } } } boolean isSameList(SiteList sites) { if (sites == null || sites.size() != this.size()) { return false; } int i; for (SiteRecord site: sites) { i = indexOfSite(site); if (i == -1 || this.get(i).isHidden != site.isHidden || this.get(i).isRecentPick != site.isRecentPick) { return false; } } return true; } int indexOfSite(SiteRecord site) { if (site != null && site.siteId > 0) { for (int i = 0; i < size(); i++) { if (site.siteId == this.get(i).siteId) { return i; } } } return -1; } int indexOfSiteId(int localId) { for (int i = 0; i < size(); i++) { if (localId == this.get(i).localId) { return i; } } return -1; } } /* * same as Long.compare() which wasn't added until API 19 */ private static int compareTimestamps(long timestamp1, long timestamp2) { return timestamp1 < timestamp2 ? -1 : (timestamp1 == timestamp2 ? 0 : 1); } }