All Products
Search
Document Center

Mobile Platform as a Service:Feature configuration

Last Updated:Jun 08, 2026

This topic covers Android-side feature configuration for mPaaS miniapps, including UI customization, resource management, lifecycle hooks, JSAPI extensions, and debugging tools.

Enable trial and official version support

  1. Enable the feature.

    Mriver.setConfig("mr_experience_required", "YES");
  2. Open the trial or official version of the miniapp.

    MriverResource.deleteApp(appId); // Delete the local version
    
    Bundle bundle = new Bundle();
    bundle.putString(RVStartParams.LONG_NB_UPDATE, "synctry");  // Force an update. Compatible with other parameters.
    bundle.putInt(RVStartParams.LONG_NB_EXPERIENCE_REQUIRED, 1); // 1: trial version. Omit for the official version.
    Mriver.startApp(this, appId, bundle);

Show a confirmation dialog when leaving a miniapp page

  1. Enable the switch to intercept the back action.

    Mriver.setConfig("enable_back_perform", "YES");
  2. Upgrade the Appx version.

    // Add to build.gradle
    api ('com.mpaas.mriver:mriverappxplus-build:2.7.18.20230130001@aar') {
          force=true
    }
  3. Call the relevant API during miniapp development.

    // Enable
    my.enableAlertBeforeUnload({
          message: 'Confirm to leave this page?',
    });
    
    // Disable
    my.disableAlertBeforeUnload()

Debug and preview miniapps on device

MriverDebug.setWssHost("your_real_wss_address");
MriverDebug.debugAppByScan(activity);

Customize the title bar

To replace the default title bar, implement TitleViewFactoryProxy and pass a custom ITitleView instance.

Register the proxy:

Mriver.setProxy(TitleViewFactoryProxy.class, new TitleViewFactoryProxy() {
                @Override
                public ITitleView createTitle(Context context, App app) {
                    return new CustomTitleView(context);
                }
            });

public class CustomTitleView implements ITitleView, View.OnClickListener {
    
    public static final String TAG = MRConstants.INTEGRATION_TAG + ":MRTitleView";
    protected TextView tvTitle;
    protected ImageView ivImageTitle;
    protected ImageView btBack;
    protected TextView btBackToHome;
    protected RelativeLayout rlTitle;
    protected View statusBarAdjustView;
    
    protected List<ImageButton> btIconList = new ArrayList<>();
    
    
    // The container view for the entire TitleBar
    protected TitleBarFrameLayout contentView;
    
    // The number of OptionMenus in the upper-right corner (default is 1)
    protected int visibleOptionNum;
    protected Page mPage;
    
    // Bottom line separator
    protected View mDivider;
    protected Context mContext;
    
    protected TitleViewIconSpec mTitleViewIconSpec;
    
    protected TitleViewStyleSpec mDarkStyleSpec;
    
    protected TitleViewStyleSpec mLightStyleSpec;
    
    //    protected  ProgressBar mNavLoadingBar;
    protected ITitleEventDispatcher mTitleEventDispatcher;
    
    public CustomTitleView(Context context) {
        mContext = context;
        ViewGroup parent = null;
        if (context instanceof Activity && ((Activity) context).getWindow() != null) {
            parent = ((Activity) mContext).findViewById(android.R.id.content);
        }
        
        mTitleViewIconSpec = TitleViewSpecProvider.g().getIconSpec();
        mDarkStyleSpec = TitleViewSpecProvider.g().getDarkSpec();
        mLightStyleSpec = TitleViewSpecProvider.g().getLightSpec();
        
        contentView = (TitleBarFrameLayout) LayoutInflater.from(context).inflate(R.layout.mriver_title_bar_demo, parent, false);
        tvTitle = contentView.findViewById(R.id.h5_tv_title);
        ivImageTitle = contentView.findViewById(R.id.h5_tv_title_img);
        statusBarAdjustView = contentView.findViewById(R.id.h5_status_bar_adjust_view);
        ivImageTitle.setVisibility(View.GONE);
        tvTitle.setOnClickListener(this);
        ivImageTitle.setOnClickListener(this);
        
        btBack = contentView.findViewById(R.id.h5_tv_nav_back);
        btBackToHome = contentView.findViewById(R.id.h5_tv_nav_back_to_home);
        
        
        mDivider = contentView.findViewById(R.id.h5_h_divider_intitle);
        
        rlTitle = contentView.findViewById(R.id.h5_rl_title);
        visibleOptionNum = 1;
        
        // ad view
        //        adViewLayout.setTag(H5Utils.TRANSPARENT_AD_VIEW_TAG);
        
        btBack.setOnClickListener(this);
        btBackToHome.setOnClickListener(this);
        
        applyViewStyleAndIcon();
        
    }
    
    protected void applyViewStyleAndIcon() {
        boolean useBackSpec = false;
        boolean useHomeSpec = false;
        if (mTitleViewIconSpec != null) {
            TitleViewIconSpec.IconSpecEntry btHomeSpec = mTitleViewIconSpec.getHomeButton();
            if (btHomeSpec != null) {
                btBackToHome.setTypeface(btHomeSpec.getKey());
                btBackToHome.setText(btHomeSpec.getValue());
                useHomeSpec = true;
            }
            
        }
        
        if (!useHomeSpec) {
            Typeface iconFont = Typeface.createFromAsset(mContext.getAssets(), "mrv_iconfont.ttf");
            btBackToHome.setTypeface(iconFont);
        }
        
        btBackToHome.setTextColor(StateListUtils.getStateColor(mLightStyleSpec.getHomeButtonColor()));
    }
    
    
    protected void setButtonIcon(Bitmap btIcon, int index) {
        if (isOutOfBound(index, btIconList.size())) {
            return;
        }
        btIconList.get(index).setImageBitmap(btIcon);
    }
    
    @Override
    public void setTitle(String title) {
        if (title != null && enableSetTitle(title)) {
            tvTitle.setText(title);
            tvTitle.setVisibility(View.VISIBLE);
            ivImageTitle.setVisibility(View.GONE);
        }
    }
    
    protected boolean enableSetTitle(String title) {
        return !title.startsWith("http://") && !title.startsWith("https://");
    }
    
    // view visible control
    protected boolean isOutOfBound(int num, int length) {
        return length == 0 || length < num;
    }
    
    @Override
    public void showBackButton(boolean show) {
        btBack.setVisibility(show ? View.VISIBLE : View.GONE);
        if (show && btBackToHome != null) {
            btBackToHome.setVisibility(View.GONE);
        }
        addLeftMarginOnTitle();
    }
    
    @Override
    public void showOptionMenu(boolean b) {
        
    }
    
    public void showHomeButton(boolean show) {
        btBackToHome.setVisibility(show ? View.VISIBLE : View.GONE);
        if (show) {
            btBack.setVisibility(View.GONE);
        }
        addLeftMarginOnTitle();
    }
    
    @Override
    public void setTitleEventDispatcher(ITitleEventDispatcher dispatcher) {
        mTitleEventDispatcher = dispatcher;
    }
    
    @Override
    public void addCapsuleButtonGroup(View view) {
        if (view == null) {
            return;
        }
    }
    
