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
-