diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonInternalImpl.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonInternalImpl.java index 40e5fa7..c357154 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonInternalImpl.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonInternalImpl.java @@ -34,6 +34,7 @@ import com.usebutton.merchant.exception.ApplicationIdNotFoundException; import java.util.ArrayList; +import java.util.concurrent.Executor; /** * ButtonInternalImpl class should implement everything needed for {@link ButtonMerchant} @@ -52,8 +53,16 @@ final class ButtonInternalImpl implements ButtonInternal { @VisibleForTesting ArrayList attributionTokenListeners; - ButtonInternalImpl() { + /** + * An {@link Executor} that ensures callbacks are run on the main thread. + * + * @see MainThreadExecutor + */ + private final Executor executor; + + ButtonInternalImpl(Executor executor) { this.attributionTokenListeners = new ArrayList<>(); + this.executor = executor; } public void configure(ButtonRepository buttonRepository, String applicationId) { @@ -82,7 +91,12 @@ public void trackOrder(ButtonRepository buttonRepository, DeviceManager manager, @NonNull Order order, @Nullable final UserActivityListener listener) { if (buttonRepository.getApplicationId() == null) { if (listener != null) { - listener.onResult(new ApplicationIdNotFoundException()); + executor.execute(new Runnable() { + @Override + public void run() { + listener.onResult(new ApplicationIdNotFoundException()); + } + }); } return; @@ -93,14 +107,24 @@ public void trackOrder(ButtonRepository buttonRepository, DeviceManager manager, @Override public void onTaskComplete(@Nullable Object object) { if (listener != null) { - listener.onResult(null); + executor.execute(new Runnable() { + @Override + public void run() { + listener.onResult(null); + } + }); } } @Override - public void onTaskError(Throwable throwable) { + public void onTaskError(final Throwable throwable) { if (listener != null) { - listener.onResult(throwable); + executor.execute(new Runnable() { + @Override + public void run() { + listener.onResult(throwable); + } + }); } } }; @@ -137,12 +161,22 @@ public void handlePostInstallIntent(final ButtonRepository buttonRepository, DeviceManager deviceManager) { if (buttonRepository.getApplicationId() == null) { - listener.onResult(null, new ApplicationIdNotFoundException()); + executor.execute(new Runnable() { + @Override + public void run() { + listener.onResult(null, new ApplicationIdNotFoundException()); + } + }); return; } if (deviceManager.isOldInstallation() || buttonRepository.checkedDeferredDeepLink()) { - listener.onResult(null, null); + executor.execute(new Runnable() { + @Override + public void run() { + listener.onResult(null, null); + } + }); return; } @@ -153,7 +187,7 @@ public void onTaskComplete(@Nullable PostInstallLink postInstallLink) { if (postInstallLink != null && postInstallLink.isMatch() && postInstallLink.getAction() != null) { - Intent deepLinkIntent = + final Intent deepLinkIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(postInstallLink.getAction())); deepLinkIntent.setPackage(packageName); @@ -162,16 +196,31 @@ public void onTaskComplete(@Nullable PostInstallLink postInstallLink) { setAttributionToken(buttonRepository, attribution.getBtnRef()); } - listener.onResult(deepLinkIntent, null); + executor.execute(new Runnable() { + @Override + public void run() { + listener.onResult(deepLinkIntent, null); + } + }); return; } - listener.onResult(null, null); + executor.execute(new Runnable() { + @Override + public void run() { + listener.onResult(null, null); + } + }); } @Override - public void onTaskError(Throwable throwable) { - listener.onResult(null, throwable); + public void onTaskError(final Throwable throwable) { + executor.execute(new Runnable() { + @Override + public void run() { + listener.onResult(null, throwable); + } + }); } }, deviceManager); } @@ -183,15 +232,22 @@ public void onTaskError(Throwable throwable) { * @param buttonRepository {@link ButtonRepository} * @param attributionToken The attributionToken. */ - private void setAttributionToken(ButtonRepository buttonRepository, String attributionToken) { + private void setAttributionToken(ButtonRepository buttonRepository, + final String attributionToken) { if (attributionToken != null && !attributionToken.isEmpty()) { // check if the sourceToken has changed if (!attributionToken.equals(getAttributionToken(buttonRepository))) { // notify all listeners that the attributionToken has changed - for (ButtonMerchant.AttributionTokenListener listener : attributionTokenListeners) { + for (final ButtonMerchant.AttributionTokenListener listener + : attributionTokenListeners) { if (listener != null) { - listener.onAttributionTokenChanged(attributionToken); + executor.execute(new Runnable() { + @Override + public void run() { + listener.onAttributionTokenChanged(attributionToken); + } + }); } } } diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java index 1e4d588..a4523e9 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java @@ -31,6 +31,7 @@ import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -43,8 +44,9 @@ private ButtonMerchant() { } + private static Executor executor = new MainThreadExecutor(); @VisibleForTesting - static ButtonInternal buttonInternal = new ButtonInternalImpl(); + static ButtonInternal buttonInternal = new ButtonInternalImpl(executor); private static ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -87,13 +89,13 @@ public static void trackOrder(@NonNull Context context, @NonNull Order order, * * For attribution to work correctly, you must: * * - * @return the last tracked Button attribution token. + * @return the last tracked Button attribution token. **/ @Nullable public static String getAttributionToken(@NonNull Context context) { diff --git a/button-merchant/src/main/java/com/usebutton/merchant/MainThreadExecutor.java b/button-merchant/src/main/java/com/usebutton/merchant/MainThreadExecutor.java new file mode 100644 index 0000000..b5288a4 --- /dev/null +++ b/button-merchant/src/main/java/com/usebutton/merchant/MainThreadExecutor.java @@ -0,0 +1,45 @@ +/* + * MainThreadExecutor.java + * + * Copyright (c) 2018 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant; + +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; + +import java.util.concurrent.Executor; + +/** + * Ensures that a given {@link Runnable} is posted and ran on the main thread. + */ +final class MainThreadExecutor implements Executor { + + private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(@NonNull Runnable command) { + handler.post(command); + } +} diff --git a/button-merchant/src/test/java/com/usebutton/merchant/ButtonInternalImplTest.java b/button-merchant/src/test/java/com/usebutton/merchant/ButtonInternalImplTest.java index f426f90..877a540 100644 --- a/button-merchant/src/test/java/com/usebutton/merchant/ButtonInternalImplTest.java +++ b/button-merchant/src/test/java/com/usebutton/merchant/ButtonInternalImplTest.java @@ -27,6 +27,7 @@ import android.content.Intent; import android.net.Uri; +import android.support.annotation.NonNull; import com.usebutton.merchant.exception.ApplicationIdNotFoundException; import com.usebutton.merchant.exception.ButtonNetworkException; @@ -37,6 +38,8 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; +import java.util.concurrent.Executor; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @@ -54,10 +57,12 @@ public class ButtonInternalImplTest { private ButtonInternalImpl buttonInternal; + private Executor executor; @Before public void setUp() { - buttonInternal = new ButtonInternalImpl(); + executor = new TestMainThreadExecutor(); + buttonInternal = new ButtonInternalImpl(executor); } @Test @@ -422,4 +427,12 @@ public void handlePostInstallIntent_checkedDeferredDeepLink_doNotUpdateCheckDefe verify(postInstallIntentListener).onResult((Intent) isNull(), (Throwable) isNull()); verify(buttonRepository, never()).updateCheckDeferredDeepLink(anyBoolean()); } + + private class TestMainThreadExecutor implements Executor { + + @Override + public void execute(@NonNull Runnable command) { + command.run(); + } + } } diff --git a/sample/src/main/java/com/usebutton/merchant/sample/KotlinActivity.kt b/sample/src/main/java/com/usebutton/merchant/sample/KotlinActivity.kt index b5cd6b1..76592b4 100644 --- a/sample/src/main/java/com/usebutton/merchant/sample/KotlinActivity.kt +++ b/sample/src/main/java/com/usebutton/merchant/sample/KotlinActivity.kt @@ -87,16 +87,12 @@ class KotlinActivity : AppCompatActivity() { private fun initAttributionTokenListener() { ButtonMerchant.addAttributionTokenListener(this) { token -> - runOnUiThread { - findViewById(id.attribution_token).text = token - } + findViewById(id.attribution_token).text = token } } private fun toastify(message: String) { - runOnUiThread { - Toast.makeText(this@KotlinActivity, message, Toast.LENGTH_SHORT).show() - } + Toast.makeText(this@KotlinActivity, message, Toast.LENGTH_SHORT).show() } companion object { diff --git a/sample/src/main/java/com/usebutton/merchant/sample/MainActivity.java b/sample/src/main/java/com/usebutton/merchant/sample/MainActivity.java index 090024b..5404e21 100644 --- a/sample/src/main/java/com/usebutton/merchant/sample/MainActivity.java +++ b/sample/src/main/java/com/usebutton/merchant/sample/MainActivity.java @@ -91,18 +91,13 @@ public void onClick(View v) { ButtonMerchant.trackOrder(context, order, new UserActivityListener() { @Override public void onResult(@Nullable final Throwable t) { - runOnUiThread(new Runnable() { - @Override - public void run() { - if (t == null) { - Toast.makeText(context, "Order track success", - Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(MainActivity.this, "Order track error", - Toast.LENGTH_SHORT).show(); - } - } - }); + if (t == null) { + Toast.makeText(context, "Order track success", + Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(MainActivity.this, "Order track error", + Toast.LENGTH_SHORT).show(); + } } }); } @@ -138,13 +133,8 @@ public void onClick(View v) { new ButtonMerchant.AttributionTokenListener() { @Override public void onAttributionTokenChanged(@NonNull final String token) { - runOnUiThread(new Runnable() { - @Override - public void run() { - TextView textView = findViewById(R.id.attribution_token); - textView.setText(token); - } - }); + TextView textView = findViewById(R.id.attribution_token); + textView.setText(token); } }); }