Commit 3791e101 authored by dfalcantara's avatar dfalcantara Committed by Commit bot

Allow installing apps via the AppBannerInfoBar

* Button cycles between "Install", "Installing", and "Open",
  depending on the installation status of the app being promoted.

* Clicking on the icon or the title of the AppBannerInfoBar should
  send the user to see details about the app that is being promoted.

* Open up access to LinkClicked() from ConfirmInfoBar.
  Piggyback on LinkClicked() to determine that the user wants to
  see the link related to the app.

* Add an ID to the icon of the infobar so it can be retrieved and
  have an OnClickListener attached to it.

* Update the status of the button when the user hops in and out
  of the Chrome application.

BUG=453170

Review URL: https://codereview.chromium.org/871103004

Cr-Commit-Position: refs/heads/master@{#315182}
parent 451a7802
......@@ -4,6 +4,12 @@
package org.chromium.chrome.browser.banners;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Looper;
import android.text.TextUtils;
import org.chromium.base.ApplicationStatus;
......@@ -12,7 +18,9 @@ import org.chromium.base.JNINamespace;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.EmptyTabObserver;
import org.chromium.chrome.browser.Tab;
import org.chromium.chrome.browser.infobar.AppBannerInfoBar;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.WindowAndroid;
/**
* Manages an AppBannerInfoBar for a Tab.
......@@ -26,7 +34,7 @@ import org.chromium.content_public.browser.WebContents;
* from the network.
*/
@JNINamespace("banners")
public class AppBannerManager extends EmptyTabObserver implements AppDetailsDelegate.Observer {
public class AppBannerManager extends EmptyTabObserver {
private static final String TAG = "AppBannerManager";
/** Retrieves information about a given package. */
......@@ -38,6 +46,12 @@ public class AppBannerManager extends EmptyTabObserver implements AppDetailsDele
/** Tab that the AppBannerView/AppBannerManager is owned by. */
private final Tab mTab;
/** Monitors an installation in progress. */
private InstallerDelegate mInstallTask;
/** Monitors for application state changes. */
private final ApplicationStatus.ApplicationStateListener mListener;
/**
* Checks if app banners are enabled.
* @return True if banners are enabled, false otherwise.
......@@ -63,6 +77,18 @@ public class AppBannerManager extends EmptyTabObserver implements AppDetailsDele
mNativePointer = nativeInit();
mTab = tab;
updatePointers();
mListener = createApplicationStateListener();
ApplicationStatus.registerApplicationStateListener(mListener);
}
private ApplicationStatus.ApplicationStateListener createApplicationStateListener() {
return new ApplicationStatus.ApplicationStateListener() {
@Override
public void onApplicationStateChange(int newState) {
if (!ApplicationStatus.hasVisibleActivities()) return;
nativeUpdateInstallState(mNativePointer);
}
};
}
@Override
......@@ -80,6 +106,11 @@ public class AppBannerManager extends EmptyTabObserver implements AppDetailsDele
* Destroys the native AppBannerManager.
*/
public void destroy() {
if (mInstallTask != null) {
mInstallTask.cancel();
mInstallTask = null;
}
ApplicationStatus.unregisterApplicationStateListener(mListener);
nativeDestroy(mNativePointer);
}
......@@ -105,23 +136,105 @@ public class AppBannerManager extends EmptyTabObserver implements AppDetailsDele
private void fetchAppDetails(String url, String packageName) {
if (sAppDetailsDelegate == null) return;
int iconSize = getPreferredIconSize();
sAppDetailsDelegate.getAppDetailsAsynchronously(this, url, packageName, iconSize);
sAppDetailsDelegate.getAppDetailsAsynchronously(
createAppDetailsObserver(), url, packageName, iconSize);
}
/**
* Called when data about the package has been retrieved, which includes the url for the app's
* icon but not the icon Bitmap itself. Kicks off a background task to retrieve it.
* @param data Data about the app. Null if the task failed.
*/
@Override
public void onAppDetailsRetrieved(AppData data) {
if (data == null) return;
private AppDetailsDelegate.Observer createAppDetailsObserver() {
return new AppDetailsDelegate.Observer() {
/**
* Called when data about the package has been retrieved, which includes the url for the
* app's icon but not the icon Bitmap itself.
* @param data Data about the app. Null if the task failed.
*/
@Override
public void onAppDetailsRetrieved(AppData data) {
if (data == null) return;
String imageUrl = data.imageUrl();
if (TextUtils.isEmpty(imageUrl)) return;
nativeOnAppDetailsRetrieved(
mNativePointer, data, data.title(), data.packageName(), data.imageUrl());
}
};
}
String imageUrl = data.imageUrl();
if (TextUtils.isEmpty(imageUrl)) return;
@CalledByNative
private boolean installOrOpenNativeApp(AppData appData) {
Context context = ApplicationStatus.getApplicationContext();
String packageName = appData.packageName();
PackageManager packageManager = context.getPackageManager();
if (InstallerDelegate.isInstalled(packageManager, packageName)) {
// Open the app.
Intent launchIntent = packageManager.getLaunchIntentForPackage(packageName);
if (launchIntent == null) return true;
context.startActivity(launchIntent);
return true;
} else {
// Try installing the app. If the installation was kicked off, return false to prevent
// the infobar from disappearing.
return !mTab.getWindowAndroid().showIntent(
appData.installIntent(), createIntentCallback(appData),
R.string.low_memory_error);
}
}
private WindowAndroid.IntentCallback createIntentCallback(final AppData appData) {
return new WindowAndroid.IntentCallback() {
@Override
public void onIntentCompleted(WindowAndroid window, int resultCode,
ContentResolver contentResolver, Intent data) {
boolean isInstalling = resultCode == Activity.RESULT_OK;
if (isInstalling) {
// Start monitoring the install.
PackageManager pm =
ApplicationStatus.getApplicationContext().getPackageManager();
mInstallTask = new InstallerDelegate(
Looper.getMainLooper(), pm, createInstallerDelegateObserver(),
appData.packageName());
mInstallTask.start();
}
nativeOnInstallIntentReturned(mNativePointer, isInstalling);
}
};
}
private InstallerDelegate.Observer createInstallerDelegateObserver() {
return new InstallerDelegate.Observer() {
@Override
public void onInstallFinished(InstallerDelegate task, boolean success) {
if (mInstallTask != task) return;
mInstallTask = null;
nativeOnInstallFinished(mNativePointer, success);
}
};
}
@CalledByNative
private void showAppDetails(AppData appData) {
WindowAndroid.IntentCallback emptyCallback = new WindowAndroid.IntentCallback() {
@Override
public void onIntentCompleted(WindowAndroid window, int resultCode,
ContentResolver contentResolver, Intent data) {
// Do nothing.
}
};
mTab.getWindowAndroid().showIntent(
appData.detailsIntent(), emptyCallback, R.string.low_memory_error);
}
@CalledByNative
private int determineInstallState(AppData data) {
if (mInstallTask != null) return AppBannerInfoBar.INSTALL_STATE_INSTALLING;
nativeOnAppDetailsRetrieved(
mNativePointer, data, data.title(), data.packageName(), data.imageUrl());
PackageManager pm = ApplicationStatus.getApplicationContext().getPackageManager();
boolean isInstalled = InstallerDelegate.isInstalled(pm, data.packageName());
return isInstalled ? AppBannerInfoBar.INSTALL_STATE_INSTALLED
: AppBannerInfoBar.INSTALL_STATE_NOT_INSTALLED;
}
private static native boolean nativeIsEnabled();
......@@ -131,6 +244,10 @@ public class AppBannerManager extends EmptyTabObserver implements AppDetailsDele
WebContents webContents);
private native boolean nativeOnAppDetailsRetrieved(long nativeAppBannerManager, AppData data,
String title, String packageName, String imageUrl);
private native void nativeOnInstallIntentReturned(
long nativeAppBannerManager, boolean isInstalling);
private native void nativeOnInstallFinished(long nativeAppBannerManager, boolean success);
private native void nativeUpdateInstallState(long nativeAppBannerManager);
// UMA tracking.
private static native void nativeRecordDismissEvent(int metric);
......
......@@ -6,6 +6,8 @@ package org.chromium.chrome.browser.infobar;
import android.content.Context;
import android.graphics.Bitmap;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.chromium.base.ApplicationStatus;
......@@ -16,11 +18,22 @@ import org.chromium.chrome.browser.banners.AppData;
/**
* Infobar informing the user about an app related to this page.
*/
public class AppBannerInfoBar extends ConfirmInfoBar {
public class AppBannerInfoBar extends ConfirmInfoBar implements View.OnClickListener {
// Installation states.
public static final int INSTALL_STATE_NOT_INSTALLED = 0;
public static final int INSTALL_STATE_INSTALLING = 1;
public static final int INSTALL_STATE_INSTALLED = 2;
// Views composing the infobar.
private Button mButton;
private View mTitleView;
private View mIconView;
private final String mAppTitle;
// Data for native app installs.
private final AppData mAppData;
private int mInstallState;
// Data for web app installs.
private final String mAppUrl;
......@@ -31,6 +44,7 @@ public class AppBannerInfoBar extends ConfirmInfoBar {
mAppTitle = appTitle;
mAppData = data;
mAppUrl = null;
mInstallState = INSTALL_STATE_NOT_INSTALLED;
}
// Banner for web apps.
......@@ -39,6 +53,7 @@ public class AppBannerInfoBar extends ConfirmInfoBar {
mAppTitle = appTitle;
mAppData = null;
mAppUrl = url;
mInstallState = INSTALL_STATE_NOT_INSTALLED;
}
@Override
......@@ -51,6 +66,14 @@ public class AppBannerInfoBar extends ConfirmInfoBar {
super.createContent(layout);
mButton = (Button) layout.findViewById(R.id.button_primary);
mTitleView = layout.findViewById(R.id.infobar_message);
// TODO(dfalcantara): Grab the icon from the layout.
// mIconView = layout.findViewById(R.id.infobar_icon);
assert mButton != null && mTitleView != null;
// Set up accessibility text.
Context context = getContext();
if (mAppData != null) {
......@@ -62,6 +85,52 @@ public class AppBannerInfoBar extends ConfirmInfoBar {
R.string.app_banner_view_web_app_accessibility, mAppTitle,
mAppUrl));
}
// Set up clicking on the controls to bring up the app details.
mTitleView.setOnClickListener(this);
if (mIconView != null) mIconView.setOnClickListener(this);
}
@Override
public void onButtonClicked(boolean isPrimaryButton) {
if (mInstallState == INSTALL_STATE_INSTALLING) {
setControlsEnabled(true);
updateButton();
return;
}
super.onButtonClicked(isPrimaryButton);
}
@CalledByNative
public void onInstallStateChanged(int newState) {
setControlsEnabled(true);
mInstallState = newState;
updateButton();
}
private void updateButton() {
String text;
String accessibilityText = null;
boolean enabled = true;
if (mInstallState == INSTALL_STATE_NOT_INSTALLED) {
text = mAppData.installButtonText();
accessibilityText =
getContext().getString(R.string.app_banner_install_accessibility, text);
} else if (mInstallState == INSTALL_STATE_INSTALLING) {
text = getContext().getString(R.string.app_banner_installing);
enabled = false;
} else {
text = getContext().getString(R.string.app_banner_open);
}
mButton.setText(text);
mButton.setContentDescription(accessibilityText);
mButton.setEnabled(enabled);
}
@Override
public void onClick(View v) {
if (v == mTitleView || v == mIconView) onLinkClicked();
}
private static String getAddToHomescreenText() {
......
......@@ -71,4 +71,9 @@ bool AppBannerInfoBarDelegate::Accept() {
return delegate_->OnButtonClicked();
}
bool AppBannerInfoBarDelegate::LinkClicked(WindowOpenDisposition disposition) {
DCHECK(delegate_);
return delegate_->OnLinkClicked();
}
} // namespace banners
......@@ -33,6 +33,10 @@ class AppBannerInfoBarDelegate : public ConfirmInfoBarDelegate {
// Returns true if the infobar should be dismissed.
virtual bool OnButtonClicked() const = 0;
// User has clicked the link.
// Returns true if the infobar should be dismissed.
virtual bool OnLinkClicked() const = 0;
// Called when the infobar has been destroyed.
virtual void OnInfoBarDestroyed() = 0;
......@@ -67,6 +71,7 @@ class AppBannerInfoBarDelegate : public ConfirmInfoBarDelegate {
base::string16 GetMessageText() const override;
int GetButtons() const override;
bool Accept() override;
bool LinkClicked(WindowOpenDisposition disposition) override;
private:
explicit AppBannerInfoBarDelegate(AppDelegate* delegate);
......
......@@ -99,7 +99,16 @@ bool AppBannerManager::OnButtonClicked() const {
if (!web_contents())
return true;
if (!web_app_data_.IsEmpty()) {
if (!native_app_data_.is_null()) {
JNIEnv* env = base::android::AttachCurrentThread();
ScopedJavaLocalRef<jobject> jobj = weak_java_banner_view_manager_.get(env);
if (jobj.is_null())
return true;
return Java_AppBannerManager_installOrOpenNativeApp(env,
jobj.obj(),
native_app_data_.obj());
} else if (!web_app_data_.IsEmpty()) {
AppBannerSettingsHelper::RecordBannerEvent(
web_contents(), web_contents()->GetURL(),
web_app_data_.start_url.spec(),
......@@ -113,6 +122,27 @@ bool AppBannerManager::OnButtonClicked() const {
return true;
}
bool AppBannerManager::OnLinkClicked() const {
if (!web_contents())
return true;
if (!native_app_data_.is_null()) {
// Try to show the details for the native app.
JNIEnv* env = base::android::AttachCurrentThread();
ScopedJavaLocalRef<jobject> jobj = weak_java_banner_view_manager_.get(env);
if (jobj.is_null())
return true;
Java_AppBannerManager_showAppDetails(env,
jobj.obj(),
native_app_data_.obj());
return true;
} else {
// Nothing should happen if the user is installing a web app.
return false;
}
}
base::string16 AppBannerManager::GetTitle() const {
return app_title_;
}
......@@ -319,6 +349,53 @@ bool AppBannerManager::OnAppDetailsRetrieved(JNIEnv* env,
return FetchIcon(GURL(image_url));
}
void AppBannerManager::OnInstallIntentReturned(JNIEnv* env,
jobject obj,
jboolean jis_installing) {
if (!weak_infobar_ptr_)
return;
if (jis_installing) {
AppBannerSettingsHelper::RecordBannerEvent(
web_contents(),
web_contents()->GetURL(),
native_app_package_,
AppBannerSettingsHelper::APP_BANNER_EVENT_DID_ADD_TO_HOMESCREEN,
base::Time::Now());
}
UpdateInstallState(env, obj);
}
void AppBannerManager::OnInstallFinished(JNIEnv* env,
jobject obj,
jboolean success) {
if (!weak_infobar_ptr_)
return;
if (success) {
UpdateInstallState(env, obj);
} else {
InfoBarService* service = InfoBarService::FromWebContents(web_contents());
service->RemoveInfoBar(weak_infobar_ptr_);
}
}
void AppBannerManager::UpdateInstallState(JNIEnv* env, jobject obj) {
if (!weak_infobar_ptr_ || native_app_data_.is_null())
return;
ScopedJavaLocalRef<jobject> jobj = weak_java_banner_view_manager_.get(env);
if (jobj.is_null())
return;
int newState = Java_AppBannerManager_determineInstallState(
env,
jobj.obj(),
native_app_data_.obj());
weak_infobar_ptr_->OnInstallStateChanged(newState);
}
bool AppBannerManager::FetchIcon(const GURL& image_url) {
if (!web_contents())
return false;
......
......@@ -94,6 +94,17 @@ class AppBannerManager : public chrome::BitmapFetcherDelegate,
jstring japp_package,
jstring jicon_url);
// Called when the installation Intent has been handled and focus has been
// returned to Chrome.
void OnInstallIntentReturned(JNIEnv* env,
jobject obj,
jboolean jis_installing);
// Called when the InstallerDelegate task has finished.
void OnInstallFinished(JNIEnv* env,
jobject obj,
jboolean success);
// Fetches the icon at the given URL asynchronously.
// Returns |false| if this couldn't be kicked off.
bool FetchIcon(const GURL& image_url);
......@@ -103,6 +114,9 @@ class AppBannerManager : public chrome::BitmapFetcherDelegate,
static void InstallManifestApp(const content::Manifest& manifest,
const SkBitmap& icon);
// Called when the AppBannerInfoBar's button needs to be updated.
void UpdateInstallState(JNIEnv* env, jobject obj);
// WebContentsObserver overrides.
void DidNavigateMainFrame(
const content::LoadCommittedDetails& details,
......@@ -117,6 +131,7 @@ class AppBannerManager : public chrome::BitmapFetcherDelegate,
// AppBannerInfoBarDelegate::AppDelegate overrides.
void Block() const override;
bool OnButtonClicked() const override;
bool OnLinkClicked() const override;
void OnInfoBarDestroyed() override;
base::string16 GetTitle() const override;
gfx::Image GetIcon() const override;
......
......@@ -75,6 +75,13 @@ AppBannerInfoBar::CreateRenderInfoBar(JNIEnv* env) {
return infobar;
}
void AppBannerInfoBar::OnInstallStateChanged(int new_state) {
JNIEnv* env = base::android::AttachCurrentThread();
Java_AppBannerInfoBar_onInstallStateChanged(env,
java_infobar_.obj(),
new_state);
}
// Native JNI methods ---------------------------------------------------------
bool RegisterAppBannerInfoBarDelegate(JNIEnv* env) {
......
......@@ -29,6 +29,11 @@ class AppBannerInfoBar : public ConfirmInfoBar {
~AppBannerInfoBar() override;
// Called when the installation state of the app may have changed.
// Updates the InfoBar visuals to match the new state and re-enables controls
// that may have been disabled.
void OnInstallStateChanged(int new_state);
private:
// InfoBarAndroid overrides.
base::android::ScopedJavaLocalRef<jobject> CreateRenderInfoBar(
......
......@@ -18,13 +18,13 @@ class ConfirmInfoBar : public InfoBarAndroid {
protected:
base::string16 GetTextFor(ConfirmInfoBarDelegate::InfoBarButton button);
ConfirmInfoBarDelegate* GetDelegate();
void OnLinkClicked(JNIEnv* env, jobject obj) override;
// InfoBarAndroid overrides.
base::android::ScopedJavaLocalRef<jobject> CreateRenderInfoBar(
JNIEnv* env) override;
private:
void OnLinkClicked(JNIEnv* env, jobject obj) override;
void ProcessButton(int action, const std::string& action_value) override;
base::android::ScopedJavaGlobalRef<jobject> java_confirm_delegate_;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment