android-activity-0.2.0.0: java/systems/obsidian/HaskellActivity.java
package systems.obsidian;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.Manifest;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.webkit.PermissionRequest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.SynchronousQueue;
import java.util.HashMap;
import java.util.HashSet;
import android.webkit.ValueCallback;
import android.webkit.CookieManager;
public class HaskellActivity extends Activity {
public native int haskellStartMain(SynchronousQueue<Long> setCallbacks);
public native void haskellOnCreate(long callbacks);
public native void haskellOnStart(long callbacks);
public native void haskellOnResume(long callbacks);
public native void haskellOnPause(long callbacks);
public native void haskellOnStop(long callbacks);
public native void haskellOnDestroy(long callbacks);
public native void haskellOnRestart(long callbacks);
public native void haskellOnBackPressed(long callbacks);
public native void haskellOnNewIntent(long callbacks, String intent, String intentdata);
public static final int REQUEST_CODE_FILE_PICKER = 51426;
// Apparently 'long' is the right way to store a C pointer in Java
// See https://stackoverflow.com/questions/337268/what-is-the-correct-way-to-store-a-native-pointer-inside-a-java-object
final long callbacks;
static {
System.loadLibrary("HaskellActivity");
}
public HaskellActivity() throws InterruptedException {
final SynchronousQueue<Long> setCallbacks = new SynchronousQueue<Long>();
permissionRequests = new HashMap<Integer, PermissionRequest>();
new Thread() {
public void run() {
final int exitCode = haskellStartMain(setCallbacks);
Log.d("HaskellActivity", String.format("Haskell main action exited with status %d", exitCode));
try {
// Since Haskell's main has exited, it won't call mainStarted.
// Instead, we unblock the main thread here.
//TODO: If continueWithCallbacks has already been called, is this safe?
setCallbacks.put(0L); //TODO: Always call finish() if we hit this
} catch(InterruptedException e) {
//TODO: Should we do something with this?
}
}
}.start();
callbacks = setCallbacks.take();
}
// This can be called by the Haskell application to unblock the construction
// of the HaskellActivity and proceed with the given callbacks.
// NOTE: This shouldn't be an instance method, because it must be called *before*
// the constructor returns (it *causes* the constructor to return).
// 'callbacks' should never be 0
private static void continueWithCallbacks(SynchronousQueue<Long> setCallbacks, long callbacks) {
try {
setCallbacks.put(callbacks);
} catch(InterruptedException e) {
Log.d("HaskellActivity", "setting callbacks interrupted");
//TODO: Should we do something with this?
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We can't call finish() in the constructor, as it will have no effect, so
// we call it here whenever we reach this code without having hit
// 'continueWithCallbacks'
if(callbacks == 0) {
finish();
} else {
haskellOnCreate(callbacks); //TODO: Pass savedInstanceState as well
}
}
@Override
public void onStart() {
super.onStart();
if(callbacks != 0) {
haskellOnStart(callbacks);
}
}
@Override
public void onResume() {
super.onResume();
if(callbacks != 0) {
haskellOnResume(callbacks);
}
}
@Override
public void onPause() {
super.onPause();
if(callbacks != 0) {
haskellOnPause(callbacks);
}
CookieManager.getInstance().flush();
}
@Override
public void onStop() {
super.onStop();
if(callbacks != 0) {
haskellOnStop(callbacks);
}
CookieManager.getInstance().flush();
}
@Override
public void onDestroy() {
super.onDestroy();
if(callbacks != 0) {
haskellOnDestroy(callbacks);
}
//TODO: Should we call hs_exit somehow here?
android.os.Process.killProcess(android.os.Process.myPid()); //TODO: Properly handle the process surviving between invocations which means that the Haskell RTS needs to not be initialized twice.
}
@Override
public void onRestart() {
super.onRestart();
if(callbacks != 0) {
haskellOnRestart(callbacks);
}
}
@Override
public void onBackPressed() {
if(callbacks != 0) {
haskellOnBackPressed(callbacks);
}
}
@Override
public void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if(callbacks != 0 && intent != null && intent.getData() != null && intent.getAction() != null) {
haskellOnNewIntent(callbacks, intent.getAction(), intent.getDataString()); //TODO: Use a more canonical way of passing this data - i.e. pass the Intent and let the Haskell side get the data out with JNI
}
}
// Proper separation of concerns is really a whole lot of work in Java, so
// we simply handle PermissionRequests directly here - it is just sooo much
// easier. Java makes it really hard to write good code.
public void requestWebViewPermissions(final PermissionRequest request) {
try {
String[] resources = request.getResources();
ArrayList<String> sysResourcesToRequestList = new ArrayList<String>();
for(int i=0; i < resources.length; i++) {
String manifestRequest = null;
switch(resources[i]) {
case PermissionRequest.RESOURCE_AUDIO_CAPTURE:
manifestRequest = Manifest.permission.RECORD_AUDIO;
break;
case PermissionRequest.RESOURCE_VIDEO_CAPTURE:
manifestRequest = Manifest.permission.CAMERA;
break;
}
if(manifestRequest != null && checkSelfPermission(manifestRequest) != PackageManager.PERMISSION_GRANTED)
sysResourcesToRequestList.add(manifestRequest);
}
String[] sysResourcesToRequest = sysResourcesToRequestList.toArray(new String[0]);
if(sysResourcesToRequest.length > 0) {
permissionRequests.put(nextRequestCode, request);
requestPermissions(sysResourcesToRequest, nextRequestCode++);
}
else {
runOnUiThread(new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
request.grant(request.getResources());
}
});
}
} catch (NoSuchMethodError e) { // Compatibility for older Android versions (Android 5 and below)
runOnUiThread(new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
request.grant(request.getResources());
}
});
}
}
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
final PermissionRequest request = permissionRequests.get(requestCode);
permissionRequests.remove(requestCode);
String[] requestedPermissions = request.getResources();
final HashSet<String> grantedPermissions = new HashSet<String>(Arrays.asList(requestedPermissions));
// We assume grantResults and permissions have same length ... obviously.
for(int i = 0; i< permissions.length; i++) {
if(grantResults[i] == PackageManager.PERMISSION_GRANTED) {
String permission = null;
switch(permissions[i]) {
case Manifest.permission.RECORD_AUDIO:
Log.d("HaskellActivity", "Granting RESOURCE_AUDIO_CAPTURE!");
permission = PermissionRequest.RESOURCE_AUDIO_CAPTURE;
break;
case Manifest.permission.CAMERA:
Log.d("HaskellActivity", "Granting RESOURCE_VIDEO_CAPTURE!");
permission = PermissionRequest.RESOURCE_VIDEO_CAPTURE;
break;
}
if(permission != null && grantResults[i] != PackageManager.PERMISSION_GRANTED)
grantedPermissions.remove(permission);
}
}
runOnUiThread(new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
if(grantedPermissions.size() > 0) {
Log.d("HaskellActivity", "Granting permissions!");
request.grant(grantedPermissions.toArray(new String[0]));
}
else {
request.deny();
}
}
});
}
// File uploads don't work out of the box.
// You have to start an 'Intent' from 'onShowFileChooser' by setting the callback with
// 'setFileUploadCallback'. The callback will be handled here.
@Override
public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) {
if (requestCode == REQUEST_CODE_FILE_PICKER) {
if (resultCode == Activity.RESULT_OK) {
if (intent != null) {
if (fileUploadCallback != null) {
Uri[] dataUris = null;
try {
if (intent.getDataString() != null) {
dataUris = new Uri[] { Uri.parse(intent.getDataString()) };
}
else {
if (intent.getClipData() != null) {
final int numSelectedFiles = intent.getClipData().getItemCount();
dataUris = new Uri[numSelectedFiles];
for (int i = 0; i < numSelectedFiles; i++) {
dataUris[i] = intent.getClipData().getItemAt(i).getUri();
}
}
}
}
catch (Exception ignored) { }
fileUploadCallback.onReceiveValue(dataUris);
fileUploadCallback = null;
}
}
}
else if (fileUploadCallback != null) {
fileUploadCallback.onReceiveValue(null);
fileUploadCallback = null;
}
}
}
// Set the file upload callback to be used by 'onActivityResult'.
public void setFileUploadCallback(ValueCallback<Uri[]> cb) {
if (fileUploadCallback != null) {
fileUploadCallback.onReceiveValue(null);
}
fileUploadCallback = cb;
}
private HashMap<Integer, PermissionRequest> permissionRequests;
private int nextRequestCode = 0;
private ValueCallback<Uri[]> fileUploadCallback;
}