// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.customtabs;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Intent;
import android.net.Uri;
import android.support.customtabs.CustomTabsIntent;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.FrameLayout;
import android.widget.FrameLayout.LayoutParams;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RemoteViews;
import org.chromium.base.Log;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.ChromeActivity;
import org.chromium.chrome.browser.metrics.LaunchMetrics;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.ui.interpolators.BakedBezierInterpolator;
import java.util.List;
/**
* Delegate that manages bottom bar area inside of {@link CustomTabActivity}.
*/
class CustomTabBottomBarDelegate {
private static final String TAG = "CustomTab";
private static final LaunchMetrics.ActionEvent REMOTE_VIEWS_SHOWN =
new LaunchMetrics.ActionEvent("CustomTabsRemoteViewsShown");
private static final LaunchMetrics.ActionEvent REMOTE_VIEWS_UPDATED =
new LaunchMetrics.ActionEvent("CustomTabsRemoteViewsUpdated");
private static final int SLIDE_ANIMATION_DURATION_MS = 400;
private ChromeActivity mActivity;
private ViewGroup mBottomBarView;
private CustomTabIntentDataProvider mDataProvider;
private PendingIntent mClickPendingIntent;
private int[] mClickableIDs;
private OnClickListener mBottomBarClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (mClickPendingIntent == null) return;
Intent extraIntent = new Intent();
extraIntent.putExtra(CustomTabsIntent.EXTRA_REMOTEVIEWS_CLICKED_ID, v.getId());
sendPendingIntentWithUrl(mClickPendingIntent, extraIntent, mActivity);
}
};
public CustomTabBottomBarDelegate(ChromeActivity activity,
CustomTabIntentDataProvider dataProvider) {
mActivity = activity;
mDataProvider = dataProvider;
}
/**
* Makes the bottom bar area to show, if any.
*/
public void showBottomBarIfNecessary() {
if (!mDataProvider.shouldShowBottomBar()) return;
RemoteViews remoteViews = mDataProvider.getBottomBarRemoteViews();
if (remoteViews != null) {
REMOTE_VIEWS_SHOWN.record();
mClickableIDs = mDataProvider.getClickableViewIDs();
mClickPendingIntent = mDataProvider.getRemoteViewsPendingIntent();
showRemoteViews(remoteViews);
} else {
List<CustomButtonParams> items = mDataProvider.getCustomButtonsOnBottombar();
if (items.isEmpty()) return;
LinearLayout layout = new LinearLayout(mActivity);
layout.setId(R.id.custom_tab_bottom_bar_wrapper);
layout.setBackgroundColor(mDataProvider.getBottomBarColor());
for (CustomButtonParams params : items) {
if (params.showOnToolbar()) continue;
final PendingIntent pendingIntent = params.getPendingIntent();
OnClickListener clickListener = null;
if (pendingIntent != null) {
clickListener = new OnClickListener() {
@Override
public void onClick(View v) {
sendPendingIntentWithUrl(pendingIntent, null, mActivity);
}
};
}
layout.addView(
params.buildBottomBarButton(mActivity, getBottomBarView(), clickListener));
}
getBottomBarView().addView(layout);
}
}
/**
* Updates the custom buttons on bottom bar area.
* @param params The {@link CustomButtonParams} that describes the button to update.
*/
public void updateBottomBarButtons(CustomButtonParams params) {
ImageButton button = (ImageButton) getBottomBarView().findViewById(params.getId());
button.setContentDescription(params.getDescription());
button.setImageDrawable(params.getIcon(mActivity.getResources()));
}
/**
* Updates the RemoteViews on the bottom bar. If the given remote view is null, animates the
* bottom bar out.
* @param remoteViews The new remote view hierarchy sent from the client.
* @param clickableIDs Array of view ids, the onclick event of which is intercepcted by chrome.
* @param pendingIntent The {@link PendingIntent} that will be sent on clicking event.
* @return Whether the update is successful.
*/
public boolean updateRemoteViews(RemoteViews remoteViews, int[] clickableIDs,
PendingIntent pendingIntent) {
// Update only makes sense if we are already showing a RemoteViews.
if (mDataProvider.getBottomBarRemoteViews() == null) return false;
REMOTE_VIEWS_UPDATED.record();
if (remoteViews == null) {
if (mBottomBarView == null) return false;
hideBottomBar();
mClickableIDs = null;
mClickPendingIntent = null;
} else {
// TODO: investigate updating the RemoteViews without replacing the entire hierarchy.
mClickableIDs = clickableIDs;
mClickPendingIntent = pendingIntent;
getBottomBarView().removeViewAt(1);
showRemoteViews(remoteViews);
}
return true;
}
/**
* Gets the {@link ViewGroup} of the bottom bar. If it has not been inflated, inflate it first.
*/
private ViewGroup getBottomBarView() {
if (mBottomBarView == null) {
ViewStub bottomBarStub = ((ViewStub) mActivity.findViewById(R.id.bottombar_stub));
bottomBarStub.setLayoutResource(R.layout.custom_tabs_bottombar);
mBottomBarView = (ViewGroup) bottomBarStub.inflate();
}
return mBottomBarView;
}
private void hideBottomBar() {
if (mBottomBarView == null) return;
((ViewGroup) mBottomBarView.getParent()).removeView(mBottomBarView);
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
lp.gravity = Gravity.BOTTOM;
final ViewGroup compositorView = mActivity.getCompositorViewHolder();
compositorView.addView(mBottomBarView, lp);
compositorView.addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
compositorView.removeOnLayoutChangeListener(this);
mBottomBarView.animate().alpha(0f).translationY(mBottomBarView.getHeight())
.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE)
.setDuration(SLIDE_ANIMATION_DURATION_MS)
.withEndAction(new Runnable() {
@Override
public void run() {
((ViewGroup) mBottomBarView.getParent()).removeView(mBottomBarView);
mBottomBarView = null;
}
}).start();
}
});
}
private void showRemoteViews(RemoteViews remoteViews) {
View inflatedView = remoteViews.apply(mActivity, getBottomBarView());
if (mClickableIDs != null && mClickPendingIntent != null) {
for (int id: mClickableIDs) {
if (id < 0) return;
View view = inflatedView.findViewById(id);
if (view != null) view.setOnClickListener(mBottomBarClickListener);
}
}
getBottomBarView().addView(inflatedView, 1);
}
private static void sendPendingIntentWithUrl(PendingIntent pendingIntent, Intent extraIntent,
ChromeActivity activity) {
Intent addedIntent = extraIntent == null ? new Intent() : new Intent(extraIntent);
Tab tab = activity.getActivityTab();
if (tab != null) addedIntent.setData(Uri.parse(tab.getUrl()));
try {
pendingIntent.send(activity, 0, addedIntent, null, null);
} catch (CanceledException e) {
Log.e(TAG, "CanceledException when sending pending intent.");
}
}
}