/*
 * AndroWish.java --
 *
 *	Main application class and some supplemental classes
 *	of the "AndroWish" Tcl/Tk runtime environment.
 *
 * Copyright (c) 2013-2024 Christian Werner <chw@ch-werner.de>
 *
 * See the file "license.terms" for information on usage and redistribution of
 * this file, and for a DISCLAIMER OF ALL WARRANTIES.
 */

package tk.tcl.wish;

import org.libsdl.app.SDLActivity;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.Map.*;
import java.io.*;
import android.os.*;
import android.os.PowerManager.*;
import android.net.*;
import android.app.*;
import android.content.*;
import android.content.pm.*;
import android.content.res.*;
import android.database.*;
import android.media.*;
import android.graphics.*;
import android.view.*;
import android.location.*;
import android.speech.*;
import android.speech.tts.TextToSpeech;
import android.speech.tts.TextToSpeech.*;
import android.net.*;
import android.util.*;
import android.bluetooth.*;
import android.widget.*;
import androidx.core.app.NotificationCompat;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.hardware.*;
import android.hardware.usb.*;
import android.telephony.*;
import android.nfc.*;
import android.nfc.tech.*;
import android.view.inputmethod.InputMethodManager;

public class AndroWish extends SDLActivity
    implements LocationListener,
	       ActivityCompat.OnRequestPermissionsResultCallback {

    static final String TAG = "AndroWish";

    protected static AndroWish mSingleton;
    protected static PackageManager mPackageManager;
    protected static Vibrator mVibrator;
    protected static TextToSpeech mTTS;
    protected static CountDownLatch mTTSLock;
    protected static TTS_UPL mTTSUPL;
    protected static boolean mTTSAvailable;
    protected static int mTTSCount;
    protected static Map<String, Location> mLocations;
    protected static LocationManager mLocationMgr;
    protected static ConnectivityManager mConnMgr;
    protected static Object mNetLock;
    protected static NetworkInfo mNetworkInfo;
    protected static String mNTl[], mNTa[], mNTe[];
    protected static BroadcastReceiver mNWRecvr;
    protected static BroadcastReceiver mNTRecvr;
    protected static NotificationManager mNotificationMgr;
    protected static BluetoothAdapter mBluetoothAdap;
    protected static BroadcastReceiver mBTRecvr, mBDRecvr;
    protected static ProgressDialog mSpinner;
    protected static ContentResolver mContentResolver;
    protected static AlarmManager mAlarmManager;
    protected static Ringtone mCurrentTone;
    protected static UsbManager mUsbManager;
    protected static Sensors mSensors;
    protected static SpeechRec mSpeechRec;
    protected static Object mConfigLock;
    protected static Configuration mConfig;
    protected static GpsInfo mGpsInfo;
    protected static NmeaInfo mNmeaInfo;
    protected static TelephonyManager mPhoneManager;
    protected static PSListener mPSListener;
    protected static Map<String, BCRecvr> mBCRecvrs;
    protected static ImageCapture mCamera;
    protected static BroadcastReceiver mUsbRecvr;
    protected static Map<String, UsbDeviceConnection> mUsbHold;
    protected static BCRecvr mNfcBCRecvr;
    protected static NfcAdapter mNfcAdapter;
    protected static Tag mNfcTag;
    protected static CountDownLatch mPermLock;

    private int mSavedOrient;

    /*
     * Callback on application startup
     */

    @Override
    public void onCreate(Bundle savedInstanceData) {
	super.onCreate(savedInstanceData);
	if (mSingleton != null) {
	    /* Should not happen, but ... */
	    finish();
	    return;
	}
	mSingleton = this;
	int glesVer = getGLESVersion(this);
	String lang = Locale.getDefault().toString();
	Log.v(TAG, "onCreate GLES=" + glesVer + " LANG=" + lang);
	nativeInit(glesVer, lang);
	mPackageManager = getPackageManager();
	mNotificationMgr = (NotificationManager)
	    getSystemService(Context.NOTIFICATION_SERVICE);
	mContentResolver = getContentResolver();
	if (hasPerm(android.Manifest.permission.VIBRATE)) {
	    mVibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
	}
	if (hasPerm(android.Manifest.permission.ACCESS_FINE_LOCATION) ||
	    hasPerm(android.Manifest.permission.ACCESS_COARSE_LOCATION)) {
	    mLocationMgr = (LocationManager)
		getSystemService(Context.LOCATION_SERVICE);
	    if (mLocationMgr != null) {
		mGpsInfo = new GpsInfo(this);
		mLocationMgr.addGpsStatusListener(mGpsInfo);
		mNmeaInfo = new NmeaInfo(this);
		mLocationMgr.addNmeaListener(mNmeaInfo);
	    }
	}
	mLocations = new HashMap<String, Location>();
	mNetLock = new Object();
	mNTl = null;
	mNTa = null;
	mNTe = null;
	if (hasPerm(android.Manifest.permission.ACCESS_NETWORK_STATE)) {
	    mConnMgr = (ConnectivityManager)
		getSystemService(Context.CONNECTIVITY_SERVICE);
	    IntentFilter filter = new IntentFilter();
	    filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
	    mNWRecvr = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
		    synchronized (mNetLock) {
			mNetworkInfo = mConnMgr.getActiveNetworkInfo();
		    }
		    mSingleton.nativeTriggerNetworkInfo();
		}
	    };
	    registerReceiver(mNWRecvr, filter);
	    filter = new IntentFilter();
	    filter.addAction("android.net.conn.TETHER_STATE_CHANGED");
	    mNTRecvr = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
		    if (intent.getAction().equals("android.net.conn.TETHER_STATE_CHANGED")) {
			ArrayList<String> a =
			    intent.getStringArrayListExtra("activeArray");
			ArrayList<String> l =
			    intent.getStringArrayListExtra("availableArray");
			ArrayList<String> e =
			    intent.getStringArrayListExtra("erroredArray");
			synchronized (mNetLock) {
			    if (a != null) {
				mNTa = new String[a.size()];
				a.toArray(mNTa);
			    } else {
				mNTa = null;
			    }
			    if (l != null) {
				mNTl = new String[l.size()];
				l.toArray(mNTl);
			    } else {
				mNTl = null;
			    }
			    if (e != null) {
				mNTe = new String[e.size()];
				e.toArray(mNTe);
			    } else {
				mNTe = null;
			    }
			}
			mSingleton.nativeTriggerTetherInfo();
		    }
		}
	    };
	    registerReceiver(mNTRecvr, filter);
	}
	if (hasPerm(android.Manifest.permission.BLUETOOTH) &&
	    hasPerm(android.Manifest.permission.BLUETOOTH_ADMIN)) {
	    mBluetoothAdap = BluetoothAdapter.getDefaultAdapter();
	    IntentFilter filter = new IntentFilter();
	    filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED);
	    filter.addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
	    mBTRecvr = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
		    mSingleton.nativeTriggerBluetooth();
		}
	    };
	    registerReceiver(mBTRecvr, filter);
	    filter = new IntentFilter();
	    filter.addAction(BluetoothDevice.ACTION_FOUND);
	    filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
	    filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
	    filter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
	    mBDRecvr = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent i) {
		    String action = i.getAction();
		    String uristr = i.getDataString();
		    String type = i.getType();
		    String[] cats = null;
		    String args[] = null;
		    try {
			BluetoothDevice dev =
			    i.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
			if ((dev.getAddress() != null) &&
			    (dev.getName() != null)) {
			    args = new String[2];
			    args[0] = dev.getAddress();
			    args[1] = dev.getName();
			}
		    } catch (Exception e) {
		    }
		    Log.v(TAG, "nativeTriggerIntent: " + action + "," +
			  uristr + "," + type + "," + cats + "," + args);
		    mSingleton.nativeTriggerIntent(action, uristr, type,
						   cats, args);
		}
	    };
	    registerReceiver(mBDRecvr, filter);
	}
	mAlarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
	if (hasPerm(android.Manifest.permission.WAKE_LOCK) &&
	    getIntent().getBooleanExtra("tk.tcl.wish.wakeup", false)) {
	    PowerManager pm =
		(PowerManager) getSystemService(Context.POWER_SERVICE);
	    WakeLock wl =
		pm.newWakeLock(PowerManager.FULL_WAKE_LOCK |
			       PowerManager.ACQUIRE_CAUSES_WAKEUP,
			       TAG);
	    wl.acquire();
	    wl.release();
	}
	mCurrentTone = null;
	if (android.os.Build.VERSION.SDK_INT >= 11) {
	    mUsbManager = (UsbManager) getSystemService(Context.USB_SERVICE);
	}
	mSensors = new Sensors(this);
	if (hasPerm(android.Manifest.permission.RECORD_AUDIO)) {
	    mSpeechRec = new SpeechRec(this);
	}
	mConfigLock = new Object();
	synchronized (mConfigLock) {
	    mConfig = new Configuration(getResources().getConfiguration());
	}
	mPSListener = null;
	mPhoneManager =
	    (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
	if (mPhoneManager != null) {
	    if (hasPerm(android.Manifest.permission.READ_PHONE_STATE)) {
		mPSListener = new PSListener(this);
		mPhoneManager.listen(mPSListener,
		    PhoneStateListener.LISTEN_DATA_CONNECTION_STATE |
		    PhoneStateListener.LISTEN_SERVICE_STATE |
		    PhoneStateListener.LISTEN_CALL_STATE |
		    PhoneStateListener.LISTEN_DATA_ACTIVITY |
		    PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
	    }
	}
	mBCRecvrs = new HashMap<String, BCRecvr>();
	if (hasPerm(android.Manifest.permission.CAMERA)) {
	    ImageCapture.init(getContext());
	    mCamera = ImageCapture.get();
	}
	if ((android.os.Build.VERSION.SDK_INT >= 11) && (mUsbManager != null)) {
	    mUsbHold = new HashMap<String, UsbDeviceConnection>();
	    IntentFilter filter = new IntentFilter();
	    filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
	    filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
	    mUsbRecvr = new BroadcastReceiver() {
		@Override
		public void onReceive(Context context, Intent intent) {
		    String a = intent.getAction();
		    if (a.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
			/*
			 * Seems to be reported on Android >= 4.4.
			 * Older version must poll for USB devices.
			 */
			mSingleton.nativeTriggerUsb(1);
		    } else if (a.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
			mSingleton.nativeTriggerUsb(0);
			UsbDevice d = (UsbDevice)
			    intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
			if (d != null) {
			    String n = d.getDeviceName();
			    UsbDeviceConnection c = mUsbHold.get(n);
			    if (c != null) {
				c.close();
				mUsbHold.remove(n);
			    }
			}
		    }
		}
	    };
	    registerReceiver(mUsbRecvr, filter);
	}
	if (hasPerm(android.Manifest.permission.NFC)) {
	    NfcManager manager =
		(NfcManager) getSystemService(Context.NFC_SERVICE);
	    mNfcAdapter = manager.getDefaultAdapter();
	} else {
	    mNfcAdapter = null;
	}
	mNfcBCRecvr = null;
	mNfcTag = null;
	mSavedOrient = getRequestedOrientation();
    }

    /*
     * Tear down everything
     */

    @Override
    protected void onDestroy() {
	Log.v(TAG, "onDestroy()");
	super.onDestroy();
	if (mPSListener != null) {
	    mPhoneManager.listen(mPSListener, PhoneStateListener.LISTEN_NONE);
	    mPSListener = null;
	}
	if (mSpeechRec != null) {
	    mSpeechRec.destroy();
	    mSpeechRec = null;
	}
	if (mNWRecvr != null) {
	    unregisterReceiver(mNWRecvr);
	    mNWRecvr = null;
	}
	if (mNTRecvr != null) {
	    unregisterReceiver(mNTRecvr);
	    mNTRecvr = null;
	}
	if (mBTRecvr != null) {
	    unregisterReceiver(mBTRecvr);
	    mBTRecvr = null;
	}
	if (mBDRecvr != null) {
	    unregisterReceiver(mBDRecvr);
	    mBDRecvr = null;
	}
	if (mGpsInfo != null) {
	    mLocationMgr.removeGpsStatusListener(mGpsInfo);
	    mGpsInfo = null;
	}
	if (mNmeaInfo != null) {
	    mLocationMgr.removeNmeaListener(mNmeaInfo);
	    mNmeaInfo = null;
	}
	if (mLocationMgr != null) {
	    mLocationMgr.removeUpdates(this);
	    mLocationMgr = null;
	}
	nfcEnable(false);
	synchronized (mBCRecvrs) {
	    for (Entry<String, BCRecvr> entry : mBCRecvrs.entrySet()) {
		BCRecvr bcr = entry.getValue();
		unregisterReceiver(bcr);
	    }
	    mBCRecvrs.clear();
	}
	mNfcBCRecvr = null;
	if (mCamera != null) {
	    mCamera.onDestroy();
	    mCamera = null;
	}
	if (mUsbRecvr != null) {
	    unregisterReceiver(mUsbRecvr);
	    mUsbRecvr = null;
	}
	if (mUsbHold != null) {
	    for (final UsbDeviceConnection c : mUsbHold.values()) {
		c.close();
	    }
	    mUsbHold.clear();
	    mUsbHold = null;
	}
    }

    /*
     * Find out if OpenGL ES >= 2.x is available.
     */

    private static int getGLESVersion(Context context) {
	final ActivityManager am = (ActivityManager)
	    context.getSystemService(Context.ACTIVITY_SERVICE);
	final ConfigurationInfo ci = am.getDeviceConfigurationInfo();
	return (ci.reqGlEsVersion & 0xFFFF0000) >> 16;
    }

    /*
     * Check for a permission
     */

    static boolean hasPerm(String which) {
	int result =
	    mSingleton.getContext().checkCallingOrSelfPermission(which);
	return result == PackageManager.PERMISSION_GRANTED;
    }

    /*
     * Pause/resume handling
     */

    @Override
    public void onPause() {
	super.onPause();
	mSensors.pause();
	if (mPSListener != null) {
	    mPhoneManager.listen(mPSListener, PhoneStateListener.LISTEN_NONE);
	}
	if (mCamera != null) {
	    mCamera.onPause();
	}
	nfcEnable(false);
	mSavedOrient = getRequestedOrientation();
	int orient;
	if (mConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
	    orient = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
	} else {
	    orient = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
	}
	if (android.os.Build.VERSION.SDK_INT >= 27) {
	    setRequestedOrientation(orient);
	}
    }

    @Override
    public void onResume() {
	super.onResume();
	mSensors.resume();
	if (mPSListener != null) {
	    mPhoneManager.listen(mPSListener,
		PhoneStateListener.LISTEN_DATA_CONNECTION_STATE |
		PhoneStateListener.LISTEN_SERVICE_STATE |
		PhoneStateListener.LISTEN_CALL_STATE |
		PhoneStateListener.LISTEN_DATA_ACTIVITY |
		PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);
	}
	Configuration config = getResources().getConfiguration();
	boolean trigger = false;
	synchronized (mConfigLock) {
	    if ((config.keyboard != mConfig.keyboard) ||
		(config.keyboardHidden != mConfig.keyboardHidden) ||
		(config.hardKeyboardHidden != mConfig.hardKeyboardHidden)) {
		trigger = true;
	    }
	    mConfig = new Configuration(config);
	}
	if (trigger) {
	    mSingleton.nativeTriggerKeyboard();
	}
	if (mCamera != null) {
	    mCamera.onResume();
	}
	nfcEnable(true);
	if (android.os.Build.VERSION.SDK_INT >= 27) {
	    setRequestedOrientation(mSavedOrient);
	}
    }

    /*
     * Configuration (e.g. keyboard) has changed
     */

    @Override
    public void onConfigurationChanged(Configuration config) {
	Log.v(TAG, "onConfigurationChanged: " + config);
	super.onConfigurationChanged(config);
	boolean trigger = false;
	boolean hideIM = false;
	synchronized (mConfigLock) {
	    if ((config.keyboard != mConfig.keyboard) ||
		(config.keyboardHidden != mConfig.keyboardHidden) ||
		(config.hardKeyboardHidden != mConfig.hardKeyboardHidden)) {
		hideIM = (mConfig.hardKeyboardHidden ==
			  Configuration.HARDKEYBOARDHIDDEN_NO) &&
			 (config.hardKeyboardHidden ==
			  Configuration.HARDKEYBOARDHIDDEN_YES);
		trigger = true;
	    }
	    mConfig = new Configuration(config);
	}
	if (hideIM) {
	    super.hideTextInput();
	}
	if (trigger) {
	    mSingleton.nativeTriggerKeyboard();
	}
    }

    /*
     * Turn NFC foreground dispatch on or off.
     */

    int nfcEnable(boolean on) {
	if (mNfcAdapter == null) {
	    return -1;
	}
	if ((mNfcBCRecvr != null) && on) {
	    PendingIntent pi =
		PendingIntent.getBroadcast(getContext(), 0,
					   new Intent("tk.tcl.wish.nfc"), 0);
	    /*
	     * This should be sufficient but doesn't work, unfortunately:
	     *
	     *   mNfcAdapter.enableForegroundDispatch(this, pi, null, null);
	     *
	     * thus use filters for both intent and tech.
	     */

	    IntentFilter ndef =
		new IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED);
	    IntentFilter[] filt = new IntentFilter[] { ndef };
	    String[][] techs = new String[][] {
		new String[] { Ndef.class.getName() },
		new String[] { NdefFormatable.class.getName() }
	    };
	    try {
		mNfcAdapter.enableForegroundDispatch(this, pi, filt, techs);
	    } catch (Exception e) {
		return 0;
	    }
	    mNfcTag = null;
	} else {
	    try {
		mNfcAdapter.disableForegroundDispatch(this);
	    } catch (Exception e) {
		return 0;
	    }
	    mNfcTag = null;
	}
	return 1;
    }

    /*
     * Encode Bundle to String[] for "array set"
     */

    String[] formatBundle(Bundle b, String[] add) {
	Set<String> keys = b.keySet();
	String[] ret = new String[keys.size() * 2 +
				  ((add != null) ? add.length : 0)];
	int k = 0;
	Iterator<String> it = keys.iterator();
	while (it.hasNext()) {
	    String key = it.next();
	    Object obj = b.get(key);
	    ret[k++] = key;
	    if (obj == null) {
		ret[k] = new String("");
	    } else if (obj instanceof String) {
		ret[k] = (String) obj;
	    } else if (obj instanceof String[]) {
		ret[k] = encodeStringList((String[]) obj);
	    } else if (obj instanceof byte[]) {
		ret[k] =
		    android.util.Base64.encodeToString((byte[]) obj,
					    android.util.Base64.DEFAULT);
	    } else if (obj instanceof Bitmap) {
		ret[k] = encodeBitmap((Bitmap) obj);
	    } else if (obj instanceof ArrayList) {
		ret[k] = encodeToList((ArrayList) obj);
	    } else if (obj instanceof double[]) {
		ret[k] = encodeDoubleList((double[]) obj);
	    } else if (obj instanceof float[]) {
		ret[k] = encodeFloatList((float[]) obj);
	    } else if (obj instanceof short[]) {
		ret[k] = encodeShortList((short[]) obj);
	    } else if (obj instanceof int[]) {
		ret[k] = encodeIntList((int[]) obj);
	    } else if (obj instanceof long[]) {
		ret[k] = encodeLongList((long []) obj);
	    } else if (obj instanceof boolean[]) {
		ret[k] = encodeBoolList((boolean []) obj);
	    } else if (obj instanceof char[]) {
		ret[k] = encodeCharList((char []) obj);
	    } else if (obj instanceof Boolean) {
		ret[k] = ((Boolean) obj).booleanValue() ? "1" : "0";
	    } else if ((obj instanceof Parcelable[]) &&
		       key.equals(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
		ArrayList<byte[]> al = new ArrayList<byte[]>();
		Parcelable[] pa = (Parcelable[]) obj;
		for (int n = 0; n < pa.length; n++) {
		    NdefMessage msg = (NdefMessage) pa[n];
		    byte[] ba;
		    if (msg != null) {
			ba = msg.toByteArray();
		    } else {
			ba = new byte[0];
		    }
		    al.add(ba);
		}
		ret[k] = encodeToList(al);
	    } else {
		ret[k] = obj.toString();
	    }
	    k++;
	}
	if (add != null) {
	    for (int i = 0; i < add.length; i++) {
		ret[k++] = add[i];
	    }
	}
	return ret;
    }

    /*
     * Encode Set<String> to String[] for "array set"
     */

    String[] formatCategories(Set<String> cset) {
	String ret[] = new String[cset.size()];
	cset.toArray(ret);
	return ret;
    }

    /*
     * Callback on re-activation
     */

    @Override
    public void onNewIntent(Intent i) {
	setIntent(i);
	if (hasPerm(android.Manifest.permission.WAKE_LOCK) &&
	    i.getBooleanExtra("tk.tcl.wish.wakeup", false)) {
	    PowerManager pm =
		(PowerManager) getSystemService(Context.POWER_SERVICE);
	    WakeLock wl =
		pm.newWakeLock(PowerManager.FULL_WAKE_LOCK |
			       PowerManager.ACQUIRE_CAUSES_WAKEUP,
			       TAG);
	    wl.acquire();
	    wl.release();
	}
	String action = i.getAction();
	String uristr = i.getDataString();
	String type = i.getType();
	String[] cats = null;
	String[] args = null;
	if (i.getCategories() != null) {
	    cats = formatCategories(i.getCategories());
	}
	if (i.getExtras() != null) {
	    args = formatBundle(i.getExtras(), null);
	}
	Log.v(TAG, "nativeTriggerIntent: " +
	      action + "," + uristr + "," + type + "," + cats + "," + args);
	mSingleton.nativeTriggerIntent(action, uristr, type, cats, args);
    }

    public static void syncIntent() {
	Runnable synci = new Runnable() {
	    public void run() {
		mSingleton.onNewIntent(mSingleton.getIntent());
	    }
	};
	mSingleton.runOnUiThread(synci);
    }

    public static Context getContext() {
	return mSingleton.getApplicationContext();
    }

    public static UsbManager getUsbManager() {
	return mUsbManager;
    }

    public static void setOrientation(final int orient) {
	Runnable do_orient = new Runnable() {
	    public void run() {
		mSingleton.setRequestedOrientation(orient);
	    }
	};
	mSingleton.runOnUiThread(do_orient);
    }

    public static int getOrientation() {
	return mSingleton.getRequestedOrientation();
    }

    /*
     * Find resource identifier by name
     */

    public static int getResourceId(String name, String res) {
	try {
	    return mSingleton.getResources().getIdentifier(name, res, mSingleton.getPackageName());
	} catch (Exception e) {
	    return -1;
	}
    }

    /*
     * Callback to deal with activity results
     */

    @Override
    public void onActivityResult(int id, int ret, Intent i) {
	super.onActivityResult(id, ret, i);
	String action = null;
	String uristr = null;
	String type = null;
	String[] cats = null;
	String[] args = null;
	if (i != null) {
	    action = i.getAction();
	    uristr = i.getDataString();
	    type = i.getType();
	    if (i.getCategories() != null) {
		cats = formatCategories(i.getCategories());
	    }
	    if (i.getExtras() != null) {
		args = formatBundle(i.getExtras(), null);
	    }
	}
	Log.v(TAG, "nativeIntentCallback: " + id + "," + ret + "," +
	      action + "," + uristr + "," + type + "," + cats + "," + args);
	nativeIntentCallback(id, ret, action, uristr, type, cats, args);
    }

    String encodeBitmap(Bitmap bm) {
	ByteArrayOutputStream bs = new ByteArrayOutputStream();
	bm.compress(Bitmap.CompressFormat.JPEG, 80, bs);
	byte[] bytes = bs.toByteArray();
	bs = null;
	return android.util.Base64.encodeToString(bytes,
					android.util.Base64.DEFAULT);
    }

    String toElement(String in) {
	int level = 0;
	int flags = 0;
	int i, len;
	if (in == null) {
	    return "{}";
	}
	len = in.length();
	if (len == 0) {
	    return "{}";
	}
	for (i = 0; i < len; i++) {
	    if ((in.charAt(i) == '{') || (in.charAt(i) == '}')) {
		flags |= 1;
	    }
	}
	for (i = 0; i < len; i++) {
	    switch (in.charAt(i)) {
	    case '{':
		level++;
		break;
	    case '}':
		level--;
		if (level < 0) {
		    flags |= 2;
		}
		break;
	    case '[':
	    case '$':
	    case ';':
	    case ' ':
	    case '\f':
	    case '\n':
	    case '\r':
	    case '\t':
	    case 0x0B:
		flags |= 1;
		break;
	    case '\\':
		if ((i + 1 == len) || (in.charAt(i + 1) == '\n')) {
		    flags = 2;
		} else {
		    flags |= 1;
		}
		break;
	    }
	}
	if (level != 0) {
	    flags = 2;
	}
	StringBuilder r = new StringBuilder(in.length() + 64);
	if ((flags & 1) == 1) {
	    r.append("{");
	    r.append(in);
	    r.append("}");
	} else {
	    i = 0;
	    if (in.charAt(i) == '{') {
		flags |= 2;
		r.append("\\");
		r.append("{");
		i++;
	    }
	    for (; i < len; i++) {
		switch (in.charAt(i)) {
		case ']':
		case '[':
		case '$':
		case ';':
		case ' ':
		case '\\':
		case '"':
		    r.append("\\");
		    r.append(in.charAt(i));
		    break;
		case '{':
		case '}':
		    if ((flags & 2) != 0) {
			r.append("\\");
		    }
		    r.append(in.charAt(i));
		    break;
		case '\f':
		    r.append("\\f");
		    break;
		case '\n':
		    r.append("\\n");
		    break;
		case '\r':
		    r.append("\\r");
		    break;
		case '\t':
		    r.append("\\t");
		    break;
		case 0x0B:
		    r.append("\\v");
		    break;
		default:
		    r.append(in.charAt(i));
		    break;
		}
	    }
	}
	return r.toString();
    }

    String encodeToList(ArrayList list) {
	StringBuilder r = new StringBuilder();
	int i;
	for (i = 0; i < list.size(); i++) {
	    Object obj = list.get(i);
	    if (i != 0) {
		r.append(" ");
	    }
	    if (obj == null) {
		r.append("{}");
	    } else if (obj instanceof String) {
		r.append(toElement((String) obj));
	    } else if (obj instanceof byte[]) {
		r.append(android.util.Base64.encodeToString((byte[]) obj,
						 android.util.Base64.DEFAULT));
	    } else {
		r.append(toElement(obj.toString()));
	    }
	}
	return r.toString();
    }

    String encodeDoubleList(double[] list) {
	StringBuilder r = new StringBuilder(list.length * 16);
	int i;
	for (i = 0; i < list.length; i++) {
	    if (i != 0) {
		r.append(" ");
	    }
	    r.append("" + list[i]);
	}
	return r.toString();
    }

    String encodeFloatList(float[] list) {
	StringBuilder r = new StringBuilder(list.length * 16);
	int i;
	for (i = 0; i < list.length; i++) {
	    if (i != 0) {
		r.append(" ");
	    }
	    r.append("" + list[i]);
	}
	return r.toString();
    }

    String encodeShortList(short[] list) {
	StringBuilder r = new StringBuilder(list.length * 8);
	int i;
	for (i = 0; i < list.length; i++) {
	    if (i != 0) {
		r.append(" ");
	    }
	    r.append("" + list[i]);
	}
	return r.toString();
    }

    String encodeIntList(int[] list) {
	StringBuilder r = new StringBuilder(list.length * 8);
	int i;
	for (i = 0; i < list.length; i++) {
	    if (i != 0) {
		r.append(" ");
	    }
	    r.append("" + list[i]);
	}
	return r.toString();
    }

    String encodeLongList(long[] list) {
	StringBuilder r = new StringBuilder(list.length * 8);
	int i;
	for (i = 0; i < list.length; i++) {
	    if (i != 0) {
		r.append(" ");
	    }
	    r.append("" + list[i]);
	}
	return r.toString();
    }

    String encodeBoolList(boolean[] list) {
	StringBuilder r = new StringBuilder(list.length * 3);
	int i;
	for (i = 0; i < list.length; i++) {
	    if (i != 0) {
		r.append(" ");
	    }
	    r.append("" + (list[i] ? "1" : "0"));
	}
	return r.toString();
    }

    String encodeCharList(char[] list) {
	StringBuilder r = new StringBuilder(list.length * 2);
	int i;
	for (i = 0; i < list.length; i++) {
	    r.append(list[i]);
	}
	return r.toString();
    }

    String encodeStringList(String[] list) {
	StringBuilder r = new StringBuilder();
	int i;
	for (i = 0; i < list.length; i++) {
	    String s = list[i];
	    if (i != 0) {
		r.append(" ");
	    }
	    if ((s == null) || s.length() == 0) {
		r.append("{}");
	    } else {
		r.append(toElement(s));
	    }
	}
	return r.toString();
    }

    static Intent buildIntent(Intent i, String uristr, String type,
			      String[] cats, String[] comp) {
	Uri uri = null;
	if ((uristr != null) && (uristr.length() > 0)) {
	    uri = Uri.parse(uristr);
	}
	if ((type != null) && (type.length() == 0)) {
	    type = null;
	}
	if ((uri != null) && (type != null)) {
	    i.setDataAndType(uri, type);
	} else if (uri != null) {
	    i.setData(uri);
	} else if (type != null) {
	    i.setType(type);
	}
	if (cats != null) {
	    int k;
	    for (k = 0; k < cats.length; k++) {
		i.addCategory(cats[k]);
	    }
	}
	if (comp != null && comp.length > 1) {
	    i.setComponent(new ComponentName(comp[0], comp[1]));
	}
	return i;
    }

    static void addArgsToIntent(Intent i, String types[], String[] argv) {
	if (argv != null) {
	    int k;
	    for (k = 0; k < argv.length; k += 2) {
		if ((types != null) && (types[k] != null)) {
		    if (types[k].equals("int")) {
			i.putExtra(argv[k], Integer.parseInt(argv[k + 1]));
		    } else if (types[k].equals("long")) {
			i.putExtra(argv[k], Long.parseLong(argv[k + 1]));
		    } else if (types[k].equals("short")) {
			i.putExtra(argv[k], Short.parseShort(argv[k + 1]));
		    } else if (types[k].equals("char")) {
			i.putExtra(argv[k], argv[k + 1].charAt(0));
		    } else if (types[k].equals("byte")) {
			i.putExtra(argv[k], Byte.parseByte(argv[k + 1]));
		    } else if (types[k].equals("boolean")) {
			i.putExtra(argv[k], Boolean.parseBoolean(argv[k + 1]));
		    } else if (types[k].equals("float")) {
			i.putExtra(argv[k], Float.parseFloat(argv[k + 1]));
		    } else if (types[k].equals("double")) {
			i.putExtra(argv[k], Double.parseDouble(argv[k + 1]));
		    } else if (types[k].equals("Uri")) {
			i.putExtra(argv[k], Uri.parse(argv[k + 1]));
		    } else if (types[k].equals("String")) {
			i.putExtra(argv[k], argv[k + 1]);
		    } else {
			i.putExtra(argv[k], argv[k + 1]);
		    }
		} else {
		    i.putExtra(argv[k], argv[k + 1]);
		}
	    }
	}
    }

    /*
     * Run activity
     */

    public static int runActivity(String action, String uristr, String type,
				  String[] cats, String types[], String[] argv,
				  int id) {
	int err;
	err = (id < 0) ? (id - 1) : -100;
	try {
	    Intent i = new Intent(action);
	    i = buildIntent(i, uristr, type, cats, null);
	    addArgsToIntent(i, types, argv);
	    Runner r = new Runner(mSingleton, i, id);
	    mSingleton.runOnUiThread(r);
	} catch (Exception e) {
	    Log.e(TAG, "runActivity failed: " + e.toString());
	    return err;
	}
	return id;
    }

    public static int runActivityEx(String action, String uristr,
				    String type, String cats[],
				    String[] comp, String types[],
				    String[] argv, int id) {
	int err;
	err = (id < 0) ? (id - 1) : -100;
	try {
	    Intent i = new Intent(action);
	    i = buildIntent(i, uristr, type, cats, comp);
	    addArgsToIntent(i, types, argv);
	    Runner r = new Runner(mSingleton, i, id);
	    mSingleton.runOnUiThread(r);
	} catch (Exception e) {
	    Log.e(TAG, "runActivity failed: " + e.toString());
	    return err;
	}
	return id;
    }

    /*
     * Callback to deal with broadcast intent
     */

    public void broadcastReceived(Intent i, int retcode) {
	String action = i.getAction();
	String uristr = i.getDataString();
	String type = i.getType();
	String[] cats = null;
	String[] args = null;
	if (i.getCategories() != null) {
	    cats = formatCategories(i.getCategories());
	}
	Bundle extras = i.getExtras();
	if (extras != null) {
	    String addargs[] = null;
	    if (action.equals("android.provider.Telephony.SMS_RECEIVED")) {
		Object[] pdus = (Object[]) extras.get("pdus");
		if (pdus.length > 0) {
		    SmsMessage[] msgs = new SmsMessage[pdus.length];
		    StringBuilder sb = new StringBuilder();
		    for (int k = 0; k < pdus.length; k++) {
			msgs[k] =
			    SmsMessage.createFromPdu((byte[]) pdus[k]);
			sb.append(msgs[k].getMessageBody());
		    }
		    addargs = new String[4];
		    addargs[0] = "phone_number";
		    addargs[1] = msgs[0].getOriginatingAddress();
		    addargs[2] = "sms_text";
		    addargs[3] = sb.toString();
		}
	    } else if (action.equals("tk.tcl.wish.nfc")) {
		if (extras.get(NfcAdapter.EXTRA_TAG) != null) {
		    mNfcTag = (Tag) extras.get(NfcAdapter.EXTRA_TAG);
		}
	    }
	    args = formatBundle(extras, addargs);
	}
	Log.v(TAG, "nativeBroadcastCallback: " + retcode + "," + action +
	      "," + uristr + "," + type + "," + cats + "," + args);
	nativeBroadcastCallback(retcode, action, uristr, type, cats, args);
    }

    /*
     * Deal with (un)registration of broadcast listeners from Tcl
     */

    public static int handleBroadcastListener(int op, String action) {
	int ret = -1;
	if (action == null) {
	    return ret;
	}
	final boolean isNfc = action.equals("tk.tcl.wish.nfc");
	synchronized (mBCRecvrs) {
	    BCRecvr bcr = mBCRecvrs.get(action);
	    if (op == 0) {
		/* unregister */
		ret = 0;
		if (bcr != null) {
		    mBCRecvrs.remove(action);
		    final BCRecvr unreg_bcr = bcr;
		    if (isNfc) {
			mNfcBCRecvr = null;
		    }
		    Runnable unreg = new Runnable() {
			public void run() {
			    mSingleton.unregisterReceiver(unreg_bcr);
			    if (isNfc) {
				mSingleton.nfcEnable(false);
			    }
			}
		    };
		    mSingleton.runOnUiThread(unreg);
		    ret = 1;
		}
	    } else if (op == 1) {
		/* register */
		ret = 0;
		if (bcr == null) {
		    final BCRecvr reg_bcr = new BCRecvr(mSingleton);
		    final IntentFilter filter = new IntentFilter();
		    filter.addAction(action);
		    mBCRecvrs.put(action, reg_bcr);
		    if (isNfc) {
			mNfcBCRecvr = reg_bcr;
		    }
		    Runnable reg = new Runnable() {
			public void run() {
			    mSingleton.registerReceiver(reg_bcr, filter);
			    if (isNfc) {
				mSingleton.nfcEnable(true);
			    }
			}
		    };
		    mSingleton.runOnUiThread(reg);
		    ret = 1;
		}
	    }
	}
	return ret;
    }

    /*
     * Send SMS, text only for now
     */

    public static int sendSMS(String phone, String msg, String actionSent,
			      String actionDelivered, String smsc) {
	PendingIntent piSent = null, piDelivered = null;
	if ((phone == null) || (msg == null)) {
	    return 0;
	}
	if (!hasPerm(android.Manifest.permission.SEND_SMS)) {
	    Log.e(TAG, "sendSMS failed: no permission");
	    return 0;
	}
	SmsManager sms = SmsManager.getDefault();
	if (sms == null) {
	    Log.e(TAG, "sendSMS failed: no SmsManager");
	    return 0;
	}
	if (actionSent != null) {
	    piSent = PendingIntent.getBroadcast(mSingleton, 0,
						new Intent(actionSent), 0);
	}
	if (actionDelivered != null) {
	    piDelivered =
		PendingIntent.getBroadcast(mSingleton, 0,
					   new Intent(actionDelivered), 0);
	}
	sms.sendTextMessage(phone, smsc, msg, piSent, piDelivered);
	return 1;
    }

    /*
     * Speech recognition
     */

    public static int speechRecognition(String types[], String[] argv, int id) {
	if (mSpeechRec == null) {
	    return -1;
	}
	if (id < 0) {
	    Intent i = null;
	    if (id == -3) {
		try {
		    i = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
		    i.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
			       mSingleton.getClass().getPackage().getName());
		    addArgsToIntent(i, types, argv);
		} catch (Exception e) {
		    Log.e(TAG, "speechRecognition failed: " +
			  e.toString());
		}
	    }
	    SpRunner sr = new SpRunner(mSpeechRec, i, id);
	    mSingleton.runOnUiThread(sr);
	    return 0;
	}
	try {
	    Intent i = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
	    i.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
		       mSingleton.getClass().getPackage().getName());
	    addArgsToIntent(i, types, argv);
	    Runner r = new Runner(mSingleton, i, id);
	    mSingleton.runOnUiThread(r);
	} catch (Exception e) {
	    Log.v(TAG, "speechRecognition failed: " + e.toString());
	    return -1;
	}
	return id;
    }

    /*
     * Query intents/activities
     */

    public static String[] queryIntents(int which, String action,
					String uristr, String type,
					String cats[], String[] comp) {
	List<String> apps = new ArrayList<String>();
	Intent i;
	List<ResolveInfo> acts;
	Uri uri = null;
	if ((uristr != null) && (uristr.length() > 0)) {
	    uri = Uri.parse(uristr);
	}
	if ((type != null) && (type.length() == 0)) {
	    type = null;
	}
	i = new Intent(action);
	if ((uri != null) && (type != null)) {
	    i.setDataAndType(uri, type);
	} else if (uri != null) {
	    i.setData(uri);
	} else if (type != null) {
	    i.setType(type);
	}
	if (cats != null) {
	    int k;
	    for (k = 0; k < cats.length; k++) {
		i.addCategory(cats[k]);
	    }
	}
	try {
	    if (comp != null && comp.length > 1) {
		i.setComponent(new ComponentName(comp[0], comp[1]));
	    }
	} catch (Exception e) {
	    return null;
	}
	switch (which) {
	default:
	    acts = mPackageManager.queryIntentActivities(i, 0);
	    break;
	case 1:
	    acts = mPackageManager.queryIntentServices(i, 0);
	    break;
	case 2:
	    acts = mPackageManager.queryBroadcastReceivers(i, 0);
	    break;
	}
	int k = 0;
	for (ResolveInfo res : acts) {
	    ActivityInfo actinfo = res.activityInfo;
	    if (actinfo != null) {
		apps.add(actinfo.name);
		k++;
	    }
	}
	String[] ret = new String[k];
	apps.toArray(ret);
	return ret;
    }

    /*
     * Query features
     */

    public static String[] queryFeatures() {
	FeatureInfo fi[] = mPackageManager.getSystemAvailableFeatures();
	if ((fi == null) || (fi.length == 0)) {
	    return null;
	}
	String ret[] = new String[fi.length];
	int i;
	for (i = 0; i < fi.length; i++) {
	    ret[i] = fi[i].name;
	}
	return ret;
    }

    /*
     * Package info
     */

    public static String[] packageInfo(String name) {
	if (name == null) {
	    List<PackageInfo> pl = mPackageManager.getInstalledPackages(0);
	    List<String> ps = new ArrayList<String>();
	    int k = 0;
	    for (PackageInfo pi : pl) {
		ps.add(pi.packageName);
		k++;
	    }
	    String[] ret = new String[k];
	    ps.toArray(ret);
	    return ret;
	}
	try {
	    PackageInfo pi =
		mPackageManager.getPackageInfo(name, PackageManager.GET_ACTIVITIES);
	    String [] ret = new String[10];
	    ret[0] = "classname";
	    ret[1] = pi.applicationInfo.className;
	    ret[2] = "datadir";
	    ret[3] = pi.applicationInfo.dataDir;
	    ret[4] = "sourcedir";
	    ret[5] = pi.applicationInfo.sourceDir;
	    ret[6] = "sharedlibrarydir";
	    ret[7] = pi.applicationInfo.nativeLibraryDir;
	    ret[8] = "publicsourcedir";
	    ret[9] = pi.applicationInfo.publicSourceDir;
	    return ret;
	} catch (PackageManager.NameNotFoundException ne) {
	    return null;
	}
    }

    /*
     * Provider info
     */

    public static String[] providerInfo() {
	List<PackageInfo> pl =
	    mPackageManager.getInstalledPackages(PackageManager.GET_PROVIDERS);
	List<String> ps = new ArrayList<String>();
	int k = 0;
	for (PackageInfo pi : pl) {
	    if (pi.providers != null) {
		for (int i = 0; i < pi.providers.length; i++) {
		    String auth[] = pi.providers[i].authority.split(";");
		    for (int j = 0; j < auth.length; j++) {
			ps.add(auth[j]);
			k++;
		    }
		}
	    }
	}
	String[] ret = new String[k];
	ps.toArray(ret);
	return ret;
    }

    /*
     * Content query and manipulation
     */

    public static WrappedCursor contentQuery(String uristr, String[] columns,
					     String where,
					     String[] whereArgs,
					     String orderby) {
	WrappedCursor wc = null;
	Uri uri = Uri.parse(uristr);
	try {
	    Cursor c = mContentResolver.query(uri, columns, where,
					      whereArgs, orderby);
	    if (c != null) {
		wc = new WrappedCursor(c);
	    }
	} catch (Exception e) {
	    Log.v(TAG, "contentQuery:" + e.toString());
	}
	return wc;
    }

    public static String contentInsert(String uristr, String[] args) {
	Uri uri = Uri.parse(uristr);
	ContentValues vals = new ContentValues();
	int i;
	for (i = 0; i < args.length / 2; i++) {
	    vals.put(args[i * 2], args[i * 2 + 1]);
	}
	try {
	    uri = mContentResolver.insert(uri, vals);
	    if (uri != null) {
		return uri.toString();
	    }
	} catch (Exception e) {
	    Log.v(TAG, "contentInsert:" + e.toString());
	}
	return null;
    }

    public static int contentUpdate(String uristr, String[] uvals,
				    String sel, String[] args) {
	Uri uri = Uri.parse(uristr);
	ContentValues vals = new ContentValues();
	int i;
	for (i = 0; i < uvals.length / 2; i++) {
	    vals.put(uvals[i * 2], uvals[i * 2 + 1]);
	}
	i = 0;
	try {
	    i = mContentResolver.update(uri, vals, sel, args);
	} catch (Exception e) {
	    Log.v(TAG, "contentUpdate:" + e.toString());
	}
	return i;
    }

    public static int contentDelete(String uristr, String sel,
				       String[] args) {
	Uri uri = Uri.parse(uristr);
	int i;
	i = 0;
	try {
	    i = mContentResolver.delete(uri, sel, args);
	} catch (Exception e) {
	    Log.v(TAG, "contentDelete:" + e.toString());
	}
	return i;
    }

    /*
     * Notification by vibration
     */

    public static void vibrate(int duration) {
	if (duration <= 0) {
	    duration = 500;
	}
	if (mVibrator != null) {
	    mVibrator.vibrate(duration);
	}
    }

    /*
     * Notification by ringtone
     */

    public static void beep() {
	Runnable beeper = new Runnable() {
	    public void run() {
		Uri ringuri =
		    RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
		Ringtone tone =
		    RingtoneManager.getRingtone(mSingleton.getBaseContext(),
						ringuri);
		if ((tone != null) && (!tone.isPlaying())) {
		    tone.play();
		}
	    }
	};
	mSingleton.runOnUiThread(beeper);
    }

    public static void beepEx(final String uristr) {
	Runnable beeper = new Runnable() {
	    public void run() {
		if (mCurrentTone != null) {
		    mCurrentTone.stop();
		    mCurrentTone = null;
		}
		Uri ringuri = null;
		if (uristr != null) {
		    if (uristr.length() == 0) {
			return;
		    }
		    ringuri = Uri.parse(uristr);
		}
		if (ringuri == null) {
		    ringuri =
			RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
		}
		Ringtone tone =
		    RingtoneManager.getRingtone(mSingleton.getBaseContext(),
						ringuri);
		if ((tone != null) && (!tone.isPlaying())) {
		    tone.play();
		    if (uristr != null) {
			mCurrentTone = tone;
		    }
		}
	    }
	};
	mSingleton.runOnUiThread(beeper);
    }

    /*
     * Notification by toast
     */

    public static void toast(final String text, final int duration) {
	Runnable toaster = new Runnable() {
	    public void run() {
		Toast toast =
		    Toast.makeText(mSingleton.getBaseContext(),
				   text, (duration > 0) ?
				   Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
		if (toast != null) {
		    toast.show();
		}
	    }
	};
	mSingleton.runOnUiThread(toaster);
    }

    public static void toastHtml(final String text, final int duration) {
	Runnable toaster = new Runnable() {
	    public void run() {
		Toast toast =
		    Toast.makeText(mSingleton.getBaseContext(),
				   TextFromHtml.text(text),
				   (duration > 0) ?
				   Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
		if (toast != null) {
		    toast.show();
		}
	    }
	};
	mSingleton.runOnUiThread(toaster);
    }

    /*
     * Text-to-speech
     */

    public static int speak(final int op, final String text, String lang,
			    float pitch, float rate) {
	int ret = 0;
	if (op < 0) {
	    /* shutdown */
	    if (mTTS != null) {
		mTTS.stop();
		mTTS.shutdown();
		if (android.os.Build.VERSION.SDK_INT >
		    android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
		    mTTSUPL.onShutdown();
		    mTTSUPL = null;
		} else {
		    mTTSLock = null;
		}
		mTTS = null;
	    }
	    return ret;
	}
	if (mTTS == null) {
	    if (android.os.Build.VERSION.SDK_INT >
		android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
		mTTSUPL = new TTS_UPL(mSingleton);
	    } else {
		mTTSLock = new CountDownLatch(1);
	    }
	    mTTSCount = 999;
	    Runnable speaker = new Runnable() {
		public void run() {
		    if (android.os.Build.VERSION.SDK_INT >
			android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
			mTTS = new TextToSpeech(mSingleton.getContext(), mTTSUPL);
		    } else {
			mTTS = new TextToSpeech(mSingleton.getContext(), new OnInitListener() {
			    @Override
			    public void onInit(int status) {
				mTTSLock.countDown();
			    }
			});
		    }
		}
	    };
	    mSingleton.runOnUiThread(speaker);
	}
	try {
	    if (android.os.Build.VERSION.SDK_INT >
		android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
		if (!mTTSAvailable) {
		    return -1;
		}
	    } else {
		mTTSLock.await();
	    }
	    switch (op) {
	    case 0:
		mTTS.stop();
		break;
	    case 1:
		if (pitch >= 0) {
		    ret = mTTS.setPitch(pitch);
		}
		if ((ret == 0) && (rate > 0)) {
		    ret = mTTS.setSpeechRate(rate);
		}
		if ((ret == 0) && (lang != null)) {
		    Locale loc = new Locale(lang);
		    ret = mTTS.isLanguageAvailable(loc);
		    if ((ret >= 0) && (text != null) && (text.length() > 0)) {
			ret = mTTS.setLanguage(loc);
		    }
		}
		if ((ret >= 0) && (text != null) && (text.length() > 0)) {
		    HashMap<String, String> uplid = null;
		    int id = 0;
		    if (android.os.Build.VERSION.SDK_INT >
			android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
			id = ++mTTSCount;
			if (id > 1999) {
			    mTTSCount = 999;
			    id = ++mTTSCount;
			}
			uplid = new HashMap<String, String>();
			uplid.put("utteranceId", "" + id);
		    }
		    ret = mTTS.speak(text, TextToSpeech.QUEUE_ADD, uplid);
		    if (ret == 0) {
			ret = id;
		    }
		}
		break;
	    case 2:
		if (mTTS.isSpeaking()) {
		    ret = 1;
		}
		break;
	    }
	} catch (InterruptedException e) {
	    /* shutdown */
	    if (mTTS != null) {
		mTTS.shutdown();
		if (android.os.Build.VERSION.SDK_INT >
		    android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
		    mTTSUPL.onShutdown();
		    mTTSUPL = null;
		} else {
		    mTTSLock = null;
		}
		mTTS = null;
	    }
	}
	return ret;
    }

    /*
     * Some reflection stuff
     */

    public static String[] queryFields(String clsname, int reqFlags) {
	List<String> list = new ArrayList<String>();
	int atLeastFlags = Modifier.PUBLIC | Modifier.PROTECTED;
	int k = 0;
	try {
	    Class<?> cls = Class.forName(clsname);
	    for (Field field : cls.getFields()) {
		if (((field.getModifiers() & reqFlags) == reqFlags) &&
		    ((field.getModifiers() & atLeastFlags) != 0)) {
		    Class<?> type = field.getType();
		    list.add(field.getName());
		    Object obj = field.get(null);
		    if (obj == null) {
			list.add("");
		    } else if (obj instanceof String) {
			list.add((String) obj);
		    } else if (obj instanceof String[]) {
			list.add(mSingleton.encodeStringList((String[]) obj));
		    } else if (obj instanceof double[]) {
			list.add(mSingleton.encodeDoubleList((double[]) obj));
		    } else if (obj instanceof float[]) {
			list.add(mSingleton.encodeFloatList((float[]) obj));
		    } else if (obj instanceof short[]) {
			list.add(mSingleton.encodeShortList((short[]) obj));
		    } else if (obj instanceof int[]) {
			list.add(mSingleton.encodeIntList((int[]) obj));
		    } else if (obj instanceof long[]) {
			list.add(mSingleton.encodeLongList((long[]) obj));
		    } else {
			list.add(obj.toString());
		    }
		    k += 2;
		}
	    }
	} catch (Exception e) {
	    Log.e(TAG, "queryConsts failed: " + e);
	}
	String[] ret = new String[k];
	list.toArray(ret);
	return ret;
    }

    public static String[] queryConsts(String clsname) {
	return queryFields(clsname, Modifier.FINAL | Modifier.STATIC);
    }

    public static String[] queryConsts(String clsname, int fields) {
	int reqFlags = Modifier.STATIC;
	if (fields == 0) {
	    reqFlags |= Modifier.FINAL;
	}
	return queryFields(clsname, reqFlags);
    }

    /*
     * Display metrics
     */

    public static String getDisplayMetrics() {
	DisplayMetrics metrics = new DisplayMetrics();
	mSingleton.getWindowManager().getDefaultDisplay().getMetrics(metrics);
	int rot =
	    mSingleton.getWindowManager().getDefaultDisplay().getRotation();
	StringBuilder sb = new StringBuilder(128);
	sb.append("density ").append(metrics.density);
	sb.append(" densitydpi ").append(metrics.densityDpi);
	sb.append(" width ").append(metrics.widthPixels);
	sb.append(" height ").append(metrics.heightPixels);
	sb.append(" xdpi ").append(metrics.xdpi);
	sb.append(" ydpi ").append(metrics.ydpi);
	sb.append(" scaleddensity ").append(metrics.scaledDensity);
	switch (rot) {
	case Surface.ROTATION_90:
	    rot = 90;
	    break;
	case Surface.ROTATION_180:
	    rot = 180;
	    break;
	case Surface.ROTATION_270:
	    rot = 270;
	    break;
	default:
	    rot = 0;
	    break;
	}
	sb.append(" rotation ").append(rot);
	return sb.toString();
    }

    /*
     * OS build information
     */

    public static String[] getOSBuildInfo() {
	String[] ret = new String[48];
	ret[0] = "version.codename";
	ret[1] = android.os.Build.VERSION.CODENAME;
	ret[2] = "version.incremental";
	ret[3] = android.os.Build.VERSION.INCREMENTAL;
	ret[4] = "version.release";
	ret[5] = android.os.Build.VERSION.RELEASE;
	ret[6] = "version.sdk";
	ret[7] = "" + android.os.Build.VERSION.SDK_INT;
	ret[8] = "board";
	ret[9] = android.os.Build.BOARD;
	ret[10] = "bootloader";
	ret[11] = android.os.Build.BOOTLOADER;
	ret[12] = "brand";
	ret[13] = android.os.Build.BRAND;
	ret[14] = "cpu_abi";
	ret[15] = android.os.Build.CPU_ABI;
	ret[16] = "cpu_abi2";
	ret[17] = android.os.Build.CPU_ABI2;
	ret[18] = "device";
	ret[19] = android.os.Build.DEVICE;
	ret[20] = "display";
	ret[21] = android.os.Build.DISPLAY;
	ret[22] = "fingerprint";
	ret[23] = android.os.Build.FINGERPRINT;
	ret[24] = "hardware";
	ret[25] = android.os.Build.HARDWARE;
	ret[26] = "host";
	ret[27] = android.os.Build.HOST;
	ret[28] = "id";
	ret[29] = android.os.Build.ID;
	ret[30] = "manufacturer";
	ret[31] = android.os.Build.MANUFACTURER;
	ret[32] = "model";
	ret[33] = android.os.Build.MODEL;
	ret[34] = "product";
	ret[35] = android.os.Build.PRODUCT;
	ret[36] = "serial";
	ret[37] = android.os.Build.SERIAL;
	ret[38] = "tags";
	ret[39] = android.os.Build.TAGS;
	ret[40] = "time";
	ret[41] = "" + android.os.Build.TIME;
	ret[42] = "type";
	ret[43] = android.os.Build.TYPE;
	ret[44] = "user";
	ret[45] = android.os.Build.USER;
	ret[46] = "radio";
	if (android.os.Build.VERSION.SDK_INT <
	    android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
	    ret[47] = android.os.Build.RADIO;
	} else {
	    ret[47] = RadioVersion_ICS.getRadioVersion();
	}
	return ret;
    }

    /*
     * Withdraw APP
     */

    public static void withdraw() {
	Runnable dowithdraw = new Runnable() {
	    public void run() {
		mSingleton.moveTaskToBack(true);
	    }
	};
	mSingleton.runOnUiThread(dowithdraw);
    }

    /*
     * Keyboard information
     */

    public static String getKeyboardInfo() {
	int keyboard, keyboardHidden, hardKeyboardHidden;
	synchronized (mSingleton.mConfigLock) {
	    keyboard = mSingleton.mConfig.keyboard;
	    keyboardHidden = mSingleton.mConfig.keyboardHidden;
	    hardKeyboardHidden = mSingleton.mConfig.hardKeyboardHidden;
	}
	StringBuilder sb = new StringBuilder(64);
	sb.append("keyboard ");
	switch (keyboard) {
	case Configuration.KEYBOARD_NOKEYS:
	    sb.append("none");
	    break;
	case Configuration.KEYBOARD_QWERTY:
	    sb.append("qwerty");
	    break;
	case Configuration.KEYBOARD_12KEY:
	    sb.append("12key");
	    break;
	default:
	    sb.append("unknown");
	    break;
	}
	sb.append(" hidden ");
	switch (keyboardHidden) {
	case Configuration.KEYBOARDHIDDEN_YES:
	    sb.append("1");
	    break;
	case Configuration.KEYBOARDHIDDEN_NO:
	    sb.append("0");
	    break;
	default:
	    sb.append("-1");
	    break;
	}
	sb.append(" hard_hidden ");
	switch (hardKeyboardHidden) {
	case Configuration.HARDKEYBOARDHIDDEN_YES:
	    sb.append("1");
	    break;
	case Configuration.HARDKEYBOARDHIDDEN_NO:
	    sb.append("0");
	    break;
	default:
	    sb.append("-1");
	    break;
	}
	return sb.toString();
    }

    /*
     * Location and GPS status handling
     */

    @Override
    public void onProviderDisabled(String provider) {
    }

    @Override
    public void onProviderEnabled(String provider) {
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
    }

    @Override
    public void onLocationChanged(Location location) {
	String provider = location.getProvider();
	if (provider == null) {
	    return;
	}
	synchronized (mLocations) {
	    mLocations.put(provider, location);
	}
	if (mSingleton.nativeTriggerLocation() < 0) {
	    stopLocating();
	}
    }

    public static String getLocation() {
	StringBuilder sb = new StringBuilder(128);
	int k = 0;
	synchronized (mLocations) {
	    for (Entry<String, Location> entry : mLocations.entrySet()) {
		Location loc = entry.getValue();
		if (loc.getProvider() != null) {
		    if (++k > 1) {
			sb.append(" ");
		    }
		    sb.append(loc.getProvider()).append(" {");
		    sb.append("latitude ").append(loc.getLatitude());
		    sb.append(" longitude ").append(loc.getLongitude());
		    sb.append(" time ").append((loc.getTime() / 1000));
		    sb.append(" velocity ").append(loc.getSpeed());
		    sb.append(" altitude ").append(loc.getAltitude());
		    sb.append(" bearing ").append(loc.getBearing());
		    sb.append(" accuracy ").append(loc.getAccuracy());
		    sb.append("}");
		}
	    }
	}
	return sb.toString();
    }

    public static String getGpsNmeaInfo(int which) {
	if (mGpsInfo != null) {
	    switch (which) {
	    case 0:
		return mGpsInfo.getInfo();
	    case 1:
		return mGpsInfo.getSatellites();
	    }
	}
	if ((mNmeaInfo != null) && (which == 2)) {
	    return mNmeaInfo.getInfo();
	}
	return null;
    }

    public static void startLocating(final int minrate, final int mindist) {
	if (mLocationMgr == null) {
	    return;
	}
	Runnable locStart = new Runnable() {
	    public void run() {
		for (String provider : mLocationMgr.getAllProviders()) {
		    mLocationMgr.requestLocationUpdates(provider,
							minrate, mindist,
							mSingleton);
		}
	    }
	};
	mSingleton.runOnUiThread(locStart);
    }

    public static void stopLocating() {
	if (mLocationMgr == null) {
	    return;
	}
	mLocationMgr.removeUpdates(mSingleton);
	synchronized (mLocations) {
	    mLocations.clear();
	}
    }

    /*
     * Network/connectivity info
     */

    public static String getNetworkInfo() {
	String result = "none";
	if (mConnMgr == null) {
	    return result;
	}
	synchronized (mNetLock) {
	    if (mNetworkInfo == null) {
		mNetworkInfo = mConnMgr.getActiveNetworkInfo();
	    }
	    if ((mNetworkInfo != null) && (mNetworkInfo.isConnected())) {
		String type = mNetworkInfo.getTypeName();
		if (type.toLowerCase().equals("mobile")) {
		    result = "mobile " + mNetworkInfo.getSubtypeName();
		} else {
		    result = type.toLowerCase();
		}
	    }
	}
	return result;
    }

    public static String[] getTetherInfo() {
	String list[] = new String[6];
	if (mConnMgr != null) {
	    synchronized (mNetLock) {
		list[0] = "active";
		if ((mNTa != null) && (mNTa.length > 0)) {
		    list[1] = mSingleton.encodeStringList(mNTa);
		} else {
		    list[1] = null;
		}
		list[2] = "available";
		if ((mNTl != null) && (mNTl.length > 0)) {
		    list[3] = mSingleton.encodeStringList(mNTl);
		} else {
		    list[3] = null;
		}
		list[4] = "error";
		if ((mNTe != null) && (mNTe.length > 0)) {
		    list[5] = mSingleton.encodeStringList(mNTe);
		} else {
		    list[5] = null;
		}
	    }
	}
	return list;
    }

    /*
     * Location service info
     */

    public static String getLocationInfo() {
	String result = "none";
	try {
	    LocationManager lm = (LocationManager)
		mSingleton.getContext().getSystemService(Context.LOCATION_SERVICE);
	    boolean on =
		androidx.core.location.LocationManagerCompat.isLocationEnabled(lm);
	    result = on ? "on" : "off";
	} catch (Exception e) {
	    Log.e(TAG, "getLocationInfo failed: " + e.toString());
	}
	return result;
    }

    /*
     * Desktop shortcuts
     */

    public static void shortcut(int op, String name, String arg, String icon) {
	switch (op) {
	case 0:		/* add */
	    if ((android.os.Build.VERSION.SDK_INT >= 19) &&
		!hasPerm("com.android.launcher.permission.INSTALL_SHORTCUT")) {
		return;
	    }
	    break;
	case 1:		/* delete */
	    if ((android.os.Build.VERSION.SDK_INT >= 19) &&
		!hasPerm("com.android.launcher.permission.UNINSTALL_SHORTCUT")) {
		return;
	    }
	    break;
	default:
	    return;
	}
	Intent shortcut, toRun;
	Intent.ShortcutIconResource iconRes = null;
	Bitmap iconBitmap = null;
	try {
	   iconRes =
		Intent.ShortcutIconResource.fromContext(mSingleton.getContext(),
							R.drawable.icon);
	} catch (Exception e) {
	    iconRes = null;
	}
	if ((android.os.Build.VERSION.SDK_INT < 19) ||
	    (android.os.Build.VERSION.SDK_INT >= 26)) {
	    ComponentName comp =
		new ComponentName(mSingleton.getPackageName(),
				  "tk.tcl.wish.AndroWishLauncher");
	    toRun = new Intent(Intent.ACTION_MAIN);
	    toRun.setComponent(comp);
	} else {
	    toRun = new Intent(Intent.ACTION_VIEW);
	    toRun.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
	    toRun.addCategory(Intent.CATEGORY_DEFAULT);
	}
	if ((arg != null) && (arg.length() > 0)) {
	    if (android.os.Build.VERSION.SDK_INT < 14) {
		toRun.putExtra("arg", arg);
	    } else {
		toRun.setData(Uri.parse(arg));
	    }
	}
	if (icon != null) {
	    byte b[];
	    try {
		b = android.util.Base64.decode(icon,
				android.util.Base64.DEFAULT);
	    } catch (Exception be) {
		b = null;
	    }
	    if ((b != null) && (b.length > 0)) {
		iconBitmap = BitmapFactory.decodeByteArray(b, 0, b.length);
	    }
	}
	switch (op) {
	case 0:		/* add */
	    if (android.os.Build.VERSION.SDK_INT >= 26) {
		if (Shortcut_API26.add(mSingleton.getContext(),
				       toRun, name,
				       iconBitmap, R.drawable.icon)) {
		    Log.v(TAG, "add shortcut(API26) '" + name + "'" +
			  ((arg != null) ? (" -> '" + arg + "'") : ""));
		    break;
		}
	    }
	    shortcut = new Intent("com.android.launcher.action.INSTALL_SHORTCUT");
	    shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
	    shortcut.putExtra("duplicate", false);
	    shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, toRun);
	    if (iconBitmap != null) {
		shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON, iconBitmap);
	    } else if (iconRes != null) {
		shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconRes);
	    }
	    Log.v(TAG, "add shortcut '" + name + "'" +
		  ((arg != null) ? (" -> '" + arg + "'") : ""));
	    mSingleton.sendBroadcast(shortcut);
	    break;
	case 1:		/* delete */
	    if (android.os.Build.VERSION.SDK_INT >= 26) {
		Log.v(TAG, "not deleting shortcut(API26) '" + name + "'");
		break;
	    }
	    shortcut = new Intent("com.android.launcher.action.UNINSTALL_SHORTCUT");
	    shortcut.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
	    shortcut.putExtra("duplicate", false);
	    shortcut.putExtra(Intent.EXTRA_SHORTCUT_INTENT, toRun);
	    if (iconBitmap != null) {
		shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON, iconBitmap);
	    } else if (iconRes != null) {
		shortcut.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, iconRes);
	    }
	    Log.v(TAG, "delete shortcut '" + name + "'" +
		  ((arg != null) ? (" -> '" + arg + "'") : ""));
	    mSingleton.sendBroadcast(shortcut);
	    break;
	}
    }

    /*
     * Notifications
     */

    public static void notify(int op, int id, String title, String text,
			      String icon, String action, String uristr,
			      String type,
			      String[] cats, String[] comp, String types[],
			      String[] argv) {
	NotificationCompat.Builder nb;
	switch (op) {
	case 0:		/* add/modify */
	    Intent i;
	    if ((comp != null) && comp[0].equals("self")) {
		i = new Intent(mSingleton.getContext(), mSingleton.getClass());
		i.setAction(action);
		comp = null;
	    } else if (action != null) {
		i = new Intent(action);
	    } else {
		i = new Intent();
	    }
	    i = buildIntent(i, uristr, type, cats, comp);
	    i.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
	    addArgsToIntent(i, types, argv);
	    PendingIntent pi =
		PendingIntent.getActivity(mSingleton.getContext(), 0, i, 0);
/*
 *	    if (android.os.Build.VERSION.SDK_INT <
 *		android.os.Build.VERSION_CODES.HONEYCOMB) {
 *		Notification n =
 *		     new Notification(R.drawable.icon, title,
 *				      System.currentTimeMillis());
 *		n.flags = Notification.FLAG_ONGOING_EVENT;
 *		n.setLatestEventInfo(mSingleton.getContext(), title, text, pi);
 *		mNotificationMgr.notify(id, n);
 *		break;
 *	    }
 */
	    nb = new NotificationCompat.Builder(mSingleton.getContext());
	    nb.setSmallIcon(R.drawable.icon);
	    nb.setContentTitle(title);
	    nb.setContentText(text);
	    if (icon != null) {
		byte b[];
		try {
		    b = android.util.Base64.decode(icon,
					android.util.Base64.DEFAULT);
		} catch (Exception be) {
		    b = null;
		}
		if ((b != null) && (b.length > 0)) {
		    Bitmap iconBitmap =
			BitmapFactory.decodeByteArray(b, 0, b.length);
		    nb.setLargeIcon(iconBitmap);
		}
	    }
	    nb.setContentIntent(pi);
	    mNotificationMgr.notify(id, nb.build());
	    break;
	case 1:		/* delete */
	    mNotificationMgr.cancel(id);
	    break;
	case 2:		/* delete all */
	    mNotificationMgr.cancelAll();
	    break;
	case 3:		/* led */
	    if (android.os.Build.VERSION.SDK_INT <
		android.os.Build.VERSION_CODES.HONEYCOMB) {
		Notification n = new Notification();
		n.defaults = 0;
		n.flags = Notification.FLAG_SHOW_LIGHTS;
		n.ledARGB = Integer.parseInt(action);
		n.ledOffMS = Integer.parseInt(uristr);
		n.ledOnMS = Integer.parseInt(type);
		mNotificationMgr.notify(id, n);
		break;
	    }
	    nb = new NotificationCompat.Builder(mSingleton.getContext());
	    nb.setDefaults(0);
	    nb.setLights(Integer.parseInt(action), Integer.parseInt(uristr),
			 Integer.parseInt(type));
	    nb.setPriority(2);
	    mNotificationMgr.notify(id, nb.build());
	    break;
	}
    }

    /*
     * Bluetooth
     */

    public static String[] bluetooth(int op, String arg) {
	if (mBluetoothAdap == null) {
	    return null;
	}
	String result[];
	int i;
	switch (op) {
	case 0:		/* devices */
	    Set<BluetoothDevice> btDevs = mBluetoothAdap.getBondedDevices();
	    i = 0;
	    for (BluetoothDevice dev : btDevs) {
		i++;
	    }
	    result = new String[i * 2];
	    i = 0;
	    for (BluetoothDevice dev : btDevs) {
		result[i++] = dev.getAddress();
		result[i++] = dev.getName();
	    }
	    return result;
	case 1:		/* state */
	    result = new String[1];
	    i = mBluetoothAdap.getState();
	    switch (i) {
	    case BluetoothAdapter.STATE_OFF:
		result[0] = "off";
		return result;
	    case BluetoothAdapter.STATE_ON:
		result[0] = "on";
		return result;
	    case BluetoothAdapter.STATE_TURNING_OFF:
		result[0] = "turning_off";
		return result;
	    case BluetoothAdapter.STATE_TURNING_ON:
		result[0] = "turning_on";
		return result;
	    }
	    result[0] = mBluetoothAdap.isEnabled() ? "on" : "off";
	    return result;
	case 2:		/* scanmode */
	    result = new String[1];
	    i = mBluetoothAdap.getScanMode();
	    if (i == BluetoothAdapter.SCAN_MODE_NONE) {
		result[0] = "passive";
	    } else if (i == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
		result[0] = "connectable";
	    } else if (i ==
		       BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
		result[0] = "visible";
	    } else {
		result[0] = "off";
	    }
	    return result;
	case 3:		/* myaddress */
	    result = new String[2];
	    result[0] = mBluetoothAdap.getAddress();
	    result[1] = mBluetoothAdap.getName();
	    return result;
	case 4:		/* remoteaddress */
	    try {
		BluetoothDevice btDev = mBluetoothAdap.getRemoteDevice(arg);
		result = new String[1];
		result[0] = btDev.getName();
		return result;
	    } catch(Exception e) {
	    }
	    return null;
	case 5:		/* on */
	    result = new String[1];
	    i = mBluetoothAdap.getState();
	    if ((i == BluetoothAdapter.STATE_ON) ||
		(i == BluetoothAdapter.STATE_TURNING_ON)) {
		result[0] = "1";
	    } else if (!hasPerm(android.Manifest.permission.BLUETOOTH_ADMIN)) {
		result[0] = "0";
	    } else {
		result[0] = mBluetoothAdap.enable() ? "1" : "0";
	    }
	    return result;
	case 6:		/* off */
	    result = new String[1];
	    i = mBluetoothAdap.getState();
	    if ((i == BluetoothAdapter.STATE_OFF) ||
		(i == BluetoothAdapter.STATE_TURNING_OFF)) {
		result[0] = "1";
	    } else if (!hasPerm(android.Manifest.permission.BLUETOOTH_ADMIN)) {
		result[0] = "0";
	    } else {
		result[0] = mBluetoothAdap.disable() ? "1" : "0";
	    }
	    return result;
	case 7:		/* start_discovery */
	    result = new String[1];
	    i = mBluetoothAdap.getState();
	    if (i != BluetoothAdapter.STATE_ON) {
		result[0] = "0";
	    } else if ((android.os.Build.VERSION.SDK_INT < 31) &&
		!hasPerm(android.Manifest.permission.BLUETOOTH_ADMIN)) {
		result[0] = "0";
	    } else if ((android.os.Build.VERSION.SDK_INT >= 31) &&
		       !hasPerm("android.permission.BLUETOOTH_SCAN")) {
		result[0] = "0";
	    } else {
		result[0] = mBluetoothAdap.startDiscovery() ? "1" : "0";
	    }
	    return result;
	case 8:		/* cancel_discovery */
	    result = new String[1];
	    i = mBluetoothAdap.getState();
	    if (i != BluetoothAdapter.STATE_ON) {
		result[0] = "0";
	    } else if ((android.os.Build.VERSION.SDK_INT < 31) &&
		!hasPerm(android.Manifest.permission.BLUETOOTH_ADMIN)) {
		result[0] = "0";
	    } else if ((android.os.Build.VERSION.SDK_INT >= 31) &&
		       !hasPerm("android.permission.BLUETOOTH_SCAN")) {
		result[0] = "0";
	    } else {
		result[0] = mBluetoothAdap.cancelDiscovery() ? "1" : "0";
	    }
	    return result;
	}
	return null;
    }

    /*
     * USB device information
     */

    static String usbHex(int i) {
	if ((i < 0) || (i > 0x0fff)) {
	    return Integer.toHexString(i);
	} else if (i <= 0x000f) {
	    return "000" + Integer.toHexString(i);
	} else if (i <= 0x00ff) {
	    return "00" + Integer.toHexString(i);
	}
	return "0" + Integer.toHexString(i);
    }

    static String usbIfString(UsbDevice dev) {
	if (dev == null) {
	    return "";
	}
	int n, k, v;
	int list[] = new int[dev.getInterfaceCount()];
	for (n = k = 0; n < dev.getInterfaceCount(); n++) {
	    int i, csp;
	    v = dev.getInterface(n).getInterfaceClass();
	    if ((v < 0) || (v > 255)) {
		continue;
	    }
	    csp = v << 16;
	    v = dev.getInterface(n).getInterfaceSubclass();
	    if ((v < 0) || (v > 255)) {
		continue;
	    }
	    csp |= v << 8;
	    v = dev.getInterface(n).getInterfaceProtocol();
	    if ((v < 0) || (v > 255)) {
		continue;
	    }
	    csp |= v;
	    for (i = 0; i < k; i++) {
		if (list[i] == csp) {
		    break;
		}
	    }
	    if (i >= k) {
		list[k++] = csp;
	    }
	}
	if (k == 0) {
	    return "";
	}
	StringBuilder ifs = new StringBuilder(k * 8);
	for (n = 0; n < k; n++) {
	    ifs.append(":");
	    v = 0xf00000;
	    while ((v != 0) && (v & list[n]) == 0) {
		ifs.append("0");
		v = v >> 4;
	    }
	    ifs.append(Integer.toHexString(list[n]));
	}
	if (n > 0) {
	    ifs.append(":");
	}
	return ifs.toString();
    }

    public static String[] usbdevices() {
	return usbdevicesEx(0);
    }

    public static String[] usbdevicesEx(int op) {
	if (mUsbManager == null) {
	    Log.v(TAG, "no USB manager");
	    return null;
	}
	int inc = (op == 0) ? 2 : 3;
	HashMap<String, UsbDevice> list = mUsbManager.getDeviceList();
	String result[] = new String[list.size() * inc];
	Iterator<UsbDevice> iter = list.values().iterator();
	int i = 0;
	while (iter.hasNext()) {
	    UsbDevice dev = iter.next();
	    result[i] = dev.getDeviceName();
	    result[i + 1] = usbHex(dev.getVendorId()) + ":" +
			    usbHex(dev.getProductId());
	    if (inc > 2) {
		result[i + 2] = usbIfString(dev);
	    }
	    Log.v(TAG, "USB '" + result[i] + "' -> " + result[i + 1]);
	    i += inc;
	}
	return result;
    }

    public static int usbpermission(String devName, int ask) {
	if (mUsbManager == null) {
	    Log.v(TAG, "no USB manager");
	    return -1;
	}
	HashMap<String, UsbDevice> list = mUsbManager.getDeviceList();
	int ret = -1;
	for (final UsbDevice dev : list.values()) {
	    if (devName.equals(dev.getDeviceName())) {
		if (mUsbManager.hasPermission(dev)) {
		    ret = 1;
		} else if (ask == 0) {
		    ret = 0;
		} else {
		    UsbPermissionTest uph = new UsbPermissionTest(mSingleton);
		    ret = uph.ask(dev);
		}
		if (ret == 1) {
		    /*
		     * Permission granted, open device and keep it
		     * open until unplugged when detected by broadcast
		     * receiver. This allows native code to fetch the
		     * /dev/bus/usb/* file descriptor using procfs.
		     */
		    UsbDeviceConnection c = mUsbHold.get(devName);
		    if (c == null) {
			c = mUsbManager.openDevice(dev);
			if (c != null) {
			    mUsbHold.put(devName, c);
			} else {
			    ret = 0;
			}
		    }
		}
		break;
	    }
	}
	return ret;
    }

    public static UsbDevice getUsbDevCheck(String devName) {
	if (mUsbManager == null) {
	    Log.v(TAG, "no USB manager");
	    return null;
	}
	HashMap<String, UsbDevice> list = mUsbManager.getDeviceList();
	for (final UsbDevice dev : list.values()) {
	    if (devName.equals(dev.getDeviceName())) {
		if (mUsbManager.hasPermission(dev)) {
		    return dev;
		}
		UsbPermissionTest uph = new UsbPermissionTest(mSingleton);
		return (uph.ask(dev) == 1) ? dev : null;
	    }
	}
	return null;
    }

    /*
     * Show/hide spinner
     */

    public static void spinner(final int show) {
	Runnable spin = new Runnable() {
	    public void run() {
		if (show != 0) {
		    if (mSpinner != null) {
			mSpinner.dismiss();
			mSpinner = null;
		    }
		    mSpinner = ProgressDialog.show(mSingleton, null, null);
		    mSpinner.setContentView(new ProgressBar(mSingleton));
		} else if (mSpinner != null) {
		    mSpinner.dismiss();
		    mSpinner = null;
		}
	    }
	};
	mSingleton.runOnUiThread(spin);
    }

    /*
     * Set/cancel alarms
     */

    public static void alarm(int op, long trigger, long interval,
			     String action, String uristr, String type,
			     String[] cats, String[] comp, String types[],
			     String[] argv) {
	Intent i;
	if ((comp != null) && comp[0].equals("self")) {
	    i = new Intent(mSingleton.getContext(), mSingleton.getClass());
	    i.setAction(action);
	    comp = null;
	} else {
	    i = new Intent(action);
	}
	i = buildIntent(i, uristr, type, cats, comp);
	i.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
	if ((op == 0) || (op == 1)) {
	    addArgsToIntent(i, types, argv);
	}
	if (op == 1) {
	    i.putExtra("tk.tcl.wish.wakeup", true);
	}
	PendingIntent pi =
	    PendingIntent.getActivity(mSingleton.getContext(), 0, i,
				      PendingIntent.FLAG_CANCEL_CURRENT);
	switch (op) {
	case 0:
	    if (interval > 0) {
		mAlarmManager.setRepeating(AlarmManager.RTC, trigger,
					   interval, pi);
	    } else {
		mAlarmManager.set(AlarmManager.RTC, trigger, pi);
	    }
	    break;
	case 1:
	    if (interval > 0) {
		mAlarmManager.setRepeating(AlarmManager.RTC_WAKEUP, trigger,
					   interval, pi);
	    } else {
		mAlarmManager.set(AlarmManager.RTC_WAKEUP, trigger, pi);
	    }
	    break;
	default:
	    mAlarmManager.cancel(pi);
	    break;
	}
    }

    /*
     * Sensor information
     */

    public static String[] sensorList() {
	return mSensors.getList();
    }

    public static int sensorState(int op, int n) {
	return mSensors.getState(op, n);
    }

    public static String sensorGet(int n) {
	return mSensors.getData(n);
    }

    /*
     * Phone information
     */

    public static String getPhoneInfo() {
	if (mPSListener == null) {
	    return "";
	}
	StringBuilder sb = new StringBuilder(512);
	sb.append("device_id ");
	sb.append(mSingleton.toElement(mPhoneManager.getDeviceId()));
	sb.append(" device_sw_version ");
	sb.append(mSingleton.toElement(mPhoneManager.getDeviceSoftwareVersion()));
	sb.append(" line1_number ");
	sb.append(mSingleton.toElement(mPhoneManager.getLine1Number()));
	sb.append(" sim_serial_number ");
	sb.append(mSingleton.toElement(mPhoneManager.getSimSerialNumber()));
	sb.append(" voice_mail_alpha_tag ");
	sb.append(mSingleton.toElement(mPhoneManager.getVoiceMailAlphaTag()));
	sb.append(" voice_mail_number ");
	sb.append(mSingleton.toElement(mPhoneManager.getVoiceMailNumber()));
	sb.append(" network_country_iso ");
	sb.append(mSingleton.toElement(mPhoneManager.getNetworkCountryIso()));
	sb.append(" network_operator ");
	sb.append(mSingleton.toElement(mPhoneManager.getNetworkOperator()));
	sb.append(" network_operator_name ");
	sb.append(mSingleton.toElement(mPhoneManager.getNetworkOperatorName()));
	sb.append(" sim_country_iso ");
	sb.append(mSingleton.toElement(mPhoneManager.getSimCountryIso()));
	sb.append(" sim_operator ");
	sb.append(mSingleton.toElement(mPhoneManager.getSimOperator()));
	sb.append(" sim_operator_name ");
	sb.append(mSingleton.toElement(mPhoneManager.getSimOperatorName()));
	sb.append(" subscriber_id ");
	sb.append(mSingleton.toElement(mPhoneManager.getSubscriberId()));
	sb.append(" call_state ");
	switch (mPhoneManager.getCallState()) {
	case TelephonyManager.CALL_STATE_IDLE:
	    sb.append("idle");
	    break;
	case TelephonyManager.CALL_STATE_OFFHOOK:
	    sb.append("offhook");
	    break;
	case TelephonyManager.CALL_STATE_RINGING:
	    sb.append("ringing");
	    break;
	default:
	    sb.append("unknown");
	    break;
	}
	sb.append(" phone_number ");
	synchronized (mPSListener.mLock) {
	    sb.append(mSingleton.toElement(mPSListener.mNumber));
	}
	sb.append(" data_state ");
	switch (mPhoneManager.getDataState()) {
	case TelephonyManager.DATA_DISCONNECTED:
	    sb.append("disconnected");
	    break;
	case TelephonyManager.DATA_CONNECTING:
	    sb.append("connecting");
	    break;
	case TelephonyManager.DATA_CONNECTED:
	    sb.append("connected");
	    break;
	case TelephonyManager.DATA_SUSPENDED:
	    sb.append("suspended");
	    break;
	default:
	    sb.append("unknown");
	    break;
	}
	sb.append(" data_activity ");
	switch (mPhoneManager.getDataActivity()) {
	case TelephonyManager.DATA_ACTIVITY_NONE:
	    sb.append("none");
	    break;
	case TelephonyManager.DATA_ACTIVITY_IN:
	    sb.append("in");
	    break;
	case TelephonyManager.DATA_ACTIVITY_OUT:
	    sb.append("out");
	    break;
	case TelephonyManager.DATA_ACTIVITY_INOUT:
	    sb.append("inout");
	    break;
	case TelephonyManager.DATA_ACTIVITY_DORMANT:
	    sb.append("dormant");
	    break;
	default:
	    sb.append("unknown");
	    break;
	}
	sb.append(" sim_state ");
	switch (mPhoneManager.getSimState()) {
	case TelephonyManager.SIM_STATE_ABSENT:
	    sb.append("absent");
	    break;
	case TelephonyManager.SIM_STATE_PIN_REQUIRED:
	    sb.append("pin_required");
	    break;
	case TelephonyManager.SIM_STATE_PUK_REQUIRED:
	    sb.append("puk_required");
	    break;
	case TelephonyManager.SIM_STATE_NETWORK_LOCKED:
	    sb.append("network_locked");
	    break;
	case TelephonyManager.SIM_STATE_READY:
	    sb.append("ready");
	    break;
	case TelephonyManager.SIM_STATE_UNKNOWN:
	default:
	    sb.append("unknown");
	    break;
	}
	sb.append(" phone_type ");
	switch (mPhoneManager.getPhoneType()) {
	case TelephonyManager.PHONE_TYPE_NONE:
	    sb.append("none");
	    break;
	case TelephonyManager.PHONE_TYPE_GSM:
	    sb.append("gsm");
	    break;
	case TelephonyManager.PHONE_TYPE_CDMA:
	    sb.append("cdma");
	    break;
	case TelephonyManager.PHONE_TYPE_SIP:
	    sb.append("sip");
	    break;
	default:
	    sb.append("unknown");
	    break;
	}
	sb.append(" network_type ");
	switch (mPhoneManager.getNetworkType()) {
	case TelephonyManager.NETWORK_TYPE_1xRTT:
	    sb.append("1xrtt");
	    break;
	case TelephonyManager.NETWORK_TYPE_CDMA:
	    sb.append("cdma");
	    break;
	case TelephonyManager.NETWORK_TYPE_EDGE:
	    sb.append("edge");
	    break;
	case TelephonyManager.NETWORK_TYPE_EHRPD:
	    sb.append("ehrpd");
	    break;
	case TelephonyManager.NETWORK_TYPE_EVDO_0:
	    sb.append("evdo_0");
	    break;
	case TelephonyManager.NETWORK_TYPE_EVDO_A:
	    sb.append("evdo_a");
	    break;
	case TelephonyManager.NETWORK_TYPE_EVDO_B:
	    sb.append("evdo_b");
	    break;
	case TelephonyManager.NETWORK_TYPE_GPRS:
	    sb.append("gprs");
	    break;
	case TelephonyManager.NETWORK_TYPE_HSPA:
	    sb.append("hspa");
	    break;
	case 0x0F:	/* TelephonyManager.NETWORK_TYPE_HSPAP, API 13 */
	    sb.append("hspa+");
	    break;
	case TelephonyManager.NETWORK_TYPE_HSUPA:
	    sb.append("hsupa");
	    break;
	case TelephonyManager.NETWORK_TYPE_IDEN:
	    sb.append("iden");
	    break;
	case TelephonyManager.NETWORK_TYPE_LTE:
	    sb.append("lte");
	    break;
	case TelephonyManager.NETWORK_TYPE_UMTS:
	    sb.append("umts");
	    break;
	case TelephonyManager.NETWORK_TYPE_UNKNOWN:
	default:
	    sb.append("unknown");
	    break;
	}
	synchronized (mPSListener.mLock) {
	    sb.append(" cdma_dbm ");
	    sb.append("" + mPSListener.mCdmaDbm);
	    sb.append(" cdma_ecio ");
	    sb.append("" + mPSListener.mCdmaEcio);
	    sb.append(" cdma_ecio ");
	    sb.append("" + mPSListener.mCdmaEcio);
	    sb.append(" evdo_dbm ");
	    sb.append("" + mPSListener.mEvdoDbm);
	    sb.append(" evdo_ecio ");
	    sb.append("" + mPSListener.mEvdoEcio);
	    sb.append(" evdo_snr ");
	    sb.append("" + mPSListener.mEvdoSnr);
	    sb.append(" gsm_bit_error_rate ");
	    sb.append("" + mPSListener.mGsmBitErrorRate);
	    sb.append(" gsm_signal_strength ");
	    sb.append("" + mPSListener.mGsmSignalStrength);
	    sb.append(" is_gsm ");
	    sb.append(mPSListener.mIsGsm ? "1" : "0");
	}
	return sb.toString();
    }

    public static int brightness(int query, int percent) {
	if (query != 0) {
	    WindowManager.LayoutParams lp =
	    mSingleton.getWindow().getAttributes();
	    return (int) (lp.screenBrightness * 100);
	}
	if (percent < 0) {
	    percent = -100;
	} else if (percent > 100) {
	    percent = 100;
	}
	final float brightness = (float) (percent / 100.0);
	final CountDownLatch latch = new CountDownLatch(1);
	Runnable setBrightness = new Runnable() {
	    public void run() {
		WindowManager.LayoutParams lp =
		    mSingleton.getWindow().getAttributes();
		lp.screenBrightness = brightness;
		mSingleton.getWindow().setAttributes(lp);
		latch.countDown();
	    }
	};
	mSingleton.runOnUiThread(setBrightness);
	try {
	    latch.await();
	} catch (InterruptedException e) {
	}
	return 0;
    }

    /*
     * Locale info
     */

    public static String[] locale(String lang) {
	if ((lang == null) || lang.equals("default")) {
	    return LocaleAPI1.get(null);
	} else if (lang.equals("tts")) {
	    if (mTTS != null) {
		return LocaleAPI1.get(mTTS.getLanguage());
	    }
	    return LocaleAPI1.get(null);
	}
	String[] str = lang.split("_|\\.", 3);
	if (str != null) {
	    if (str.length > 2) {
		return LocaleAPI1.get(new Locale(str[0], str[1], str[2]));
	    } else if (str.length > 1) {
		return LocaleAPI1.get(new Locale(str[0], str[1]));
	    }
	}
	return LocaleAPI1.get(new Locale(lang));
    }

    public static String setLocale(String lang) {
	if (lang == null) {
	    return null;
	}
	String[] str = lang.split("_|\\.", 3);
	if (str != null) {
	    if (str.length > 2) {
		Locale.setDefault(new Locale(str[0], str[1], str[2]));
	    } else if (str.length > 1) {
		Locale.setDefault(new Locale(str[0], str[1]));
	    } else {
		return null;
	    }
	}
	return Locale.getDefault().toString();
    }

    /*
     * Camera methods
     */

    public static boolean cameraOpen(int id) {
	if (mCamera != null) {
	    return mCamera.open(id);
	}
	return false;
    }

    public static boolean cameraClose() {
	if (mCamera != null) {
	    return mCamera.close();
	}
	return false;
    }

    public static boolean cameraStartStop(int start) {
	if (mCamera != null) {
	    if (start != 0) {
		return mCamera.startPreview();
	    }
	    return mCamera.stopPreview();
	}
	return false;
    }

    public static int cameraGetID(int current) {
	if (mCamera != null) {
	    return mCamera.getID(current != 0);
	}
	return (current != 0) ? -1 : 0;
    }

    public static String cameraParameters(String set) {
	if (mCamera != null) {
	    if (set == null) {
		return mCamera.getParameters();
	    }
	    return mCamera.setParameters(set);
	}
	return null;
    }

    public static int cameraOrientation(int degrees) {
	if (mCamera != null) {
	    return mCamera.setCameraDisplayOrientation(degrees);
	}
	return -1;
    }

    public static byte[] cameraGetImage(int[] info) {
	if (mCamera != null) {
	    return mCamera.lastImage(info);
	}
	return null;
    }

    public static int cameraGetState() {
	if (mCamera != null) {
	    return mCamera.getState();
	}
	return -1;
    }

    public static boolean cameraTakePicture() {
	if (mCamera != null) {
	    return mCamera.takePicture();
	}
	return false;
    }

    public static byte[] cameraGetPicture() {
	if (mCamera != null) {
	    return mCamera.lastPicture();
	}
	return null;
    }

    public static String cameraInfo() {
	if (mCamera != null) {
	    return mCamera.getInfo();
	}
	return null;
    }

    /*
     * Manifest permission
     */

    public static boolean checkPermission(String perm) {
	if (perm == null) {
	    return false;
	}
	return hasPerm(perm);
    }

    public static boolean[] checkPermission(int ask, String[] perms) {
	if (perms == null) {
	    return null;
	}
	if (ask != 0) {
	    askForPermissions(perms);
	}
	int len = perms.length;
	boolean ret[] = new boolean[len];
	for (int i = 0; i < len; i++) {
	    ret[i] = hasPerm(perms[i]);
	}
	return ret;
    }

    /*
     * Try to show input method picker popup window.
     */

    public static void showIMPicker() {
	Runnable r = new Runnable() {
	    public void run() {
		InputMethodManager imm = (InputMethodManager)
		    mSingleton.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
		if (imm != null) {
		    imm.showInputMethodPicker();
		}
	    }
	};
	mSingleton.runOnUiThread(r);
    }

    /*
     * android.os.Environment related
     */

    public static String osenvGetString(int op, String arg) {
	File f = null;
	if ((op != 3) && (arg != null)) {
	    f = new File(arg);
	}
	switch (op) {
	case 0:
	    return android.os.Environment.getDataDirectory().getAbsolutePath();
	case 1:
	    return android.os.Environment.getDownloadCacheDirectory().getAbsolutePath();
	case 2:
	    return android.os.Environment.getExternalStorageDirectory().getAbsolutePath();
	case 3:
	    if (arg == null) {
		return android.os.Environment.getExternalStorageDirectory().getAbsolutePath();
	    }
	    return android.os.Environment.getExternalStoragePublicDirectory(arg).getAbsolutePath();
	case 4:
	    return android.os.Environment.getExternalStorageState();
	case 5:
	    return android.os.Environment.getRootDirectory().getAbsolutePath();
	default:
	    return null;
	}
    }

    public static int osenvGetFlag(int op) {
	boolean b = false;
	switch (op) {
	case 0:
	    b = android.os.Environment.isExternalStorageEmulated();
	    break;
	case 1:
	    b = android.os.Environment.isExternalStorageRemovable();
	    break;
	}
	return b ? 1 : 0;
    }

    /*
     * NFC related: read/write NDEF content to tag.
     */

    public static String[] ndefRead(String tagstr, int cached) {
	String result[] = new String[2];
	Tag tag = mNfcTag;
	byte tb[] = null;
	result[0] = "unknown error";
	result[1] = null;
	try {
	    tb = android.util.Base64.decode(tagstr,
				android.util.Base64.DEFAULT);
	} catch (Exception te) {
	    tb = null;
	}
	if (tb == null) {
	    result[0] = "tag invalid";
	    Log.e(TAG, "ndefRead: " + result[0]);
	    return result;
	}
	if (tag == null) {
	    result[0] = "no current tag";
	    Log.e(TAG, "nfcRead: " + result[0]);
	    return result;
	}
	if (!Arrays.equals(tag.getId(), tb)) {
	    result[0] = "wrong tag id";
	    Log.e(TAG, "nfcRead: tag not current: " + tagstr + " expecting " +
		  android.util.Base64.encodeToString(tag.getId(),
					android.util.Base64.DEFAULT));
	    return result;
	}
	try {
	    Ndef ndef = Ndef.get(tag);
	    NdefMessage ndmsg;
	    if (cached == 0) {
		ndef.connect();
		ndmsg = ndef.getNdefMessage();
		ndef.close();
	    } else {
		ndmsg = ndef.getCachedNdefMessage();
	    }
	    if (ndmsg != null) {
		result[1] =
		    android.util.Base64.encodeToString(ndmsg.toByteArray(),
					android.util.Base64.DEFAULT);
	    }
	} catch (Exception e) {
	    result[0] = "read error";
	    result[1] = null;
	    Log.e(TAG, "nfcRead: exception: " + e.toString());
	    return result;
	}
	result[0] = null;
	return result;
    }

    public static String ndefWrite(String tagstr, String msg) {
	Tag tag = mNfcTag;
	byte tb[];
	try {
	    tb = android.util.Base64.decode(tagstr,
				android.util.Base64.DEFAULT);
	} catch (Exception te) {
	    tb = null;
	}
	if (tb == null) {
	    Log.e(TAG, "ndefWrite: tag invalid");
	    return "tag invalid";
	}
	byte mb[];
	try {
	    mb = android.util.Base64.decode(msg,
				android.util.Base64.DEFAULT);
	} catch (Exception me) {
	    mb = null;
	}
	if (mb == null) {
	    Log.e(TAG, "ndefWrite: message invalid");
	    return "message invalid";
	}
	if (tag == null) {
	    Log.e(TAG, "ndefWrite: no current tag");
	    return "no current tag";
	}
	if (!Arrays.equals(tag.getId(), tb)) {
	    Log.e(TAG, "ndefWrite: tag not current: " + tagstr + " expecting " +
		  android.util.Base64.encodeToString(tag.getId(),
					android.util.Base64.DEFAULT));
	    return "wrong tag id";
	}
	try {
	    NdefMessage ndmsg = new NdefMessage(mb);
	    int size = ndmsg.toByteArray().length;
	    Ndef ndef = Ndef.get(tag);
	    if (ndef == null) {
		NdefFormatable nfmt = NdefFormatable.get(tag);
		if (nfmt == null) {
		    Log.e(TAG, "ndefWrite: unsupported tech");
		    return "unsupported tech";
		}
		nfmt.connect();
		nfmt.format(ndmsg);
		nfmt.close();
	    } else {
		ndef.connect();
		if (!ndef.isWritable()) {
		    ndef.close();
		    Log.e(TAG, "ndefWrite: read only tag");
		    return "read only tag";
		}
		if (size > ndef.getMaxSize()) {
		    ndef.close();
		    Log.e(TAG, "ndefWrite: message too large (" + size + ">" +
			  ndef.getMaxSize() + ")");
		    return "message too large";
		}
		ndef.writeNdefMessage(ndmsg);
		ndef.close();
	    }
	} catch (FormatException fe) {
	    Log.e(TAG, "ndefWrite: format exception: " + fe.toString());
	    return "wrong ndef format";
	} catch (IOException ie) {
	    Log.e(TAG, "ndefWrite: I/O exception: " + ie.toString());
	    return "I/O error";
	} catch (Exception e) {
	    Log.e(TAG, "ndefWrite: other exception: " + e.toString());
	    return "unknown error";
	}
	return null;
    }

    public static String ndefFormat(String tagstr, String msg) {
	Tag tag = mNfcTag;
	byte tb[];
	try {
	    tb = android.util.Base64.decode(tagstr,
					 android.util.Base64.DEFAULT);
	} catch (Exception te) {
	    tb = null;
	}
	if (tb == null) {
	    Log.e(TAG, "ndefFormat: tag invalid");
	    return "tag invalid";
	}
	byte mb[] = null;
	try {
	    mb = android.util.Base64.decode(msg,
					android.util.Base64.DEFAULT);
	} catch (Exception me) {
	    mb = null;
	}
	if (mb == null) {
	    Log.e(TAG, "ndefFormat: message invalid");
	    return "message invalid";
	}
	if (tag == null) {
	    Log.e(TAG, "ndefFormat: no current tag");
	    return "no current tag";
	}
	if (!Arrays.equals(tag.getId(), tb)) {
	    Log.e(TAG, "ndefFormat: tag not current: " + tagstr +
		  " expecting " +
		  android.util.Base64.encodeToString(tag.getId(),
					android.util.Base64.DEFAULT));
	    return "wrong tag id";
	}
	try {
	    NdefFormatable nfmt = NdefFormatable.get(tag);
	    if (nfmt == null) {
		Log.e(TAG, "ndefFormat: unsupported tech");
		return "unsupported tech";
	    }
	    nfmt.connect();
	    NdefMessage ndmsg = new NdefMessage(mb);
	    nfmt.format(ndmsg);
	    nfmt.close();
	} catch (FormatException fe) {
	    Log.e(TAG, "ndefFormat: format exception: " + fe.toString());
	    return "wrong ndef format";
	} catch (IOException ie) {
	    Log.e(TAG, "ndefFormat: I/O exception: " + ie.toString());
	    return "I/O error";
	} catch (Exception e) {
	    Log.e(TAG, "ndefFormat: other exception: " + e.toString());
	    return "unknown error";
	}
	return null;
    }

    /*
     * SharedPreferences related: read/write configuration values.
     */

    public static int shpGetBoolean(String file, String key, int def) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	return sp.getBoolean(key, (def != 0)) ? 1 : 0;
    }

    public static float shpGetFloat(String file, String key, float def) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	return sp.getFloat(key, def);
    }

    public static int shpGetInt(String file, String key, int def) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	return sp.getInt(key, def);
    }

    public static long shpGetLong(String file, String key, long def) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	return sp.getLong(key, def);
    }

    public static String shpGetString(String file, String key, String def) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	return sp.getString(key, def);
    }

    public static void shpSetBoolean(String file, String key, int val) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	SharedPreferences.Editor ed = sp.edit();
	ed.putBoolean(key, (val != 0));
	ed.apply();
    }

    public static void shpSetFloat(String file, String key, float val) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	SharedPreferences.Editor ed = sp.edit();
	ed.putFloat(key, val);
	ed.apply();
    }

    public static void shpSetInt(String file, String key, int val) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	SharedPreferences.Editor ed = sp.edit();
	ed.putInt(key, val);
	ed.apply();
    }

    public static void shpSetLong(String file, String key, long val) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	SharedPreferences.Editor ed = sp.edit();
	ed.putLong(key, val);
	ed.apply();
    }

    public static void shpSetString(String file, String key, String val) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	SharedPreferences.Editor ed = sp.edit();
	ed.putString(key, val);
	ed.apply();
    }

    public static void shpRemove(String file, String key) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	SharedPreferences.Editor ed = sp.edit();
	ed.remove(key);
	ed.apply();
    }

    public static void shpClear(String file) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	SharedPreferences.Editor ed = sp.edit();
	ed.clear();
	ed.apply();
    }

    public static String[] shpKeys(String file, int how) {
	SharedPreferences sp =
	    mSingleton.getContext().getSharedPreferences(file,
							 Context.MODE_PRIVATE);
	Map<String,?> keys = sp.getAll();
	int size = keys.size();
	if ((how >= 1) && (how <= 2)) {
	    size *= 2;
	}
	String result[] = new String[size];
	int i = 0;
	for (Map.Entry<String,?> entry : keys.entrySet()) {
	    Object obj = entry.getValue();
	    if ((obj == null) || (obj instanceof Set)) {
		continue;
	    }
	    result[i] = entry.getKey();
	    i++;
	    if (how == 2) {
		/* "all", i.e. Tcl array set form */
		result[i] = obj.toString();
		i++;
	    } else if (how == 1) {
		/* "alltypes" */
		if (obj instanceof Boolean) {
		    result[i] = "boolean";
		} else if (obj instanceof Float) {
		    result[i] = "float";
		} else if (obj instanceof Integer) {
		    result[i] = "int";
		} else if (obj instanceof Long) {
		    result[i] = "long";
		} else {
		    result[i] = "string";
		}
		i++;
	    }
	}
	return result;
    }

    public static String getPermissionList() {
	ArrayList<String> r = new ArrayList<String>();
	PackageManager pm = mSingleton.getContext().getPackageManager();
	try {
	    CharSequence csg;
	    List<PermissionGroupInfo> g = pm.getAllPermissionGroups(0);
	    for (PermissionGroupInfo pgi : g) {
		csg = pgi.loadLabel(pm);
		try {
		    List<PermissionInfo> p =
			pm.queryPermissionsByGroup(pgi.name, 0);
		    for (PermissionInfo pi : p) {
			r.add(pi.name);
		    }
		} catch (Exception e) {
		}
	    }
	} catch (Exception e2) {
	}
	return mSingleton.encodeToList(r);
    }

    /*
     * Permission handling for API >= 24
     */

    @Override
    public void onRequestPermissionsResult(int requestCode,
					   String[] perms,
					   int[] results) {
	/*
	 * No need to invoke
	 *   super.onRequestPermissionsResult(requestCode, perms, results);
	 * since we do not derive the Activity from AppCompatActivity.
	 */

	if ((requestCode == 42) && (mPermLock != null)) {
	    mPermLock.countDown();
	}
    }

    public static synchronized void askForPermissions(final String[] perms) {

	/*
	 * The synchronized keyword above ensures only one
	 * invocation at any single time since we deal with
	 * mSingleton and mPermLock.
	 */

	mPermLock = new CountDownLatch(1);
	Runnable r = new Runnable() {
	    public void run() {
		ActivityCompat.requestPermissions(mSingleton, perms, 42);
	    }
	};
	mSingleton.runOnUiThread(r);
	try {
	    mPermLock.await();
	} catch (InterruptedException e) {
	}
	mPermLock = null;
    }

    /*
     * Native setenv
     */

    public static native int nativeSetenv(String name, String value);

    /*
     * Native add path component to environment variable
     */

    public static native int nativeAddpath(String name, String path);

    /*
     * Native initializer
     */

    public static native void nativeInit(int api, String lang);

    /*
     * Native set file to be sourced
     */

    public static native void nativeSetStartupFile(String file);

    /*
     * Callback when activity reports result back
     */

    public static native void nativeIntentCallback(int id, int ret,
						   String action,
						   String uristr,
						   String type,
						   String[] cats,
						   String[] args);
    /*
     * Callback to report broadcast intent
     */

    public static native void nativeBroadcastCallback(int ret,
						      String action,
						      String uristr,
						      String type,
						      String[] cats,
						      String[] args);

    /*
     * Callback to indicate location update
     */

    public static native int nativeTriggerLocation();

    /*
     * Callback to indicate GPS update
     */

    public static native int nativeTriggerGpsUpdate();

    /*
     * Callback to indicate NMEA update
     */

    public static native int nativeTriggerNmeaUpdate();

    /*
     * Callback to indicate network status update
     */

    public static native int nativeTriggerNetworkInfo();

    /*
     * Callback to indicate tether status update
     */

    public static native int nativeTriggerTetherInfo();

    /*
     * Callback to indicate bluetooth status update
     */

    public static native int nativeTriggerBluetooth();

    /*
     * Callback to indicate keyboard status update
     */

    public static native int nativeTriggerKeyboard();

    /*
     * Callback to indicate sensor update
     */

    public static native int nativeTriggerSensor(int n);

    /*
     * Callback to indicate USB attached/detached
     */

    public static native int nativeTriggerUsb(int n);

    /*
     * Callback to indicate Intent
     */

    public static native void nativeTriggerIntent(String action, String uristr,
						  String type, String[] cats,
						  String[] args);

    /*
     * Callback to indicate RecognitionListener events
     */

    public static native void nativeTriggerSpeech(String[] args);

    /*
     * Callback to indicate phone state events
     */

    public static native void nativeTriggerPhoneState(int type);

    /*
     * Callback to indicate text-to-speech state events
     */

    public static native void nativeTriggerTTS(int type, int id);

}