    protected void addLeftMarginOnTitle() {
        boolean needAdd = btBack.getVisibility() != View.VISIBLE &&
            btBackToHome.getVisibility() != View.VISIBLE;
        RelativeLayout.LayoutParams rlTitleLayoutParams =
            (RelativeLayout.LayoutParams) rlTitle.getLayoutParams();
        rlTitleLayoutParams.setMargins(!needAdd ? 0 : DimensionUtil.dip2px(mContext, 16), 0, 0, 0);
        
    }
    
    @Override
    public void showTitleLoading(boolean show) {
    }
    
    @Override
    public View getContentView() {
        return contentView;
    }
    
    @Override
    public void onClick(View view) {
        RVLogger.d(TAG, "onClick " + view);
        if (mPage == null) {
            return;
        }
        if (view.equals(btBack)) {
            if (mTitleEventDispatcher != null) {
                mTitleEventDispatcher.onBackPressed();
            }
        } else if (view.equals(tvTitle) || view.equals(ivImageTitle)) {
            if (mTitleEventDispatcher != null) {
                mTitleEventDispatcher.onTitleClick();
            }
        } else if (view.equals(btBackToHome)) {
            if (mTitleEventDispatcher != null) {
                mTitleEventDispatcher.onHomeClick();
            }
        }
    }
    
    @Override
    public void setPage(Page page) {
        mPage = page;
        tvTitle.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                mPage.getApp().restartFromServer(null);
                return false;
            }
        });
    }
    
    
    public View getDivider() {
        return mDivider;
    }
    
    protected void switchToLightTheme() {
        tvTitle.setTextColor(mLightStyleSpec.getTitleTextColor());
        
        btBackToHome.setTextColor(StateListUtils.getStateColor(mLightStyleSpec.getHomeButtonColor()));
        
    }
    
    protected void switchToDarkTheme() {
        tvTitle.setTextColor(mDarkStyleSpec.getTitleTextColor());
        
        btBackToHome.setTextColor(StateListUtils.getStateColor(mDarkStyleSpec.getHomeButtonColor()));
        
        
    }
    
    public void onRelease() {
        btIconList.clear();
    }
    
    
    /***
    * Enable support for immersive status bars
    */
    @Override
    public void setStatusBarColor(int color) {
        if (StatusBarUtils.isSupport()) {
            int statusBarHeight = StatusBarUtils.getStatusBarHeight(mContext);
            
            if (statusBarHeight == 0) { // Safeguard for ROMs that cannot retrieve the status bar height. Has no effect.
                return;
            }
            LinearLayout.LayoutParams layoutParams =
                (LinearLayout.LayoutParams) statusBarAdjustView.getLayoutParams();
            layoutParams.height = statusBarHeight;
            statusBarAdjustView.setLayoutParams(layoutParams);
            statusBarAdjustView.setVisibility(View.VISIBLE);
            
            try {
                StatusBarUtils.setTransparentColor((Activity) mContext, color);
            } catch (Exception e) {
                RVLogger.e(TAG, e);
            }
        }
    }
    
    @Override
    public void setBackgroundColor(int color) {
        contentView.getContentBgView().setColor(color);
    }
    
    @Override
    public void setAlpha(int alpha, boolean titleTextAlphaEnabled) {
        contentView.getContentBgView().setAlpha(alpha);
        if (titleTextAlphaEnabled) {
            tvTitle.setAlpha(alpha);
        }
    }
    
    @Override
    public void setOptionMenu(Bitmap bitmap) {
        visibleOptionNum = 2;
        setButtonIcon(bitmap, 1);
    }
    
    @Override
    public void setTitleImage(Bitmap image, String contentDesc) {
        if (!TextUtils.isEmpty(contentDesc)) {
            ivImageTitle.setContentDescription(contentDesc);
        }
        if (image != null) {
            RVLogger.d(TAG, "imgTitle width " + image.getWidth() + ", imgTitle height " + image
                       .getHeight());
            ivImageTitle.setImageBitmap(image);
            ivImageTitle.setVisibility(View.VISIBLE);
            tvTitle.setVisibility(View.GONE);
            RVLogger.d(TAG, "ivImageTitle width " + ivImageTitle
                       .getWidth() + ", ivImageTitle height " + ivImageTitle.getHeight());
        }
    }
    
    @Override
    public void setTitlePenetrate(boolean enable) {
        contentView.setPreventTouchEvent(!enable);
    }
    
    @Override
    public void applyTheme(TitleBarTheme theme) {
        if (theme == TitleBarTheme.DARK) {
            switchToDarkTheme();
        } else if (theme == TitleBarTheme.LIGHT) {
            switchToLightTheme();
        }
    }
}

Customize the miniapp loading animation

Implement SplashViewFactoryProxy to replace the default splash screen with a custom loading view.

Mriver.setProxy(SplashViewFactoryProxy.class, new SplashViewFactoryProxy() {

                @Override
                public ISplashView createSplashView(Context context) {
                    return new CustomLoadingView(context);
                }
            });

public class CustomLoadingView extends FrameLayout implements ISplashView {

    private static final String TAG = "CustomLoadingView";

    private static final int defaultAlphaColor = 855638016;//Color.argb(51, 0, 0, 0);//Default transparent color
    private static final long TIME_DELAY_FOR_SHOW_PERCENTAGE = 2000;//Delay for showing percentage: 2s

    public final static String MSG_UPDATE_APPEARANCE = "UPDATE_APPEARANCE";
    public final static String DATA_UPDATE_APPEARANCE_BG_COLOR = "UPDATE_APPEARANCE_BG_COLOR"; //Page background color #RGB
    public final static String DATA_UPDATE_APPEARANCE_LOADING_ICON = "UPDATE_APPEARANCE_LOADING_ICON"; //Loading icon Drawable
    public final static String DATA_UPDATE_APPEARANCE_LOADING_TEXT = "UPDATE_APPEARANCE_LOADING_TEXT"; //Loading text
    public final static String DATA_UPDATE_APPEARANCE_LOADING_TEXT_COLOR = "UPDATE_APPEARANCE_LOADING_TEXT_COLOR"; //Loading text color #RGB
    public final static String DATA_UPDATE_APPEARANCE_LOADING_BOTTOM_TIP = "UPDATE_APPEARANCE_LOADING_BOTTOM_TIP"; //Bottom tip text

    public final static String ANIMATION_STOP_LOADING_PREPARE = "ANIMATION_STOP_LOADING_PREPARE";

    private Context mContext;

    protected ImageView mLoadingIcon;
    protected TextView mLoadingTitle;
    protected TextView mLoadingPercentTip;
    protected TextView mBottomTip;
    protected TextView mBackButton;

    private Paint mDotPaint;
    private Timer mTimer;
    private TimerTask mTimerTask;
    private boolean mPlayingStartAnim;
    private int mDarkDotX;
    private int mDarkDotY;
    private int mDarkGap;
    private int mDotSize;
    private int mLightDotIndex = 0;
    private int mPercentValue;
    private long mStartLoadingTime = 0;

    private OnCancelListener onCancelListener;
    private Activity hostActivity;

