Pages

Thursday, 12 November 2015

Android M run-time permission using Fragments in Unity 3D

If application is built for Android M then the dangerous permissions are granted at run time on devices with Android M, not installation time. Users can revoke permission whenever they want, irrespective of the target SDK set.

Run-time permission support can be implemented in your Activity implementation or Fragment implementation.

In Unity, Activity is created by its framework and adding Fragment to support permission required for your Android plug-in looks the better approach for me. With this approach we do not need to modify the Activity implementation at all.

We would need to get the permission granted that are additionally required by us. Permissions required by unity features will be handled by Unity itself(e.g. WebCamTexture).

Now lets add run-time permission support for our Unity application.

Update the target SDK

 First thing to build application with Unity for M is to set the target SDK as 23. At the time of writing this blog Unity does not have the support for Android M.
So we can go ahead and set the target SDK as Android M in the manifest file. If you do not have the AndroidManifest file available with you, you can export
the Android project(To export select File->Build Settings and select Android and then Google Android Project) form Unity editor.
Modify the manifest file to set the target SDK as,

 <uses-sdk android:minsdkversion="14" android:targetsdkversion="23"/>
Manifest file should be kept at UnityProject\Assets\Plugins\Android.

Create Android plug-in which requests permissions

Our Android plug-in will be a jar library with just two classes, first one the fragment which requests permissions and the second one a helper class which will be used from c# code.

The jar library will also be kept at UnityProject\Assets\Plugins\Android

To create the jar library you can use Eclipse or Android Studio or you can use javac and jar directly.

Fragment class is given below,

package com.example.androidproject.flash;

import android.app.Fragment;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

public class PermissionFragment extends Fragment {

 // This is used while requesting for permission
    public static final int PERMISSION_REQUEST_CODE = 1000;

 // Required permissions list
    private static final String [] REQUIRED_PERMISSIOS = {
            "android.permission.CAMERA"
    };

    private boolean mAskedPermission = false;

    public PermissionFragment() {}


    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        checkThemePermissions();
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    public void checkThemePermissions() {

  // Check and request for permissions for Android M and older versions
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !mAskedPermission) {

            ArrayList<String> requiredPermissions = new ArrayList<String>();

            for (String perm : REQUIRED_PERMISSIOS) {
                if (getActivity().checkSelfPermission(perm) != PackageManager.PERMISSION_GRANTED) {
                    requiredPermissions.add(perm);
                }
            }

            if (requiredPermissions.size() > 0) {
                boolean pr = false;
                for (String p: requiredPermissions) {
                    pr = shouldShowRequestPermissionRationale(p);
                    if (pr) {
                        break;
                    }
                }

                if (pr) {
                    // We've been denied once before,
                    // Add you logic here as to why we should request for permission once again
                }
                this.requestPermissions(requiredPermissions.toArray(new String[requiredPermissions.size()]), PERMISSION_REQUEST_CODE);
            } else {
                PermissionRequester.instance().onComplete(true);
            }
        } else {
            PermissionRequester.instance().onComplete(true);
        }

        mAskedPermission = true;
    }

 // Once user allow or deny permissions this method is called
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        if (requestCode == PERMISSION_REQUEST_CODE) {
            boolean res = true;
            for (int i: grantResults) {
                if (i != PackageManager.PERMISSION_GRANTED) {
                    res = false;
                    break;
                }
            }

            PermissionRequester.instance().onComplete(res);
        }

        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}
The helper class is given below, this class will be used from Unity from c# code,
package com.example.androidproject.flash;

import android.app.Activity;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.hardware.Camera;

public class PermissionRequester {
    // A derived class will be created from this interface in the unity side
 // to notify once onRequestPermissionsResult is called.
 // This method onComplete will be called from PermissionFragment.onRequestPermissionsResult
    public static interface Listener {
        public void onComplete(boolean status);
    }

    private Activity mActivity;
    private Listener mListener;

    public static PermissionRequester mListner = null;

    public static PermissionRequester instance() {
        return mListner;
    }

    public PermissionRequester(Activity a, Listener l) {
        mListner = this;

        mActivity = a;
        mListener = l;

  // Create fragment PermissionFragment and add to our activity.
        FragmentManager fm = mActivity.getFragmentManager();
        FragmentTransaction ft = fm.beginTransaction();
        ft.add(new PermissionFragment(), "perm_fragment");
        ft.commit();
    }

 // Called from PermissionFragment.onRequestPermissionsResult
 // Notify the listner once this method is called
    public void onComplete(boolean v) {
        mListener.onComplete(v);
    }    
}

Requesting permission from C# code

Create the proxy class(derived from PermissionRequester.Listener) to receive onRequestPermissionsResult notification
private sealed class PermissionListnerProxy : AndroidJavaProxy {
  
 public PermissionListnerProxy()
  : base("com.example.androidproject.flash.PermissionRequester$Listener") {
 }
  
 public void onComplete(bool res) {
  Debug.Log("Permission status :" + res);
 }
}

Now lets make use of PermissionRequester to actually request for permissions.
To do that lets get a reference to the Activity class first,

 AndroidJavaClass unityPlayer = new AndroidJavaClass ("com.unity3d.player.UnityPlayer");
 AndroidJavaObject currentActivity = unityPlayer.GetStatic<androidjavaobject> ("currentActivity");

Now lets create an instance of PermissionRequester which will be requesting for permissions, if required.


    currentActivity.Call ("runOnUiThread", new AndroidJavaRunnable (() => {
        permissionRequester = new AndroidJavaObject("com.example.androidproject.flash.PermissionRequester",
                                      currentActivity,
                                      new PermissionListnerProxy());
    }));

Thats it, we have actually added support for Android M without doing any modification to the Activity.


Another way to support Android M permission related changes is,
  1. Get the Android project exported from Unity editor
  2. Modify the Activity implementation class in the exported project similar to what we done in PermissionFragment
  3. Create the jar file
  4. Copy the jar and manifest file to UnityProject\Assets\Plugins\Android

4 comments:

  1. Replies
    1. You can ignore SwitchScript, it is the script which is attached to a GameObject. I will remove that to avoid any confusion

      Delete
  2. thanks for this article!

    I've run into a problem where saving a file doesnt work after granting the write_external_storage permission, the funny thing is it works once the app is relaunched. any ideas?

    ReplyDelete
  3. Thanks for the article, but I have the same issue. Permission is not granted until I restart the app. (using Unity 4.7.1f ). Is there any solution to this?

    ReplyDelete