/*
 * Helper class to carry out activity
 */

class Runner implements Runnable {
    AndroWish mAW;
    Intent mI;
    int mId;

    public Runner(AndroWish aw, Intent i, int id) {
	mAW = aw;
	mI = i;
	mId = id;
    }

    public void run() {
	try {
	    if (mId == -2) {
		mAW.sendBroadcast(mI);
	    } else if (mId < 0) {
		mAW.startActivity(mI);
	    } else {
		mAW.startActivityForResult(mI, mId);
	    }
	} catch (Exception e) {
	}
    }
}

/*
 * Start AndroWish activity from a script
 */

class AndroWishScript extends AndroWish {

    private static final String TAG = AndroWish.TAG;

    /*
     * Callback on application startup
     */

    @Override
    public void onCreate(Bundle savedInstanceData) {
	Intent intent = getIntent();
	String file = intent.getStringExtra("arg");
	if (file == null) {
	    file = intent.getDataString();
	}
	super.onCreate(savedInstanceData);
	if (file == null) {
	    finish();
	    return;
	}
	Log.v(TAG, "run script '" + file + "'");
	nativeSetStartupFile(file);
    }

}

/*
 * Ice cream sandwich and beyond
 */

final class RadioVersion_ICS {

    public static String getRadioVersion() {
	return android.os.Build.getRadioVersion();
    }

}