    public interface OnCancelListener {
        void onCancel();
    }

    public CustomLoadingView(Context context) {
        this(context, null);
    }

    public CustomLoadingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomLoadingView(final Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mContext = context;

        hostActivity = (Activity) context;

        initView();

        mBackButton.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View view) {
                cancel();
                if (context instanceof Activity) {
                    RVLogger.d(TAG, "user want close app when splash loading");
                    ((Activity) context).finish();
                }
            }
        });
    }

    public final void cancel() {
        if (this.onCancelListener != null) {
            this.onCancelListener.onCancel();
        }

    }

    public void initView() {
        mLoadingIcon = new ImageView(mContext);
        mLoadingIcon.setScaleType(ImageView.ScaleType.FIT_XY);
        mLoadingIcon.setImageResource(R.drawable.ic_launcher_foreground);
        mLoadingTitle = new TextView(mContext);
        mLoadingTitle.setGravity(Gravity.CENTER);
        mLoadingTitle.setTextColor(Color.BLACK);
        mLoadingTitle.setSingleLine();
        mLoadingTitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18);
        mLoadingTitle.setEllipsize(TextUtils.TruncateAt.END);
        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        mLoadingTitle.setLayoutParams(lp);
        addView(mLoadingIcon);
        addView(mLoadingTitle);

        mBackButton = new TextView(mContext);
        mBackButton.setGravity(Gravity.CENTER);
        addView(mBackButton);

        // Loading percentage
        mPercentValue = 0;
        mLoadingPercentTip = new TextView(mContext);
        mLoadingPercentTip.setGravity(Gravity.CENTER);
        mLoadingPercentTip.setSingleLine();
        mLoadingPercentTip.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12);
        mLoadingPercentTip.setEllipsize(TextUtils.TruncateAt.END);
        lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        mLoadingPercentTip.setLayoutParams(lp);
        mLoadingPercentTip.setText("");
        addView(mLoadingPercentTip);

        mBottomTip = new TextView(mContext);
        mBottomTip.setTextSize(12);
        mBottomTip.setGravity(Gravity.CENTER);
        lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        mBottomTip.setLayoutParams(lp);
        addView(mBottomTip);

        mDotSize = 30;
        mDotPaint = new Paint();
        mDotPaint.setStyle(Paint.Style.FILL);
        mDarkGap = 10;

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int size = 150;
        mLoadingIcon.measure(makeMeasureSpec(size), makeMeasureSpec(size));

        int height = 200;
        int width = 500;
        mLoadingTitle.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), makeMeasureSpec(height));

        height = 200;
        width = 500;
        mLoadingPercentTip.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), makeMeasureSpec(height));

        width = 200;
        height = 100;
        mBottomTip.measure(makeMeasureSpec(width), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));

        width = 200;
        height = 200;
        mBackButton.measure(makeMeasureSpec(width), makeMeasureSpec(height));

        setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int offsetX = 0;
        int offsetY = 0;

        mBackButton.layout(offsetX, offsetY, mBackButton.getMeasuredWidth(), mBackButton.getMeasuredHeight() + offsetY);

        offsetX = (getMeasuredWidth() - mLoadingIcon.getMeasuredWidth()) / 2;
        mLoadingIcon.layout(offsetX, offsetY, offsetX + mLoadingIcon.getMeasuredWidth(),
                offsetY + mLoadingIcon.getMeasuredHeight());

        offsetX = (getMeasuredWidth() - mLoadingTitle.getMeasuredWidth()) / 2;
        offsetY = offsetY + mLoadingIcon.getMeasuredHeight();
        mLoadingTitle.layout(offsetX, offsetY, offsetX + mLoadingTitle.getMeasuredWidth(),
                offsetY + mLoadingTitle.getMeasuredHeight());

        mDarkDotX = getMeasuredWidth() / 2 - mDotSize - mDarkGap;
        mDarkDotY = offsetY + mLoadingTitle.getMeasuredHeight();

        offsetX = (getMeasuredWidth() - mLoadingPercentTip.getMeasuredWidth()) / 2;
        offsetY = offsetY + mLoadingPercentTip.getMeasuredHeight();
        mLoadingPercentTip.layout(offsetX, offsetY, offsetX + mLoadingPercentTip.getMeasuredWidth(),
                offsetY + mLoadingPercentTip.getMeasuredHeight());

        offsetX = (getMeasuredWidth() - mBottomTip.getMeasuredWidth()) / 2;
        offsetY = getMeasuredHeight() - mBottomTip.getMeasuredHeight();
        mBottomTip.layout(offsetX, offsetY, offsetX + mBottomTip.getMeasuredWidth(), offsetY + mBottomTip.getMeasuredHeight());
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mPlayingStartAnim) {
            mDotPaint.setColor(Color.BLACK);
            mDarkDotX = getMeasuredWidth() / 2 - mDotSize - mDarkGap;
            for (int i = 0; i < 3; i++) {
                mDotPaint.setColor(mLightDotIndex == i ? Color.WHITE : Color.BLACK);
                canvas.drawCircle(mDarkDotX, mDarkDotY, mDotSize / 2, mDotPaint);
                mDarkDotX = mDarkDotX + mDarkGap + mDotSize;
            }
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        super.onTouchEvent(ev);
        return true;
    }

    public void startLoadingAnimation() {
        if (mPlayingStartAnim) return;
        mPlayingStartAnim = true;

        if (mTimerTask == null) {
            mTimerTask = new TimerTask() {
                @Override
                public void run() {
                    mLightDotIndex++;
                    if (mLightDotIndex > 2) {
                        mLightDotIndex = 0;
                    }
                    ExecutorUtils.runOnMain(new Runnable() {
                        @Override
                        public void run() {
                            invalidate();
                            // Update the percentage value
                            if (isCanShowPercentage()) {
                                if (mPercentValue == 0) {
                                    mPercentValue = 52;
                                } else if (mPercentValue < 99) {
                                    mPercentValue++;
                                }
                                mLoadingPercentTip.setText(String.format("%d%%", mPercentValue));
                            }

                        }
                    });
                }
            };
        }

        if (mTimer == null) {
            try {
                mTimer = new Timer();
                mTimer.schedule(mTimerTask, 0, 200);
            } catch (Throwable throwable) {
                RVLogger.e(TAG, "printMonitor error", throwable);
            }
        }

        RVLogger.d(TAG, "SplashLoadingView... startLoading Animation");
    }

    public void stopLoadingAnimation() {
        mPlayingStartAnim = false;

        if (mTimer != null) {
            mTimer.cancel();
        }
        if (mTimerTask != null) {
            mTimerTask.cancel();
        }
        invalidate();

        RVLogger.d(TAG, "SplashLoadingView... stopLoading Animation");
    }

    private int getDimen(int id) {
        return mContext.getResources().getDimensionPixelSize(id);
    }

    private int makeMeasureSpec(int size) {
        return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
    }

    public void onStart() {
        updateStatusBar();
        startLoadingAnimation();
    }

    public void onStop() {
        stopLoadingAnimation();
        mLoadingPercentTip.setVisibility(GONE);
        RVLogger.d(TAG, "SplashLoadingView... stop");
    }

    @Override
    public void onFail() {
        onStop();
        Map<String, Object> msgData = new HashMap<>();
        msgData.put(CustomLoadingView.DATA_UPDATE_APPEARANCE_LOADING_BOTTOM_TIP, "");
        sendMessage(CustomLoadingView.MSG_UPDATE_APPEARANCE, msgData);
    }

    public void onHandleMessage(String msg, Map<String, Object> data) {
        if (MSG_UPDATE_APPEARANCE.equals(msg)) {

            String bgColor = (String) data.get(DATA_UPDATE_APPEARANCE_BG_COLOR);
            if (!TextUtils.isEmpty(bgColor)) {
                setBackgroundColor(Color.parseColor(bgColor));
            }

            Drawable loadingIcon = (Drawable) data.get(DATA_UPDATE_APPEARANCE_LOADING_ICON);
            if (loadingIcon != null) {
                mLoadingIcon.setImageDrawable(loadingIcon);
            }

            String text = (String) data.get(DATA_UPDATE_APPEARANCE_LOADING_TEXT);
            if (text != null) {
                mLoadingTitle.setText(text);
            }

            String textColor = (String) data.get(DATA_UPDATE_APPEARANCE_LOADING_TEXT_COLOR);
            if (!TextUtils.isEmpty(textColor)) {
                mLoadingTitle.setTextColor(Color.parseColor(textColor));
            }

            String bottomTip = (String) data.get(DATA_UPDATE_APPEARANCE_LOADING_BOTTOM_TIP);
            if (bottomTip != null) {
                mBottomTip.setText(bottomTip);
            }
        }
    }

    public void performAnimation(final String animationType, final Animator.AnimatorListener animationListener) {
        if (Looper.myLooper() == Looper.getMainLooper()) {
            doPerformAnimation(animationType, animationListener);
        } else {
            post(new Runnable() {
                @Override
                public void run() {
                    doPerformAnimation(animationType, animationListener);
                }
            });
        }
    }

    private void doPerformAnimation(final String animationType, final Animator.AnimatorListener animationListener) {

        if (getParent() == null) {
            RVLogger.e(TAG, "loading view has not added to parent container");
            return;
        }

        if (ANIMATION_STOP_LOADING_PREPARE.equals(animationType)) {
            mPlayingStartAnim = false;

            int offsetTargetY = 0;
            float titleTargetX = 0f;
            if (isBackButtonVisible()) {
                titleTargetX = mBackButton.getX() + mBackButton.getMeasuredWidth();
            } else {
                titleTargetX = getTitleLeftMargin();
            }
            float titleTargetY = (200 - mLoadingTitle.getMeasuredHeight()) / 2;

            AnimatorSet prepareStopLoadingAnimator = new AnimatorSet();
            prepareStopLoadingAnimator.setDuration(400);
            if (animationListener != null) {
                prepareStopLoadingAnimator.addListener(animationListener);
            }
            prepareStopLoadingAnimator.play(ObjectAnimator.ofFloat(mLoadingIcon, "y", mLoadingIcon.getY(), offsetTargetY))
                    .with(ObjectAnimator.ofFloat(mLoadingIcon, "scaleX", mLoadingIcon.getScaleX(), 0))
                    .with(ObjectAnimator.ofFloat(mLoadingIcon, "scaleY", mLoadingIcon.getScaleY(), 0))
                    .with(ObjectAnimator.ofFloat(mLoadingTitle, "x", mLoadingTitle.getX(), titleTargetX))
                    .with(ObjectAnimator.ofFloat(mLoadingTitle, "y", mLoadingTitle.getY(), titleTargetY));

            prepareStopLoadingAnimator.start();
        } else {
            performAnimation(animationType, animationListener);
        }
    }

    @Override
    public void updateLoadingInfo(EntryInfo entryInfo) {
        Map<String, Object> msgData = new HashMap<>();
        msgData.put(CustomLoadingView.DATA_UPDATE_APPEARANCE_LOADING_TEXT, entryInfo.title);

        sendMessage(CustomLoadingView.MSG_UPDATE_APPEARANCE, msgData);

        H5ImageUtil.loadImage(entryInfo.iconUrl, null, new H5ImageListener() {
            @Override
            public void onImage(Bitmap bitmap) {
                RVLogger.d(TAG, "onBitmapLoaded!");
                Map<String, Object> msgData = new HashMap<>();
                int dimen = 100;
                Bitmap displayBitmap = ImageUtil.scaleBitmap(bitmap, dimen, dimen);
                msgData.put(CustomLoadingView.DATA_UPDATE_APPEARANCE_LOADING_ICON, new BitmapDrawable(displayBitmap));
                sendMessage(CustomLoadingView.MSG_UPDATE_APPEARANCE, msgData);
            }
        });
    }

    @Override
    public View getView() {
        return this;
    }

    @Override
    public void onExit() {
        performAnimation(CustomLoadingView.ANIMATION_STOP_LOADING_PREPARE, new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                RVLogger.d(TAG, "onAnimationStart");
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                RVLogger.d(TAG, "onAnimationEnd");
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                RVLogger.d(TAG, "onAnimationCancel");
            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
    }

    private void updateStatusBar() {
        if (hostActivity != null && hostActivity.getClass().getName().equals("com.alipay.mobile.core.loading.impl.LoadingPage")) {
            StatusBarUtils.setTransparentColor(hostActivity, defaultAlphaColor);
        }
    }

    protected boolean isBackButtonVisible() {
        return true;
    }

    protected float getTitleLeftMargin() {
        return 0f;
    }

    private boolean isCanShowPercentage() {
        if (mStartLoadingTime == 0) {
            mStartLoadingTime = System.currentTimeMillis();
        }
        long time = System.currentTimeMillis();
        return ((time - mStartLoadingTime) > TIME_DELAY_FOR_SHOW_PERCENTAGE);
    }

    public final void sendMessage(final String msg, final Map<String, Object> data) {
        this.post(new Runnable() {
            public void run() {
                try {
                    CustomLoadingView.this.onHandleMessage(msg, data);
                } catch (Throwable e) {
                    RVLogger.e(TAG, e);
                }

            }
        });
    }
}

