Flutter learning - Implementation of flutter plug-in (flutter calls Android native)

Original address: Flutter learning (9) -- implementation of flutter plug-in (flutter calls Android native) | small nest of stars one

Recently, we need to add apk integrity detection to a Flutter project. We need to get the md5 value of the currently installed apk. Because it cannot be implemented in Flutter, we need to call the native Android code to implement it. Therefore, we spent some time studying the implementation of the next plug-in, which is hereby recorded

Step description

1. Open the android folder

There are IOS and Android folders in fluent, which correspond to the native code of Android and IOS respectively

We want to implement fluent to call the native code and write the native code in it

In the Android folder, a new class is created. Android can choose Java or Kotlin coding

Android directory structure is actually a common Android project directory

Then use Android Studio to open, right-click the menu, and select fluent - > Open Android module in Android Studio

After that, you can see that a project has been opened like Android Development (of course, you can use Android Studio to select the Android folder and open it as a project)

2. Create a new Activity

This Activity needs to inherit the fluteractivity and override the configureFlutterEngine method to initialize the plug-in

public class MainActivity extends FlutterActivity {
    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        
        //Registration of plug-in instances
        
        //This is a must, don't delete!!
        GeneratedPluginRegistrant.registerWith(flutterEngine);
    }
}

So you need plug-in instances here. How do you get plug-in instances? In fact, you write your own class and implement the FlutterPlugin interface provided by Flutter

3. Native code writing

Create a new class, implement the FlutterPlugin interface, create a MethodChannel object, use the setMethodCallHandler method of this object to set the method processing callback, and call our native method by judging the method name

public class MyTestPlugin implements FlutterPlugin {
    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
        //You can use the binding object to obtain the Context object required in Android
        //Context applicationContext = binding.getApplicationContext();
        
        //Set the channel name, and then it will be the same in the shuttle
        MethodChannel channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), "test-plugin");
        
        //Set the current MethodCallHandler to
        channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
                String method = call.method;
                if (method.equals("getText")) {
                    
                    //Call the native method. For convenience, I'll write the method in the current class
                    String str = getText();
                    
                    //Return the result to fluent
                    result.success(str);

                    //There is also the error method, which can be used according to the situation
                    //result.error("code", "message", "detail");
                } else {
                    //If the id method name passed by Flutter is not found, call this method
                    result.notImplemented();
                }
            }
        });
    }

    private String getText() {
        return "hello world";
    }
    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {

    }
}

If you want an Application context object, you can use the binding getApplicationContext() method in the onAttachedToEngine() method, as shown in the following code

Context applicationContext = binding.getApplicationContext();

If you want to obtain the context object of the current Activity, you can let the current class implement the ActivityAware interface, but it is a little cumbersome. Generally, the context object of Application should meet most requirements. Choose according to the situation

private Context context;
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
    context = binding.getActivity();
}

@Override
public void onDetachedFromActivityForConfigChanges() {

}

@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {

}

@Override
public void onDetachedFromActivity() {
context = null;
}

4. Register plug-ins in activity

Previously, fill in the registered code in the Activity in step 2

public class MainActivity extends FlutterActivity {
    @Override
    public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        //Registration of plug-in instances
        flutterEngine.getPlugins().add(new MyTestPlugin());
        
        GeneratedPluginRegistrant.registerWith(flutterEngine);
    }
}

5. Initialization and encapsulation of plug-ins in fluent

Create a file in fluent with arbitrary file name and class name, which is only used to declare and initialize the above Java classes

class Md5Plugin{
    //Note that the name here needs to be the same as that defined in Android native
    static const MethodChannel _channel = MethodChannel("apk_md5");

    static Future<String> getMd5() async{
        //Pass a method name, that is, call the native method of Android
        return await _channel.invokeMethod("getMd5");
    }

}

Remember the judgment of method name written before? Here is to pass a method name, and then the callback will be triggered, and then the return result can be obtained

PS: note that the native methods of calling Android are asynchronous operations!

6. Using plug-ins in the shutter page

Then you can call it in the corresponding code of the corresponding page file.

Md5Plugin.getMd5().then(value=>{
    //Related operations
});

If you want to use synchronization code, you can write it like this

var result = Md5Plugin.getMd5()

PS: when testing, note that if you change the native layer code (Java or Kotlin), you'd better run the project again and don't use the hot overload function of flutter (unless you only move the flutter code)

Pass parameter supplement

In the above example, there is no reference to communication. Here I will add my own research use

Here we only talk about how Flutter passes parameters to Android natively

The calling method in FLutter (that is, the fifth step operation above):

class Md5Plugin{
    //Note that the name here needs to be the same as that defined in Android native
    static const MethodChannel _channel = MethodChannel("apk_md5");

    static Future<String> getMd5() async{
        //Pass string to Android
        var param = "hello";
        
        //Pass a method name, that is, call the native method of Android
        //Notice the second parameter here
        return await _channel.invokeMethod("getMd5",param);
    }
}

Receiving in Android (step 3 above):

After determining the method name, you can obtain data through the corresponding method (type conversion is required)