/*
 * Sensor data of one sensor
 */

final class SensorItem {

    Sensor mSensor;
    int mType;
    boolean mEnabled;
    int mTrigger;
    int mOverflow;
    int mAccuracy;
    float mValues[];
    float mOrientation[];
    float mInclination;
    float mRotationMatrix[];

    public SensorItem(Sensor sensor) {
	mSensor = sensor;
	mType = (sensor == null) ? -1 : sensor.getType();
	mEnabled = false;
	mTrigger = 0;
	mOverflow = 0;
	mAccuracy = 0;
	mValues = null;
	mOrientation = null;
	mInclination = 0;
	mRotationMatrix = null;
    }

}

/*
 * Helper class to deal with sensors
 */

class Sensors implements SensorEventListener {

    AndroWish mAW;
    SensorManager mSensorManager;
    SensorItem mSensors[];

    public Sensors(AndroWish aw) {
	mAW = aw;
	mSensorManager =
	    (SensorManager) mAW.getSystemService(Context.SENSOR_SERVICE);
	List<Sensor> l = mSensorManager.getSensorList(Sensor.TYPE_ALL);
	mSensors = new SensorItem[26];
	int i = 0;
	for (Sensor s : l) {
	    if (s.getType() == 17) {
		/* Sensor.TYPE_SIGNIFICANT_MOTION not handled */
		continue;
	    }
	    mSensors[i++] = new SensorItem(s);
	    if (i >= mSensors.length) {
		break;
	    }
	}
    }