Enable the debug panel

// Call after Mriver initialization is complete. By default, the panel is only shown during miniapp preview and on-device debugging.
MriverEngine.enableDebugConsole();

// Force the debug panel to appear for all miniapps.
Mriver.setConfig("mriver_show_debug_menu_all", "YES");

Customize the More menu

Mriver.setProxy(MRTinyMenuProxy.class, new MRTinyMenuProxy() {
                @Override
                public ITinyMenuPopupWindow createTinyMenuPopupWindow(Context context, TinyMenuViewModel tinyMenuViewModel) {
                    return new DemoTinyMenuPopupWindow(context, tinyMenuViewModel);
                }
            });

// For the DemoTinyMenuPopupWindow implementation, refer to the internal TinyMenuModalWindow.

Listen for and intercept the back action

Enable the back action switch, then register a BackInterceptPoint implementation:

Mriver.setConfig("enable_back_perform", "YES");
List<String> tt = new ArrayList<String>();
tt.add(BackInterceptPoint.class.getName());// Interface class name
Mriver.registerPoint(DemoBackInterceptPointProviderImp.class.getName(), tt);

public class DemoBackInterceptPointProviderImp implements BackInterceptPoint {

    @Override
    public boolean intercepted(final Render render, int i, CommonBackPerform.BackHandler backHandler, GoBackCallback goBackCallback) {
        new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(render.getActivity(), "Back button" ,Toast.LENGTH_LONG).show();
            }
        });
        return false; // true intercepts the action.
    }

    @Override
    public void onInitialized() {
        Log.i("BackPoint", "BackInterceptPoint--onInitialized--:");
    }

    @Override
    public void onFinalized() {
        Log.i("BackPoint", "BackInterceptPoint--onFinalized--:");
    }
}

