Skip to content

Commit 96b07e3

Browse files
authored
Improve action click browser handling for FIAM and implement dismiss listener (firebase#1781)
1 parent a2dae12 commit 96b07e3

File tree

10 files changed

+260
-33
lines changed

10 files changed

+260
-33
lines changed

firebase-inappmessaging-display/firebase-inappmessaging-display.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ dependencies {
6868
exclude group: 'com.google.firebase', module: 'firebase-common'
6969
}
7070

71-
implementation 'com.google.android.gms:play-services-tasks:17.0.2'
71+
implementation 'com.google.android.gms:play-services-tasks:17.1.0'
7272
implementation 'androidx.appcompat:appcompat:1.1.0'
7373
implementation 'androidx.cardview:cardview:1.0.0'
7474
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
@@ -88,6 +88,7 @@ dependencies {
8888
testImplementation "junit:junit:4.12"
8989
testImplementation "org.mockito:mockito-core:2.25.0"
9090
testImplementation "com.google.truth:truth:1.0"
91+
testImplementation 'androidx.test:core:1.2.0'
9192
testImplementation 'com.google.guava:guava:27.1-android'
9293

9394
androidTestImplementation "org.mockito:mockito-core:2.25.0"

firebase-inappmessaging-display/src/main/java/com/google/firebase/inappmessaging/display/FirebaseInAppMessagingDisplay.java

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import android.annotation.SuppressLint;
2020
import android.app.Activity;
2121
import android.app.Application;
22+
import android.content.Intent;
23+
import android.content.pm.ResolveInfo;
2224
import android.content.res.Configuration;
2325
import android.net.Uri;
2426
import android.text.TextUtils;
@@ -305,25 +307,20 @@ public void onClick(View v) {
305307
};
306308

307309
Map<Action, View.OnClickListener> actionListeners = new HashMap<>();
308-
// If the message has an action, but not an action url, we dismiss when the action
309-
// button is clicked
310310
for (Action action : extractActions(inAppMessage)) {
311-
312311
final View.OnClickListener actionListener;
313-
314312
// TODO: need an onclick listener per action
313+
// If the message has an action and an action url, set up an intent to handle the url
315314
if (action != null && !TextUtils.isEmpty(action.getActionUrl())) {
316315
actionListener =
317316
new View.OnClickListener() {
318317
@Override
319318
public void onClick(View v) {
320319
if (callbacks != null) {
320+
Logging.logi("Calling callback for click action");
321321
callbacks.messageClicked(action);
322322
}
323-
final CustomTabsIntent i =
324-
new CustomTabsIntent.Builder().setShowTitle(true).build();
325-
326-
i.launchUrl(activity, Uri.parse(action.getActionUrl()));
323+
launchUriIntent(activity, Uri.parse(action.getActionUrl()));
327324
notifyFiamClick();
328325
// Ensure that we remove the displayed FIAM, and ensure that on re-load, the message
329326
// isn't re-displayed
@@ -333,7 +330,7 @@ public void onClick(View v) {
333330
}
334331
};
335332
} else {
336-
Logging.loge("No action url found for action.");
333+
Logging.logi("No action url found for action. Treating as dismiss.");
337334
actionListener = dismissListener;
338335
}
339336
actionListeners.put(action, actionListener);
@@ -535,4 +532,35 @@ private void notifyFiamDismiss() {
535532
fiamListener.onFiamDismiss();
536533
}
537534
}
535+
536+
private void launchUriIntent(Activity activity, Uri uri) {
537+
if (supportsCustomTabs(activity)) {
538+
// If we can launch a chrome view, try that.
539+
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder().build();
540+
Intent intent = customTabsIntent.intent;
541+
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
542+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
543+
customTabsIntent.launchUrl(activity, uri);
544+
} else {
545+
// If we can't launch a chrome view try to launch anything that can handle a URL.
546+
Intent browserIntent = new Intent(Intent.ACTION_VIEW, uri);
547+
ResolveInfo info = activity.getPackageManager().resolveActivity(browserIntent, 0);
548+
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
549+
browserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
550+
if (info != null) {
551+
activity.startActivity(browserIntent);
552+
} else {
553+
// If the device can't resolve a url then log, but don't crash.
554+
Logging.loge("Device cannot resolve intent for: " + Intent.ACTION_VIEW);
555+
}
556+
}
557+
}
558+
559+
private boolean supportsCustomTabs(Activity activity) {
560+
Intent customTabIntent = new Intent("android.support.customtabs.action.CustomTabsService");
561+
customTabIntent.setPackage("com.android.chrome");
562+
List<ResolveInfo> resolveInfos =
563+
activity.getPackageManager().queryIntentServices(customTabIntent, 0);
564+
return !resolveInfos.isEmpty();
565+
}
538566
}

firebase-inappmessaging-display/src/test/java/com/google/firebase/inappmessaging/display/FirebaseInAppMessagingDisplayTest.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import static org.robolectric.Shadows.shadowOf;
3838

3939
import android.app.Activity;
40+
import android.content.Intent;
41+
import android.content.pm.ResolveInfo;
4042
import android.content.res.Configuration;
4143
import android.net.Uri;
4244
import android.util.DisplayMetrics;
@@ -47,6 +49,7 @@
4749
import android.view.ViewTreeObserver;
4850
import android.widget.Button;
4951
import android.widget.ImageView;
52+
import androidx.test.core.app.ApplicationProvider;
5053
import com.google.firebase.inappmessaging.FirebaseInAppMessaging;
5154
import com.google.firebase.inappmessaging.FirebaseInAppMessagingDisplay;
5255
import com.google.firebase.inappmessaging.FirebaseInAppMessagingDisplayCallbacks;
@@ -83,6 +86,7 @@
8386
import org.robolectric.RobolectricTestRunner;
8487
import org.robolectric.annotation.Config;
8588
import org.robolectric.shadows.ShadowActivity;
89+
import org.robolectric.shadows.ShadowPackageManager;
8690

8791
@RunWith(RobolectricTestRunner.class)
8892
@Config(sdk = 21, qualifiers = "port")
@@ -126,6 +130,7 @@ public class FirebaseInAppMessagingDisplayTest {
126130
private TestActivity activity;
127131
private TestSecondActivity activityTwo;
128132
private ShadowActivity shadowActivity;
133+
private ShadowPackageManager shadowPackageManager;
129134
private FirebaseInAppMessagingDisplay listener;
130135
private FiamImageRequestCreator fakeRequestCreator = spy(new FakeRequestCreater(null));
131136

@@ -174,6 +179,8 @@ public InAppMessageLayoutConfig get() {
174179
}
175180
});
176181

182+
shadowPackageManager =
183+
shadowOf(ApplicationProvider.getApplicationContext().getPackageManager());
177184
activity = Robolectric.buildActivity(TestActivity.class).create().get();
178185
activityTwo = Robolectric.buildActivity(TestSecondActivity.class).create().get();
179186
shadowActivity = shadowOf(activity);
@@ -571,13 +578,33 @@ public void dismissTimer_onComplete_dismissesFiam() {
571578
}
572579

573580
@Test
574-
public void fiamClickListener_whenActionUrlProvided_opensCustomTab() {
581+
public void fiamClickListener_whenActionUrlProvided_andChromeAvailable_opensCustomTab() {
582+
final ResolveInfo resolveInfo = new ResolveInfo();
583+
resolveInfo.resolvePackageName = "garbage";
584+
final Intent customTabIntent =
585+
new Intent("android.support.customtabs.action.CustomTabsService");
586+
customTabIntent.setPackage("com.android.chrome");
587+
shadowPackageManager.addResolveInfoForIntent(customTabIntent, resolveInfo);
575588
resumeActivity(activity);
576589
listener.displayMessage(IMAGE_MESSAGE_MODEL, callbacks);
577590
verify(imageBindingWrapper)
578591
.inflate(onClickListenerArgCaptor.capture(), any(OnClickListener.class));
579592
onClickListenerArgCaptor.getValue().get(IMAGE_MESSAGE_MODEL.getAction()).onClick(null);
593+
assertThat(shadowActivity.getNextStartedActivity().getData())
594+
.isEqualTo(Uri.parse(ACTION_URL_STRING));
595+
}
580596

597+
@Test
598+
public void fiamClickListener_whenActionUrlProvided_andBrowserAvailable_opensBrowserIntent() {
599+
final ResolveInfo resolveInfo = new ResolveInfo();
600+
resolveInfo.resolvePackageName = "garbage";
601+
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(ACTION_URL_STRING));
602+
shadowPackageManager.addResolveInfoForIntent(browserIntent, resolveInfo);
603+
resumeActivity(activity);
604+
listener.displayMessage(IMAGE_MESSAGE_MODEL, callbacks);
605+
verify(imageBindingWrapper)
606+
.inflate(onClickListenerArgCaptor.capture(), any(OnClickListener.class));
607+
onClickListenerArgCaptor.getValue().get(IMAGE_MESSAGE_MODEL.getAction()).onClick(null);
581608
assertThat(shadowActivity.getNextStartedActivity().getData())
582609
.isEqualTo(Uri.parse(ACTION_URL_STRING));
583610
}

firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/FirebaseInAppMessaging.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,15 @@ public void addClickListener(@NonNull FirebaseInAppMessagingClickListener clickL
237237
developerListenerManager.addClickListener(clickListener);
238238
}
239239

240+
/**
241+
* Registers a dismiss listener with FIAM, which will be notified on every FIAM dismiss
242+
*
243+
* @param dismissListener
244+
*/
245+
public void addDismissListener(@NonNull FirebaseInAppMessagingDismissListener dismissListener) {
246+
developerListenerManager.addDismissListener(dismissListener);
247+
}
248+
240249
/**
241250
* Registers a display error listener with FIAM, which will be notified on every FIAM display
242251
* error
@@ -275,6 +284,18 @@ public void addClickListener(
275284
developerListenerManager.addClickListener(clickListener, executor);
276285
}
277286

287+
/**
288+
* Registers a dismiss listener with FIAM, which will be notified on every FIAM dismiss, and
289+
* triggered on the provided executor
290+
*
291+
* @param dismissListener
292+
* @param executor
293+
*/
294+
public void addDismissListener(
295+
@NonNull FirebaseInAppMessagingDismissListener dismissListener, @NonNull Executor executor) {
296+
developerListenerManager.addDismissListener(dismissListener, executor);
297+
}
298+
278299
/**
279300
* Registers a display error listener with FIAM, which will be notified on every FIAM display
280301
* error, and triggered on the provided executor
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.inappmessaging;
16+
17+
import androidx.annotation.NonNull;
18+
import com.google.firebase.inappmessaging.model.InAppMessage;
19+
20+
public interface FirebaseInAppMessagingDismissListener {
21+
22+
// Triggered when a message is dismissed (eg: "x" button, in the modal view)
23+
void messageDismissed(@NonNull InAppMessage inAppMessage);
24+
}

firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/DeveloperListenerManager.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import androidx.annotation.NonNull;
1818
import com.google.firebase.inappmessaging.FirebaseInAppMessagingClickListener;
19+
import com.google.firebase.inappmessaging.FirebaseInAppMessagingDismissListener;
1920
import com.google.firebase.inappmessaging.FirebaseInAppMessagingDisplayCallbacks;
2021
import com.google.firebase.inappmessaging.FirebaseInAppMessagingDisplayErrorListener;
2122
import com.google.firebase.inappmessaging.FirebaseInAppMessagingImpressionListener;
@@ -47,6 +48,8 @@ public class DeveloperListenerManager {
4748
public static DeveloperListenerManager instance = new DeveloperListenerManager();
4849
private Map<FirebaseInAppMessagingClickListener, ClicksExecutorAndListener>
4950
registeredClickListeners = new HashMap<>();
51+
private Map<FirebaseInAppMessagingDismissListener, DismissExecutorAndListener>
52+
registeredDismissListeners = new HashMap<>();
5053
private Map<FirebaseInAppMessagingDisplayErrorListener, ErrorsExecutorAndListener>
5154
registeredErrorListeners = new HashMap<>();;
5255
private Map<FirebaseInAppMessagingImpressionListener, ImpressionExecutorAndListener>
@@ -93,6 +96,14 @@ public void messageClicked(InAppMessage inAppMessage, Action action) {
9396
}
9497
}
9598

99+
public void messageDismissed(InAppMessage inAppMessage) {
100+
for (DismissExecutorAndListener listener : registeredDismissListeners.values()) {
101+
listener
102+
.withExecutor(CALLBACK_QUEUE_EXECUTOR)
103+
.execute(() -> listener.getListener().messageDismissed(inAppMessage));
104+
}
105+
}
106+
96107
// pass through from FirebaseInAppMessaging public api
97108
public void addImpressionListener(FirebaseInAppMessagingImpressionListener impressionListener) {
98109
registeredImpressionListeners.put(
@@ -103,6 +114,11 @@ public void addClickListener(FirebaseInAppMessagingClickListener clickListener)
103114
registeredClickListeners.put(clickListener, new ClicksExecutorAndListener(clickListener));
104115
}
105116

117+
public void addDismissListener(FirebaseInAppMessagingDismissListener dismissListener) {
118+
registeredDismissListeners.put(
119+
dismissListener, new DismissExecutorAndListener(dismissListener));
120+
}
121+
106122
public void addDisplayErrorListener(
107123
FirebaseInAppMessagingDisplayErrorListener displayErrorListener) {
108124
registeredErrorListeners.put(
@@ -122,6 +138,12 @@ public void addClickListener(
122138
clickListener, new ClicksExecutorAndListener(clickListener, executor));
123139
}
124140

141+
public void addDismissListener(
142+
FirebaseInAppMessagingDismissListener dismissListener, Executor executor) {
143+
registeredDismissListeners.put(
144+
dismissListener, new DismissExecutorAndListener(dismissListener, executor));
145+
}
146+
125147
public void addDisplayErrorListener(
126148
FirebaseInAppMessagingDisplayErrorListener displayErrorListener, Executor executor) {
127149
registeredErrorListeners.put(
@@ -229,6 +251,26 @@ public FirebaseInAppMessagingClickListener getListener() {
229251
}
230252
}
231253

254+
private static class DismissExecutorAndListener
255+
extends ExecutorAndListener<FirebaseInAppMessagingDismissListener> {
256+
FirebaseInAppMessagingDismissListener listener;
257+
258+
public DismissExecutorAndListener(FirebaseInAppMessagingDismissListener listener, Executor e) {
259+
super(e);
260+
this.listener = listener;
261+
}
262+
263+
public DismissExecutorAndListener(FirebaseInAppMessagingDismissListener listener) {
264+
super(null);
265+
this.listener = listener;
266+
}
267+
268+
@Override
269+
public FirebaseInAppMessagingDismissListener getListener() {
270+
return listener;
271+
}
272+
}
273+
232274
private static class ErrorsExecutorAndListener
233275
extends ExecutorAndListener<FirebaseInAppMessagingDisplayErrorListener> {
234276
FirebaseInAppMessagingDisplayErrorListener listener;

firebase-inappmessaging/src/main/java/com/google/firebase/inappmessaging/internal/MetricsLoggerClient.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ void logDismiss(InAppMessage message, InAppMessagingDismissType dismissType) {
161161
// For dismiss log to analytics as well
162162
logEventAsync(message, AnalyticsConstants.ANALYTICS_DISMISS_EVENT, false);
163163
}
164+
// No matter what, always trigger developer callbacks
165+
developerListenerManager.messageDismissed(message);
164166
}
165167

166168
private CampaignAnalytics createEventEntry(

0 commit comments

Comments
 (0)