    void pause() {
	mSensorManager.unregisterListener(this);
    }

    void resume() {
	for (int i = 0; i < mSensors.length; i++) {
	    SensorItem si = mSensors[i];
	    if ((si != null) && si.mEnabled) {
		mSensorManager.registerListener(this, si.mSensor,
						SensorManager.SENSOR_DELAY_NORMAL);
	    }
	}
    }

    public void onSensorChanged(SensorEvent event) {
	int i, ti = -1;
	SensorItem sa = null, sm = null, st = null;
	for (i = 0; i < mSensors.length; i++) {
	    SensorItem si = mSensors[i];
	    if ((si != null) && (si.mSensor == event.sensor)) {
		boolean doRM = false;
		if (si.mEnabled) {
		    if (si.mType == Sensor.TYPE_ACCELEROMETER) {
			sa = si;
		    } else if (si.mType == Sensor.TYPE_MAGNETIC_FIELD) {
			sm = si;
		    }
		    if ((si.mType == Sensor.TYPE_ROTATION_VECTOR) ||
			(si.mType == 15) || /* Sensor.TYPE_GAME_ROTATION_VECTOR */
			(si.mType == 20)) { /* Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR */
			doRM = true;
		    }
		}
		synchronized (si) {
		    si.mAccuracy = event.accuracy;
		    if ((si.mValues == null) ||
			(si.mValues.length != event.values.length)) {
			si.mValues = new float[event.values.length];
		    }
		    System.arraycopy(event.values, 0, si.mValues, 0,
				     event.values.length);
		    if (doRM) {
			if (si.mRotationMatrix == null) {
			    si.mRotationMatrix = new float[16];
			}
			SensorManager.getRotationMatrixFromVector(si.mRotationMatrix,
								  event.values);
		    }
		    if (si.mEnabled) {
			st = si;
			ti = i;
		    }
		}
		break;
	    }
	}
	if ((sa != null) || (sm != null)) {
	    if (sa == null) {
		for (i = 0; i < mSensors.length; i++) {
		    SensorItem si = mSensors[i];
		    if ((si != null) &&
			(si.mType == Sensor.TYPE_ACCELEROMETER)) {
			if (si.mEnabled && (si.mValues != null)) {
			    sa = si;
			}
			break;
		    }
		}
	    } else {
		for (i = 0; i < mSensors.length; i++) {
		    SensorItem si = mSensors[i];
		    if ((si != null) &&
			(si.mType == Sensor.TYPE_MAGNETIC_FIELD)) {
			if (si.mEnabled && (si.mValues != null)) {
			    sm = si;
			}
			break;
		    }
		}
	    }
	}
	if ((sa != null) && (sm != null)) {
	    float R[] = new float[9];
	    float I[] = new float[9];
	    if (SensorManager.getRotationMatrix(R, I, sa.mValues, sm.mValues)) {
		synchronized (sm) {
		    if (sm.mOrientation == null) {
			sm.mOrientation = new float[3];
		    }
		    SensorManager.getOrientation(R, sm.mOrientation);
		    sm.mInclination = SensorManager.getInclination(I);
		}
	    }
	}
	if (st != null) {
	    synchronized (st) {
		if (st.mTrigger < 4) {
		    if (mAW.nativeTriggerSensor(ti) > 0) {
			st.mTrigger++;
		    } else {
			st.mOverflow++;
		    }
		} else {
		    st.mOverflow++;
		}
		if (st.mOverflow > 64) {
		    mSensorManager.unregisterListener(this, st.mSensor);
		    st.mEnabled = false;
		    st.mOverflow = 0;
		    st.mOrientation = null;
		    st.mRotationMatrix = null;
		}
	    }
	}
    }

