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
-
Enable the feature.
Mriver.setConfig("mr_experience_required", "YES"); -
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
-
Enable the switch to intercept the back action.
Mriver.setConfig("enable_back_perform", "YES"); -
Upgrade the Appx version.
// Add to build.gradle api ('com.mpaas.mriver:mriverappxplus-build:2.7.18.20230130001@aar') { force=true } -
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
.amrpackage and its metadata in theassets/mriver/legacyfolder.ImportantThe file name must follow the
appId.amrformat. Do not include the version number.
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);
If
refererBizis set tohome, the specified page is ignored when the keepalive miniapp is awakened. If no keepalive miniapp exists, the specified page is opened.If
refererBizis set to any other value (customizable), it is compared with thereferervalue 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 |
|
|
Marks the method as a JSAPI handler |
|
|
Injects the call ID |
|
|
Injects the current |
|
|
Injects the current |
|
|
Injects the API context (provides access to the |
|
|
Runs the handler on the UI thread |
|
|
Injects the full JSON request params |
|
|
Injects a single named parameter |
|
|
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();
}
}
Use setExtData to add more capabilities to the permission dialog.
Embed a custom native view
-
Upgrade Appx to version
2.7.18.api ('com.mpaas.mriver:mriverappxplus-build:2.7.18.20220825001@aar') { force=true } -
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; } }); -
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
-
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); -
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
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 |
|
|
The time the miniapp was tapped |
|
|
The time the preparation phase completed (includes download on first launch) |
|
|
The time the core phase completed |
|
|
The time T2 rendering completed |
-
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(); } }); } } -
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
-
Enable Google Maps.
Mriver.setConfig("ta_map_type", "1"); Add the Google Maps dependency and configure the Google Maps API key.