Intercept the miniapp close action

Mriver.setProxy(AppCloseInterceptProxy.class, new AppCloseInterceptProxy() {
 @Override
 public boolean intercept(Context context, Page page) {
 showToast("Close button");
 return false; // true intercepts the action.
 }
});

Customize the Appx loading animation (GIF only)

In the miniapp mini.project.json file, add "nonLoadingIndicator": false.

Mriver.registerPoint(MriverResourceInterceptor.class.getName(),
                    Arrays.asList("com.alibaba.ariver.resource.api.extension.ResourceInterceptPoint"));
            Mriver.setConfig("mriver_custom_appxloading", CUSTOM_LOADING_RESOURCE);
						// Loading animation size as a percentage of screen width. For example, 20 means the control is 20% of screen width.
            Mriver.setConfig("mriver_custom_appxloading_size", "20");
            ResourcePackage resourcePackage = new GlobalResourcePackage("00000001") {
                @Override
                protected boolean needWaitSetupWhenGet() {
                    return false;
                }

                @Override
                public boolean needWaitForSetup() {
                    return false;
                }

                @Override
                protected boolean canHotUpdate(String hotVersion) {
                    return false;
                }

                @Override
                public Resource get(ResourceQuery query) {
                  	// Intercepts all resources.
                    if (TextUtils.equals(CUSTOM_LOADING_RESOURCE, query.pureUrl)) {
                        return getPresetImageResource(UIStyleActivity.this, query);
                    }
                    return null;
                }
            };
GlobalPackagePool.getInstance().add(resourcePackage);

private Resource getPresetImageResource(Context application, ResourceQuery query) {

    Resource sResource = null;
    if (sResource == null) {

        InputStream inputStream = null;
        AssetManager am = application.getAssets();
        try {
            inputStream = am.open("preset/custom_loading.gif");
            int length = inputStream.available();
            byte[] buffer = new byte[length];
            inputStream.read(buffer);
            sResource = new OfflineResource(query.pureUrl, buffer);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
        }
    }
    return sResource;
}

Manage miniapp resources

Use MriverResource to manage miniapp packages locally. The following examples cover the most common operations.

// Delete a local miniapp
MriverResource.deleteApp("xxxx");

// List all miniapp information
Map<String, List<AppModel>> allApp = MriverResource.getAllApp();

// Update a specific miniapp
Map<String, String> updateApp = new HashMap<>();
            updateApp.put("xxx", "");
            MriverResource.updateApp(updateApp, new UpdateAppCallback() {
                @Override
                public void onSuccess(List<AppModel> list) {
                    showToast("The miniapp with appid=2021042520210425 was updated successfully");
                }

                @Override
                public void onError(UpdateAppException e) {
                    showToast(e.getMessage());
                }
            });

// Download a miniapp package
MriverResource.downloadAppPackage("xxx", new PackageDownloadCallback() {
                @Override
                public void onPrepare(String s) {
                    // Perform auxiliary tasks, such as logging.
                }

                @Override
                public void onProgress(String s, int i) {
                    // Progress
                    showToast("i=" + i);
                }

                @Override
                public void onCancel(String s) {
                    // Cancellation is handled by the internal network library's cancel API.
                }

                @Override
                public void onFinish(String s) {
                    showToast(s);
                }

                @Override
                public void onFailed(String s, int i, String s1) {
                    showToast("onFailed--" + s);
                }
            });
// Delete a local miniapp
MriverResource.deleteApp("xxxx");

// List all miniapp information
Map<String, List<AppModel>> allApp = MriverResource.getAllApp();

// Update all miniapps
MriverResource.updateAll(new UpdateAppCallback() {
                @Override
                public void onSuccess(List<AppModel> list) {

                    showToast("All miniapps that can be pulled by the user ID were updated successfully");
                }

                @Override
                public void onError(UpdateAppException e) {
                    showToast(e.getMessage());
                }
            });

// Update a specific miniapp
Map<String, String> updateApp = new HashMap<>();
            updateApp.put("xxx", "");
            MriverResource.updateApp(updateApp, new UpdateAppCallback() {
                @Override
                public void onSuccess(List<AppModel> list) {
                    showToast("The miniapp with appid=2021042520210425 was updated successfully");
                }

                @Override
                public void onError(UpdateAppException e) {
                    showToast(e.getMessage());
                }
            });

// Download a miniapp package
MriverResource.downloadAppPackage("xxx", new PackageDownloadCallback() {
                @Override
                public void onPrepare(String s) {
                    // Perform auxiliary tasks, such as logging.
                }

                @Override
                public void onProgress(String s, int i) {
                    // Progress
                    showToast("i=" + i);
                }

                @Override
                public void onCancel(String s) {
                    // Cancellation is handled by the internal network library's cancel API.
                }

                @Override
                public void onFinish(String s) {
                    showToast(s);
                }

                @Override
                public void onFailed(String s, int i, String s1) {
                    showToast("onFailed--" + s);
                }
            });