    public void onAccuracyChanged(Sensor sensor, int accuracy) {
	for (int i = 0; i < mSensors.length; i++) {
	    SensorItem si = mSensors[i];
	    if ((si != null) && (si.mSensor == sensor)) {
		synchronized (si) {
		    si.mAccuracy = accuracy;
		    if (si.mEnabled) {
			if (si.mTrigger < 4) {
			    if (mAW.nativeTriggerSensor(i) > 0) {
				si.mTrigger++;
			    } else {
				si.mOverflow++;
			    }
			} else {
			    si.mOverflow++;
			}
		    }
		    if (si.mOverflow > 64) {
			mSensorManager.unregisterListener(this, si.mSensor);
			si.mEnabled = false;
			si.mOverflow = 0;
			si.mOrientation = null;
			si.mRotationMatrix = null;
		    }
		}
	    }
	}
    }

    public String[] getList() {
	int i, n;
	for (n = i = 0; n < mSensors.length; n++) {
	    if (mSensors[n] != null) {
		i++;
	    }
	}
	if (i == 0) {
	    return null;
	}
	String ret[] = new String[i];
	for (n = i = 0; n < mSensors.length; n++) {
	    if (mSensors[n] == null) {
		continue;
	    }
	    Sensor s = mSensors[n].mSensor;
	    StringBuilder sb = new StringBuilder(256);
	    sb.append("index ").append(i).append(" name ");
	    sb.append(mAW.toElement(s.getName()));
	    sb.append(" type ");
	    switch (mSensors[n].mType) {
	    case Sensor.TYPE_ACCELEROMETER:
		sb.append("accelerometer");
		break;
	    case Sensor.TYPE_AMBIENT_TEMPERATURE:
	    case Sensor.TYPE_TEMPERATURE:
		sb.append("temperature");
		break;
	    case 15: /* Sensor.TYPE_GAME_ROTATION_VECTOR */
		sb.append("game_rotation_vector");
		break;
	    case 20: /* Sensor.TYPE_GEOMAGNETIC_ROTATION_VECTOR */
		sb.append("geomagnetic_rotation_vector");
		break;
	    case Sensor.TYPE_GRAVITY:
		sb.append("gravity");
		break;
	    case Sensor.TYPE_GYROSCOPE:
		sb.append("gyroscope");
		break;
	    case 16: /* Sensor.TYPE_GYROSCOPE_UNCALIBRATED */
		sb.append("gyroscope_uncalibrated");
		break;
	    case Sensor.TYPE_LIGHT:
		sb.append("light");
		break;
	    case Sensor.TYPE_LINEAR_ACCELERATION:
		sb.append("linear_acceleration");
		break;
	    case Sensor.TYPE_MAGNETIC_FIELD:
		sb.append("magnetic_field");
		break;
	    case 14: /* Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED */
		sb.append("magnetic_field_uncalibrated");
		break;
	    case Sensor.TYPE_ORIENTATION:
		sb.append("orientation");
		break;
	    case Sensor.TYPE_PRESSURE:
		sb.append("pressure");
		break;
	    case Sensor.TYPE_PROXIMITY:
		sb.append("proximity");
		break;
	    case Sensor.TYPE_RELATIVE_HUMIDITY:
		sb.append("relative_humidity");
		break;
	    case 11: /* Sensor.TYPE_ROTATION_VECTOR */
		sb.append("rotation_vector");
		break;
	    case 17: /* Sensor.TYPE_SIGNIFICANT_MOTION */
		sb.append("significant_motion");
		break;
	    case 19: /* Sensor.TYPE_STEP_COUNTER */
		sb.append("step_counter");
		break;
	    case 18: /* Sensor.TYPE_STEP_DETECTOR */
		sb.append("step_detector");
		break;
	    default:
		sb.append("unknown");
		break;
	    }
	    int mindelay = s.getMinDelay();
	    if (mindelay > 0) {
		mindelay = mindelay / 1000;	/* milliseconds */
		if (mindelay <= 0) {
		    mindelay = 1;
		}
	    } else {
		mindelay = 0;
	    }
	    sb.append(" mindelay ").append(mindelay);
	    sb.append(" maxrange ").append(s.getMaximumRange());
	    sb.append(" resolution ").append(s.getResolution());
	    sb.append(" power ").append(s.getPower());
	    ret[i++] = sb.toString();
	}
	return ret;
    }

