| /* |
| * 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) { |
| |
| } |
| } |
| |
| } |