Preset miniapps

  • Place the miniapp .amr package and its metadata in the assets/mriver/legacy folder.

    Important

    The file name must follow the appId.amr format. Do not include the version number.

    image.png

    Place the miniapp metadata in nebula_preset.json. Example:

    {
    	"config":{
    		"updateReqRate":16400,
    		"limitReqRate":13600,
    		"appPoolLimit":3,
    		"versionRefreshRate":86400
    	},
    	"data":[
    		{
    			"app_desc":"Preset miniapp",
    			"app_id":"2022080915350001",
    			"auto_install":1,
    			"extend_info":{
    				"launchParams":{
    					"enableTabBar":"YES",
    					"enableKeepAlive":"NO",
    					"enableDSL":"YES",
    					"nboffline":"sync",
    					"enableWK":"YES",
    					"page":"page/tabBar/component/index",
    					"tinyPubRes":"YES",
    					"enableJSC":"YES"
    				},
    				"usePresetPopmenu":"YES"
    			},
    			"fallback_base_url":"https://xxx/2022080915350001/1.0.1.0_all/nebula/fallback/",
    			"global_pack_url":"",
    			"installType":1,
    			"main_url":"/index.html#page/tabBar/component/index",
    			"name":"Preset miniapp",
    			"online":1,
    			"package_url":"https://xxx/2022080915350001/1.0.1.0_all/nebula/2022080915350001_1.0.1.0.amr",
    			"patch":"",
    			"sub_url":"",
    			"version":"1.0.1.0",
    			"vhost":"https://2022080915350001.h5app.com"
    		}
    	],
    	"resultCode":100,
    	"resultMsg":"Operation successful",
    	"state":"success"
    }
  • To pre-install the miniapp, use the following code.

    private void checkPresetInstalled() {
            Map<String, AppModel> appModelMap = RVProxy.get(RVResourcePresetProxy.class).getPresetAppInfos();
            Map<String, RVResourcePresetProxy.PresetPackage> packageMap = RVProxy.get(RVResourcePresetProxy.class).getPresetPackage();
            Set<String> stringSet = packageMap.keySet();
            for (String key: stringSet) {
                RVResourcePresetProxy.PresetPackage presetPackage = packageMap.get(key);
                AppModel presetModel = appModelMap.get(key);
                if (presetModel != null && presetPackage != null && presetPackage.getInputStream() != null) {
                    AppModel appModel = MriverResource.getAppModel(presetModel.getAppId());
                    boolean available = ((RVResourceManager)RVProxy.get(RVResourceManager.class)).isAvailable(appModel);
                    if (!available) {
                        if (TextUtils.equals(appModel.getAppVersion(), presetModel.getAppVersion())) {
                            InternalUtils.installApp(presetModel, presetPackage.getInputStream());
                        } else {
                            // Decide whether to install the online version in advance.
                        }
                    }
    
                }
            }
        }

Intercept resource loading

ResourcePackage resourcePackage = new GlobalResourcePackage("00000001") {
                @Override
                protected boolean needWaitSetupWhenGet() {
                    return false;
                }

                @Override
                public boolean needWaitForSetup() {
                    return false;
                }

                @Override
                protected boolean canHotUpdate(String hotVersion) {
                    return false;
                }

                @Override
                public Resource get(ResourceQuery query) {
                  	// Intercepts all resources.
                    if (TextUtils.equals("specified_resource_path", query.pureUrl)) {
                        return getLocalResource(UIStyleActivity.this, query);
                    }
                    return null;
                }
            };
GlobalPackagePool.getInstance().add(resourcePackage);

Enable signature verification

// Enable signature verification
MriverResource.enableVerify(MriverResource.VERIFY_TYPE_YES,"public_key");

// Disable signature verification
MriverResource.disableVerify( );

Enable keepalive

Set the keepalive config keys during initialization:

Mriver.setConfig("enable_keep_alive", "YES");
Mriver.setConfig("mriver_keep_alive_time", "120000"); // Keepalive duration: 2 minutes
Mriver.setConfig("mriver_keepalive_max", "3"); // Maximum keepalive instances: 3

Android miniapp keepalive is based on the Activity Task Stack. If your miniapp navigates to a native page (such as a sign-in page) or to another app (such as a third-party payment or share page), note the following:

  • If the page is in the same stack as the miniapp page, there is no risk.

  • If those pages are in a separate activity stack, it may affect the back stack order. Perform regression testing on the related logic.

By default, when a keepalive miniapp is awakened, it refreshes to the specified page if that page is not the current page.

Set refererBiz to skip the refresh and resume the miniapp on its current page:

 Bundle intent  = new Bundle();
intent.putString("page", "page/component/view/view");
intent.putString("refererBiz", "home");
Mriver.startApp(appId, intent);
Note
  • If refererBiz is set to home, the specified page is ignored when the keepalive miniapp is awakened. If no keepalive miniapp exists, the specified page is opened.

  • If refererBiz is set to any other value (customizable), it is compared with the referer value from the previous launch. If the values match, the specified page is ignored. If no keepalive miniapp exists, the specified page is opened.

Start a specific miniapp version

MriverResource.deleteApp("2022080918000001"); // Delete the local miniapp

Bundle bundle = new Bundle();
bundle.putString(RVStartParams.LONG_NB_TARGET_VERSION, "your_version_number");
Mriver.startApp(TargetVersionActivity.this, "2022080918000001", bundle);

Expose a custom JSAPI

To expose a native capability to miniapp JavaScript, extend SimpleBridgeExtension and register it with MriverEngine.registerBridge().

Each method annotated with @ActionFilter becomes a callable JSAPI. The following annotations control how the method receives its parameters:

Annotation

Description

@ActionFilter

Marks the method as a JSAPI handler

@BindingId

Injects the call ID

@BindingNode(App.class)

Injects the current App instance

@BindingNode(Page.class)

Injects the current Page instance

@BindingApiContext

Injects the API context (provides access to the Activity)

@BindingExecutor(ExecutorType.UI)

Runs the handler on the UI thread

@BindingRequest

Injects the full JSON request params

@BindingParam("key")

Injects a single named parameter

@BindingCallback

Injects the callback for returning a result to the miniapp

// Register the custom JSAPI
MriverEngine.registerBridge(CustomApiBridgeExtension.class);

public class CustomApiBridgeExtension extends SimpleBridgeExtension {

    private static final String TAG = "CustomApiBridgeExtension";

    @ActionFilter
    public void tinyToNative(@BindingId String id,
                             @BindingNode(App.class) App app,
                             @BindingNode(Page.class) Page page,
                             @BindingApiContext ApiContext apiContext,
                             @BindingExecutor(ExecutorType.UI) Executor executor,
                             @BindingRequest JSONObject params,
                             @BindingParam("param1") String param1,
                             @BindingParam("param2") String param2,
                             @BindingCallback BridgeCallback callback) {
        RVLogger.d(TAG, "id: "+id+
                "\napp: "+app.toString()+
                "\npage: "+page.toString()+
                "\napiContext: "+apiContext.toString()+
                "\nexecutor: "+executor.toString());
        RVLogger.d(TAG, JSONUtils.toString(params));
        JSONObject result = BridgeResponse.SUCCESS.get();
        //result.put("message", "The client received the parameters: " + param1 + ", " + param2 + "\nReturns the current package name of the Demo: " + apiContext.getActivity().getPackageName());
        // Return the result to the miniapp
        Stack stack = MriverApp.getAppStack();
        Enumeration enumerationLists = stack.elements();

        JSONArray jsonArray = new JSONArray();
        while (enumerationLists.hasMoreElements()) {
            JSONObject jsonObject = new JSONObject();
            MRApp o = (MRApp) enumerationLists.nextElement();
            jsonObject.put("AppId", o.getAppId());
            jsonObject.put("AppVersion", o.getAppVersion());
            jsonArray.add(jsonObject);
        }
        String tinyappStr = jsonArray.toJSONString();
        // result.put("message", "The client received the parameters: " + param1 + ", " + param2 + "\nReturns the current package name of the Demo: " + apiContext.getActivity().getPackageName());
        result.put("message", tinyappStr);
        callback.sendJSONResponse(result);
    }

}

Enable the share feature

Enable the share menu item, then implement ShareApiBridgeExtension to handle the shareTinyAppMsg JSAPI:

Mriver.setConfig("mr_showShareMenuItem", "YES");

// Implement ShareApiBridgeExtension
public class ShareApiBridgeExtension extends SimpleBridgeExtension {
    private static final String TAG = "CustomApiBridgeExtension";