    public int getState(int op, int n) {
	if (n < 0 || n > mSensors.length) {
	    return 0;
	}
	if (mSensors[n] == null) {
	    return 0;
	}
	final SensorItem si = mSensors[n];
	final Sensors s = this;
	switch (op) {
	case 0:		/* enable */
	    if (!si.mEnabled) {
		final SensorManager sensorManager = mSensorManager;
		Runnable doit = new Runnable() {
		    public void run() {
			if (sensorManager.registerListener(s, si.mSensor,
							   SensorManager.SENSOR_DELAY_NORMAL)) {
			    si.mEnabled = true;
			}
		    }
		};
		mAW.runOnUiThread(doit);
	    }
	    n = 1;
	    break;
	case 1:		/* disable */
	    if (si.mEnabled) {
		final SensorManager sensorManager = mSensorManager;
		Runnable doit = new Runnable() {
		    public void run() {
			sensorManager.unregisterListener(s, si.mSensor);
			si.mEnabled = false;
			si.mOrientation = null;
		    }
		};
		mAW.runOnUiThread(doit);
	    }
	    n = 1;
	    break;
	default:	/* query */
	    n = si.mEnabled ? 1 : 0;
	    break;
	}
	return n;
    }

    public String getData(int n) {
	if (n < 0 || n > mSensors.length) {
	    return null;
	}
	if (mSensors[n] == null) {
	    return null;
	}
	SensorItem si = mSensors[n];
	StringBuilder sb = new StringBuilder(256);
	sb.append("index ").append(n);
	sb.append(" enabled ").append(si.mEnabled ? 1 : 0);
	sb.append(" maxrange ").append(si.mSensor.getMaximumRange());
	sb.append(" resolution ").append(si.mSensor.getResolution());
	synchronized (si) {
	    sb.append(" accuracy ").append(si.mAccuracy).append(" values {");
	    if (si.mValues != null) {
		for (n = 0; n < si.mValues.length; n++) {
		    if (n > 0) {
			sb.append(" ");
		    }
		    sb.append(si.mValues[n]);
		}
	    }
	    sb.append("}");
	    if ((si.mType == Sensor.TYPE_MAGNETIC_FIELD) &&
		(si.mOrientation != null)) {
		sb.append(" orientation {");
		for (n = 0; n < si.mOrientation.length; n++) {
		    if (n > 0) {
			sb.append(" ");
		    }
		    sb.append(si.mOrientation[n]);
		}
		sb.append("} inclination ").append(si.mInclination);
	    } else if ((si.mType == Sensor.TYPE_PRESSURE) &&
		(si.mValues != null)) {
		sb.append(" altitude ");
		sb.append(SensorManager.getAltitude(SensorManager.PRESSURE_STANDARD_ATMOSPHERE, si.mValues[0]));
	    }
	    if (si.mRotationMatrix != null && si.mRotationMatrix.length > 0) {
		sb.append(" rotation_matrix {");
		for (n = 0; n < si.mRotationMatrix.length; n++) {
		    if (n > 0) {
			sb.append(" ");
		    }
		    sb.append(si.mRotationMatrix[n]);
		}
		sb.append("}");
	    }
	    si.mTrigger = si.mOverflow = 0;
	}
	return sb.toString();
    }

}

