diff --git a/README.md b/README.md index 8426e8f3372f..9b00655c8b68 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,25 @@ test the project: You can use [Android Studio][3] by importing the project as a Gradle project. +### Additional Build Instructions for Windows ### + +The [visual editor][10] uses a linux-style symlink for its [assets folder][11], +which has to be converted to a Windows symlink. + +From git bash, inside the project root: + + $ rm libs/editor/WordPressEditor/src/main/assets + $ git ls-files --deleted -z | git update-index --assume-unchanged -z --stdin + +Then, from a Windows command prompt: + + mklink /D [PROJECT_ROOT]\libs\editor\WordPressEditor\src\main\assets %PROJECT_ROOT%\libs\editor\libs\editor-common\assets + +Finally, update `[PROJECT_ROOT]\.git\info\exclude` to ignore the symlink locally: + + # editor assets symlink + libs/editor/WordPressEditor/src/main/assets + ## Directory structure ## |-- libs # dependencies used to build debug variants @@ -122,3 +141,5 @@ be covered by a different license compatible with the GPLv2. [7]: https://developer.wordpress.com/docs/api/ [8]: https://facebook.github.io/buck [9]: https://facebook.github.io/watchman/docs/install.html +[10]: https://github.com/wordpress-mobile/WordPress-Editor-Android +[11]: https://github.com/wordpress-mobile/WordPress-Android/blob/develop/libs/editor/WordPressEditor/src/main/assets \ No newline at end of file diff --git a/WordPress/build.gradle b/WordPress/build.gradle index 73801ed72ddc..e67fdd86f3ce 100644 --- a/WordPress/build.gradle +++ b/WordPress/build.gradle @@ -151,7 +151,7 @@ task generateCrashlyticsConfig(group: "generate", description: "Generate Crashly inputFile.withInputStream { stream -> properties.load(stream) } - def crashlyticsApiKey = properties.getProperty('crashlytics.apikey', '0') + def crashlyticsApiKey = properties.getProperty('wp.crashlytics.apikey', '0') def writer = new FileWriter(outputFile) writer.write("""// auto-generated file from ${rootDir}/gradle.properties do not modify apiKey=${crashlyticsApiKey}""") diff --git a/WordPress/src/main/AndroidManifest.xml b/WordPress/src/main/AndroidManifest.xml index 3985170bcd6c..429fd210a423 100644 --- a/WordPress/src/main/AndroidManifest.xml +++ b/WordPress/src/main/AndroidManifest.xml @@ -256,7 +256,8 @@ + android:theme="@style/Calypso.NoActionBar"> + getQueryStrings() { + return getQueryStrings(null); + } + public static List getQueryStrings(String filter) { + List queries = new ArrayList<>(); + Cursor cursor; + if (TextUtils.isEmpty(filter)) { + cursor = ReaderDatabase.getReadableDb().rawQuery( + "SELECT query_string FROM tbl_search_history ORDER BY date_used DESC", null); + } else { + String likeFilter = filter + "%"; + cursor = ReaderDatabase.getReadableDb().rawQuery( + "SELECT query_string FROM tbl_search_history WHERE query_string LIKE ? ORDER BY date_used DESC", new String[]{likeFilter}); + } + + try { + while (cursor.moveToNext()) { + queries.add(cursor.getString(0)); + } + return queries; + } finally { + SqlUtils.closeCursor(cursor); + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderTagTable.java b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderTagTable.java index aa05cd8e7092..3867f940a119 100644 --- a/WordPress/src/main/java/org/wordpress/android/datasets/ReaderTagTable.java +++ b/WordPress/src/main/java/org/wordpress/android/datasets/ReaderTagTable.java @@ -29,7 +29,7 @@ protected static void createTables(SQLiteDatabase db) { + " tag_title TEXT COLLATE NOCASE," + " tag_type INTEGER DEFAULT 0," + " endpoint TEXT," - + " date_updated TEXT," + + " date_updated TEXT," + " PRIMARY KEY (tag_slug, tag_type)" + ")"); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java index e41604179971..b58c72b3aa41 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/FilteredRecyclerView.java @@ -1,6 +1,7 @@ package org.wordpress.android.ui; import android.content.Context; +import android.support.annotation.MenuRes; import android.support.design.widget.AppBarLayout; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; @@ -8,11 +9,11 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.LayoutInflater; +import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; -import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.RelativeLayout; import android.widget.Spinner; @@ -41,7 +42,6 @@ public class FilteredRecyclerView extends RelativeLayout { private RecyclerView mRecyclerView; private TextView mEmptyView; private View mCustomEmptyView; - private LinearLayout mCustomComponentsContainer; private Toolbar mToolbar; private AppBarLayout mAppBarLayout; @@ -124,7 +124,6 @@ private void init() { mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical)); - mCustomComponentsContainer = (LinearLayout) findViewById(R.id.custom_components_toolbar_container); mToolbar = (Toolbar) findViewById(R.id.toolbar_with_spinner); mAppBarLayout = (AppBarLayout) findViewById(R.id.app_bar_layout); @@ -278,15 +277,12 @@ public void hideLoadingProgress() { } /* - * use this to add custom control/component views to your toolbar. Components are added on the right side of the - * toolbar */ - public void addToolbarCustomControl(View v, OnClickListener clickListener){ - if (v != null){ - mCustomComponentsContainer.addView(v); - if (clickListener != null){ - v.setOnClickListener(clickListener); - } - } + * add a menu to the right side of the toolbar, returns the toolbar menu so the caller + * can act upon it + */ + public Menu addToolbarMenu(@MenuRes int menuResId) { + mToolbar.inflateMenu(menuResId); + return mToolbar.getMenu(); } public void setToolbarBackgroundColor(int color){ @@ -362,6 +358,10 @@ public void removeOnScrollListener(RecyclerView.OnScrollListener listener) { } } + public RecyclerView getInternalRecyclerView() { + return mRecyclerView; + } + public void hideToolbar(){ mAppBarLayout.setExpanded(false, true); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java index 9c92c40af34a..ba738ff63a57 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/main/WPMainActivity.java @@ -80,6 +80,14 @@ public interface OnScrollToTopListener { void onScrollToTop(); } + /* + * tab fragments implement this and return true if the fragment handles the back button + * and doesn't want the activity to handle it as well + */ + public interface OnActivityBackPressedListener { + boolean onActivityBackPressed(); + } + @Override public void onCreate(Bundle savedInstanceState) { ProfilingUtils.split("WPMainActivity.onCreate"); @@ -314,6 +322,23 @@ protected void onResume() { ProfilingUtils.stop(); } + @Override + public void onBackPressed() { + // let the fragment handle the back button if it implements our OnParentBackPressedListener + Fragment fragment = getActiveFragment(); + if (fragment instanceof OnActivityBackPressedListener) { + boolean handled = ((OnActivityBackPressedListener) fragment).onActivityBackPressed(); + if (handled) { + return; + } + } + super.onBackPressed(); + } + + private Fragment getActiveFragment() { + return mTabAdapter.getFragment(mViewPager.getCurrentItem()); + } + private void checkMagicLinkSignIn() { if (getIntent() != null) { if (getIntent().getBooleanExtra(MagicLinkSignInActivity.MAGIC_LOGIN, false)) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansActivity.java index 40bdd43c167f..813b8195125e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansActivity.java @@ -21,13 +21,18 @@ import android.widget.TextView; import android.widget.Toast; +import org.json.JSONException; +import org.json.JSONObject; import org.wordpress.android.BuildConfig; import org.wordpress.android.R; import org.wordpress.android.WordPress; +import org.wordpress.android.models.AccountHelper; +import org.wordpress.android.models.Blog; import org.wordpress.android.ui.plans.adapters.PlansPagerAdapter; import org.wordpress.android.ui.plans.models.Plan; import org.wordpress.android.ui.plans.util.IabHelper; import org.wordpress.android.ui.plans.util.IabResult; +import org.wordpress.android.ui.plans.util.Purchase; import org.wordpress.android.ui.prefs.AppPrefs; import org.wordpress.android.util.AniUtils; import org.wordpress.android.util.AppLog; @@ -43,6 +48,7 @@ public class PlansActivity extends AppCompatActivity { public static final String ARG_LOCAL_TABLE_BLOG_ID = "ARG_LOCAL_TABLE_BLOG_ID"; private static final String ARG_LOCAL_AVAILABLE_PLANS = "ARG_LOCAL_AVAILABLE_PLANS"; + private static final int PURCHASE_PLAN_REQUEST = 0; private int mLocalBlogID = -1; private Plan[] mAvailablePlans; @@ -52,6 +58,7 @@ public class PlansActivity extends AppCompatActivity { private TabLayout mTabLayout; private IabHelper mIabHelper; + private boolean mIABSetupDone = false; @Override public void onCreate(Bundle savedInstanceState) { @@ -135,18 +142,59 @@ public void onPause() { EventBus.getDefault().unregister(this); } - private void updatePurchaseUI(int position) { - Plan plan = getPageAdapter().getPlan(position); - boolean showPurchaseButton; - if (plan.isCurrentPlan()) { - showPurchaseButton = false; - } else { - // don't show the purchase button unless the plan at this position is "greater" than - // the current plan for this site - long currentPlanProductId = WordPress.wpDB.getPlanIdForLocalTableBlogId(mLocalBlogID); - showPurchaseButton = plan.isAvailable() && plan.getProductID() > currentPlanProductId; + /** + * The 'Buy' button should be available if the current plan is free, and the selected plan upgrade is available. + * @param position + * @return boolean - true if the current plan could be added to the blog. + */ + private boolean isBuyButtonAvailable(int position) { + if (!mIABSetupDone) { + return false; + } + long currentPlanProductId = WordPress.wpDB.getPlanIdForLocalTableBlogId(mLocalBlogID); + if (!PlansUtils.isFreePlan(currentPlanProductId)) { + return false; + } + final Plan plan = getPageAdapter().getPlan(position); + return plan.isAvailable() && !plan.isCurrentPlan(); + } + + /** + * The 'Upgrade' button should be available if the current plan is NOT free, + * and the selected plan upgrade is available, and the current plan ID < new plan ID. + * + * Note: Not used now, but will be when we'll implement upgrade of plan within the app. + * + * @param position + * @return boolean - true if the current plan could be upgraded to the blog. + */ + private boolean isUpgradeButtonAvailable(int position) { + long currentPlanProductId = WordPress.wpDB.getPlanIdForLocalTableBlogId(mLocalBlogID); + if (isBuyButtonAvailable(position) || PlansUtils.isFreePlan(currentPlanProductId)) { + return false; + } + + Plan currentPlan = PlansUtils.getPlan(mAvailablePlans, currentPlanProductId); + if (currentPlan == null) { + // Blog's current plan is not available anymore. Weird, it should be available, but not purchasable. + AppLog.w(AppLog.T.PLANS, "Blog's current plan with ID " + currentPlanProductId + "is not available anymore on wpcom!"); + return false; + } + + final Plan selectedPlan = getPageAdapter().getPlan(position); + + // No downgrade! + if (PlansUtils.isGreaterEquals(currentPlan, selectedPlan)) { + return false; } + return selectedPlan.isAvailable() && !selectedPlan.isCurrentPlan(); + } + + private void updatePurchaseUI(final int position) { + final Plan plan = getPageAdapter().getPlan(position); + boolean showPurchaseButton = isBuyButtonAvailable(position); + ViewGroup framePurchase = (ViewGroup) findViewById(R.id.frame_purchase); ViewGroup containerPurchase = (ViewGroup) findViewById(R.id.purchase_container); if (showPurchaseButton) { @@ -155,7 +203,7 @@ private void updatePurchaseUI(int position) { containerPurchase.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - startPurchaseProcess(); + startPurchaseProcess(position); } }); } else { @@ -170,7 +218,7 @@ public void onClick(View v) { } private void setupPlansUI() { - if (mAvailablePlans == null || mAvailablePlans.length == 0) { + if (mAvailablePlans == null || mAvailablePlans.length == 0) { // This should never be called with empty plans. Toast.makeText(PlansActivity.this, R.string.plans_loading_error, Toast.LENGTH_LONG).show(); finish(); @@ -301,14 +349,57 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } - private void startPurchaseProcess() { - // TODO: this should start the Google Play purchase process, for now it shows the - // post-purchase on-boarding - boolean isBusinessPlan = (mViewPager.getCurrentItem() == mViewPager.getAdapter().getCount() - 1); - Intent intent = new Intent(this, PlanPostPurchaseActivity.class); - intent.putExtra(PlanPostPurchaseActivity.ARG_IS_BUSINESS_PLAN, isBusinessPlan); - startActivity(intent); - finish(); + private void startPurchaseProcess(int position) { + final Plan plan = getPageAdapter().getPlan(position); + String sku = plan.getAndroidSKU(); + Blog currentBlog = WordPress.getBlog(mLocalBlogID); + JSONObject extraData = new JSONObject(); + try { + extraData.put("blog_id", currentBlog.getDotComBlogId()); + extraData.put("user_id", AccountHelper.getDefaultAccount().getUserId()); + } catch (JSONException e) { + AppLog.e(AppLog.T.PLANS, "Can't add extra info to purchase data!", e); + return; + } + + mIabHelper.launchSubscriptionPurchaseFlow(this, sku, PURCHASE_PLAN_REQUEST, + new IabHelper.OnIabPurchaseFinishedListener() { + public void onIabPurchaseFinished(IabResult result, Purchase info) { + if (result != null) { + AppLog.d(AppLog.T.PLANS, "IabResult: " + result.toString()); + if (result.isSuccess()) { + if (info != null) { + /* + Sync the purchase info with the wpcom backend, and enabled the product on the site. + We need to use an app setting here for security reasons. + If something bad happens during this sync, we need to re-sync it later (See onAppComesFromBackground in WordPress.java) + Without this initial sync the backend doesn't have any info about the purchase, and the product will NOT be enabled + without a manual action on backend side. + */ + AppPrefs.setInAppPurchaseRefreshRequired(true); + PlansUtils.synchIAPsWordPressCom(); + AppLog.d(AppLog.T.PLANS, "Purchase: " + info.toString()); + AppLog.d(AppLog.T.PLANS, "You have bought the " + info.getSku() + ". Excellent choice, adventurer!"); + boolean isBusinessPlan = (mViewPager.getCurrentItem() == mViewPager.getAdapter().getCount() - 1); + Intent intent = new Intent(PlansActivity.this, PlanPostPurchaseActivity.class); + intent.putExtra(PlanPostPurchaseActivity.ARG_IS_BUSINESS_PLAN, isBusinessPlan); + startActivity(intent); + } + } else { + AppLog.e(AppLog.T.PLANS, "Purchase failure: " + result.getMessage()); + // Not a success. It seems that the buy activity already shows an error. + // Or at least, it shows an error if you try to purchase a subscription you already own. + } + } + } + }, + extraData.toString()); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + mIabHelper.handleActivityResult(requestCode, resultCode, data); } /* @@ -319,8 +410,7 @@ private void startPurchaseProcess() { private void startInAppBillingHelper() { mIabHelper = new IabHelper(this, BuildConfig.APP_LICENSE_KEY); if (BuildConfig.DEBUG) { - String tag = AppLog.TAG + "-" + AppLog.T.PLANS.toString(); - mIabHelper.enableDebugLogging(true, tag); + mIabHelper.enableDebugLogging(true); } try { mIabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { @@ -328,6 +418,7 @@ private void startInAppBillingHelper() { public void onIabSetupFinished(IabResult result) { if (result.isSuccess()) { AppLog.d(AppLog.T.PLANS, "IAB started successfully"); + mIABSetupDone = true; } else { AppLog.w(AppLog.T.PLANS, "IAB failed with " + result); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansUtils.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansUtils.java index 10299596a95e..d50d11a35d58 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansUtils.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/PlansUtils.java @@ -1,5 +1,6 @@ package org.wordpress.android.ui.plans; +import android.os.AsyncTask; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; @@ -7,6 +8,8 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.AccountHelper; import org.wordpress.android.ui.plans.models.Feature; import org.wordpress.android.ui.plans.models.Plan; import org.wordpress.android.ui.prefs.AppPrefs; @@ -94,6 +97,43 @@ public static String getIconUrlForPlan(Plan plan, int iconSize) { return PhotonUtils.getPhotonImageUrl(plan.getIconUrl(), iconSize, iconSize); } + public static boolean isFreePlan(Plan plan) { + return isFreePlan(plan.getProductID()); + } + + /** + * Weather the plan ID is a free plan. + * + * @param planID - The plan ID + * @return boolean - true if the current blog is on a free plan. + */ + public static boolean isFreePlan(long planID) { + return planID == PlansConstants.JETPACK_FREE_PLAN_ID || planID == PlansConstants.FREE_PLAN_ID; + } + + /** + * Weather the plan A is "greater" than or "equal to" the plan B + * + * TODO: Improve this, since we're assuming that a greater plan ID meant a more expensive plan. + */ + public static boolean isGreaterEquals(Plan planA, Plan planB) { + return planA.getProductID() >= planB.getProductID(); + } + + public static Plan getPlan(Plan[] plans, long planID) { + if (plans == null) { + AppLog.w(AppLog.T.PLANS, "The passed plans list is null!!"); + return null; + } + for (Plan currentPlan: plans) { + if (currentPlan.getProductID() == planID) { + return currentPlan; + } + } + AppLog.w(AppLog.T.PLANS, "Plan with ID " + planID + " wasn't found in the plans list"); + return null; + } + /** * Removes stored plan data - for testing purposes */ @@ -102,4 +142,16 @@ public static void clearPlanData() { AppPrefs.setGlobalPlansFeatures(null); } + + /** + * Synch IAPs with wpcom backend. This need to be called to add/remove upgrades on wpcom side. + * Those upgrades the user has already bought/cancelled on mobile side (from the Google Store). + */ + public static boolean synchIAPsWordPressCom() { + if (AccountHelper.isSignedInWordPressDotCom() && AppPrefs.isInAppPurchaseRefreshRequired()) { + new UpdateIAPTask(WordPress.getContext()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + return true; + } + return false; + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/UpdateIAPTask.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/UpdateIAPTask.java new file mode 100644 index 000000000000..2e4ab2c200e5 --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/UpdateIAPTask.java @@ -0,0 +1,138 @@ +package org.wordpress.android.ui.plans; + +import android.content.Context; +import android.os.AsyncTask; + +import com.android.volley.VolleyError; +import com.wordpress.rest.RestRequest; + +import org.json.JSONException; +import org.json.JSONObject; +import org.wordpress.android.BuildConfig; +import org.wordpress.android.WordPress; +import org.wordpress.android.models.AccountHelper; +import org.wordpress.android.ui.plans.util.IabException; +import org.wordpress.android.ui.plans.util.IabHelper; +import org.wordpress.android.ui.plans.util.IabResult; +import org.wordpress.android.ui.plans.util.Inventory; +import org.wordpress.android.ui.plans.util.Purchase; +import org.wordpress.android.ui.prefs.AppPrefs; +import org.wordpress.android.util.AppLog; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Synch IAPs on the wpcom backend. This need to be called to add/remove upgrades on wpcom side. + * + */ +public class UpdateIAPTask extends AsyncTask { + private static final int GET_IAP_BINDER_TIMEOUT = 30000; + private static final String IAP_ENDPOINT = "/iap/validate"; + private final Context mContext; + private IabHelper mIabHelper; + private boolean mIABSetupDone = false; + + public UpdateIAPTask(Context context) { + mContext = context; + } + + @Override + protected Void doInBackground(Void... args) { + if (!AccountHelper.isSignedInWordPressDotCom()) { + return null; + } + + final CountDownLatch countDownLatch = new CountDownLatch(1); + + mIabHelper = new IabHelper(this.mContext, BuildConfig.APP_LICENSE_KEY); + if (BuildConfig.DEBUG) { + mIabHelper.enableDebugLogging(true); + } + try { + mIabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() { + @Override + public void onIabSetupFinished(IabResult result) { + if (result.isSuccess()) { + mIABSetupDone = true; + AppLog.d(AppLog.T.PLANS, "IAB started successfully"); + } else { + AppLog.w(AppLog.T.PLANS, "IAB failed with " + result); + } + countDownLatch.countDown(); + } + }); + try { + countDownLatch.await(GET_IAP_BINDER_TIMEOUT, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + AppLog.e(AppLog.T.PLANS, "IAP setup took too long! > " + GET_IAP_BINDER_TIMEOUT + " msecs!", e); + return null; + } + } catch (NullPointerException e) { + // will happen when play store isn't available on device + //AppLog.e(AppLog.T.PLANS, e); + AppLog.w(AppLog.T.PLANS, "Unable to start IAB helper. Happen when Play Store isn't available on device."); + return null; + } + + listIAPs(); + stopInAppBillingHelper(); + return null; + } + + private void listIAPs() { + if (mIabHelper == null || !mIABSetupDone) { + return; + } + try { + Inventory inventory = mIabHelper.queryInventory(true, null, null); + List iaps = inventory.getAllPurchases(); + for (Purchase purchase : iaps) { + AppLog.d(AppLog.T.PLANS, "Original purchase JSON " + purchase.getOriginalJson()); + try { + JSONObject developerPayload = new JSONObject(purchase.getDeveloperPayload()); + Map params = new HashMap<>(); + params.put("blog_id", developerPayload.getString("blog_id")); + params.put("iap_sku", purchase.getSku()); + params.put("iap_token", purchase.getToken()); + WordPress.getRestClientUtilsV1_1().post(IAP_ENDPOINT, params, null, + new RestRequest.Listener() { + @Override + public void onResponse(JSONObject response) { + if (response != null) { + AppLog.d(AppLog.T.PLANS, "Response from the server: " + response.toString()); + } + AppPrefs.setInAppPurchaseRefreshRequired(false); + } + }, + new RestRequest.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + AppLog.e(AppLog.T.PLANS, "Response from the server", error); + } + }); + } catch (JSONException e) { + AppLog.e(AppLog.T.PLANS, "Unable to parse IAP info - " + purchase.getOriginalJson(), e); + } + } + } catch (IabException e) { + AppLog.e(AppLog.T.PLANS, "Unable to refresh the inventory", e); + } + } + + private void stopInAppBillingHelper() { + if (mIabHelper != null) { + try { + mIabHelper.dispose(); + } catch (IllegalArgumentException e) { + // this can happen if the IAB helper was created but failed to bind to its service + // when started, which will occur on emulators + AppLog.w(AppLog.T.PLANS, "Unable to dispose IAB helper"); + } + mIabHelper = null; + } + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/util/IabHelper.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/util/IabHelper.java index 86bff92d00f8..0b14aae8fc4e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/util/IabHelper.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/util/IabHelper.java @@ -32,6 +32,7 @@ import com.android.vending.billing.IInAppBillingService; import org.json.JSONException; +import org.wordpress.android.util.AppLog; import java.util.ArrayList; import java.util.List; @@ -1037,14 +1038,14 @@ public void run() { } void logDebug(String msg) { - if (mDebugLog) Log.d(mDebugTag, msg); + if (mDebugLog) AppLog.d(AppLog.T.PLANS, msg); } void logError(String msg) { - Log.e(mDebugTag, "In-app billing error: " + msg); + AppLog.e(AppLog.T.PLANS, "In-app billing error: " + msg); } void logWarn(String msg) { - Log.w(mDebugTag, "In-app billing warning: " + msg); + AppLog.w(AppLog.T.PLANS, "In-app billing warning: " + msg); } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/plans/util/Inventory.java b/WordPress/src/main/java/org/wordpress/android/ui/plans/util/Inventory.java index b4508beecb99..9c8467e984f2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/plans/util/Inventory.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/plans/util/Inventory.java @@ -63,12 +63,12 @@ public void erasePurchase(String sku) { } /** Returns a list of all owned product IDs. */ - List getAllOwnedSkus() { + public List getAllOwnedSkus() { return new ArrayList(mPurchaseMap.keySet()); } /** Returns a list of all owned product IDs of a given type */ - List getAllOwnedSkus(String itemType) { + public List getAllOwnedSkus(String itemType) { List result = new ArrayList(); for (Purchase p : mPurchaseMap.values()) { if (p.getItemType().equals(itemType)) result.add(p.getSku()); @@ -77,7 +77,7 @@ List getAllOwnedSkus(String itemType) { } /** Returns a list of all purchases. */ - List getAllPurchases() { + public List getAllPurchases() { return new ArrayList(mPurchaseMap.values()); } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java index a7e5b172ad09..b52231f479fe 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/prefs/AppPrefs.java @@ -82,6 +82,9 @@ public enum UndeletablePrefKey implements PrefKey { // Global plans features GLOBAL_PLANS_PLANS_FEATURES, + // When we need to sync IAP data with the wpcom backend + IAP_SYNC_REQUIRED, + // When we need to show the Gravatar Change Promo Tooltip GRAVATAR_CHANGE_PROMO_REQUIRED, } @@ -377,4 +380,11 @@ public static void setGlobalPlansFeatures(String jsonOfFeatures) { public static String getGlobalPlansFeatures() { return getString(UndeletablePrefKey.GLOBAL_PLANS_PLANS_FEATURES, ""); } + + public static boolean isInAppPurchaseRefreshRequired() { + return getBoolean(UndeletablePrefKey.IAP_SYNC_REQUIRED, false); + } + public static void setInAppPurchaseRefreshRequired(boolean required) { + setBoolean(UndeletablePrefKey.IAP_SYNC_REQUIRED, required); + } } diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java index f79347c1d6b8..aaae767806e5 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderActivityLauncher.java @@ -14,6 +14,7 @@ import org.wordpress.android.R; import org.wordpress.android.analytics.AnalyticsTracker; +import org.wordpress.android.datasets.ReaderSearchTable; import org.wordpress.android.models.AccountHelper; import org.wordpress.android.models.ReaderComment; import org.wordpress.android.models.ReaderPost; @@ -134,6 +135,21 @@ public static void showReaderTagPreview(Context context, ReaderTag tag) { context.startActivity(intent); } + public static void showReaderSearchResults(Context context, String query) { + if (TextUtils.isEmpty(query)) return; + + // record this search query + ReaderSearchTable.addOrUpdateQueryString(query); + + // TODO: track analytics + //AnalyticsTracker.track(AnalyticsTracker.Stat.???); + + Intent intent = new Intent(context, ReaderPostListActivity.class); + intent.putExtra(ReaderConstants.ARG_SEARCH_QUERY, query); + intent.putExtra(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.SEARCH_RESULTS); + context.startActivity(intent); + } + /* * show comments for the passed Ids diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java index b5f7181799bf..f4732f21ec5c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderConstants.java @@ -23,6 +23,7 @@ public class ReaderConstants { static final String ARG_POST_LIST_TYPE = "post_list_type"; static final String ARG_CONTENT = "content"; static final String ARG_IS_SINGLE_POST = "is_single_post"; + static final String ARG_SEARCH_QUERY = "search_query"; static final String KEY_ALREADY_UPDATED = "already_updated"; static final String KEY_ALREADY_REQUESTED = "already_requested"; diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java index b1ea96be4c1f..36b44f9edb67 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListActivity.java @@ -8,6 +8,7 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; +import android.text.TextUtils; import android.view.MenuItem; import org.wordpress.android.R; @@ -58,6 +59,12 @@ public void onCreate(Bundle savedInstanceState) { if (tag != null && savedInstanceState == null) { showListFragmentForTag(tag, mPostListType); } + } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { + String query = getIntent().getStringExtra(ReaderConstants.ARG_SEARCH_QUERY); + if (!TextUtils.isEmpty(query) && savedInstanceState == null) { + setTitle(String.format(getString(R.string.reader_title_search_results), query)); + showListFragmentForSearch(query); + } } } @@ -105,7 +112,7 @@ public void onSaveInstanceState(@NonNull Bundle outState) { @Override public void onBackPressed() { ReaderPostListFragment fragment = getListFragment(); - if (fragment == null || !fragment.goBackInTagHistory()) { + if (fragment == null || !fragment.onActivityBackPressed()) { super.onBackPressed(); } } @@ -158,6 +165,20 @@ private void showListFragmentForFeed(long feedId) { .commit(); } + /* + * show fragment containing list of posts matching the passed search query + */ + private void showListFragmentForSearch(@NonNull String query) { + if (isFinishing()) { + return; + } + Fragment fragment = ReaderPostListFragment.newInstanceForSearch(query); + getFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, fragment, getString(R.string.fragment_tag_reader_post_list)) + .commit(); + } + private ReaderPostListFragment getListFragment() { Fragment fragment = getFragmentManager().findFragmentByTag(getString(R.string.fragment_tag_reader_post_list)); if (fragment == null) { diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java index c12e5a0287f1..430b0f95738c 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderPostListFragment.java @@ -4,10 +4,16 @@ import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; +import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.ListPopupWindow; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.SearchView; +import android.text.TextUtils; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.animation.Animation; @@ -39,6 +45,7 @@ import org.wordpress.android.ui.reader.actions.ReaderBlogActions.BlockedBlogResult; import org.wordpress.android.ui.reader.adapters.ReaderMenuAdapter; import org.wordpress.android.ui.reader.adapters.ReaderPostAdapter; +import org.wordpress.android.ui.reader.adapters.ReaderSearchSuggestionAdapter; import org.wordpress.android.ui.reader.services.ReaderPostService; import org.wordpress.android.ui.reader.services.ReaderPostService.UpdateAction; import org.wordpress.android.ui.reader.services.ReaderUpdateService; @@ -50,6 +57,7 @@ import org.wordpress.android.util.AppLog; import org.wordpress.android.util.AppLog.T; import org.wordpress.android.util.DateTimeUtils; +import org.wordpress.android.util.DisplayUtils; import org.wordpress.android.util.NetworkUtils; import org.wordpress.android.util.ToastUtils; import org.wordpress.android.util.WPActivityUtils; @@ -69,20 +77,29 @@ public class ReaderPostListFragment extends Fragment implements ReaderInterfaces.OnPostSelectedListener, ReaderInterfaces.OnTagSelectedListener, ReaderInterfaces.OnPostPopupListener, + WPMainActivity.OnActivityBackPressedListener, WPMainActivity.OnScrollToTopListener { private ReaderPostAdapter mPostAdapter; + private ReaderSearchSuggestionAdapter mSearchSuggestionAdapter; + private FilteredRecyclerView mRecyclerView; private boolean mFirstLoad = true; private ReaderTagList mTags = new ReaderTagList(); private View mNewPostsBar; private View mEmptyView; + private View mEmptyViewBoxImages; private ProgressBar mProgress; + private SearchView mSearchView; + private MenuItem mSettingsMenuItem; + private MenuItem mSearchMenuItem; + private ReaderTag mCurrentTag; private long mCurrentBlogId; private long mCurrentFeedId; + private String mCurrentSearchQuery; private ReaderPostListType mPostListType; private int mRestorePosition; @@ -174,6 +191,19 @@ public static ReaderPostListFragment newInstanceForFeed(long feedId) { return fragment; } + public static ReaderPostListFragment newInstanceForSearch(@NonNull String query) { + AppLog.d(T.READER, "reader post list > newInstance (search)"); + + Bundle args = new Bundle(); + args.putString(ReaderConstants.ARG_SEARCH_QUERY, query); + args.putSerializable(ReaderConstants.ARG_POST_LIST_TYPE, ReaderPostListType.SEARCH_RESULTS); + + ReaderPostListFragment fragment = new ReaderPostListFragment(); + fragment.setArguments(args); + + return fragment; + } + @Override public void setArguments(Bundle args) { super.setArguments(args); @@ -188,6 +218,7 @@ public void setArguments(Bundle args) { mCurrentBlogId = args.getLong(ReaderConstants.ARG_BLOG_ID); mCurrentFeedId = args.getLong(ReaderConstants.ARG_FEED_ID); + mCurrentSearchQuery = args.getString(ReaderConstants.ARG_SEARCH_QUERY); if (getPostListType() == ReaderPostListType.TAG_PREVIEW && hasCurrentTag()) { mTagPreviewHistory.push(getCurrentTagName()); @@ -210,6 +241,9 @@ public void onCreate(Bundle savedInstanceState) { if (savedInstanceState.containsKey(ReaderConstants.ARG_FEED_ID)) { mCurrentFeedId = savedInstanceState.getLong(ReaderConstants.ARG_FEED_ID); } + if (savedInstanceState.containsKey(ReaderConstants.ARG_SEARCH_QUERY)) { + mCurrentSearchQuery = savedInstanceState.getString(ReaderConstants.ARG_SEARCH_QUERY); + } if (savedInstanceState.containsKey(ReaderConstants.ARG_POST_LIST_TYPE)) { mPostListType = (ReaderPostListType) savedInstanceState.getSerializable(ReaderConstants.ARG_POST_LIST_TYPE); } @@ -344,6 +378,7 @@ public void onSaveInstanceState(Bundle outState) { outState.putLong(ReaderConstants.ARG_BLOG_ID, mCurrentBlogId); outState.putLong(ReaderConstants.ARG_FEED_ID, mCurrentFeedId); + outState.putString(ReaderConstants.ARG_SEARCH_QUERY, mCurrentSearchQuery); outState.putBoolean(ReaderConstants.KEY_WAS_PAUSED, mWasPaused); outState.putBoolean(ReaderConstants.KEY_ALREADY_UPDATED, mHasUpdatedPosts); outState.putBoolean(ReaderConstants.KEY_FIRST_LOAD, mFirstLoad); @@ -371,7 +406,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa // view that appears when current tag/blog has no posts - box images in this view are // displayed and animated for tags only mEmptyView = rootView.findViewById(R.id.empty_custom_view); - mEmptyView.findViewById(R.id.layout_box_images).setVisibility(shouldShowBoxAndPagesAnimation() ? View.VISIBLE : View.GONE); + mEmptyViewBoxImages = mEmptyView.findViewById(R.id.layout_box_images); mRecyclerView.setLogT(AppLog.T.READER); mRecyclerView.setCustomEmptyView(mEmptyView); @@ -459,16 +494,6 @@ public void onShowCustomEmptyView (EmptyViewMessageType emptyViewMsgType) { int spacingVertical = context.getResources().getDimensionPixelSize(R.dimen.reader_card_gutters); mRecyclerView.addItemDecoration(new RecyclerItemDecoration(spacingHorizontal, spacingVertical, false)); - if (!ReaderUtils.isLoggedOutReader()) { - View settingsControl = inflater.inflate(R.layout.filtered_recyclerview_settings_control, null); - mRecyclerView.addToolbarCustomControl(settingsControl, new View.OnClickListener() { - @Override - public void onClick(View v) { - ReaderActivityLauncher.showReaderSubs(v.getContext()); - } - }); - ReaderUtils.setBackgroundToRoundRipple(settingsControl); - } // the following will change the look and feel of the toolbar to match the current design mRecyclerView.setToolbarBackgroundColor(getResources().getColor(R.color.blue_medium)); mRecyclerView.setToolbarSpinnerTextColor(getResources().getColor(R.color.white)); @@ -477,6 +502,11 @@ public void onClick(View v) { getResources().getDimensionPixelSize(R.dimen.margin_medium) + spacingHorizontal, getResources().getDimensionPixelSize(R.dimen.margin_extra_large) + spacingHorizontal); + // add a menu to the filtered recycler's toolbar + if (!ReaderUtils.isLoggedOutReader() && getPostListType() == ReaderPostListType.TAG_FOLLOWED) { + setupRecyclerToolbar(); + } + // bar that appears at top after new posts are loaded mNewPostsBar = rootView.findViewById(R.id.layout_new_posts); mNewPostsBar.setVisibility(View.GONE); @@ -488,7 +518,6 @@ public void onClick(View view) { } }); - // progress bar that appears when loading more posts mProgress = (ProgressBar) rootView.findViewById(R.id.progress_footer); mProgress.setVisibility(View.GONE); @@ -496,6 +525,147 @@ public void onClick(View view) { return rootView; } + /* + * adds a menu to the recycler's toolbar containing settings & search items - only called + * for followed tags + */ + private void setupRecyclerToolbar() { + Menu menu = mRecyclerView.addToolbarMenu(R.menu.reader_list); + mSettingsMenuItem = menu.findItem(R.id.menu_settings); + mSearchMenuItem = menu.findItem(R.id.menu_search); + + mSettingsMenuItem.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + ReaderActivityLauncher.showReaderSubs(getActivity()); + return true; + } + }); + + mSearchView = (SearchView) mSearchMenuItem.getActionView(); + mSearchView.setQueryHint(getString(R.string.reader_hint_post_search)); + mSearchView.setSubmitButtonEnabled(false); + mSearchView.setIconifiedByDefault(true); + mSearchView.setIconified(true); + + MenuItemCompat.setOnActionExpandListener(mSearchMenuItem, new MenuItemCompat.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + showSearchUI(); + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + hideSearchUI(); + return true; + } + }); + + mSearchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { + // TODO: reuse existing fragment + } else { + ReaderActivityLauncher.showReaderSearchResults(getActivity(), query); + } + mSearchMenuItem.collapseActionView(); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + mSearchSuggestionAdapter.populate(newText); + return true; + } + } + ); + } + + private void showSearchUI() { + if (!isAdded()) return; + + // hide settings icon + mSettingsMenuItem.setVisible(false); + + // create the suggestion adapter if it doesn't already exist, otherwise repopulate it + // so the latest suggestions appear + if (mSearchSuggestionAdapter == null) { + setupSearchSuggestions(); + } else { + mSearchSuggestionAdapter.populate(); + } + + // show message letting user know what they're querying, but only if the user is in + // portrait mode or the device is a tablet (since there's not enough space for the + // message when the virtual keyboard is visible) + boolean isLandscape = DisplayUtils.isLandscape(getActivity()); + boolean isTablet = DisplayUtils.isXLarge(getActivity()); + if (isTablet || !isLandscape) { + TextView txtSearchExplainer = (TextView) getView().findViewById(R.id.text_search_explainer); + if (txtSearchExplainer.getVisibility() != View.VISIBLE) { + AniUtils.fadeIn(txtSearchExplainer, AniUtils.Duration.LONG); + } + } + + // hide the recycler (post list) + RecyclerView recycler = mRecyclerView.getInternalRecyclerView(); + if (recycler != null && recycler.getVisibility() == View.VISIBLE) { + AniUtils.fadeOut(recycler, AniUtils.Duration.LONG); + } + } + + private void hideSearchUI() { + if (!isAdded()) return; + + // redisplay settings icon + mSettingsMenuItem.setVisible(true); + + // hide the explainer + TextView txtSearchExplainer = (TextView) getView().findViewById(R.id.text_search_explainer); + if (txtSearchExplainer.getVisibility() == View.VISIBLE) { + AniUtils.fadeOut(txtSearchExplainer, AniUtils.Duration.LONG); + } + txtSearchExplainer.setVisibility(View.GONE); + + // show the recycler + RecyclerView recycler = mRecyclerView.getInternalRecyclerView(); + if (recycler != null && recycler.getVisibility() != View.VISIBLE) { + AniUtils.fadeIn(recycler, AniUtils.Duration.LONG); + } + } + + /* + * create and assign the suggestion adapter for the search view + */ + private void setupSearchSuggestions() { + mSearchSuggestionAdapter = new ReaderSearchSuggestionAdapter(getActivity()); + mSearchView.setSuggestionsAdapter(mSearchSuggestionAdapter); + + mSearchView.setOnSuggestionListener(new SearchView.OnSuggestionListener() { + @Override + public boolean onSuggestionSelect(int position) { + return false; + } + + @Override + public boolean onSuggestionClick(int position) { + String query = mSearchSuggestionAdapter.getSuggestion(position); + if (!TextUtils.isEmpty(query)) { + mSearchView.setQuery(query, false); + } + return true; + } + }); + } + + /* + * is the search input showing? + */ + private boolean isSearchViewExpanded() { + return mSearchView != null && !mSearchView.isIconified(); + } /* * called when user taps follow item in popup menu for a post */ @@ -576,10 +746,9 @@ public void onClick(View v) { private boolean shouldShowBoxAndPagesAnimation() { return getPostListType().isTagType(); } + private void startBoxAndPagesAnimation() { - if (!isAdded()) { - return; - } + if (!isAdded()) return; ImageView page1 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page1); ImageView page2 = (ImageView) mEmptyView.findViewById(R.id.empty_tags_box_page2); @@ -591,49 +760,57 @@ private void startBoxAndPagesAnimation() { } private void setEmptyTitleAndDescription(boolean requestFailed) { - if (!isAdded()) { - return; - } + if (!isAdded()) return; - int titleResId; - int descriptionResId = 0; + String title; + String description = null; if (!NetworkUtils.isNetworkAvailable(getActivity())) { - titleResId = R.string.reader_empty_posts_no_connection; + title = getString(R.string.reader_empty_posts_no_connection); } else if (requestFailed) { - titleResId = R.string.reader_empty_posts_request_failed; + title = getString(R.string.reader_empty_posts_request_failed); } else if (isUpdating()) { - titleResId = R.string.reader_empty_posts_in_tag_updating; + title = getString(R.string.reader_empty_posts_in_tag_updating); } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) { - titleResId = R.string.reader_empty_posts_in_blog; + title = getString(R.string.reader_empty_posts_in_blog); } else if (getPostListType() == ReaderPostListType.TAG_FOLLOWED && hasCurrentTag()) { if (getCurrentTag().isFollowedSites()) { if (ReaderBlogTable.hasFollowedBlogs()) { - titleResId = R.string.reader_empty_followed_blogs_no_recent_posts_title; - descriptionResId = R.string.reader_empty_followed_blogs_no_recent_posts_description; + title = getString(R.string.reader_empty_followed_blogs_no_recent_posts_title); + description = getString(R.string.reader_empty_followed_blogs_no_recent_posts_description); } else { - titleResId = R.string.reader_empty_followed_blogs_title; - descriptionResId = R.string.reader_empty_followed_blogs_description; + title = getString(R.string.reader_empty_followed_blogs_title); + description = getString(R.string.reader_empty_followed_blogs_description); } } else if (getCurrentTag().isPostsILike()) { - titleResId = R.string.reader_empty_posts_liked; + title = getString(R.string.reader_empty_posts_liked); } else if (getCurrentTag().tagType == ReaderTagType.CUSTOM_LIST) { - titleResId = R.string.reader_empty_posts_in_custom_list; + title = getString(R.string.reader_empty_posts_in_custom_list); } else { - titleResId = R.string.reader_empty_posts_in_tag; + title = getString(R.string.reader_empty_posts_in_tag); } + } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { + title = getString(R.string.reader_empty_posts_in_search_title); + description = String.format(getString(R.string.reader_empty_posts_in_search_description), mCurrentSearchQuery); } else { - titleResId = R.string.reader_empty_posts_in_tag; + title = getString(R.string.reader_empty_posts_in_tag); } + setEmptyTitleAndDescription(title, description); + mEmptyViewBoxImages.setVisibility(shouldShowBoxAndPagesAnimation() ? View.VISIBLE : View.GONE); + } + + private void setEmptyTitleAndDescription(@NonNull String title, String description) { + if (!isAdded()) return; + TextView titleView = (TextView) mEmptyView.findViewById(R.id.title_empty); - titleView.setText(getString(titleResId)); + titleView.setText(title); TextView descriptionView = (TextView) mEmptyView.findViewById(R.id.description_empty); - if (descriptionResId == 0) { + if (description == null) { descriptionView.setVisibility(View.INVISIBLE); } else { - descriptionView.setText(getString(descriptionResId)); + descriptionView.setText(description); descriptionView.setVisibility(View.VISIBLE); } } @@ -720,6 +897,8 @@ private ReaderPostAdapter getPostAdapter() { mPostAdapter.setCurrentTag(getCurrentTag()); } else if (getPostListType() == ReaderPostListType.BLOG_PREVIEW) { mPostAdapter.setCurrentBlogAndFeed(mCurrentBlogId, mCurrentFeedId); + } else if (getPostListType() == ReaderPostListType.SEARCH_RESULTS) { + mPostAdapter.setCurrentSearchQuery(mCurrentSearchQuery); } } return mPostAdapter; @@ -783,12 +962,28 @@ && getPostAdapter().isCurrentTag(tag)) { updateCurrentTagIfTime(); } + /* + * called by the activity when user hits the back button - returns true if the back button + * is handled here and should be ignored by the activity + */ + @Override + public boolean onActivityBackPressed() { + if (isSearchViewExpanded()) { + mSearchMenuItem.collapseActionView(); + return true; + } else if (goBackInTagHistory()) { + return true; + } else { + return false; + } + } + /* * when previewing posts with a specific tag, a history of previewed tags is retained so * the user can navigate back through them - this is faster and requires less memory * than creating a new fragment for each previewed tag */ - boolean goBackInTagHistory() { + private boolean goBackInTagHistory() { if (mTagPreviewHistory.empty()) { return false; } @@ -876,6 +1071,13 @@ public void onEventMainThread(ReaderEvents.UpdatePostsEnded event) { return; } + // don't show new posts if user is entering a search query - posts will automatically + // appear when search is exited + if (isSearchViewExpanded()) { + AppLog.d(T.READER, "skipping post reload, search view is expanded"); + return; + } + // determine whether to show the "new posts" bar - when this is shown, the newly // downloaded posts aren't displayed until the user taps the bar - only appears // when there are new posts in a followed tag and the user has scrolled the list @@ -1177,9 +1379,7 @@ private void trackTagLoaded(ReaderTag tag) { */ @Override public void onShowPostPopup(View view, final ReaderPost post) { - if (view == null || post == null || !isAdded()) { - return; - } + if (view == null || post == null || !isAdded()) return; Context context = view.getContext(); final ListPopupWindow listPopup = new ListPopupWindow(context); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java index 74718bddd643..9b94773dada6 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/ReaderTypes.java @@ -5,10 +5,11 @@ public class ReaderTypes { public static final ReaderPostListType DEFAULT_POST_LIST_TYPE = ReaderPostListType.TAG_FOLLOWED; - public static enum ReaderPostListType { + public enum ReaderPostListType { TAG_FOLLOWED, // list posts in a followed tag TAG_PREVIEW, // list posts in a specific tag - BLOG_PREVIEW; // list posts in a specific blog/feed + BLOG_PREVIEW, // list posts in a specific blog/feed + SEARCH_RESULTS; // list posts matching a specific search keyword or phrase public boolean isTagType() { return this.equals(TAG_FOLLOWED) || this.equals(TAG_PREVIEW); diff --git a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java index 5d18ea78917e..08f422170f2e 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java +++ b/WordPress/src/main/java/org/wordpress/android/ui/reader/adapters/ReaderPostAdapter.java @@ -2,6 +2,7 @@ import android.content.Context; import android.os.AsyncTask; +import android.support.annotation.NonNull; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; import android.text.TextUtils; @@ -46,6 +47,7 @@ public class ReaderPostAdapter extends RecyclerView.Adapter mSearchSuggestions; + + public ReaderSearchSuggestionAdapter(Context context) { + super(context, + android.R.layout.simple_list_item_1, + null, + new String[]{"query"}, + new int[]{android.R.id.text1}, + 0); + populate(); + } + + public void populate() { + populate(null); + } + + public void populate(String filter) { + mSearchSuggestions = ReaderSearchTable.getQueryStrings(filter); + MatrixCursor cursor = new MatrixCursor(new String[]{"_id", "query"}); + + int id = 0; + for (String query : mSearchSuggestions) { + cursor.addRow(new Object[] {id++, query}); + } + + swapCursor(cursor); + } + + public String getSuggestion(int position) { + if (position < 0 || position > mSearchSuggestions.size() - 1) { + return null; + } + return mSearchSuggestions.get(position); + } + +} diff --git a/WordPress/src/main/res/layout-sw720dp/stats_activity.xml b/WordPress/src/main/res/layout-sw720dp/stats_activity.xml index 68c844301037..54585e28cd93 100644 --- a/WordPress/src/main/res/layout-sw720dp/stats_activity.xml +++ b/WordPress/src/main/res/layout-sw720dp/stats_activity.xml @@ -21,7 +21,7 @@ android:id="@+id/toolbar_filter" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/FilteredRecyclerViewFilterContainer" + style="@style/FilteredRecyclerViewToolbar" app:layout_scrollFlags="scroll|enterAlways"> - + android:layout_width="match_parent" + android:layout_height="wrap_content"> - - - - - - - - + android:overlapAnchor="false" /> diff --git a/WordPress/src/main/res/layout/filtered_recyclerview_settings_control.xml b/WordPress/src/main/res/layout/filtered_recyclerview_settings_control.xml deleted file mode 100644 index 606ba8dcc8a9..000000000000 --- a/WordPress/src/main/res/layout/filtered_recyclerview_settings_control.xml +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/WordPress/src/main/res/layout/reader_empty_view.xml b/WordPress/src/main/res/layout/reader_empty_view.xml index c20165c3eb47..f1a80a0b0c8f 100644 --- a/WordPress/src/main/res/layout/reader_empty_view.xml +++ b/WordPress/src/main/res/layout/reader_empty_view.xml @@ -13,7 +13,9 @@ android:id="@+id/layout_box_images" android:layout_width="wrap_content" android:layout_height="100dp" - android:layout_marginBottom="8dp"> + android:layout_marginBottom="8dp" + android:visibility="gone" + tools:visibility="visible"> + android:layout_height="match_parent" /> + tools:targetApi="LOLLIPOP" + tools:visibility="visible"> + + diff --git a/WordPress/src/main/res/layout/stats_activity.xml b/WordPress/src/main/res/layout/stats_activity.xml index 138773a15c82..c7d2e93b3ff2 100644 --- a/WordPress/src/main/res/layout/stats_activity.xml +++ b/WordPress/src/main/res/layout/stats_activity.xml @@ -21,7 +21,7 @@ android:id="@+id/toolbar_filter" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/FilteredRecyclerViewFilterContainer" + style="@style/FilteredRecyclerViewToolbar" app:contentInsetLeft="@dimen/margin_filter_spinner" app:contentInsetStart="@dimen/margin_filter_spinner" app:layout_scrollFlags="scroll|enterAlways"> diff --git a/WordPress/src/main/res/menu/reader_list.xml b/WordPress/src/main/res/menu/reader_list.xml new file mode 100644 index 000000000000..c7de94075540 --- /dev/null +++ b/WordPress/src/main/res/menu/reader_list.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 89aa349a6779..e7cb63451328 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1070,6 +1070,7 @@ Tags & Blogs %1$d of %2$d @string/comments + Search for %s Followed tags @@ -1095,6 +1096,7 @@ Reply to post… Reply to comment… Enter a URL or tag to follow + Search on WordPress.com New posts @@ -1114,6 +1116,7 @@ %,d followers SEND Load more posts + Search all public WordPress.com blogs Like @@ -1176,6 +1179,8 @@ You haven\'t liked any posts No comments yet This blog is empty + No Results + No posts found for \"%s\" for your language Originally posted by %1$s on %2$s diff --git a/WordPress/src/main/res/values/styles.xml b/WordPress/src/main/res/values/styles.xml index 56ee2b3ce2d7..afe93ffa570e 100644 --- a/WordPress/src/main/res/values/styles.xml +++ b/WordPress/src/main/res/values/styles.xml @@ -27,8 +27,12 @@ true @style/WordPress.SwipeToRefresh + @style/WordPress.SearchViewStyle + + @@ -86,10 +90,9 @@ @dimen/text_sz_large -