    @ActionFilter
    public void shareTinyAppMsg(@BindingId String id,
                                @BindingNode(App.class) App app,
                                @BindingNode(Page.class) Page page,
                                @BindingApiContext ApiContext apiContext,
                                @BindingExecutor(ExecutorType.UI) Executor executor,
                                @BindingRequest JSONObject params,
                                final @BindingCallback BridgeCallback callback) {
        Log.i("ShareApiBridge", "share: " + (params == null ? "null" : params.toJSONString()));

        String title = params.getString("title");

        String desc = params.getString("desc");

        String myprop = params.getString("myprop");

        String path = params.getString("page");

        String appId = app.getAppId();

        // Call your share component here to implement the share flow.

        String message = "Application ID: " + appId + "\n"

            + "title: " + title + "\n"

            + "desc: " + desc + "\n"

            + "myprop: " + myprop + "\n"

            + "path: " + path + "\n";

        AUNoticeDialog dialog = new AUNoticeDialog(apiContext.getActivity(),

            "Share Result", message, "Share Successful", "Share Failed");

        dialog.setPositiveListener(new AUNoticeDialog.OnClickPositiveListener() {

            @Override

            public void onClick() {

                JSONObject result = BridgeResponse.SUCCESS.get();

                result.put("success", true);

                callback.sendJSONResponse(result);

            }

        });

        dialog.setNegativeListener(new AUNoticeDialog.OnClickNegativeListener() {

            @Override

            public void onClick() {

                callback.sendBridgeResponse(BridgeResponse.newError(11, "Share failed"));

            }

        });

        dialog.show();
    }
}

Customize the permission dialog

Implement LocalPermissionDialogProxy to replace the default permission prompt with a custom dialog. Use setExtData to access per-request context such as the permission list, appModel, and JSAPI action.

// Register the custom permission dialog proxy
Mriver.setProxy(LocalPermissionDialogProxy.class, new LocalPermissionDialogProxy() {
                    @Override
                    public LocalPermissionDialog create(Context context) {
                        return new DemoLocalPermissionDialog(context);
                    }

                    @Override
                    public boolean interceptPermission(String appId, String page, String action, String scope, List<String> permissions) {
                        showToast("jsapi: " + action + " miniapp:"+appId+" page:"+page);
                        return false;
                    }

                });
// Dialog implementation example
public class DemoLocalPermissionDialog implements LocalPermissionMultiDialog {
    private Dialog mDialog;
    private final Context mContext;
    private PermissionPermitListener mPermissionPermitListener;

    public DemoLocalPermissionDialog(Context context) {
        this.mContext = context;
    }

	public void setExtData(String[] permissions, AppModel appModel, Page page, String action, String scope) {
  	 // Use these parameters to build custom behaviors, such as multi-level dialogs or custom text styles.
     // permissions: All permissions required for the current action.
     // appModel: The appModel for the current action. Use it to retrieve miniapp-defined text.
     // action: The JSAPI action name.
     // scope: The JSAPI scope.
	}

    public void setDialogContent(String content, String title, String icon) {
        AlertDialog.Builder builder = new AlertDialog.Builder(this.mContext);
        builder.setTitle("Permission Dialog Box");
        builder.setMessage(content);
        builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface pDialogInterface, int pI) {
                if (DemoLocalPermissionDialog.this.mPermissionPermitListener != null) {
                    DemoLocalPermissionDialog.this.mPermissionPermitListener.onSuccess();
                }
            }
        });
        builder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface pDialogInterface, int pI) {
                if (DemoLocalPermissionDialog.this.mPermissionPermitListener != null) {
                    DemoLocalPermissionDialog.this.mPermissionPermitListener.onFailed(-1, "", true);
                }
            }
        });
        this.mDialog = builder.create();
        this.mDialog.show();
    }

    public void setPermissionPermitListener(PermissionPermitListener permissionPermitListener) {
        this.mPermissionPermitListener = permissionPermitListener;
    }

    public void show() {
        if (this.mDialog != null && this.mContext instanceof Activity && !((Activity)this.mContext).isFinishing()) {
            this.mDialog.show();
        }

    }
Note

Use setExtData to add more capabilities to the permission dialog.

Embed a custom native view

  1. Upgrade Appx to version 2.7.18.

    api ('com.mpaas.mriver:mriverappxplus-build:2.7.18.20220825001@aar') {
        force=true
    }
  2. Register the embed view proxy.

    RVProxy.set(RVEmbedProxy.class, new RVEmbedProxy() {
    
                    @Override
                    public Class<?> getEmbedViewClass(String type) {
                        if ("custom_barrage".equalsIgnoreCase(type)) {
                            // The type must match the type defined on the miniapp side. Return the corresponding custom View class.
                            return EmbedCustomView.class;
                        }
                        return null;
                    }
                });
  3. Implement the custom view.

       package com.mpaas.demo.tinyapp.engine;
    
       import android.content.Context;
       import android.text.TextUtils;
       import android.util.Log;
       import android.view.View;
       import android.widget.FrameLayout;
    
       import com.alibaba.ariver.app.api.Page;
       import com.alibaba.ariver.engine.api.bridge.extension.BridgeCallback;
       import com.alibaba.ariver.engine.api.bridge.extension.BridgeResponse;
       import com.alibaba.ariver.engine.api.embedview.IEmbedView;
       import com.alibaba.fastjson.JSONArray;
       import com.alibaba.fastjson.JSONObject;
       import com.alipay.mobile.beehive.video.h5.live.MRLivePlayerHelper;
       import com.mpaas.mriver.integration.embed.IMREmbedView;
    
       import java.util.Map;
    
       public class EmbedCustomView implements IMREmbedView {
           private Context mContext;
           private Page mPage;
           private CustomBarrageView mCustomBarrageView; // Live comment view example
    
           @Override
           public void onCreate(Context context, Page page, IEmbedView iEmbedView) {
               mContext = context;
               mPage = page;
           }
    
           @Override
           public View getView(int width, int height, final String viewId, String type, Map<String, String> params) {
               Log.i("EmneCustomV", "getView: " + mCustomBarrageView + " " + viewId + " " + type + " " + params);
               if (mCustomBarrageView == null) {
                   mCustomBarrageView = new CustomBarrageView(mContext);
               }
               FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
               mCustomBarrageView.setLayoutParams(layoutParams);
               return mCustomBarrageView;
           }
    
           // Receive data sent from the miniapp
           @Override
           public void onReceivedMessage(String actionType, JSONObject data, BridgeCallback bridgeCallback) {
               Log.i("EmneCustomV", "onReceivedMessage: " +  actionType);
               if ("mpaasCustomEvent".equalsIgnoreCase(actionType)) {
                   String innerAction = data.getString("actionType");
                   if (TextUtils.equals(innerAction, "bindLivePlayer")) {
                       JSONObject dataJSON = data.getJSONObject("data");
    
                       // Set the live comment data
                       JSONArray barrages = dataJSON.getJSONArray("barrages");
                       mCustomBarrageView.setData(barrages);
    
                       // Bind the liveplayer
                       String bindId = dataJSON.getString("id");
                       if (!TextUtils.isEmpty(bindId)) {
                           MRLivePlayerHelper.bind(bindId, mCustomBarrageView);
                       }
                   }
    
               }
           }
    
           protected void notifySuccess(final BridgeCallback bridgeContext) {
               if (bridgeContext != null) {
                   bridgeContext.sendBridgeResponse(BridgeResponse.SUCCESS);
               }
           }
    
           
           @Override
           public void onReceivedRender(JSONObject params, BridgeCallback bridgeCallback) {
               Log.i("EmneCustomV", "onReceivedRender: " +  params);
               notifySuccess(bridgeCallback);
           }
    
           @Override
           public void onWebViewResume() {
    
           }
    
           @Override
           public void onWebViewPause() {
    
           }
    
           @Override
           public void onAttachedToWebView() {
    
           }
    
           @Override
           public void onDetachedToWebView() {
    
           }
    
           @Override
           public void onDestroy() {
    
           }
    
           @Override
           public void onRequestPermissionResult(int i, String[] strings, int[] ints) {
    
           }
    
           @Override
           public void onEmbedViewVisibilityChanged(int i) {
    
           }
    
           @Override
           public void initElementId(String s) {
    
           }
    
       }
    
    }