/*
 * GPS status handling
 */

class GpsInfo implements GpsStatus.Listener {

    AndroWish mAW;
    boolean mStarted;
    int mFirstFix;
    GpsStatus mGpsStatus;

    public GpsInfo(AndroWish aw) {
	mStarted = false;
	mFirstFix = 0;
	mGpsStatus = null;
	mAW = aw;
    }

    @Override
    public void onGpsStatusChanged(int event) {
	boolean trigger = false;
	synchronized (this) {
	    switch (event) {
	    case GpsStatus.GPS_EVENT_STARTED:
		mGpsStatus = mAW.mLocationMgr.getGpsStatus(mGpsStatus);
		if (mGpsStatus != null) {
		    mFirstFix = mGpsStatus.getTimeToFirstFix();
		}
		mStarted = true;
		trigger = true;
		break;
	    case GpsStatus.GPS_EVENT_STOPPED:
		mGpsStatus = null;
		mStarted = false;
		mFirstFix = 0;
		trigger = true;
		break;
	    case GpsStatus.GPS_EVENT_FIRST_FIX:
	    case GpsStatus.GPS_EVENT_SATELLITE_STATUS:
		mGpsStatus = mAW.mLocationMgr.getGpsStatus(mGpsStatus);
		if (mGpsStatus != null) {
		    mFirstFix = mGpsStatus.getTimeToFirstFix();
		    trigger = true;
		}
		break;
	    }
	}
	if (event == GpsStatus.GPS_EVENT_STOPPED) {
	    if (mAW.mNmeaInfo != null) {
		mAW.mNmeaInfo.clear();
	    }
	}
	if (trigger) {
	    mAW.nativeTriggerGpsUpdate();
	}
    }

