package com.aptoide.amethyst.fragments.store; import android.graphics.PorterDuff; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.widget.SwipeRefreshLayout; import android.view.View; import android.widget.ProgressBar; import android.widget.ScrollView; import android.widget.TextView; import com.aptoide.amethyst.GridRecyclerFragment; import com.aptoide.amethyst.R; import com.aptoide.amethyst.dialogs.AdultHiddenDialog; import com.aptoide.amethyst.models.EnumStoreTheme; import com.aptoide.models.displayables.StoreHeaderRow; import com.aptoide.amethyst.preferences.Preferences; import com.aptoide.amethyst.utils.AptoideUtils; import com.aptoide.amethyst.utils.IconSizeUtils; import com.aptoide.amethyst.utils.Logger; import com.aptoide.amethyst.webservices.ListApksInstallsRequest; import com.aptoide.amethyst.webservices.json.TimelineListAPKsJson; import com.aptoide.amethyst.webservices.v2.GetAdsRequest; import com.aptoide.dataprovider.webservices.GetReviews; import com.aptoide.dataprovider.webservices.json.review.Review; import com.aptoide.dataprovider.webservices.json.review.ReviewListJson; import com.aptoide.dataprovider.webservices.models.Constants; import com.aptoide.dataprovider.webservices.models.StoreHomeTab; import com.aptoide.models.displayables.AdItem; import com.aptoide.models.displayables.AdultItem; import com.aptoide.models.ApkSuggestionJson; import com.aptoide.models.displayables.Displayable; import com.aptoide.models.displayables.HeaderRow; import com.aptoide.models.displayables.ReviewRowItem; import com.aptoide.models.displayables.TimelineRow; import com.aptoide.models.displayables.AdPlaceHolderRow; import com.aptoide.models.displayables.ReviewPlaceHolderRow; import com.aptoide.models.displayables.TimeLinePlaceHolderRow; import com.octo.android.robospice.exception.NoNetworkException; import com.octo.android.robospice.persistence.DurationInMillis; import com.octo.android.robospice.persistence.exception.SpiceException; import com.octo.android.robospice.request.listener.RequestListener; import java.util.ArrayList; import java.util.List; import java.util.UUID; import com.aptoide.amethyst.adapter.BaseAdapter; import static com.aptoide.dataprovider.webservices.models.v7.GetStoreWidgets.Datalist.WidgetList.ADS_TYPE; import static com.aptoide.dataprovider.webservices.models.v7.GetStoreWidgets.Datalist.WidgetList.REVIEWS_TYPE; import static com.aptoide.dataprovider.webservices.models.v7.GetStoreWidgets.Datalist.WidgetList.TIMELINE_TYPE; /** * Json example for this kind of tab: * "type": "API", * "name": "getStoreWidgets", * "label": "Aptoide Publishers", * "action": "http://ws2.aptoide.com/api/7/getStoreWidgets/store_id/15/context/store/widget/apps_list%3Aucat_3239 * * Created by rmateus on 23/06/15. */ public abstract class BaseWebserviceFragment extends GridRecyclerFragment { protected SwipeRefreshLayout swipeContainer; protected ProgressBar progressBar; protected ScrollView layoutNoNetwork; protected ScrollView layoutError; protected TextView retryError; protected TextView retryNoNetwork; @Nullable protected BaseAdapter adapter; // flag to feed the storeHeader button protected boolean subscribed; protected boolean useCache = true; private long storeId; protected String storeName; protected String versionName; protected String packageName; private EnumStoreTheme storeTheme; protected Bundle args; protected List<Displayable> displayableList = new ArrayList<>(); protected String sponsoredCache; //used only on HomeFragment protected int offset, total; protected RequestListener<StoreHomeTab> listener = new RequestListener<StoreHomeTab>() { @Override public void onRequestFailure(SpiceException spiceException) { handleErrorCondition(spiceException); } @Override public void onRequestSuccess(StoreHomeTab tab) { if (getView() == null) { return; } handleSuccessCondition(); adapter = getAdapter(); setRecyclerAdapter(adapter); displayableList.clear(); if (isStorePage()) { displayableList.add(getStoreHeaderRow(tab)); } displayableList.addAll(tab.list); if (isHomePage()) { displayableList.add(new AdultItem(BUCKET_SIZE)); } for (Displayable row : tab.list) { if (row instanceof ReviewPlaceHolderRow) { executeReviewsSpiceRequest(); } else if (row instanceof AdPlaceHolderRow) { executeAdsSpiceRequest(); } else if (row instanceof TimeLinePlaceHolderRow) { executeTimelineRequest(); } } // total and new offset is red here offset = tab.offset; total = tab.total; // check for hidden items if (tab.hidden > 0 && AptoideUtils.getSharedPreferences().getBoolean(Constants.SHOW_ADULT_HIDDEN, true) && getFragmentManager().findFragmentByTag(Constants.HIDDEN_ADULT_DIALOG) == null) { new AdultHiddenDialog().show(getFragmentManager(), Constants.HIDDEN_ADULT_DIALOG); } } }; protected void handleErrorCondition(SpiceException spiceException) { Logger.printException(spiceException); progressBar.setVisibility(View.GONE); swipeContainer.setRefreshing(false); swipeContainer.setEnabled(false); getRecyclerView().setVisibility(View.GONE); if (spiceException instanceof NoNetworkException) { layoutError.setVisibility(View.GONE); layoutNoNetwork.setVisibility(View.VISIBLE); retryNoNetwork.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { retry(); } }); } else { layoutNoNetwork.setVisibility(View.GONE); layoutError.setVisibility(View.VISIBLE); retryError.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { retry(); } }); } } protected void handleNoItemsCondition() { layoutNoNetwork.setVisibility(View.GONE); layoutError.setVisibility(View.GONE); progressBar.setVisibility(View.GONE); swipeContainer.setRefreshing(false); swipeContainer.setEnabled(false); getRecyclerView().setVisibility(View.GONE); } private void retry() { layoutError.setVisibility(View.GONE); layoutNoNetwork.setVisibility(View.GONE); progressBar.setVisibility(View.VISIBLE); executeSpiceRequest(false); } protected void handleSuccessCondition() { // TODO find memory leak if (getView() == null) { return; } progressBar.setVisibility(View.GONE); swipeContainer.setRefreshing(false); swipeContainer.setEnabled(true); layoutError.setVisibility(View.GONE); layoutNoNetwork.setVisibility(View.GONE); getRecyclerView().setVisibility(View.VISIBLE); } protected void executeSpiceRequest(boolean useCache) { this.swipeContainer.setEnabled(false); this.useCache = useCache; long cacheExpiryDuration = useCache ? DurationInMillis.ONE_HOUR * 6 : DurationInMillis.ALWAYS_EXPIRED; // in order to present the right info on screen after a screen rotation, always pass the bucketsize as cachekey spiceManager.execute( AptoideUtils.RepoUtils.buildStoreRequest(getStoreId(), getBaseContext()), getBaseContext() + "-" + getStoreId() + "-" + BUCKET_SIZE + "--" + AptoideUtils.getSharedPreferences().getBoolean(Constants.MATURE_CHECK_BOX, false), cacheExpiryDuration, listener); } private void executeTimelineRequest() { if (AptoideUtils.AccountUtils.isLoggedIn(getActivity()) && AptoideUtils.getSharedPreferences().getBoolean(Preferences.TIMELINE_ACEPTED_BOOL, false)) { ListApksInstallsRequest listRelatedApkRequest = new ListApksInstallsRequest(); listRelatedApkRequest.setLimit(String.valueOf(BUCKET_SIZE)); // in order to present the right info on screen after a screen rotation, always pass the bucketsize as cachekey spiceManager.execute(listRelatedApkRequest, "MoreFriendsInstalls--" + BUCKET_SIZE, useCache ? DurationInMillis.ONE_HOUR : DurationInMillis.ALWAYS_EXPIRED, new RequestListener<TimelineListAPKsJson>() { @Override public void onRequestFailure(SpiceException spiceException) { Logger.printException(spiceException); } @Override public void onRequestSuccess(TimelineListAPKsJson timelineListAPKsJson) { if (timelineListAPKsJson != null && timelineListAPKsJson.usersapks != null && timelineListAPKsJson.usersapks.size() > 0) { // range is the size of the list. Because the HeaderRow replaces the placeholder, it's not considered an insertion // why is this important? because of notifyItemRangeInserted int range = timelineListAPKsJson.usersapks.size(); int index = 0, originalIndex = 0; boolean placeHolderFound = false; for (Displayable display : displayableList) { if (display instanceof TimeLinePlaceHolderRow) { placeHolderFound = true; originalIndex = index = displayableList.indexOf(display); break; } } // prevent multiple requests adding to beginning of the list if (!placeHolderFound) { return; } HeaderRow header = new HeaderRow(getString(R.string.friends_installs), true, TIMELINE_TYPE, BUCKET_SIZE, isHomePage(), getStoreId()); displayableList.set(index++, header); for (int i = 0; i < timelineListAPKsJson.usersapks.size(); i++) { TimelineListAPKsJson.UserApk userApk = timelineListAPKsJson.usersapks.get(i); TimelineRow timeline = getTimelineRow(userApk); displayableList.add(index++, timeline); } adapter.notifyItemRangeInserted(originalIndex + 1, range); } } }); } } private void executeAdsSpiceRequest() { if (sponsoredCache == null) { if (args != null && args.getString("sponsoredCache") != null) { sponsoredCache = args.getString("sponsoredCache"); } else { sponsoredCache = UUID.randomUUID().toString(); } } final GetAdsRequest request = new GetAdsRequest(); request.setLimit(BUCKET_SIZE); request.setLocation("homepage"); request.setKeyword("__NULL__"); // in order to present the right info on screen after a screen rotation, always pass the bucketsize as cachekey spiceManager.execute(request, sponsoredCache + "--" + BUCKET_SIZE, useCache ? DurationInMillis.ONE_HOUR : DurationInMillis.ALWAYS_EXPIRED, new RequestListener<ApkSuggestionJson>() { @Override public void onRequestFailure(SpiceException spiceException) { } @Override public void onRequestSuccess(ApkSuggestionJson apkSuggestionJson) { if (apkSuggestionJson != null && apkSuggestionJson.ads != null && apkSuggestionJson.ads.size() > 0) { // range is the size of the list. Because the HeaderRow replaces the placeholder, it's not considered an insertion // why is this important? because of notifyItemRangeInserted int range = apkSuggestionJson.ads.size(); int index = 0, originalIndex = 0; boolean adPlaceHolderFound = false; for (Displayable display : displayableList) { if (display instanceof AdPlaceHolderRow) { adPlaceHolderFound = true; originalIndex = index = displayableList.indexOf(display); break; } } // prevent multiple requests adding to beginning of the list if (!adPlaceHolderFound) return; HeaderRow header = new HeaderRow(getString(R.string.highlighted_apps), true, ADS_TYPE, BUCKET_SIZE, isHomePage(), getStoreId()); displayableList.set(index++, header); for (int i = 0; i < apkSuggestionJson.ads.size(); i++) { ApkSuggestionJson.Ads ad = apkSuggestionJson.ads.get(i); AdItem adItem = getAdItem(ad); displayableList.add(index++, adItem); } adapter.notifyItemRangeInserted(originalIndex + 1, range); } } }); } private void executeReviewsSpiceRequest() { GetReviews.GetReviewList reviewRequest = new GetReviews.GetReviewList(); reviewRequest.setOrderBy("rand"); reviewRequest.store_id = getStoreId(); reviewRequest.homePage = isHomePage(); reviewRequest.limit = 1; // in order to present the right info on screen after a screen rotation, always pass the bucketsize as cachekey spiceManager.execute(reviewRequest, "review-store-" + getStoreId() + "--" + BUCKET_SIZE, useCache ? DurationInMillis.ONE_HOUR : DurationInMillis.ALWAYS_EXPIRED, new RequestListener<ReviewListJson>() { @Override public void onRequestFailure(SpiceException spiceException) { } @Override public void onRequestSuccess(ReviewListJson reviewListJson) { if ("OK".equals(reviewListJson.status) && reviewListJson.reviews != null && reviewListJson.reviews.size() > 0) { // range is the size of the list. Because the HeaderRow replaces the placeholder, it's not considered an insertion // why is this important? because of notifyItemRangeInserted int range = reviewListJson.reviews.size(); int index = 0, originalIndex = 0; boolean reviewPlaceHolderFound = false; for (Displayable display : displayableList) { if (display instanceof ReviewPlaceHolderRow) { reviewPlaceHolderFound = true; originalIndex = index = displayableList.indexOf(display); break; } } // prevent multiple requests adding to the beginning of the list if (!reviewPlaceHolderFound) return; HeaderRow header = new HeaderRow(getString(R.string.more_reviews), true, REVIEWS_TYPE, BUCKET_SIZE, isHomePage(), getStoreId()); displayableList.set(index++, header); for (Review review : reviewListJson.reviews) { ReviewRowItem reviewRowItem = getReviewRow(review); displayableList.add(index++, reviewRowItem); } adapter.notifyItemRangeInserted(originalIndex + 1, range); } } }); } @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); bindViews(getView()); if (savedInstanceState != null) { args = savedInstanceState; } else { args = getArguments(); } // args is null on Fragment's first entry if (args != null) { storeId = args.getLong(Constants.STOREID_KEY, 0); storeName = args.getString(Constants.STORENAME_KEY); versionName = args.getString(Constants.VERSIONNAME_KEY); packageName = args.getString(Constants.PACKAGENAME_KEY); storeTheme = EnumStoreTheme.values()[args.getInt(Constants.THEME_KEY, 0)]; subscribed = args.getBoolean(Constants.STORE_SUBSCRIBED_KEY, false); progressBar.getIndeterminateDrawable().setColorFilter(getResources().getColor(storeTheme.getStoreHeader()), PorterDuff.Mode.SRC_IN); retryNoNetwork.getBackground().setColorFilter(getResources().getColor(storeTheme.getStoreHeader()), PorterDuff.Mode.SRC_IN); retryError.getBackground().setColorFilter(getResources().getColor(storeTheme.getStoreHeader()), PorterDuff.Mode.SRC_IN); } swipeContainer.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh() { offset = 0; executeSpiceRequest(false); } }); swipeContainer.setColorSchemeResources(R.color.default_progress_bar_color, R.color.default_color, R.color.default_progress_bar_color, R.color .default_color); executeSpiceRequest(true); } @Override public void onDestroyView() { adapter = null; super.onDestroyView(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (args != null) { outState.putLong(Constants.STOREID_KEY, storeId); outState.putString(Constants.STORENAME_KEY, storeName); outState.putInt(Constants.THEME_KEY, storeTheme.ordinal()); outState.putBoolean(Constants.STORE_SUBSCRIBED_KEY, subscribed); outState.putString("sponsoredCache", sponsoredCache); outState.putString(Constants.VERSIONNAME_KEY, versionName); outState.putString(Constants.PACKAGENAME_KEY, packageName); } } @NonNull protected StoreHeaderRow getStoreHeaderRow(StoreHomeTab tab) { StoreHeaderRow storeHeaderRow = new StoreHeaderRow(BUCKET_SIZE); storeHeaderRow.id = tab.store.nodes.meta.data.id.longValue(); storeHeaderRow.name = tab.store.nodes.meta.data.name; storeHeaderRow.avatar = tab.store.nodes.meta.data.avatar; storeHeaderRow.description = tab.store.nodes.meta.data.appearance.description; storeHeaderRow.apps = tab.store.nodes.meta.data.stats.apps.longValue(); storeHeaderRow.subscribers = tab.store.nodes.meta.data.stats.subscribers.longValue(); storeHeaderRow.downloads = tab.store.nodes.meta.data.stats.downloads.longValue(); return storeHeaderRow; } @NonNull protected ReviewRowItem getReviewRow(Review review) { ReviewRowItem reviewRowItem = new ReviewRowItem(BUCKET_SIZE); reviewRowItem.appIcon = review.apk.icon; reviewRowItem.appName = review.apk.title; reviewRowItem.avatar = review.user.avatar; reviewRowItem.reviewer = review.user.name; reviewRowItem.description = review.finalVerdict; reviewRowItem.rating = review.average.floatValue(); reviewRowItem.reviewId = review.id; return reviewRowItem; } @NonNull protected AdItem getAdItem(ApkSuggestionJson.Ads ad) { AdItem adItem = new AdItem(BUCKET_SIZE); adItem.setSpanSize(2); adItem.appName = ad.data.name; adItem.icon = ad.data.icon; adItem.packageName = ad.data.packageName; adItem.storeName = ad.data.repo; adItem.id = ad.data.id.longValue(); adItem.adId = ad.info.ad_id; adItem.cpcUrl = ad.info.cpc_url; adItem.cpiUrl = ad.info.cpi_url; adItem.cpdUrl = ad.info.cpd_url; adItem.partnerName = ad.partner != null ? ad.partner.partnerInfo.name : null; adItem.partnerClickUrl = ad.partner != null ? ad.partner.partnerData.click_url : null; return adItem; } @NonNull protected TimelineRow getTimelineRow(TimelineListAPKsJson.UserApk userApk) { TimelineRow timeline = new TimelineRow(BUCKET_SIZE); timeline.setSpanSize(2); timeline.appName = userApk.apk.name; timeline.appIcon = parseIcon(userApk.apk); timeline.appFriend = AptoideUtils.StringUtils.getFormattedString(getContext(), R.string.installed_this, userApk.info.username); timeline.userAvatar = userApk.info.avatar; timeline.repoName = userApk.apk.repo; timeline.md5sum = userApk.apk.md5sum; return timeline; } @NonNull private String parseIcon(TimelineListAPKsJson.UserApk.APK apk) { String icon = apk.icon_hd; if (icon == null) { icon = apk.icon; } if (icon.contains("_icon")) { String[] splittedUrl = icon.split("\\.(?=[^\\.]+$)"); icon = splittedUrl[0] + "_" + IconSizeUtils.generateSizeString(getContext()) + "." + splittedUrl[1]; } return icon; } public CharSequence parseActionUrlIntoCacheKey(String actionUrl) { try { return actionUrl.replace(":", "").replace("/", ""); } catch (Exception e) { return UUID.randomUUID().toString(); } } protected long getStoreId() { return storeId; } public EnumStoreTheme getStoreTheme() { return storeTheme; } public String getStoreName() { return storeName; } public String getVersionName() { return versionName; } public String getPackageName() { return packageName; } // by default, no fragments are either HomePage nor StorePages. Override the methods in their subclasses according to their needs. protected boolean isHomePage() { return false; } protected boolean isStorePage() { return false; } protected abstract BaseAdapter getAdapter(); protected abstract String getBaseContext(); protected void bindViews(View view) { swipeContainer = (SwipeRefreshLayout )view.findViewById(R.id.swipe_container); progressBar = (ProgressBar )view.findViewById(R.id.progress_bar); layoutNoNetwork = (ScrollView )view.findViewById(R.id.no_network_connection); layoutError = (ScrollView )view.findViewById(R.id.error); retryError = (TextView )view.findViewById(R.id.retry_error); retryNoNetwork = (TextView )view.findViewById(R.id.retry_no_network); } }