blob: 2687095cb9dc7f3675697f00bea37dfe41c2fd01 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.weex.ui.component;
import android.annotation.SuppressLint;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.apache.weex.R;
import org.apache.weex.IWXRenderListener;
import org.apache.weex.WXEnvironment;
import org.apache.weex.WXSDKInstance;
import org.apache.weex.annotation.Component;
import org.apache.weex.common.Constants;
import org.apache.weex.common.OnWXScrollListener;
import org.apache.weex.common.WXErrorCode;
import org.apache.weex.common.WXRenderStrategy;
import org.apache.weex.instance.InstanceOnFireEventInterceptor;
import org.apache.weex.performance.WXInstanceApm;
import org.apache.weex.ui.action.BasicComponentData;
import org.apache.weex.utils.WXLogUtils;
import org.apache.weex.utils.WXUtils;
import org.apache.weex.utils.WXViewUtils;
import java.util.ArrayDeque;
import java.util.Comparator;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Queue;
import org.apache.weex.WXSDKInstance.OnInstanceVisibleListener;
@Component(lazyload = false)
public class WXEmbed extends WXDiv implements OnInstanceVisibleListener,NestedContainer {
public static final String STRATEGY_NONE = "none";
public static final String STRATEGY_NORMAL = "normal";
public static final String STRATEGY_HIGH = "high";
public static final String PRIORITY_LOW = "low";
public static final String PRIORITY_NORMAL = "normal";
public static final String PRIORITY_HIGH = "high";
public static final String ITEM_ID = "itemId";
private String src;
protected WXSDKInstance mNestedInstance;
private static int ERROR_IMG_WIDTH = (int) WXViewUtils.getRealPxByWidth(270,750);
private static int ERROR_IMG_HEIGHT = (int) WXViewUtils.getRealPxByWidth(260,750);
private boolean mIsVisible = true;
private EmbedRenderListener mListener;
private EmbedInstanceOnScrollFireEventInterceptor mInstanceOnScrollFireEventInterceptor;
private String priority = PRIORITY_NORMAL;
private String strategy = "normal"; //none, normal, high(ignore priority)
private long hiddenTime;
public interface EmbedManager {
WXEmbed getEmbed(String itemId);
void putEmbed(String itemId,WXEmbed comp);
}
public static class FailToH5Listener extends ClickToReloadListener {
@SuppressLint("SetJavaScriptEnabled")
@Override
public void onException(NestedContainer comp, String errCode, String msg) {
//downgrade embed
if( errCode != null && comp instanceof WXEmbed && errCode.startsWith("1|")) {
ViewGroup container = comp.getViewContainer();
WebView webView = new WebView(container.getContext());
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
webView.setLayoutParams(params);
webView.getSettings().setJavaScriptEnabled(true);
//WebView Remote Code Execution Vulnerability
webView.removeJavascriptInterface("searchBoxJavaBridge_");
webView.removeJavascriptInterface("accessibility");
webView.removeJavascriptInterface("accessibilityTraversal");
webView.getSettings().setSavePassword(false);
container.removeAllViews();
container.addView(webView);
webView.loadUrl(((WXEmbed) comp).src);
}else{
super.onException(comp,errCode,msg);
}
}
}
/**
* Default event listener.
*/
public static class ClickToReloadListener implements OnNestedInstanceEventListener {
@Override
public void onException(NestedContainer container, String errCode, String msg) {
if (TextUtils.equals(errCode, WXErrorCode.
WX_DEGRAD_ERR_NETWORK_BUNDLE_DOWNLOAD_FAILED.getErrorCode()) && container instanceof WXEmbed) {
final WXEmbed comp = ((WXEmbed)container);
final ImageView imageView = new ImageView(comp.getContext());
imageView.setImageResource(R.drawable.weex_error);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(ERROR_IMG_WIDTH, ERROR_IMG_HEIGHT);
layoutParams.gravity = Gravity.CENTER;
imageView.setLayoutParams(layoutParams);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
imageView.setAdjustViewBounds(true);
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
imageView.setOnClickListener(null);
imageView.setEnabled(false);
comp.loadContent();
}
});
FrameLayout hostView = comp.getHostView();
hostView.removeAllViews();
hostView.addView(imageView);
WXLogUtils.e("WXEmbed", "NetWork failure :" + errCode + ",\n error message :" + msg);
}
}
@Override
public boolean onPreCreate(NestedContainer comp, String src) {
return true;
}
@Override
public String transformUrl(String origin) {
return origin;
}
@Override
public void onCreated(NestedContainer comp, WXSDKInstance nestedInstance) {
}
}
static class EmbedRenderListener implements IWXRenderListener {
WXEmbed mComponent;
OnNestedInstanceEventListener mEventListener;
EmbedRenderListener(WXEmbed comp) {
mComponent = comp;
mEventListener = new ClickToReloadListener();
}
@Override
public void onViewCreated(WXSDKInstance instance, View view) {
FrameLayout hostView = mComponent.getHostView();
hostView.removeAllViews();
hostView.addView(view);
}
@Override
public void onRenderSuccess(WXSDKInstance instance, int width, int height) {
}
@Override
public void onRefreshSuccess(WXSDKInstance instance, int width, int height) {
}
@Override
public void onException(WXSDKInstance instance, String errCode, String msg) {
if (mEventListener != null) {
mEventListener.onException(mComponent, errCode, msg);
}
}
}
@Deprecated
public WXEmbed(WXSDKInstance instance, WXVContainer parent, String instanceId, boolean isLazy, BasicComponentData basicComponentData) {
this(instance, parent, basicComponentData);
}
public WXEmbed(WXSDKInstance instance, WXVContainer parent, BasicComponentData basicComponentData) {
super(instance, parent, basicComponentData);
mListener = new EmbedRenderListener(this);
mInstanceOnScrollFireEventInterceptor = new EmbedInstanceOnScrollFireEventInterceptor(this);
ERROR_IMG_WIDTH = (int) WXViewUtils.getRealPxByWidth(270, instance.getInstanceViewPortWidth());
ERROR_IMG_HEIGHT = (int) WXViewUtils.getRealPxByWidth(260, instance.getInstanceViewPortWidth());
if (instance instanceof EmbedManager) {
Object itemId = getAttrs().get(ITEM_ID);
if (itemId != null) {
((EmbedManager) instance).putEmbed(itemId.toString(), this);
}
}
this.priority = WXUtils.getString(getAttrs().get(Constants.Name.PRIORITY), PRIORITY_NORMAL);
this.strategy = WXUtils.getString(getAttrs().get(Constants.Name.STRATEGY), STRATEGY_NONE);
instance.getApmForInstance().updateDiffStats(WXInstanceApm.KEY_PAGE_STATS_EMBED_COUNT,1);
}
@Override
public void setOnNestEventListener(OnNestedInstanceEventListener listener){
mListener.mEventListener = listener;
}
@Override
public ViewGroup getViewContainer() {
return getHostView();
}
@Override
protected boolean setProperty(String key, Object param) {
switch (key) {
case Constants.Name.SRC:
String src = WXUtils.getString(param, null);
if (src != null)
setSrc(src);
return true;
case Constants.Name.PRIORITY:
String priority = WXUtils.getString(param, null);
if (priority != null) {
setPriority(priority);
}
return true;
}
return super.setProperty(key, param);
}
@Override
public void renderNewURL(String url) {
src = url;
loadContent();
}
@Override
public void reload() {
if (!TextUtils.isEmpty(src)) {
loadContent();
}
}
public String getOriginUrl() {
return originUrl;
}
public void setOriginUrl(String originUrl) {
this.originUrl = originUrl;
}
private String originUrl;
@Override
public void addEvent(String type) {
super.addEvent(type);
if(Constants.Event.SCROLL_START.equals(type)){
mInstanceOnScrollFireEventInterceptor.addInterceptEvent(type);
}else if(Constants.Event.SCROLL_END.equals(type)){
mInstanceOnScrollFireEventInterceptor.addInterceptEvent(type);
}else if(Constants.Event.SCROLL.equals(type)){
mInstanceOnScrollFireEventInterceptor.addInterceptEvent(type);
}
}
@WXComponentProp(name = Constants.Name.SRC)
public void setSrc(String src) {
originUrl=src;
this.src = src;
if (mNestedInstance != null) {
mNestedInstance.destroy();
mNestedInstance = null;
}
if (mIsVisible && !TextUtils.isEmpty(this.src)) {
loadContent();
}
}
public String getSrc() {
return src;
}
@WXComponentProp(name = Constants.Name.PRIORITY)
public void setPriority(String priority) {
if(TextUtils.isEmpty(priority)){
return;
}
this.priority = priority;
}
/**
* Load embed content, default behavior is create a nested instance.
*/
protected void loadContent(){
if(mNestedInstance != null){
mNestedInstance.destroy();
}
mNestedInstance = createInstance();
if(mListener != null && mListener.mEventListener != null){
if(!mListener.mEventListener.onPreCreate(this,src)){
//cancel render
mListener.mEventListener.onCreated(this, mNestedInstance);
}
}
}
private static final int getLevel(WXEmbed embed){
String priority = embed.priority;
String strategy = embed.strategy;
int level = 5;
if(!STRATEGY_HIGH.equals(strategy)) {
if (TextUtils.equals(priority, PRIORITY_LOW)) {
level = 0;
} else if (TextUtils.equals(priority, PRIORITY_HIGH)) {
level = 10;
}
}
return level;
}
private WXSDKInstance createInstance() {
WXSDKInstance sdkInstance = getInstance().createNestedInstance(this);
sdkInstance.setParentInstance(getInstance());
boolean needsAdd = !getAttrs().containsKey("disableInstanceVisibleListener");
if(needsAdd){ //prevent switch off fire viewappear event twice
getInstance().addOnInstanceVisibleListener(this);
}
sdkInstance.registerRenderListener(mListener);
mInstanceOnScrollFireEventInterceptor.resetFirstLaterScroller();;
sdkInstance.addInstanceOnFireEventInterceptor(mInstanceOnScrollFireEventInterceptor);
sdkInstance.registerOnWXScrollListener(mInstanceOnScrollFireEventInterceptor);
String url=src;
if(mListener != null && mListener.mEventListener != null){
url=mListener.mEventListener.transformUrl(src);
if(!mListener.mEventListener.onPreCreate(this,src)){
//cancel render
return null;
}
}
if(TextUtils.isEmpty(url)){
mListener.mEventListener.onException(this,
WXErrorCode.WX_DEGRAD_ERR_BUNDLE_CONTENTTYPE_ERROR.getErrorCode(),
WXErrorCode.WX_DEGRAD_ERR_BUNDLE_CONTENTTYPE_ERROR.getErrorMsg() + "!!wx embed src url is null"
);
return sdkInstance;
}
sdkInstance.setContainerInfo(WXInstanceApm.KEY_PAGE_PROPERTIES_INSTANCE_TYPE,"embed");
sdkInstance.setContainerInfo(WXInstanceApm.KEY_PAGE_PROPERTIES_PARENT_PAGE,getInstance().getWXPerformance().pageName);
sdkInstance.renderByUrl(url,
url,
null, null,
WXRenderStrategy.APPEND_ASYNC);
return sdkInstance;
}
@Override
public void setVisibility(String visibility) {
super.setVisibility(visibility);
boolean visible = TextUtils.equals(visibility, Constants.Value.VISIBLE);
if(mIsVisible != visible){
if (!TextUtils.isEmpty(src) && visible) {
if (mNestedInstance == null) {
loadContent();
} else {
mNestedInstance.onViewAppear();
}
}
if (!visible) {
if (mNestedInstance != null) {
mNestedInstance.onViewDisappear();
}
}
mIsVisible = visible;
doAutoEmbedMemoryStrategy();
}
}
@Override
public void destroy() {
super.destroy();
destoryNestInstance();
src = null;
if (getInstance() != null) {
getInstance().removeOnInstanceVisibleListener(this);
}
}
private void doAutoEmbedMemoryStrategy(){
/**
* auto manage embed amount in current instance, save memory
* */
if(!STRATEGY_NONE.equals(this.strategy)){
if(!mIsVisible && mNestedInstance != null){
if(PRIORITY_LOW.equals(this.priority)){
destoryNestInstance();
}else{
if(getInstance().hiddenEmbeds == null){ // low is in front, when priority is same, hidden time pre in first
getInstance().hiddenEmbeds = new PriorityQueue<>(8, new Comparator<WXEmbed>() {
@Override
public int compare(WXEmbed o1, WXEmbed o2) {
int level = getLevel(o1) - getLevel(o2);
if(level != 0){
return level;
}
return (int) (o1.hiddenTime - o2.hiddenTime);
}
});
}
//getInstance().hiddenEmbeds.remove(this);
if(!getInstance().hiddenEmbeds.contains(this)) {
this.hiddenTime = System.currentTimeMillis();
getInstance().hiddenEmbeds.add(this);
}
if(getInstance().hiddenEmbeds != null && getInstance().getMaxHiddenEmbedsNum() >= 0){
while (getInstance().hiddenEmbeds.size() > getInstance().getMaxHiddenEmbedsNum()){
WXEmbed embed = getInstance().hiddenEmbeds.poll();
if(embed.mIsVisible){
continue;
}
if(embed != null) {
embed.destoryNestInstance();
}
}
}
}
}
if(mIsVisible && mNestedInstance != null){
if(getInstance().hiddenEmbeds != null && getInstance().hiddenEmbeds.contains(this)){
getInstance().hiddenEmbeds.remove(this);
}
}
}
}
@Override
public void onAppear() {
//appear event from root instance will not trigger visibility change
if(mIsVisible && mNestedInstance != null){
WXComponent comp = mNestedInstance.getRootComponent();
if(comp != null)
comp.fireEvent(Constants.Event.VIEWAPPEAR);
}
}
@Override
public void onDisappear() {
//appear event from root instance will not trigger visibility change
if(mIsVisible && mNestedInstance != null){
WXComponent comp = mNestedInstance.getRootComponent();
if(comp != null)
comp.fireEvent(Constants.Event.VIEWDISAPPEAR);
}
}
@Override
public void onActivityStart() {
super.onActivityStart();
if (mNestedInstance != null) {
mNestedInstance.onActivityStart();
}
}
@Override
public void onActivityResume() {
super.onActivityResume();
if (mNestedInstance != null) {
mNestedInstance.onActivityResume();
}
}
@Override
public void onActivityPause() {
super.onActivityPause();
if (mNestedInstance != null) {
mNestedInstance.onActivityPause();
}
}
@Override
public void onActivityStop() {
super.onActivityStop();
if (mNestedInstance != null) {
mNestedInstance.onActivityStop();
}
}
@Override
public void onActivityDestroy() {
super.onActivityDestroy();
if (mNestedInstance != null) {
mNestedInstance.onActivityDestroy();
}
}
public void setStrategy(String strategy) {
this.strategy = strategy;
}
private void destoryNestInstance(){
if(getInstance().hiddenEmbeds != null && getInstance().hiddenEmbeds.contains(this)){
getInstance().hiddenEmbeds.remove(this);
}
if (mNestedInstance != null) {
mNestedInstance.destroy();
mNestedInstance = null;
}
if(WXEnvironment.isApkDebugable()){
WXLogUtils.w("WXEmbed destoryNestInstance priority " + priority + " index " + getAttrs().get("index")
+ " " + hiddenTime + " embeds size " + (getInstance().hiddenEmbeds == null ? 0 : getInstance().hiddenEmbeds.size())
+ " strategy " + this.strategy);
}
}
@Override
public void addLayerOverFlowListener(String ref) {
if (mNestedInstance != null)
mNestedInstance.addLayerOverFlowListener(getRef());
}
@Override
public void removeLayerOverFlowListener(String ref) {
if (mNestedInstance != null)
mNestedInstance.removeLayerOverFlowListener(ref);
}
static class EmbedInstanceOnScrollFireEventInterceptor extends
InstanceOnFireEventInterceptor implements OnWXScrollListener {
private WXEmbed mEmbed;
private WXComponent firstLayerScroller;
public EmbedInstanceOnScrollFireEventInterceptor(WXEmbed embed) {
this.mEmbed = embed;
}
public void resetFirstLaterScroller(){
firstLayerScroller = null;
}
@Override
public void onFireEvent(String instanceId, String elementRef, String type, Map<String, Object> params, Map<String, Object> domChanges) {
if(mEmbed == null
|| mEmbed.mNestedInstance == null
|| !mEmbed.mNestedInstance.getInstanceId().equals(instanceId)){
return;
}
if(firstLayerScroller == null){
initFirstLayerScroller();
}
if(firstLayerScroller == null){
return;
}
if(firstLayerScroller.getRef().equals(elementRef)){
mEmbed.getInstance().fireEvent(mEmbed.getRef(), type, params, domChanges);
}
}
private void initFirstLayerScroller(){
if(firstLayerScroller == null){
firstLayerScroller = findFirstLayerScroller();
if(firstLayerScroller != null){
for(String event : getListenEvents()){
if(!firstLayerScroller.containsEvent(event)){
firstLayerScroller.getEvents().add(event);
firstLayerScroller.addEvent(event);
}
}
}
}
}
/**
* get first layer scroller ref
* */
private WXComponent findFirstLayerScroller(){
if(mEmbed.mNestedInstance == null){
return null;
}
WXComponent rootComponent = mEmbed.mNestedInstance.getRootComponent();
if(rootComponent instanceof Scrollable){
return rootComponent;
}
Queue<WXComponent> queues = new ArrayDeque<>();
queues.offer(rootComponent);
while (!queues.isEmpty()){
WXComponent component = queues.poll();
if(component == null){
break;
}
if(component instanceof Scrollable){
return component;
}
if(component instanceof WXVContainer){
WXVContainer container = (WXVContainer) component;
for(int i=0; i<container.getChildCount(); i++){
queues.offer(container.getChild(i));
}
}
}
return null;
}
@Override
public void onScrolled(View view, int x, int y) {
if(firstLayerScroller != null){
return;
}
if(getListenEvents().size() > 0){
initFirstLayerScroller();
}
}
@Override
public void onScrollStateChanged(View view, int x, int y, int newState) {
}
}
}