    public String getInfo() {
	StringBuilder sb = new StringBuilder(64);
	synchronized (this) {
	    sb.append("state ").append(mStarted ? "on" : "off");
	    sb.append(" first_fix ").append(mFirstFix);
	}
	return sb.toString();
    }

    public String getSatellites() {
	StringBuilder sb = new StringBuilder(4096);
	synchronized (this) {
	    if (mGpsStatus != null) {
		int n = 0;
		for (GpsSatellite s : mGpsStatus.getSatellites()) {
		    if (n > 0) {
			sb.append(" ");
		    }
		    sb.append(n).append(" {index ").append(n);
		    sb.append(" azimuth ").append(s.getAzimuth());
		    sb.append(" elevation ").append(s.getElevation());
		    sb.append(" prn ").append(s.getPrn());
		    sb.append(" snr ").append(s.getSnr());
		    sb.append(" almanac ").append(s.hasAlmanac() ? 1 : 0);
		    sb.append(" ephemeris ").append(s.hasEphemeris() ? 1 : 0);
		    sb.append(" infix ").append(s.usedInFix() ? 1 : 0);
		    sb.append("}");
		    n++;
		}
	    }
	}
	return sb.toString();
    }
}

/*
 * NMEA sentence handling
 */

class NmeaInfo implements GpsStatus.NmeaListener {

    AndroWish mAW;
    long mTime0, mTime1;
    StringBuilder mNmeaBuf;
    String mNmeaStr;

    public NmeaInfo(AndroWish aw) {
	mAW = aw;
	mTime0 = System.currentTimeMillis() / 1000;
	mTime1 = 0;
	mNmeaBuf = new StringBuilder(4096);
	mNmeaStr = null;
    }

    @Override
    public void onNmeaReceived(long time, String nmea) {
	boolean trigger = false;
	time = time / 1000;
	synchronized (this) {
	    if (mTime0 != time) {
		if (mNmeaBuf.length() > 0) {
		    mTime1 = mTime0;
		    mNmeaStr = new String(mNmeaBuf);
		    trigger = true;
		}
	    }
	}
	if (trigger) {
	    mAW.nativeTriggerNmeaUpdate();
	    mTime0 = time;
	    mNmeaBuf.setLength(0);
	}
	mNmeaBuf.append(nmea);
    }

    public void clear() {
	mTime0 = System.currentTimeMillis() / 1000;
	mNmeaBuf.setLength(0);
    }

    public String getInfo() {
	long time;
	String nmea = null;
	synchronized (this) {
	    time = mTime1;
	    if ((mNmeaStr != null) && (mNmeaStr.length() > 0)) {
		nmea = mNmeaStr;
	    }
	}
	if (nmea != null) {
	    StringBuilder sb = new StringBuilder(nmea.length() + 64);
	    sb.append("time ").append(time);
	    sb.append(" nmea ").append(mAW.toElement(nmea));
	    return sb.toString();
	}
	return null;
    }

}

/*
 * Helper class to carry out speech recognition call
 */

class SpRunner implements Runnable {
    SpeechRec mSpeechRec;
    Intent mI;
    int mOp;

    public SpRunner(SpeechRec sr, Intent i, int op) {
	mSpeechRec = sr;
	mI = i;
	mOp = op;
    }

    public void run() {
	try {
	    switch (mOp) {
	    case -1:		/* cancel */
		if (mSpeechRec != null) {
		    mSpeechRec.cancel();
		}
		break;
	    case -2:		/* stop */
		if (mSpeechRec != null) {
		    mSpeechRec.stop();
		}
		break;
	    case -3:		/* start */
		if (mSpeechRec != null) {
		    mSpeechRec.start(mI);
		}
		break;
	    }
	} catch (Exception e) {
	}
    }
}

/*
 * RecognitionListener helper class
 */

class SpeechRec implements RecognitionListener {

    private static final String TAG = AndroWish.TAG;

    AndroWish mAW;
    SpeechRecognizer mSpeechRecognizer;

    public SpeechRec(AndroWish aw) {
	mAW = aw;
	mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(mAW);
    }

    public void onBeginningOfSpeech() {
	speechCallback("begin");
    }

    public void onBufferReceived(byte[] buffer) {
    }

    public void onEndOfSpeech() {
	speechCallback("end");
    }

    public void onError(int error) {
	speechCallback("error", error);
    }

    public void onEvent(int eventType, Bundle params) {
	speechCallback("event", eventType, params);
    }

    public void onPartialResults(Bundle partialResults) {
	speechCallback("partialresult", partialResults);
    }

    public void onReadyForSpeech(Bundle params) {
	speechCallback("ready", params);
    }

    public void onResults(Bundle results) {
	speechCallback("result", results);
    }

    public void onRmsChanged(float rmsdB) {
	speechCallback("rms", rmsdB);
    }

    void speechCallback(String op) {
	int k = 0;
	String args[] = new String[2];
	args[k++] = "type";
	args[k++] = op;
	Log.v(TAG, "nativeTriggerSpeech: " + op + " " + args);
	mAW.nativeTriggerSpeech(args);
    }

    void speechCallback(String op, Bundle data) {
	int k = 0;
	String args[];
	if (data != null) {
	    String[] addargs = new String[2];
	    addargs[0] = "type";
	    addargs[1] = op;
	    args = mAW.formatBundle(data, addargs);
	} else {
	    args = new String[2];
	    args[0] = "type";
	    args[1] = op;
	}
	Log.v(TAG, "nativeTriggerSpeech: " + op + " " + args);
	mAW.nativeTriggerSpeech(args);
    }

    void speechCallback(String op, int num, Bundle data) {
	int k = 0;
	String args[];
	if (data != null) {
	    String[] addargs = new String[4];
	    addargs[0] = "type";
	    addargs[1] = op;
	    addargs[2] = "value";
	    addargs[3] = "" + num;
	    args = mAW.formatBundle(data, addargs);
	} else {
	    args = new String[4];
	    args[0] = "type";
	    args[1] = op;
	    args[2] = "value";
	    args[3] = "" + num;
	}
	Log.v(TAG, "nativeTriggerSpeech: " + op + " " + args);
	mAW.nativeTriggerSpeech(args);
    }

    void speechCallback(String op, int data) {
	String args[] = new String[4];
	args[0] = "type";
	args[1] = op;
	args[2] = "value";
	args[3] = "" + data;
	Log.v(TAG, "nativeTriggerSpeech: " + op + " " + args);
	mAW.nativeTriggerSpeech(args);
    }

    void speechCallback(String op, float data) {
	String args[] = new String[4];
	args[0] = "type";
	args[1] = op;
	args[2] = "value";
	args[3] = "" + data;
	Log.v(TAG, "nativeTriggerSpeech: " + op + " " + args);
	mAW.nativeTriggerSpeech(args);
    }

    public void cancel() {
	if (mSpeechRecognizer != null) {
	    mSpeechRecognizer.cancel();
	}
    }

    public void stop () {
	if (mSpeechRecognizer != null) {
	    mSpeechRecognizer.stopListening();
	}
    }

    public void start(Intent i) {
	if (mSpeechRecognizer != null) {
	    mSpeechRecognizer.cancel();
	    mSpeechRecognizer.setRecognitionListener(this);
	    mSpeechRecognizer.startListening(i);
	}
    }

    public void destroy() {
	if (mSpeechRecognizer != null) {
	    mSpeechRecognizer.cancel();
	    mSpeechRecognizer.destroy();
	    mSpeechRecognizer = null;
	}
    }

}

/*
 * Phone state listener helper class
 */

class PSListener extends PhoneStateListener {

    private static final String TAG = AndroWish.TAG;

    AndroWish mAW;
    Object mLock;
    String mNumber;
    int mCdmaDbm;
    int mCdmaEcio;
    int mEvdoDbm;
    int mEvdoEcio;
    int mEvdoSnr;
    int mGsmBitErrorRate;
    int mGsmSignalStrength;
    boolean mIsGsm;

    public PSListener(AndroWish aw) {
	super();
	mAW = aw;
	mLock = new Object();
    }

    public void onCallForwardingIndicatorChanged(boolean cfi) {
    }

    public void onCallStateChanged(int state, String incomingNumber) {
	if ((state != TelephonyManager.CALL_STATE_OFFHOOK) &&
	    (state != TelephonyManager.CALL_STATE_RINGING)) {
	    incomingNumber = null;
	}
	synchronized (mLock) {
	    if (incomingNumber != null) {
		mNumber = new String(incomingNumber);
	    } else {
		mNumber = null;
	    }
	}
	mAW.nativeTriggerPhoneState(1);
    }

    public void onCellLocationChanged(CellLocation location) {
    }

    public void onDataActivity(int direction) {
	mAW.nativeTriggerPhoneState(2);
    }

    public void onDataConnectionStateChanged(int state) {
	mAW.nativeTriggerPhoneState(3);
    }

    public void onDataConnectionStateChanged(int state, int networkType) {
    }

    public void onMessageWaitingIndicatorChanged(boolean mwi) {
    }

    public void onServiceStateChanged(ServiceState serviceState) {
	mAW.nativeTriggerPhoneState(4);
    }

    public void onSignalStrengthChanged(int asu) {
    }

    public void onSignalStrengthsChanged(SignalStrength signalStrength) {
	synchronized (mLock) {
	    mCdmaDbm = signalStrength.getCdmaDbm();
	    mCdmaEcio = signalStrength.getCdmaEcio();
	    mEvdoDbm = signalStrength.getEvdoDbm();
	    mEvdoEcio = signalStrength.getEvdoEcio();
	    mEvdoSnr = signalStrength.getEvdoSnr();
	    mGsmBitErrorRate = signalStrength.getGsmBitErrorRate();
	    mGsmSignalStrength = signalStrength.getGsmSignalStrength();
	    mIsGsm = signalStrength.isGsm();
	}
	mAW.nativeTriggerPhoneState(5);
    }

}

class BCRecvr extends BroadcastReceiver {
    AndroWish mAW;

    public BCRecvr(AndroWish aw) {
	super();
	mAW = aw;
    }

    @Override
    public void onReceive(Context context, Intent intent) {
	if (intent != null) {
	    mAW.broadcastReceived(intent, getResultCode());
	}
    }

}

/*
 * Helper class to deal with java.util.Locale
 */

class LocaleAPI1 {

    public static String[] get(Locale loc) {
	if (loc == null) {
	    loc = Locale.getDefault();
	}
	int i = 0;
	String result[] = new String[18];
	result[i++] = "country";
	result[i++] = loc.getCountry();
	result[i++] = "display_country";
	result[i++] = loc.getDisplayCountry();
	result[i++] = "display_language";
	result[i++] = loc.getDisplayLanguage();
	result[i++] = "display_name";
	result[i++] = loc.getDisplayName();
	result[i++] = "display_variant";
	result[i++] = loc.getDisplayVariant();
	result[i++] = "iso3_country";
	result[i++] = loc.getISO3Country();
	result[i++] = "iso3_language";
	result[i++] = loc.getISO3Language();
	result[i++] = "language";
	result[i++] = loc.getLanguage();
	result[i++] = "variant";
	result[i++] = loc.getVariant();
	return result;
    }

}

/*
 * Helper class for USB permission dialog
 */

class UsbPermissionTest {
    AndroWish mAW;
    UsbDevice mDev;
    boolean mGranted;
    final String USB_PERMISSION = "tk.tcl.wish.UsbPermission";

    public UsbPermissionTest(AndroWish aw) {
	mAW = aw;
	mDev = null;
	mGranted = false;
    }

    public int ask(UsbDevice dev) {
	mDev = dev;
	mGranted = false;
	final CountDownLatch lock = new CountDownLatch(1);
	final BroadcastReceiver recvr = new BroadcastReceiver() {
	    public void onReceive(Context ctx, Intent i) {
		String a = i.getAction();
		if (a.equals(USB_PERMISSION)) {
		    UsbDevice d = (UsbDevice) i.getParcelableExtra(UsbManager.EXTRA_DEVICE);
		    if (i.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
			if ((d != null) &&
			    (d.getDeviceName().equals(mDev.getDeviceName()))) {
			    mGranted = true;
			}
		    } else {
			Log.e("AndroWish", "permission denied for device " +
			      d.getDeviceName());
		    }
		    lock.countDown();
		}
	    }
	};
	Context ctx = mAW.getContext();
	PendingIntent pi =
	    PendingIntent.getBroadcast(ctx, 0, new Intent(USB_PERMISSION), 0);
	IntentFilter filter = new IntentFilter(USB_PERMISSION);
	ctx.registerReceiver(recvr, filter);
	mAW.mUsbManager.requestPermission(dev, pi);
	try {
	    lock.await();
	} catch (InterruptedException ie) {
	    pi.cancel();
	}
	ctx.unregisterReceiver(recvr);
	return mGranted ? 1 : 0;
    }
}

/*
 * Helper class for text-to-speech events
 */

class TTS_UPL extends android.speech.tts.UtteranceProgressListener
    implements TextToSpeech.OnInitListener {

    AndroWish mAW;

    public TTS_UPL(AndroWish aw) {
	mAW = aw;
    }

    public void onInit(int status) {
	if (status == TextToSpeech.SUCCESS) {
	    mAW.mTTS.setOnUtteranceProgressListener(this);
	    mAW.mTTSAvailable = true;
	}
	mAW.nativeTriggerTTS(0, status);
    }

    public void onShutdown() {
	mAW.mTTSAvailable = false;
	mAW.nativeTriggerTTS(0, -2);
    }

    public void onDone(String id) {
	mAW.nativeTriggerTTS(2, Integer.parseInt(id));
    }

    public void onError(String id) {
	mAW.nativeTriggerTTS(3, Integer.parseInt(id));
    }

    public void onStart(String id) {
	mAW.nativeTriggerTTS(1, Integer.parseInt(id));
    }

}

final class TextFromHtml {

    public static android.text.Spanned text(String text) {
	android.text.Spanned s = null;
	if (android.os.Build.VERSION.SDK_INT >= 24) {
	    Method m = null;
	    try {
		Class c[] = new Class[2];
		c[0] = String.class;
		c[1] = int.class;
		m = android.text.Html.class.getMethod("fromHtml", c);
	    } catch (Exception e) {
	    }
	    if (m != null) {
		try {
		    s = (android.text.Spanned) m.invoke(null, text, 0);
		} catch (Exception e) {
		}
	    }
	}
	if (s == null) {
	    s = android.text.Html.fromHtml(text);
	}
	return s;
    }

}