public class MyTestPlugin implements FlutterPlugin {
    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
        ...
        
        //Set the current MethodCallHandler to
        channel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
                String method = call.method;
                if (method.equals("getText")) {
                    
                    //Note the acquired data (forced rotation) here
                    String packageName = (String)call.arguments;
                   
                    ellipsis...
                } else {
                    //If the id method name passed by Flutter is not found, call this method
                    result.notImplemented();
                }
            }
        });
    }
   ...
}

The above code is just a data flyer. What if you want to wear multiple data?

Because the invokeMethod() method only supports leaflet data, we need to transfer map or json format data to Android native

Data sent by Flutter:

var param = {"myKey":"hello"}

//Pass a method name, that is, call the native method of Android
//Notice the second parameter here
return await _channel.invokeMethod("getMd5",param);

Android receive data:

String packageName = call.argument("myKey");

Note that there is an arguments attribute and an arguments() method in call, as shown in the following figure

If the data passed from the fluent is map or json, you have to use arguments() to obtain the parameter data; Otherwise, the arguments attribute is used

Of course, if the passed data is of map or json type, call provides a convenient and fast method. We can directly use argument(key) to directly obtain the value corresponding to the key (note that type coercion is also required here, and note that type correspondence is required)

Finally, the corresponding type table of fluent and Java is given here:

Dart Android
null null
bool java.lang.Boolean
int java.lang.Integer
int, if 32 bits not enough java.lang.Long
double java.lang.Double
String java.lang.String
Uint8List byte[]
Int32List int[]
Int64List long[]
Float64List double[]
List java.util.ArrayList
Map java.util.HashMap

Code reference

Click to view the source code (ApkMd5CheckPlugin class)
package com.example.taiji_lianjiang.checkplugin;

import android.app.Activity;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.text.TextUtils;

import com.example.taiji_lianjiang.BuildConfig;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import androidx.annotation.NonNull;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;

public class ApkMd5CheckPlugin implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware {

    public static ApkMd5CheckPlugin getInstance() {
        return new ApkMd5CheckPlugin();
    }

    private MethodChannel channel;
    private Activity context;

    @Override
    public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
        //Set the channel name, and then it will be the same in the shuttle
        channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), "apk_md5");
        //Set the current MethodCallHandler to
        channel.setMethodCallHandler(this);
    }

    @Override
    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {

    }

    @Override
    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
        String method = call.method;
        if (method.equals("getMd5")) {
            String md5 = getMd5(context);
            if (!TextUtils.isEmpty(md5)) {
                result.success(md5);
            } else {
                result.error("101", "obtain md5 fail", "");
            }
        } else {
            result.notImplemented();
        }
    }

    //Get your own installation package location, usually in / data/app / package name / xxx apk
    private String getApkPath(Context context) {
        try {
            PackageInfo packageInfo = context.getPackageManager().getPackageInfo(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA);
            ApplicationInfo applicationInfo = packageInfo.applicationInfo;
            return applicationInfo.publicSourceDir; // Gets the absolute path of the current apk package
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return "";
    }

    //Get the hash value of the whole apk. Note that the code here is not too rigorous. Just knock the demo to run through
    private String getMd5(Context context) {
        String apkPath = getApkPath(context);
        StringBuffer sb = new StringBuffer("");
        try {

            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(readFileToByteArray(new File(apkPath)));
            byte b[] = md.digest();
            int d;
            for (int i = 0; i < b.length; i++) {
                d = b[i];
                if (d < 0) {
                    d = b[i] & 0xff;
                    // Same as the previous line
                    // i += 256;
                }
                if (d < 16)
                    sb.append("0");
                sb.append(Integer.toHexString(d));
            }
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sb.toString().toUpperCase();
    }

    private byte[] readFileToByteArray(File file) throws IOException {
        InputStream in = null;
        try {
            in = new FileInputStream(file);
            return toByteArray(in, file.length());
        } finally {
            in.close();
        }
    }

    private byte[] toByteArray(InputStream input, long size) throws IOException {
        if (size > Integer.MAX_VALUE) {
            throw new IllegalArgumentException("Size cannot be greater than Integer max value: " + size);
        }

        return toByteArray(input, (int) size);
    }

    private byte[] toByteArray(InputStream input, int size) throws IOException {

        if (size < 0) {
            throw new IllegalArgumentException("Size must be equal or greater than zero: " + size);
        }

        if (size == 0) {
            return new byte[0];
        }

        byte[] data = new byte[size];
        int offset = 0;
        int readed;

        while (offset < size && (readed = input.read(data, offset, size - offset)) != -1) {
            offset += readed;
        }

        if (offset != size) {
            throw new IOException("Unexpected readed size. current: " + offset + ", excepted: " + size);
        }

        return data;
    }


    @Override
    public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
        context = binding.getActivity();
    }

    @Override
    public void onDetachedFromActivityForConfigChanges() {

    }

    @Override
    public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {

    }

    @Override
    public void onDetachedFromActivity() {
        context = null;
    }
}

reference resources

Keywords: Android Flutter

Added by munuindra on Mon, 27 Dec 2021 20:50:44 +0200