1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.chrome.browser.banners;
6
7import android.app.PendingIntent;
8import android.graphics.Bitmap;
9import android.graphics.drawable.BitmapDrawable;
10import android.text.TextUtils;
11
12import org.chromium.base.CalledByNative;
13import org.chromium.base.JNINamespace;
14import org.chromium.chrome.browser.EmptyTabObserver;
15import org.chromium.chrome.browser.Tab;
16import org.chromium.chrome.browser.TabObserver;
17import org.chromium.content.browser.ContentViewCore;
18import org.chromium.content_public.browser.WebContents;
19import org.chromium.ui.R;
20
21/**
22 * Manages an AppBannerView for a Tab and its ContentView.
23 *
24 * The AppBannerManager manages a single AppBannerView, dismissing it when the user navigates to a
25 * new page or creating a new one when it detects that the current webpage is requesting a banner to
26 * be built. The actual observation of the WebContents (which triggers the automatic creation and
27 * removal of banners, among other things) is done by the native-side AppBannerManager.
28 *
29 * This Java-side class owns its native-side counterpart, which is basically used to grab resources
30 * from the network.
31 */
32@JNINamespace("banners")
33public class AppBannerManager implements AppBannerView.Observer, AppDetailsDelegate.Observer {
34    private static final String TAG = "AppBannerManager";
35
36    /** Retrieves information about a given package. */
37    private static AppDetailsDelegate sAppDetailsDelegate;
38
39    /** Pointer to the native side AppBannerManager. */
40    private final long mNativePointer;
41
42    /** Tab that the AppBannerView/AppBannerManager is owned by. */
43    private final Tab mTab;
44
45    /** ContentViewCore that the AppBannerView/AppBannerManager is currently attached to. */
46    private ContentViewCore mContentViewCore;
47
48    /** Current banner being shown. */
49    private AppBannerView mBannerView;
50
51    /** Data about the app being advertised. */
52    private AppData mAppData;
53
54    /**
55     * Checks if app banners are enabled.
56     * @return True if banners are enabled, false otherwise.
57     */
58    public static boolean isEnabled() {
59        return nativeIsEnabled();
60    }
61
62    /**
63     * Sets the delegate that provides information about a given package.
64     * @param delegate Delegate to use.  Previously set ones are destroyed.
65     */
66    public static void setAppDetailsDelegate(AppDetailsDelegate delegate) {
67        if (sAppDetailsDelegate != null) sAppDetailsDelegate.destroy();
68        sAppDetailsDelegate = delegate;
69    }
70
71    /**
72     * Constructs an AppBannerManager for the given tab.
73     * @param tab Tab that the AppBannerManager will be attached to.
74     */
75    public AppBannerManager(Tab tab) {
76        mNativePointer = nativeInit();
77        mTab = tab;
78        mTab.addObserver(createTabObserver());
79        updatePointers();
80    }
81
82    /**
83     * Creates a TabObserver for monitoring a Tab, used to react to changes in the ContentView
84     * or to trigger its own destruction.
85     * @return TabObserver that can be used to monitor a Tab.
86     */
87    private TabObserver createTabObserver() {
88        return new EmptyTabObserver() {
89            @Override
90            public void onWebContentsSwapped(Tab tab, boolean didStartLoad,
91                    boolean didFinishLoad) {
92                updatePointers();
93            }
94
95            @Override
96            public void onContentChanged(Tab tab) {
97                updatePointers();
98            }
99
100            @Override
101            public void onDestroyed(Tab tab) {
102                nativeDestroy(mNativePointer);
103                mContentViewCore = null;
104                resetState();
105            }
106        };
107    }
108
109    /**
110     * Updates which ContentView and WebContents the AppBannerView is monitoring.
111     */
112    private void updatePointers() {
113        if (mContentViewCore != mTab.getContentViewCore())
114            mContentViewCore = mTab.getContentViewCore();
115        nativeReplaceWebContents(mNativePointer, mTab.getWebContents());
116    }
117
118    /**
119     * Grabs package information for the banner asynchronously.
120     * @param url         URL for the page that is triggering the banner.
121     * @param packageName Name of the package that is being advertised.
122     */
123    @CalledByNative
124    private void prepareBanner(String url, String packageName) {
125        // Get rid of whatever banner is there currently.
126        if (mBannerView != null) dismissCurrentBanner(AppBannerMetricsIds.DISMISS_ERROR);
127
128        if (sAppDetailsDelegate == null || !isBannerForCurrentPage(url)) return;
129
130        int iconSize = AppBannerView.getIconSize(mContentViewCore.getContext());
131        sAppDetailsDelegate.getAppDetailsAsynchronously(this, url, packageName, iconSize);
132    }
133
134    /**
135     * Called when data about the package has been retrieved, which includes the url for the app's
136     * icon but not the icon Bitmap itself.  Kicks off a background task to retrieve it.
137     * @param data Data about the app.  Null if the task failed.
138     */
139    @Override
140    public void onAppDetailsRetrieved(AppData data) {
141        if (data == null || !isBannerForCurrentPage(data.siteUrl())) return;
142
143        mAppData = data;
144        String imageUrl = data.imageUrl();
145        if (TextUtils.isEmpty(imageUrl) || !nativeFetchIcon(mNativePointer, imageUrl)) resetState();
146    }
147
148    /**
149     * Called when all the data required to show a banner has finally been retrieved.
150     * Creates the banner and shows it, as long as the banner is still meant for the current page.
151     * @param imageUrl URL of the icon.
152     * @param appIcon Bitmap containing the icon itself.
153     * @return Whether or not the banner was created.
154     */
155    @CalledByNative
156    private boolean createBanner(String imageUrl, Bitmap appIcon) {
157        if (mAppData == null || !isBannerForCurrentPage(mAppData.siteUrl())) return false;
158
159        if (!TextUtils.equals(mAppData.imageUrl(), imageUrl)) {
160            resetState();
161            return false;
162        }
163
164        mAppData.setIcon(new BitmapDrawable(mContentViewCore.getContext().getResources(), appIcon));
165        mBannerView = AppBannerView.create(mContentViewCore, this, mAppData);
166        return true;
167    }
168
169    /**
170     * Dismisses whatever banner is currently being displayed. This is treated as an automatic
171     * dismissal and not one that blocks the banner from appearing in the future.
172     * @param dismissalType What triggered the dismissal.
173     */
174    @CalledByNative
175    private void dismissCurrentBanner(int dismissalType) {
176        if (mBannerView != null) mBannerView.dismiss(dismissalType);
177        resetState();
178    }
179
180    @Override
181    public void onBannerRemoved(AppBannerView banner) {
182        if (mBannerView != banner) return;
183        resetState();
184    }
185
186    @Override
187    public void onBannerBlocked(AppBannerView banner, String url, String packageName) {
188        if (mBannerView != banner) return;
189        nativeBlockBanner(mNativePointer, url, packageName);
190    }
191
192    @Override
193    public void onBannerDismissEvent(AppBannerView banner, int eventType) {
194        if (mBannerView != banner) return;
195        nativeRecordDismissEvent(eventType);
196    }
197
198    @Override
199    public void onBannerInstallEvent(AppBannerView banner, int eventType) {
200        if (mBannerView != banner) return;
201        nativeRecordInstallEvent(eventType);
202    }
203
204    @Override
205    public boolean onFireIntent(AppBannerView banner, PendingIntent intent) {
206        if (mBannerView != banner) return false;
207        return mTab.getWindowAndroid().showIntent(intent, banner, R.string.low_memory_error);
208    }
209
210    /**
211     * Resets all of the state, killing off any running tasks.
212     */
213    private void resetState() {
214        if (mBannerView != null) {
215            mBannerView.destroy();
216            mBannerView = null;
217        }
218
219        mAppData = null;
220    }
221
222    /**
223     * Checks to see if the banner is for the currently displayed page.
224     * @param bannerUrl URL that requested a banner.
225     * @return          True if the user is still on the same page.
226     */
227    private boolean isBannerForCurrentPage(String bannerUrl) {
228        return mContentViewCore != null &&
229               TextUtils.equals(mContentViewCore.getWebContents().getUrl(), bannerUrl);
230    }
231
232    private static native boolean nativeIsEnabled();
233    private native long nativeInit();
234    private native void nativeDestroy(long nativeAppBannerManager);
235    private native void nativeReplaceWebContents(long nativeAppBannerManager,
236            WebContents webContents);
237    private native void nativeBlockBanner(
238            long nativeAppBannerManager, String url, String packageName);
239    private native boolean nativeFetchIcon(long nativeAppBannerManager, String imageUrl);
240
241    // UMA tracking.
242    private static native void nativeRecordDismissEvent(int metric);
243    private static native void nativeRecordInstallEvent(int metric);
244}
245