Mini Program-side implementation

//page.axml
<mpaas-component
          id="mpaas-barrage"
          type="custom_barrage"                    // The type must match the native type.
          style="{{ width: 400, height: 200 }}"    // Only width and height are configurable.
          onMpaasCustomEvent="onMpaasCustomEvent"  // Receive native events.
/>

//page.js

barrageContext = my.createMpaasComponentContext('mpaas-barrage');
// Send data to native
barrageContext.mpaasCustomEvent({
        actionType: 'bindLivePlayer',
        data: {
          "id": "liveplayer",
          "barrages": ["Interesting", "First comment", "Live comment 1", "Live comment 2", "Live comment 3"]
        }
      });
  }, 100)

Listen to the page lifecycle

  1. Register the lifecycle extension during initialization.

    List<String> miniAppPoint = new ArrayList<>();
    miniAppPoint.add(PageResumePoint.class.getName());
    miniAppPoint.add(PagePausePoint.class.getName());
    miniAppPoint.add(PageEnterPoint.class.getName());
    miniAppPoint.add(AppExitPoint.class.getName());
    Mriver.registerPoint(PageLifeCycleExtension.class.getName(), miniAppPoint);
  2. Implement PageLifeCycleExtension.java.

    public class PageLifeCycleExtension implements PageResumePoint, PageEnterPoint, PagePausePoint, AppExitPoint {
    
        private static final String TAG = "PageLifeCycleExtension";
    
        @Override
        public void onPageResume(Page page) {
        }
    
        @Override
        public void onInitialized() {
    
        }
    
        @Override
        public void onFinalized() {
    
        }
    
        @Override
        public void onPageEnter(Page page) {
    
        }
    
        @Override
        public void onPagePause(final Page page) {
        }
    
        @Override
        public void onAppExit(App app) {
            
        }
    }

Set up an APM listener

Important

If the page has a large blank area, MiniPage_Load_T2 is not called back.

The APM listener reports two events: MiniAppStart and MiniPage_Load_T2. Each event payload contains key-value pairs (delimited by ^) with the following timestamps:

Key

Description

mini_st_ts0

The time the miniapp was tapped

mini_st_ts7

The time the preparation phase completed (includes download on first launch)

mini_st_end_ts

The time the core phase completed

mini_t2_ts

The time T2 rendering completed

  1. Set up the APM proxy during initialization.

    RVProxy.set(PrepareNotifyProxy.class, new PrepareNotifyProxy() {
    
                               @Override
                               public void notify(String s, PrepareStatus prepareStatus) {
    
                               }
    
                               @Override
                               public void apmEvent(final String s, final String s1, final String s2, final String s3, final String s4) {
                                 // Not on the UI thread  
                                 Log.i("MiniStartTime", "apmE: " + s + " " + s4);
                                 if ("MiniAppStart".equalsIgnoreCase(s) || "MiniPage_Load_T2".equalsIgnoreCase(s)) {
                                     boolean isT2 = "MiniPage_Load_T2".equalsIgnoreCase(s);
                                     String apmData = s4;
                                     if (!TextUtils.isEmpty(apmData)) {
                                         parseTime(isT2, apmData);
                                     }
                                 }
                             });
                   }
    
       private void parseTime(boolean isT2, String s4) {
               String[] kvArrs = s4.split("\\^");
               long miniStart = 0; // The time when the miniapp was clicked
               long miniPrepared = 0; // The time when the miniapp preparation phase is complete. The first time includes the download.
               long miniAppStarted = 0; // The time when the miniapp core phase is complete.
               long miniT2 = 0; // The time when the miniapp T2 rendering is complete.
               boolean needIgnore = false;  // true indicates the rendering callback for a secondary page during an internal navigation. This should be ignored for the first screen.
    
               for (String kvItem : kvArrs) {
                   String[] kv = kvItem.split("=");
                   if (kv.length == 2) {
                       String key = kv[0];
                       String value = kv[1];
                       if ("mini_st_ts0".equalsIgnoreCase(key)) {
                           // The time when the miniapp was clicked
                           miniStart = Long.parseLong(value);
                       } else if ("mini_st_ts7".equalsIgnoreCase(key)) {
                           // The time when the miniapp preparation phase is complete.
                           miniPrepared = Long.parseLong(value);
                       } else if ("mini_st_end_ts".equalsIgnoreCase(key)) {
                           // The time when the miniapp core phase is complete.
                           miniAppStarted = Long.parseLong(value);
                       } else if ("mini_t2_ts".equalsIgnoreCase(key)) {
                           // The time when the miniapp T2 rendering is complete.
                           miniT2 = Long.parseLong(value);
                       } else if (isT2 && "isFirstPage".equalsIgnoreCase(key)) {
                           if ("false".equalsIgnoreCase(value)) {
                               needIgnore = true;
                           }
                       }
                   }
               }
    
               if (!needIgnore && miniStart > 0 && miniPrepared > 0 && miniAppStarted > 0 && miniT2 > 0) {
                   final String toastStr = "Preparation time=" + (miniPrepared - miniStart) + " Core time=" + (miniAppStarted - miniPrepared) + " Business time=" + (miniT2 - miniAppStarted) + " Total time=" + (miniT2 - miniStart);
                   Log.i("MiniStartTime", toastStr);
                   mUIHandler.post(new Runnable() {
                       @Override
                       public void run() {
                           Toast.makeText(MRiverApp.sApp, toastStr, Toast.LENGTH_LONG).show();
                       }
                   });
               }
           }
  2. Pass a timestamp when launching the miniapp.

    Bundle intent  = new Bundle();
    intent.putString("miniapp_start_ts", Long.toString(System.currentTimeMillis()));
    Mriver.startApp(FastStartActivity.this, "appId", intent);

Enable Google Maps support

  1. Enable Google Maps.

    Mriver.setConfig("ta_map_type", "1");
  2. Add the Google Maps dependency and configure the Google Maps API key.