
/*
 * Copyright 2010 - 2020 Anywhere Software (www.b4x.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

 package anywheresoftware.b4a.phone;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.concurrent.Callable;

import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.os.Vibrator;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.speech.RecognizerIntent;
import android.telephony.SmsManager;
import android.telephony.TelephonyManager;
import android.text.Html;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import anywheresoftware.b4a.BA;
import anywheresoftware.b4a.IOnActivityResult;
import anywheresoftware.b4a.BA.CheckForReinitialize;
import anywheresoftware.b4a.BA.Events;
import anywheresoftware.b4a.BA.Hide;
import anywheresoftware.b4a.BA.Permissions;
import anywheresoftware.b4a.BA.ShortName;
import anywheresoftware.b4a.BA.Version;
import anywheresoftware.b4a.keywords.Common;
import anywheresoftware.b4a.objects.ActivityWrapper;
import anywheresoftware.b4a.objects.collections.List;

@ShortName("Phone")
@Version(2.61f)
public class Phone {

    /**
     * <b>The Phone library changed and several methods belonging to the Phone object have moved to PhoneId and PhoneVibrate.</b>
     * The reason for this change is to avoid adding unnecessary permissions.
     * The Phone library contains all kinds of features related to the Android phone.
     *<b>Phone</b> object includes information about the device and also other general features.
     *<b>PhoneAccelerometer</b> and PhoneOrientation objects are now replaced with PhoneSensors which support other sensors as well.
     *<b>PhoneEvents</b> allows you to handle all kinds of system events.
     *<b>PhoneId</b> gives access to the the specific phone values.
     *<b>PhoneSms</b> supports sending Sms messages.
     *<b>PhoneVibrate</b> vibrates the phone.
     *<b>SmsMessages</b> together with Sms support fetching messages from the phone database.
     *<b>SmsInterceptor</b> intercepts incoming Sms messages.
     *<b>PhoneIntents</b> and PhoneCalls include several useful intents.
     *<b>Email</b> helps with building an Intent that sends an email.
     *<b>PhoneWakeState</b> allows you to force the screen and power to keep on.
     *<b>Contact</b> and Contacts give access to the stored contacts.
     *<b>CallLog</b> and CallItem give access to the phone calls log.
     *<b>ContentChooser</b> allows the user to choose content from other applications. For example the user can choose an image from the Gallery application.
     *<b>VoiceRecognition</b> converts speech to text.
     *<b>LogCat</b> tracks the internal phone logs.
     *<b>PackageManager</b> allows you to retrieve information about the installed applications.
     */
    public static void LIBRARY_DOC() {
        //
    }
    /**
     * Runs a native shell command. Many commands are inaccessible because of OS security restrictions.
     *Calling Shell will block the calling thread until the other process completes.
     *Command - Command to run.
     *Args - Additional arguments. Can be Null if not needed.
     *StdOut - A StringBuilder that will hold the standard output value. Can be Null if not needed.
     *StdErr - A StringBuilder that will hold the standard error value. Can be Null if not needed.
     *Returns the process exit value.
     *Example:<code>
     *Dim p As Phone
     *Dim sb As StringBuilder
     *sb.Initialize
     *p.Shell("df", Null, sb, Null)
     *Msgbox(sb.ToString, "Free space:")</code>
     */
    public static int Shell(String Command, String[] Args, StringBuilder StdOut, StringBuilder StdErr) throws InterruptedException, IOException {
        Process process;
        if (Args == null || Args.length == 0)
            process = Runtime.getRuntime().exec(Command);
        else {
            String[] a = new String[1 + Args.length];
            a[0] = Command;
            System.arraycopy(Args, 0, a, 1, Args.length);
            process = Runtime.getRuntime().exec(a);
        }
        byte[] buffer = new byte[4096];
        int count;
        InputStream in;
        if (StdOut != null) {
            in = process.getInputStream();
            while ((count = in.read(buffer)) != -1) {
                StdOut.append(new String(buffer, 0, count, "UTF8"));
            }
        }
        if (StdErr != null) {
            in = process.getErrorStream();
            while ((count = in.read(buffer)) != -1) {
                StdErr.append(new String(buffer, 0, count, "UTF8"));
            }
        }
        process.waitFor();
        return process.exitValue();
    }
    /**
     * Asynchronous version of Shell. Should be used with Wait For:
     * <code>
     *Dim p As Phone
     *Wait For (p.ShellAsync("ping", Array As String("-c", "1", "b4x.com"))) Complete (Success As Boolean, ExitValue As Int, StdOut As String, StdErr As String)
     *If Success Then
     *  Log(ExitValue)
     *  Log("Out: " & StdOut)
     *  Log("Err: "&  StdErr)
     *Else
     *  Log("Error: " & LastException)
     *End If</code>
     */
    public static Object ShellAsync (final BA ba, final String Command, final String[] Args) {
        final StringBuilder out = new StringBuilder();
        final StringBuilder err = new StringBuilder();
        final Object sender = new Object();
        BA.runAsync(ba, sender, "complete", new Object[] {false, -1, out.toString(), err.toString()}, new Callable<Object[]>() {

            @Override
            public Object[] call() throws Exception {
                int res = Shell(Command, Args, out, err);
                return new Object[] {true, res, out.toString(), err.toString()};
            }
        });
        return sender;
    }
    /**
     * Returns an internal drawable object.
     *See this <link>page|http://developer.android.com/intl/fr/reference/android/R.drawable.html</link> for a list of available resources.
     *Example:<code>
     *Dim p As Phone
     *Dim bd As BitmapDrawable
     *bd = p.GetResourceDrawable(17301618)
     *Activity.AddMenuItem2("Menu1", "Menu1", bd.Bitmap)</code>
     */
    public Drawable GetResourceDrawable(int ResourceId) {
        return BA.applicationContext.getResources().getDrawable(ResourceId);
    }
    /**
     * Tests whether the phone "airplane mode" is on.
     */
    public static boolean IsAirplaneModeOn(){
        return Settings.System.getInt(BA.applicationContext.getContentResolver(),Settings.System.AIRPLANE_MODE_ON, 0) != 0;
    }
    /**
     * Returns the value of the phone settings based on the given key.
     *The possible keys are listed <link>here|http://developer.android.com/intl/fr/reference/android/provider/Settings.Secure.html</link>.
     *The keys are lower cased.
     *Example:<code>
     *Dim p As Phone
     *Log(p.GetSettings("android_id"))</code>
     */
    public static String GetSettings(String Settings) {
        String s = Secure.getString(BA.applicationContext.getContentResolver(), Settings);
        if (s == null) {
            s = android.provider.Settings.System.getString(BA.applicationContext.getContentResolver(), Settings);
            if (s == null)
                s = "";
        }
        return s;
    }
    /**
     * Sends an intent to all BroadcastReceivers that listen to this type of intents.
     *Example of asking the media scanner to rescan a file:<code>
     *Dim i As Intent
     *i.Initialize("android.intent.action.MEDIA_SCANNER_SCAN_FILE", _
     *  "file://" & File.Combine(File.DirRootExternal, "pictures/1.jpg"))
     *Dim p As Phone
     *p.SendBroadcastIntent(i)</code>
     */
    public static void SendBroadcastIntent(Intent Intent) {
        BA.applicationContext.sendBroadcast(Intent);
    }
    /**
     * Sets the brightness of the current activity. This method cannot be called from a service module.
     *Value - A float between 0 to 1. Set -1 for automatic brightness.
     *Example:<code>
     *Sub Process_Globals
     *  Dim phone1 As Phone
     *End Sub
     *
     *Sub Globals
     *  Dim sb As SeekBar
     *End Sub
     *
     *Sub Activity_Create(FirstTime As Boolean)
     *  sb.Initialize("sb")
     *  sb.Max = 100
     *  sb.Value = 50
     *  Activity.AddView(sb, 10dip, 10dip, 90%x, 30dip)
     *End Sub
     *Sub sb_ValueChanged (Value As Int, UserChanged As Boolean)
     *  phone1.SetScreenBrightness(Max(Value, 5) / 100)
     *End Sub</code>
     */
    public static void SetScreenBrightness(BA ba, float Value) {
        WindowManager.LayoutParams lp = ba.sharedProcessBA.activityBA.get().activity.getWindow().getAttributes();
        lp.screenBrightness = Value;
        ba.sharedProcessBA.activityBA.get().activity.getWindow().setAttributes(lp);
    }
    /**
     * Changes the current activity orientation. This method cannot be called from a service module.
     *Orientation - -1 for unspecified, 0 for landscape and 1 for portrait.
     */
    public static void SetScreenOrientation(BA ba, int Orientation) {
        ba.sharedProcessBA.activityBA.get().activity.setRequestedOrientation(Orientation);
    }

    /**
     * Returns an integer describing the SDK version.
     */
    public static int getSdkVersion() {
        return Build.VERSION.SDK_INT;
    }
    public static String getModel() {
        return Build.MODEL;
    }
    public static String getManufacturer() {
        return Build.MANUFACTURER;
    }
    public static String getProduct() {
        return Build.PRODUCT;
    }

    /**
     * Hides the soft keyboard if it is displayed.
     *Example:<code>
     *Dim p As Phone
     *p.HideKeyboard(Activity)</code>
     */
    public static void HideKeyboard(ActivityWrapper Activity) {
        InputMethodManager imm = (InputMethodManager)BA.applicationContext.getSystemService(Context.INPUT_METHOD_SERVICE);
        imm.hideSoftInputFromWindow(Activity.getObject().getWindowToken(), 0);

    }
    /**
     * Returns the code of the SIM provider.
     *Returns an empty string if it is not available.
     */
    public static String GetSimOperator() {
        TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
        String s = tm.getSimOperator();
        return s == null ? "" : s;
    }
    /**
     * Returns the name of current registered operator.
     *Returns an empty string if it is not available.
     */
    public static String GetNetworkOperatorName() {
        TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
        String s = tm.getNetworkOperatorName();
        return s == null ? "" : s;
    }
    /**
     * Returns true if the device is considered roaming on the current network.
     */
    public static boolean IsNetworkRoaming() {
        TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
        return tm.isNetworkRoaming();
    }
    /**
     * Returns the currently used cellular network type.
     *Possible values: 1xRTT, CDMA, EDGE, EHRPD, EVDO_0, EVDO_A, EVDO_B, GPRS, HSDPA,
     *HSPA, HSPAP, HSUPA, IDEN, LTE, UMTS, UNKNOWN.
     */
    public static String GetNetworkType() {
        TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
        int i = tm.getNetworkType();
        switch (i) {
        case 7:
            return "1xRTT";
        case 4:
            return "CDMA";
        case 14:
            return "EHRPD";
        case 2:
            return "EDGE";
        case 5:
            return "EVDO_0";
        case 6:
            return "EVDO_A";
        case 12:
            return "EVDO_B";
        case 1:
            return "GPRS";
        case 8:
            return "HSDPA";
        case 10:
            return "HSPA";
        case 15:
            return "HSPAP";
        case 9:
            return "HSUPA";
        case 11:
            return "IDEN";
        case 13:
            return "LTE";
        case 3:
            return "UMTS";
        default:
            return "UNKNOWN";
        }
    }
    /**
     * Returns the phone radio type. Possible values: CDMA, GSM, NONE.
     */
    public static String GetPhoneType() {
        TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
        int i = tm.getPhoneType();
        switch (i) {
        case 2:
            return "CDMA";
        case 1:
            return "GSM";
        default:
            return "NONE";
        }
    }
    /**
     * Returns the current cellular data connection state.
     *Possible values: DISCONNECTED, CONNECTING, CONNECTED, SUSPENDED.
     */
    public static String GetDataState() {
        TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
        int i = tm.getDataState();
        switch (i) {
        case 2:
            return "CONNECTED";
        case 0:
            return "DISCONNECTED";
        case 3:
            return "SUSPENDED";
        case 1:
            return "CONNECTING";
        }
        return "DISCONNECTED";
    }
    /**
     * Phone ring channel.
     */
    public static final int VOLUME_RING = AudioManager.STREAM_RING;
    /**
     * Alarms channel.
     */
    public static final int VOLUME_ALARM = AudioManager.STREAM_ALARM;
    /**
     * Music channel.
     */
    public static final int VOLUME_MUSIC = AudioManager.STREAM_MUSIC;
    /**
     * Notifications channel.
     */
    public static final int VOLUME_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
    /**
     * System sounds channel.
     */
    public static final int VOLUME_SYSTEM = AudioManager.STREAM_SYSTEM;
    /**
     * Voice calls channel.
     */
    public static final int VOLUME_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
    /**
     * Phone ringer will be silent and it will not vibrate.
     */
    public static final int RINGER_SILENT = AudioManager.RINGER_MODE_SILENT;
    /**
     * Normal phone ringer mode.
     */
    public static final int RINGER_NORMAL = AudioManager.RINGER_MODE_NORMAL;
    /**
     * Phone ringer will vibrate and silent.
     */
    public static final int RINGER_VIBRATE = AudioManager.RINGER_MODE_VIBRATE;
    /**
     * Gets the maximum volume index (value) for the given channel.
     *Channel - One of the VOLUME constants.
     */
    public static int GetMaxVolume(int Channel) {
        return getAudioManager().getStreamMaxVolume(Channel);
    }
    /**
     * Sets the volume of the specified channel.
     *Channel - One of the VOLUME constants.
     *VolumeIndex - The volume index. GetMaxVolume can be used to find the largest possible value.
     *ShowUI - Whether to show the volume UI windows.
     *Starting from Android 7+ this method will throw an exception if the user set the Do Not Disturb mode,
     *unless your app has requested a special permission with NOTIFICATION_POLICY_ACCESS_SETTINGS.
     *Example:<code>
     *Dim p As Phone
     *p.SetVolume(p.VOLUME_MUSIC, 3, True)</code>
     */
    public static void SetVolume(int Channel, int VolumeIndex, boolean ShowUI) {
        getAudioManager().setStreamVolume(Channel, VolumeIndex, ShowUI ? AudioManager.FLAG_SHOW_UI : 0);
    }
    /**
     * Returns the volume of the specified channel.
     *Channel - One of the VOLUME constants.
     */
    public static int GetVolume(int Channel) {
        return getAudioManager().getStreamVolume(Channel);
    }
    /**
     * Mutes or unmutes the given channel.
     *Channel - One of the VOLUME constants.
     *Mute - Whether to mute or unmute the channel.
     *Starting from Android 7+ this method will throw an exception if the user set the Do Not Disturb mode,
     *unless your app has requested a special permission with NOTIFICATION_POLICY_ACCESS_SETTINGS.
     */
    public static void SetMute(int Channel, boolean Mute) {
        getAudioManager().setStreamMute(Channel, Mute);
    }
    /**
     * Sets the phone ringer mode.
     *Mode - One of the RINGER constants.
     *Starting from Android 7+ this method will throw an exception if the user set the Do Not Disturb mode,
     *unless your app has requested a special permission with NOTIFICATION_POLICY_ACCESS_SETTINGS.
     *Example:<code>
     *Dim p As Phone
     *p.SetRingerMode(p.RINGER_VIBRATE)</code>
     */
    public static void SetRingerMode(int Mode) {
        getAudioManager().setRingerMode(Mode);
    }

    /**
     * Returns the phone ringer mode.
     *Value will be one of the RINGER constants.
     */
    public static int GetRingerMode() {
        return getAudioManager().getRingerMode();
    }
    private static AudioManager getAudioManager() {
        return (AudioManager)BA.applicationContext.getSystemService(Context.AUDIO_SERVICE);
    }
    @ShortName("PhoneId")
    @Permissions(values={"android.permission.READ_PHONE_STATE"})
    public static class PhoneId {
        /**
         * Returns a unique device Id. Returns an empty string if the device Id is not available (usually on wifi only devices).
         *<b>This method will not work on Android 10+ devices. It will throw an exception in some cases. Do not use.</b>
         */
        public static String GetDeviceId() {
            TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
            String s = tm.getDeviceId();
            return s == null ? "" : s;
        }
        /**
         * Returns the phone number string for line 1 as configured in the SIM card.
         *Returns an empty string if it is not available.
         */
        public static String GetLine1Number() {
            TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
            String s = tm.getLine1Number();
            return s == null ? "" : s;
        }
        /**
         * Returns the unique subscriber id.
         *Returns an empty string if it is not available.
         */
        public static String GetSubscriberId() {
            TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
            String s = tm.getSubscriberId();
            return s == null ? "" : s;
        }
        /**
         * Returns the serial number of the SIM card.
         *Returns an empty string if it is not available.
         */
        public static String GetSimSerialNumber() {
            TelephonyManager tm = (TelephonyManager) BA.applicationContext.getSystemService(Context.TELEPHONY_SERVICE);
            String s = tm.getSimSerialNumber();
            return s == null ? "" : s;
        }
    }
    @ShortName("PhoneVibrate")
    @Permissions(values={"android.permission.VIBRATE"})
    public static class PhoneVibrate {
        /**
         * Vibrates the phone for the specified duration.
         */
        public static void Vibrate(BA ba, long TimeMs) {
            Vibrator v = (Vibrator)ba.context.getSystemService(Context.VIBRATOR_SERVICE);
            v.vibrate(TimeMs);
        }
    }
    /**
     * The PhoneWakeState object allows you to prevent the device from turning off the screen.
     *Once you call KeepAlive the phone screen will stay on till you call ReleaseKeepAlive.
     *It is important to eventually release it.
     *A recommended usage is to call KeepAlive in Activity_Resume and call ReleaseKeepAlive in Activity_Pause.
     *Note that the user can still turn off the screen by pressing on the power button.
     *Calling PartialLock will prevent the CPU from going to sleep even if the user presses on the power button.
     *It will not however affect the screen.
     */
    @ShortName("PhoneWakeState")
    @Permissions(values={"android.permission.WAKE_LOCK"})
    public static class PhoneWakeState {
        private static PowerManager.WakeLock wakeLock;
        private static PowerManager.WakeLock partialLock;
        /**
         * Prevents the device from going to sleep.
         *Call ReleaseKeepAlive to release the power lock.
         *BrightScreen - Whether to keep the screen bright or dimmed.
         */
        public static void KeepAlive(BA ba, boolean BrightScreen) {
            if (wakeLock != null && wakeLock.isHeld()) {
                Common.Log("WakeLock already held.");
                return;
            }
            PowerManager pm = (PowerManager)ba.context.getSystemService(
                    Context.POWER_SERVICE);
            wakeLock = pm.newWakeLock((BrightScreen ? PowerManager.SCREEN_BRIGHT_WAKE_LOCK :
                PowerManager.SCREEN_DIM_WAKE_LOCK )
                | PowerManager.ACQUIRE_CAUSES_WAKEUP,
            "B4A");
            wakeLock.acquire();
        }
        /**
         * Acquires a partial lock. This will prevent the CPU from going to sleep, even if the user presses on the power button.
         *Make sure to call ReleasePartialLock eventually to release this lock.
         */
        public static void PartialLock(BA ba) {
            if (partialLock != null && partialLock.isHeld()) {
                Common.Log("Partial wakeLock already held.");
                return;
            }
            PowerManager pm = (PowerManager)ba.context.getSystemService(
                    Context.POWER_SERVICE);
            partialLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "B4A-Partial");
            partialLock.acquire();
        }
        /**
         * Releases the power lock and allows the device to go to sleep.
         */
        public static void ReleaseKeepAlive() {
            if (wakeLock == null || wakeLock.isHeld() == false) {
                Common.Log("No wakelock.");
                return;
            }
            wakeLock.release();
        }
        /**
         * Releases a partial lock that was previously acquired by calling PartialLock.
         */
        public static void ReleasePartialLock() {
            if (partialLock == null || partialLock.isHeld() == false) {
                Common.Log("No partial wakelock.");
                return;
            }
            partialLock.release();
        }
    }

    /**
     * This object contains methods that create intents objects. An intent does nothing until you call StartActivity with the intent.
     *Calling StartActivity sends the intent to the OS.
     */
    @ShortName("PhoneIntents")
    public static class PhoneIntents {
        /**
         * Creates an intent that will open the specified URI.
         *Example:<code>
         *StartActivity (PhoneIntents.OpenBrowser("http://www.google.com"))</code>
         */
        public static Intent OpenBrowser(String Uri) {
            Intent i = new Intent(Intent.ACTION_VIEW, android.net.Uri.parse(Uri));
            return i;
        }
        /**
         * Creates an intent that will start playing the given audio file with the default player.
         *This method cannot work with internal files.
         */
        public static Intent PlayAudio(String Dir, String File) {
            Uri uri;
            if (Dir.equals(anywheresoftware.b4a.objects.streams.File.ContentDir))
                uri = Uri.parse(File);
            else
                uri = Uri.parse("file://" + new java.io.File(Dir, File).toString());
            Intent it = new Intent(Intent.ACTION_VIEW);
            it.setDataAndType(uri, "audio/*");
            return it;
        }
        /**
         * Creates an intent that will start playing the given video file with the default player.
         *This method cannot work with internal files.
         */
        public static Intent PlayVideo(String Dir, String File) {
            Uri uri;
            if (Dir.equals(anywheresoftware.b4a.objects.streams.File.ContentDir))
                uri = Uri.parse(File);
            else
                uri = Uri.parse("file://" + new java.io.File(Dir, File).toString());
            Intent it = new Intent(Intent.ACTION_VIEW);
            it.setDataAndType(uri, "video/*");
            return it;
        }

    }
    /**
     * This object creates an intent that launches the phone application.
     *The reason that it is not part of the PhoneIntents library is that it requires an additional permission.
     */
    @ShortName("PhoneCalls")
    @Permissions(values={"android.permission.CALL_PHONE"})
    public static class PhoneCalls {
        /**
         * Creates an intent that will call a phone number.
         *Example:<code>
         *Dim p As PhoneCalls
         *StartActivity(p.Call("1234567890"))</code>
         */
        public static Intent Call(String PhoneNumber) {
            Uri uri = Uri.parse("tel:" + PhoneNumber);
            Intent it = new Intent(Intent.ACTION_CALL, uri);
            return it;
        }
    }

    @ShortName("PhoneSms")
    @Permissions(values={"android.permission.SEND_SMS"})
    public static class PhoneSms {
        /**
         * Sends an Sms message. Note that this method actually sends the message (unlike most other methods that
         *create an intent object).
         *You can use PhoneEvents to handle the SmsSentStatus and SmsDelivered events.
         *This method is equivalent to calling Send2(PhoneNumber, Text, True, True).
         */
        public static void Send(String PhoneNumber, String Text) {
            Send2(PhoneNumber, Text, true, true);
        }
        /**
         * Sends an Sms message. Note that this method actually sends the message (unlike most other methods that
         *create an intent object).
         *You can use PhoneEvents to handle the SmsSentStatus and SmsDelivered events.
         *ReceiveSentNotification - If true then the SmsSentStatus event (PhoneEvents) will be raised when the message is sent.
         *ReceiveDeliveredNotification - If true then the SmsDelivered event (PhoneEvents) will be raised when the message is delivered.
         *Note that the two above notifications might incur an additional payment.
         */
        public static void Send2(String PhoneNumber, String Text, boolean ReceiveSentNotification, boolean ReceiveDeliveredNotification) {
            SmsManager sm = SmsManager.getDefault();
            Intent i1 = new Intent("b4a.smssent");
            i1.putExtra("phone", PhoneNumber);
            int flags = PendingIntent.FLAG_UPDATE_CURRENT;
            if (Build.VERSION.SDK_INT >= 31)
                flags |= 33554432; //FLAG_MUTABLE
            PendingIntent pi = ReceiveSentNotification ? PendingIntent.getBroadcast(BA.applicationContext, 0,i1, flags) : null;
            Intent i2 = new Intent("b4a.smsdelivered");
            i2.putExtra("phone", PhoneNumber);
            PendingIntent pi2 = ReceiveDeliveredNotification ? PendingIntent.getBroadcast(BA.applicationContext, 0,i2, flags) : null;
            sm.sendTextMessage(PhoneNumber, null, Text, pi, pi2);
        }


    }

    /**
     * Using an Email object you can create an intent that holds a complete email message.
     *You can then launch the email application by calling StartActivity. Note that the email will not be sent automatically. The user will need to press on the send button.
     *Example:<code>
     *Dim Message As Email
     *Message.To.Add("SomeEmail@example.com")
     *Message.Attachments.Add(File.Combine(File.DirRootExternal, "SomeFile.txt"))
     *StartActivity(Message.GetIntent)</code>
     */
    @ShortName("Email")
    public static class Email {
        public String Subject = "";
        public String Body = "";
        public anywheresoftware.b4a.objects.collections.List To = new anywheresoftware.b4a.objects.collections.List();
        public anywheresoftware.b4a.objects.collections.List CC = new anywheresoftware.b4a.objects.collections.List();
        public anywheresoftware.b4a.objects.collections.List BCC = new anywheresoftware.b4a.objects.collections.List();
        public anywheresoftware.b4a.objects.collections.List Attachments = new anywheresoftware.b4a.objects.collections.List();
        public Email() {
            To.Initialize();
            CC.Initialize();
            BCC.Initialize();
            Attachments.Initialize();
        }
        /**
         * Returns the Intent that should be sent with StartActivity.
         *The email message will be a Html message.
         */
        public Intent GetHtmlIntent() {
            return getIntent(true);
        }
        private Intent getIntent(boolean html) {
            Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE);
            emailIntent.setType(html ? "text/html" : "text/plain");
            emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
                    To.getObject().toArray(new String[0]));
            emailIntent.putExtra(android.content.Intent.EXTRA_CC,
                    CC.getObject().toArray(new String[0]));
            emailIntent.putExtra(android.content.Intent.EXTRA_BCC,
                    BCC.getObject().toArray(new String[0]));
            emailIntent.putExtra(Intent.EXTRA_SUBJECT, Subject);
            emailIntent.putExtra(Intent.EXTRA_TEXT, html ? Html.fromHtml(Body) : Body);
            ArrayList<Uri> uris = new ArrayList<Uri>();
            for (Object file : Attachments.getObject())
            {
                if (file instanceof Uri) {
                    uris.add((Uri)file);
                }
                else {
                    File fileIn = new File((String)file);
                    Uri u = Uri.fromFile(fileIn);
                    uris.add(u);
                }
            }
            if (uris.size() == 1) {
                emailIntent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
                emailIntent.setAction(Intent.ACTION_SEND);
            }
            else if (uris.size() > 1) {
                emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
            }
            return emailIntent;
        }
        /**
         * Returns the Intent that should be sent with StartActivity.
         */
        public Intent GetIntent() {
            return getIntent(false);
        }
    }
    /**
     * LogCat allows you to read the internal phone logs.
     *Refer to the <link>LogCat documentation|http://developer.android.com/intl/fr/guide/developing/tools/adb.html#logcat</link> for more information about the optional arguments.
     *The LogCatData event is raised when there is new data available.
     *You should use BytesToString to convert the raw bytes to string.
     *<b>Note that the LogCatData event is raised in a different thread.</b> This means that you can only log the messages.
     *You can also use the Threading library to delegate the data to the main thread.
     */
    @ShortName("LogCat")
    @Events(values={"LogCatData (Buffer() As Byte, Length As Int)"})
    public static class LogCat {
        private static Thread logcatReader;
        private static volatile boolean logcatWorking;
        private static volatile InputStream logcatStream;
        private static Process lc;
        /**
         * Starts tracking the logs.
         *Args - Optional arguments passed to the internal LogCat command.
         *EventName - The Sub that will handle the LogCatData event.
         */
        public static void LogCatStart(final BA ba, String[] Args, String EventName) throws InterruptedException, IOException {
            LogCatStop();
            if (logcatReader != null) {
                int wait = 10;
                while (logcatReader.isAlive() && wait-- > 0) {
                    Thread.sleep(50);
                    logcatReader.interrupt();
                }
            }
            final String[] a = new String[1 + Args.length];
            a[0] = "/system/bin/logcat";
            System.arraycopy(Args, 0, a, 1, Args.length);
            final String ev = EventName.toLowerCase(BA.cul) + "_logcatdata";
            logcatReader = new Thread(new Runnable() {

                @Override
                public void run() {
                    try {
                        lc = Runtime.getRuntime().exec(a);
                        logcatStream = lc.getInputStream();
                        InputStream in = logcatStream;
                        logcatWorking = true;
                        byte[] buffer = new byte[4092];
                        while (logcatWorking) {
                            int count = in.read(buffer);
                            Thread.sleep(100);
                            while (count > 0 && in.available() > 0 && count < buffer.length) {
                                count += in.read(buffer, count, buffer.length - count);
                                Thread.sleep(100);
                            }
                            if (count == -1) {
                                logcatWorking = false;
                                break;
                            }
                            ba.raiseEvent(null, ev, buffer, count);
                        }
                        lc.destroy();
                    } catch (Exception e) {
                        if (lc != null)
                            lc.destroy();
                    }

                }

            });
            logcatReader.setDaemon(true);
            logcatReader.start();
        }
        /**
         * Stops tracking the logs.
         */
        public static void LogCatStop() throws IOException {
            logcatWorking = false;
            if (logcatStream != null)
                logcatStream.close();
            if (lc != null)
                lc.destroy();
        }
    }
    /**
     * The ContentChooser object allows the user to select a specific type of content using other installed applications.
     *For example the user can use the internal Gallery application to select an image.
     *If the user has installed a file manager then the ContentChooser can be used to select general files.
     *This object should usually be declared as a process global object.
     *After initializing the object you can let the user select content by calling Show with the required MIME types.
     *The Result event will be raised with a Success flag and with the content Dir and FileName.
     *Note that these values may point to resources other than regular files. Still you can pass them to methods that expect Dir and FileName.
     *Only content types that can be opened with an InputStream are supported.
     */
    @ShortName("ContentChooser")
    @Events(values={"Result (Success As Boolean, Dir As String, FileName As String)"})
    public static class ContentChooser implements CheckForReinitialize{
        private String eventName;
        private IOnActivityResult ion;
        /**
         * Initializes the object and sets the Sub that will handle the Result event.
         *Example:<code>
         *Dim CC As ContentChooser
         *CC.Initialize("CC")</code>
         */
        public void Initialize(String EventName) {
            eventName = EventName.toLowerCase(BA.cul);
        }
        @Override
        public boolean IsInitialized() {
            return eventName != null;
        }
        /**
         * Sends the request to the system. If there are more than one applications that support the given Mime then a list with the applications will be displayed to the user.
         *The Result event will be raised after the user chose an item or canceled the dialog.
         *Mime - The content MIME type.
         *Title - The title of the chooser dialog (when there is more than one application).
         *Examples:<code>
         *CC.Show("image/*", "Choose image")
         *CC.Show("audio/*", "Choose audio file")</code>
         */
        public void Show(final BA ba, String Mime, String Title) {
            if (eventName == null)
                throw new RuntimeException("ContentChooser not initialized.");
            Intent in = new Intent(Intent.ACTION_GET_CONTENT);
            in.setType(Mime);
            in.addCategory(Intent.CATEGORY_OPENABLE);
            in = Intent.createChooser(in, Title);
            ion = new IOnActivityResult() {
                @Override
                public void ResultArrived(int resultCode, Intent intent) {
                    String Dir = null, File = null;
                    if (resultCode == Activity.RESULT_OK && intent != null && intent.getData() != null) {
                        try {
                            Uri uri = intent.getData();
                            String scheme = uri.getScheme();
                            if (ContentResolver.SCHEME_FILE.equals(scheme)) {
                                Dir = "";
                                File = uri.getPath();
                            }
                            else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
                                Dir = anywheresoftware.b4a.objects.streams.File.ContentDir;
                                File = uri.toString();
                            }

                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                    ion = null;
                    if (Dir != null && File != null) {
                        ba.raiseEvent(ContentChooser.this, eventName + "_result", true, Dir, File);
                    }
                    else {
                        ba.raiseEvent(ContentChooser.this, eventName + "_result", false, "", "");
                    }
                }
            };
            ba.startActivityForResult(ion, in);
        }

        public void ShowMultipleMIME(final BA ba, String[] MimeTypes, String Title) {
            if (eventName == null)
                throw new RuntimeException("ContentChooser not initialized.");
            Intent in = new Intent(Intent.ACTION_GET_CONTENT);
            in.setType("*/*");
            in.putExtra(Intent.EXTRA_MIME_TYPES, MimeTypes);
            in.addCategory(Intent.CATEGORY_OPENABLE);
            in = Intent.createChooser(in, Title);
            ion = new IOnActivityResult() {
                @Override
                public void ResultArrived(int resultCode, Intent intent) {
                    String Dir = null, File = null;
                    if (resultCode == Activity.RESULT_OK && intent != null && intent.getData() != null) {
                        try {
                            Uri uri = intent.getData();
                            String scheme = uri.getScheme();
                            if (ContentResolver.SCHEME_FILE.equals(scheme)) {
                                Dir = "";
                                File = uri.getPath();
                            }
                            else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) {
                                Dir = anywheresoftware.b4a.objects.streams.File.ContentDir;
                                File = uri.toString();
                            }

                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                    ion = null;
                    if (Dir != null && File != null) {
                        ba.raiseEvent(ContentChooser.this, eventName + "_result", true, Dir, File);
                    }
                    else {
                        ba.raiseEvent(ContentChooser.this, eventName + "_result", false, "", "");
                    }
                }
            };
            ba.startActivityForResult(ion, in);
        }

    }
    /**
     * Most Android devices support voice recognition (speech to text). Usually the service works by sending the audio stream to some external server
     *which analyzes the stream and returns the possible results.
     *Working with this object is quite simple.
     *You should declare a VoiceRecognition object as a process global object and initialize it in Activity_Create when FirstTime is True.
     *Later when you call Listen a dialog will be displayed, asking the user to speak. The Result event will be raised with a Success flag and a list with the possible results (usually one result).
     */
    @ShortName("VoiceRecognition")
    @Events(values={"Result (Success As Boolean, Texts As List)"})
    public static class VoiceRecognition {
        private String eventName;
        private IOnActivityResult ion;
        private String prompt;
        private String language;
        /**
         * Initializes the object and sets the Sub that will catch the Result event.
         *Example:<code>
         *Dim VR As VoiceRecognition
         *VR.Initialize("VR")</code>
         */
        public void Initialize(String EventName) {
            eventName = EventName.toLowerCase(BA.cul);
        }
        /**
         * Sets the prompt that is displayed in the "Speak now" dialog in addition to the "Speak now" message.
         */
        public void setPrompt(String value) {
            prompt = value;
        }
        /**
         * Sets the language used. By default the device default language is used.
         *Example:<code>
         *VR.Language = "en"</code>
         */
        public void setLanguage(String value) {
            language = value;
        }
        /**
         * Tests whether voice recognition is supported on this device.
         */
        public boolean IsSupported() {
            PackageManager pm = BA.applicationContext.getPackageManager();
            return pm.queryIntentActivities(
                    new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0).size() > 0;
        }
        /**
         * Starts listening. The Result event will be raised when the result arrives.
         */
        public void Listen(final BA ba) {
            if (eventName == null)
                throw new RuntimeException("VoiceRecognition was not initialized.");
            Intent i = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);

            Listen2(ba, i);
        }
        /**
         * Similar to Listen. Allows you to build the intent yourself.
         */
        public void Listen2(final BA ba, Intent RecognizeIntent) {
            if (prompt != null && prompt.length() > 0)
                RecognizeIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
            if (language != null && language.length() > 0)
                RecognizeIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
            ion = new IOnActivityResult() {

                @SuppressWarnings("unchecked")
                @Override
                public void ResultArrived(int resultCode, Intent intent) {
                    List list = new List();
                    if (resultCode == Activity.RESULT_OK) {
                        ArrayList<String> t = intent.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
                        if (t.size() > 0) {
                            list.setObject((java.util.ArrayList)t);
                        }
                    }
                    ion = null;
                    ba.raiseEvent(VoiceRecognition.this, eventName + "_result", list.IsInitialized(), list);

                }
            };
            ba.startActivityForResult(ion, RecognizeIntent);
        }
    }
    /**
     * This object gives access to the internal orientation sensors.
     *See the <link>Orientation and accelerometers example|http://www.basic4ppc.com/forum/basic4android-getting-started-tutorials/6647-orientation-accelerometer.html</link>.
     *This object should be declared as a process global object.
     */
    @ShortName("PhoneOrientation")
    @Events(values={"OrientationChanged (Azimuth As Float, Pitch As Float, Roll As Float)"})
    public static class PhoneOrientation {
        private SensorEventListener listener;
        /**
         * Starts listening for OrientationChanged events.
         */
        public void StartListening(final BA ba, String EventName) {
            SensorManager sm = (SensorManager) ba.context.getSystemService(Context.SENSOR_SERVICE);
            final String s = EventName.toLowerCase(BA.cul) + "_orientationchanged";
            listener = new SensorEventListener() {

                @Override
                public void onAccuracyChanged(Sensor sensor, int accuracy) {
                    //
                }

                @Override
                public void onSensorChanged(SensorEvent event) {
                    ba.raiseEvent(this, s, event.values[0], event.values[1], event.values[2]);
                }

            };
            sm.registerListener(listener, sm.getDefaultSensor(Sensor.TYPE_ORIENTATION), SensorManager.SENSOR_DELAY_NORMAL);
        }
        /**
         * Stops listening for events.
         */
        public void StopListening(BA ba) {
            if (listener != null) {
                SensorManager sm = (SensorManager) ba.context.getSystemService(Context.SENSOR_SERVICE);
                sm.unregisterListener(listener);
            }
        }
    }
    /**
     * This object gives access to the internal accelerometers sensors.
     *See the <link>Orientation and accelerometers example|http://www.basic4ppc.com/forum/basic4android-getting-started-tutorials/6647-orientation-accelerometer.html</link>.
     *This object should be declared as a process global object.
     */
    @ShortName("PhoneAccelerometer")
    @Events(values={"AccelerometerChanged (X As Float, Y As Float, Z As Float)"})
    public static class PhoneAccelerometer {
        private SensorEventListener listener;
        /**
         * Starts listening for AccelerometerChanged events.
         */
        public void StartListening(final BA ba, String EventName) {
            SensorManager sm = (SensorManager) ba.context.getSystemService(Context.SENSOR_SERVICE);
            final String s = EventName.toLowerCase(BA.cul) + "_accelerometerchanged";
            listener = new SensorEventListener() {

                @Override
                public void onAccuracyChanged(Sensor sensor, int accuracy) {
                    //
                }

                @Override
                public void onSensorChanged(SensorEvent event) {
                    ba.raiseEvent(this, s, event.values[0], event.values[1], event.values[2]);
                }

            };
            sm.registerListener(listener, sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_NORMAL);
        }
        /**
         * Stops listening for events.
         */
        public void StopListening(BA ba) {
            if (listener != null) {
                SensorManager sm = (SensorManager) ba.context.getSystemService(Context.SENSOR_SERVICE);
                sm.unregisterListener(listener);
            }
        }
    }
    /**
     * The PhoneSensors object allows you to listen for changes in one of the device sensors.
     *See the <link>Sensors example|http://www.basic4ppc.com/forum/basic4android-getting-started-tutorials/6647-orientation-accelerometer.html</link>.
     *Most devices do not support all sensors. The StartListening method returns False if the sensor is not supported.
     *After initializing the object and calling StartListening, the SensorChanged event will be raised each time the sensor value changes.
     *The value is passed as an array of Floats. Some sensors pass a single value and some pass three values.
     */
    @ShortName("PhoneSensors")
    @Events(values={"SensorChanged (Values() As Float)"})
    public static class PhoneSensors{
        /**
         * Single value - Ambient light level measured in SI lux units.
         */
        public static int TYPE_LIGHT = Sensor.TYPE_LIGHT;
        /**
         * Three values - Ambient magnetic field measured in micro-Tesla for the X, Y and Z axis.
         */
        public static int TYPE_MAGNETIC_FIELD = Sensor.TYPE_MAGNETIC_FIELD;
        /**
         * Single value - Atmospheric pressure.
         */
        public static int TYPE_PRESSURE = Sensor.TYPE_PRESSURE;
        /**
         * Single value - Proximity measured in centimeters. Most devices will return only two possible values representing "near" and "far".
         *"far" should match MaxRange and "near" should be a value smaller than MaxRange.
         */
        public static int TYPE_PROXIMITY = Sensor.TYPE_PROXIMITY;
        /**
         * Single value - Ambient temperature.
         */
        public static int TYPE_TEMPERATURE = Sensor.TYPE_TEMPERATURE;
        /**
         * Three values - Angular velocity measured in Radians / Second around each of the three axis.
         */
        public static int TYPE_GYROSCOPE = Sensor.TYPE_GYROSCOPE;
        /**
         * Three values - Acceleration measured in Meters / Second ^ 2 for each axis (X, Y and Z).
         */
        public static int TYPE_ACCELEROMETER = Sensor.TYPE_ACCELEROMETER;
        /**
         * Three values - Orientation measured in degrees for azimuth, pitch and roll.
         */
        public static int TYPE_ORIENTATION = Sensor.TYPE_ORIENTATION;
        private int currentType;
        private int sensorDelay;
        private SensorEventListener listener;
        /**
         * Initializes the object and sets the sensor type (one of the TYPE_ constants).
         */
        public void Initialize(int SensorType) {
            Initialize2(SensorType, 3);
        }
        /**
         * Initializes the object and sets the sensor type and sensor events rate.
         *SensorType - One of the TYPE_ constants.
         *SensorDelay - A value between 0 (fastest rate) to 3 (slowest rate). This is only a hint to the system.
         */
        public void Initialize2(int SensorType, int SensorDelay) {
            this.currentType = SensorType;
            this.sensorDelay = SensorDelay;
        }
        @Hide
        public SensorEvent sensorEvent;
        /**
         * Starts listening for sensor events.
         *Returns True if the sensor is supported.
         *Usually you will want to start listening in Sub Activity_Resume and stop listening in Sub Activity_Pause.
         */
        public boolean StartListening(final BA ba, String EventName) {
            SensorManager sm = (SensorManager) BA.applicationContext.getSystemService(Context.SENSOR_SERVICE);
            final String s = EventName.toLowerCase(BA.cul) + "_sensorchanged";
            listener = new SensorEventListener() {

                @Override
                public void onAccuracyChanged(Sensor sensor, int accuracy) {
                    //
                }

                @Override
                public void onSensorChanged(SensorEvent event) {
                    sensorEvent = event;
                    ba.raiseEvent(PhoneSensors.this, s, event.values);
                }

            };
            return sm.registerListener(listener, sm.getDefaultSensor(currentType), sensorDelay);
        }
        /**
         * Returns the event accuracy, between 0 (unreliable) to 3 (maximum accuracy).
         */
        public int getAccuracy() {
            if (sensorEvent == null)
                return SensorManager.SENSOR_STATUS_UNRELIABLE;
            return sensorEvent.accuracy;
        }
        /**
         * Returns the event timestamp measured in nanoseconds. Note that the actual value has different meanings on different devices.
         *Thus it should only be used to compare between sensor events.
         */
        public long getTimestamp() {
            if (sensorEvent == null)
                return 0;
            return sensorEvent.timestamp;
        }
        /**
         * Stops listening for events.
         */
        public void StopListening(BA ba) {
            if (listener != null) {
                SensorManager sm = (SensorManager) BA.applicationContext.getSystemService(Context.SENSOR_SERVICE);
                sm.unregisterListener(listener);
            }
        }
        /**
         * Returns the maximum value for this sensor.
         *Returns -1 if this sensor is not supported.
         */
        public float getMaxValue() {
            java.util.List<Sensor> l = ((SensorManager) BA.applicationContext.getSystemService(Context.SENSOR_SERVICE)).getSensorList(currentType);
            if (l == null || l.size() == 0)
                return -1;
            return l.get(0).getMaximumRange();
        }


    }

}
