Android Tutorial B4x / PHP compatibility thread

KMatle

Expert
Licensed User
A lot of developers like me use php as the backend component on a server to communicate with B4x apps. My intention is to create a thread with some "How to's" and best practices. Please feel free to expand it. It's not the place to post questions (please open a new thread then). The examples posted here can be used in all B4x products (maybe with a slight change).

All examples are simplified! Of course I use secured scripts (password hashing, encryption and some other methods)

If you are new to PHP, see this nice website. It has thousands of PHP examples with and without MySQL: https://www.w3schools.com/php/default.asp

1. Test environment

I use XAMPP https://www.apachefriends.org/de/index.html on my laptop I use developing my apps. Just install the version you need. It comes with a full package (Apache Server and MySQL). Just set the root password and start Apache and MySQL from the console or chose to run it as a service (red crosses on the left side). Nothing else to do here. Just run it. It listens to the standard ports. If it does not start, maybe another program is using port 80.

I have installed it a lot of times without any problems. It comes as a "ready to run" package. Please start it as an ADMIN and check your firewall settings to accept incoming connections!

xampp.JPG


Xampp installs directly on the boot drive (usually C:/). Put the PHP scripts in the htdocs folder and you are ready to go :)

2. Prod environment

After testing you can copy the php scripts to your prod server (mostly via FTP). Please ensure you change e.g. Database names, etc. (if you use MySQL or similar). Very easy. See your hoster's documentation how to access (paths) the scripts

3. B4x: Send data to a php script very easy

I use OKHttpUtils to call the scripts. Just use a variable to set the server's ip/address like

B4X:
'servername="https://example.com" 'prod
'Servername="http://192.168.178.23" 'test -> where Xampp runs
After some years of experience I use the POST-method to send data to my php scripts. All the data is put in a list containing maps which I convert into a nice JSON string.

- It's easy to handle ("just a string")
. It's very dynamic and can be expanded by just adding more maps
- One can encrypt/decrypt it in one step
- Usable for all data (even files/images, etc.)

B4X:
JsonList.Initialize
JsonMap.Initialize
JsonMap.put("uname", "Klaus")
JsonMap.put("upw", "test")
JsonMap.put("umail", "kk@kk.de")

JsonList.add(JsonZeileMap)
   
    Dim JSONGenerator As JSONGenerator
    JSONGenerator.Initialize2(JsonList)
   
    Dim JSONstring As String
    JSONstring = JSONGenerator.ToString
    Log(JSONstring)
   
    Dim LoginJob As HttpJob
    LoginJob.Initialize("LoginJob", Me)
    LoginJob.PostString(Servername & "/login/login.php", JSONstring)
So right here we are sending just a string ("JSONstring)

On the PHP side a list with maps is just an array containing arrays:

B4X:
$json = file_get_contents("php://input"); 'get all the data sent by the app and put it in a string

    $jsall = array(); 'the list we send
    $jsone = array(); 'one map inside the list (we could send a list containing n maps)
    $jsall=json_decode($json, true); 'decode the json string we have sent. 

    $jsone=$jsall[0]; 'get the first map (index=0)
Steps here:

- get the posted data (get_contents...)
- decode it and put it into an array ($jsall) = list
- get the first array ($jsone) = out map

You could send more maps just by adding the to the list and use a loop in php to process all the maps. In my example I only send ONE map and I get it by the index 0.

To access the data from the map (array) just use the map's key (like we know it from B4x):

B4X:
$umail=$jsone["umail"];
        $upw=$jsone["upw"];
        $uname=$jsone["uname"];
4. Limitations

On the serverside there are size limitations (set by the admin). 1-5 MB shouldn't be a problem. Just test it

5. No barriers

You can send even binary data like files, pics, etc. (Encode it to Base64 before you send it and decode it in the php script).

6. Important: Add security!

Trust no data! Check it all! Encrypt everything!
 

nwhitfield

Active Member
Licensed User
Some other tips:

1. You can get a free certificate from LetsEncrypt to secure the link to your server; I've had no problems with Android or iOS devices communicating with PHP scripts over links secured with these.

2. If talking to a MySQL database, make life simple for yourself, and ensure the tables are all using UTF8, and the connection is set to UTF8 too:

B4X:
$v4read = new mysqli('localhost','dbUsername','dbPassword','dbName') ;
$v4read->query('set names utf8mb4') ;
If you have a project where the database doesn't use UTF8, trust me, it's best to convert it to that, before you even start trying to make it work with your apps, otherwise you have a whole load of pain ahead of you, converting data to and from UTF8 with utf8_encode and utf8_decode.

What's "utf8mb4" ? It's the MySQL/MariaDB encoding for proper UTF8; the basic 'utf8' on MySQL doesn't support the whole range - it only allows for up to three bytes. So the minute someone tries to send other stuff, you enter yet another world of pain and suffering. (In particular, this means you can't store unicode emoji in a bog-standard utf8 field with MySQL - it must be utf8mb4).

3. To re-iterate the security point above, always check the data types. Everything may look like a string, and you may be sure no one will ever be calling your scripts except you, but it's astonishing the amount of probing that goes on. If, say, a user id that your send from your app is expected to be a number, check. For example, use functions like is_numeric and intval to get the actual number value, before you put it anywhere near your database. If there are string values, either check them against a list of allowed values, if you can, or make sure you use the escape functions, like mysqli_real_escape_string, to make them safe

4. Use prepared queries in your PHP. These are a bit more hassle to write, but much, much safer.

5. If you have time-related functions, and users around the world, and control the server, set it to the UTC timezone, with no daylight saving. This means you can properly manage all your timestamps. And it's easy to allow your apps to work out the correct time to display to the user. For instance, the backend script that drives one of my apps, returns JSON based on the API request made; that's stuffed into the $response variable, and the last thing I do is this

B4X:
$response['server'] = array( 'timestamp' => time(), 'api' => $apiVersion ) ;
  
print json_encode($response) ;
So, every response from the server includes the UTC timestamp. Then on the B4A side, you can use DateTime.now to work out the device's current time, and calculate the offset from UTC. If you ensure all time data send from your server is in timestamp format, you can then just add a function in B4A to format it and display the local time. Times and dates will be displayed in the local time of the device, without you needing to worry about anything server side. Main.timeOffset is the difference calculated earlier, here:

B4X:
Sub formatBLUFshortdate(timestamp As String) As String
   ' convert a server timestamp to time and date
   If timestamp = "" Or timestamp = "null" Then
     Return "never"
   End If
   
   Dim when As Long
   when = (tsToLong(timestamp) + Main.timeOffset)*1000 ' convert to milliseconds
   
   DateTime.DateFormat = "dd MMM"
   DateTime.TimeFormat = "HH:mm"
   
   Return DateTime.Time(when) & " on " & DateTime.Date(when)

End Sub
Sub tsToLong( o As Object ) As Long
   If o = "null" Then
     Return 0
   Else
     Return Floor(o)
   End If
End Sub
 

KMatle

Expert
Licensed User
Upload/Download a file (any content will do)

Upload

Get the file's content, store it into a byte array and convert it to a Base64 string. If you already have a byte array from another source, you can use it, too.

B4X:
Dim su As StringUtils
Dim FileBase64String As String = su.EncodeBase64(Bit.InputStreamToBytes(File.OpenInput(Dir, FileName)))
As it is a string, just use it as a parameter. Store it into a LONGTEXT row in MySql. Use the filename and other needed info as additional parameters.

Download

In my php scripts I use "mysqli_fetch_assoc" which (combined with JSON_Encode) creates a list with maps. A bit overheaded as it adds the column name to any data (it's a map!) but easy to handle. Maybe you want to compress it (search the forum and Google for that).


B4X:
$rows = array();
     while($r = mysqli_fetch_assoc($q))
     {
         $rows[] = $r;
     }
            
     print Json_Encode($rows);
B4X:
Sub JobDone(Job As HttpJob)
   
    If Job.Success Then
        Dim res As String
       
        res = Job.GetString
        Log("Back from Job:" & Job.JobName )
        Log("Response from server: " & res)
        
        Dim parser As JSONParser
        parser.Initialize(res)

        ItemsList.Initialize
        ItemMap.Initialize
        parser.Initialize(res)
        ItemsList=parser.NextArray
        
        For i=0 To ItemsList.Size-1 'list
           ItemMap=ItemsList.Get(i) 'map

           FileString64=ItemMap.Get("itemimg") 
           Dim su As StringUtils  
           Dim FileBytes() As Byte
           FileBytes=su.DecodeBase64(FileString64)

       'Put Image to an ImageView (B4J)
           Dim img As Image         
           Dim Inputstream1 As InputStream
           Inputstream1.InitializeFromBytesArray(FileBytes,0,FileBytes.Length)
           img.Initialize2(Inputstream1)
           Inputstream1.Close   
           Dim iv As ImageView
           iv.Initialize("")
           iv.SetImage(img)
        
        'Save data to file  (if you want)     
       
        Dim out As OutputStream = File.OpenOutput(Dir, FileName, False)
        out.WriteBytes(FileBytes, 0, FileBytes.Length)
        out.Close
...
 

KMatle

Expert
Licensed User
If you return values from your php script, it's good to have it structured. If you use

B4X:
mysqli_fetch_assoc($q)
to get some rows from you db, it is done automatically (list with maps). But what about data you return like "User found" or something similar. Here you would scan the return from the script with "contains" which works but is not a good style. So why not return list/maps, too?

Hint: A list starts with "[", a map with "{"

Return a LIST

B4X:
print json_encode(array(array('userexists' => 'no')));
returns

B4X:
[{"userexists":"no"}]
Return a MAP

B4X:
print json_encode(array('userexists' => 'no'));
returns

B4X:
{"userexists":"no"}
In JobDone:

B4X:
Sub JobDone(Job As HttpJob)
   
    If Job.Success Then
        Dim res As String
        res = Job.GetString
        Dim parser As JSONParser
        parser.Initialize(res)
        MyList.Initialize
        MyMap.Initialize
                   
        MyList=parser.NextArray 'If it's a list
        For i = 0 To MyList.Size -1
             MyMap=MyList.Get(i)
             If MyMap.Get("userexists") = "no" then
               'Do some stuff
             Else
               'Do something else
             End If
         Next

         MyMap=parser.NextObject 'if you send a map
         ...
...
 

nwhitfield

Active Member
Licensed User
With regard to compressing data, if you're using an Apache web server, you can have it automatically compress certain types of information, before it's sent to the client. You need to amend the host configuration, by adding lines like this in the appropriate Apache conf file

B4X:
  # Compress some response types
  AddOutputFilterByType DEFLATE application/json application/javascript text/html text/css
  BrowserMatch ^Mozilla/4 gzip-only-text/html
  BrowserMatch ^Mozilla/4\.0[678] no-gzip
  BrowserMatch \bMSIE !no-gzip !gzip-only-text/html
The BrowserMatch lines allow you to exclude some browsers that promise things they can't deliver. Clients usually indicate whether or not they can accept compressed data in their headers; if not, Apache sends uncompressed.

On the B4x side, if you're making a call from a B4i app via iHttpUtils2, it does request and correctly process gzipped data. In B4A you need to be using OKHttpUtils2 rather than HttpUtils2. If you're using HttpUtils2 then Apache will send uncompressed data.
 

KMatle

Expert
Licensed User
Send FCM message to a single device (via FCM token). It sends a DATA message which is a BACKGROUND message. This means that you need to throw a notification on your own if you need it (e.g. your app is in the background). Store the fcmtoken to a database on your server (userid -> fcmtoken) and retrieve it on demand.

Call the function below
B4X:
$rc=notify("token", "Hello");


B4X:
function notify ($devt, $m)
    {
    // API access key from Google API's Console
        if (!defined('API_ACCESS_KEY')) define( 'API_ACCESS_KEY', 'your server key from the console goes here...' );
        $tokenarray = array($devt);
        // prep the bundle
        $msg = array
        (
            'message'     => $m   
        );
        $fields = array
        (
            'to'     => $devt,
            'data'    => $msg
        );
        
        $headers = array
        (
            'Authorization: key=' . API_ACCESS_KEY,
            'Content-Type: application/json'
        );
        
        $ch = curl_init();
        curl_setopt( $ch,CURLOPT_URL, 'fcm.googleapis.com/fcm/send' );
        curl_setopt( $ch,CURLOPT_POST, true );
        curl_setopt( $ch,CURLOPT_HTTPHEADER, $headers );
        curl_setopt( $ch,CURLOPT_RETURNTRANSFER, true );
        curl_setopt( $ch,CURLOPT_SSL_VERIFYPEER, false );
        curl_setopt( $ch,CURLOPT_POSTFIELDS, json_encode( $fields ) );
        $result = curl_exec($ch );
        curl_close( $ch );
       
        return $result;
    }
Server Key (German: "Serverschlüssel")

Unbenannt.JPG


B4A Code (insinde the FirebaseMessaging service)

B4X:
Sub fm_MessageArrived (Message As RemoteMessage)
    Log("Message arrived")
    Log($"Message data: ${Message.GetData}"$)
    Log("ID: " & Message.MessageId)
    Log("From: " & Message.From)
    Log("Collapse: " & Message.CollapseKey)
    Log(Message.GetData)
  
    Dim MessCompleteMap As Map
    MessCompleteMap=Message.GetData

End Sub
Output

B4X:
...
** Service (firebasemessaging) Start **
Message arrived
Message data: {message=Hello}
ID: 0:xxxxxxxxxxxxxxxxx
From: yyyyyyyyyyyyyy
Collapse:
(MyMap) {message=Hello}
...

To send to topics change the line

B4X:
'to'     => $devt,
to

B4X:
'to' => '/topics/' . 'MyTopic',
 

KMatle

Expert
Licensed User
Force https:// in your php script (someone could call your script with just http://)

B4X:
if($_SERVER["HTTPS"] != "on")
    {
        header("Location: https://" . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"]);
        exit();
    }
This code just restarts your script in https:// mode when it was called with http://
 

KMatle

Expert
Licensed User
From PHP 7.1 Mcrypt is deprecated: http://php.net/manual/de/function.mcrypt-encrypt.php

As an alternative you can use OpenSSL:

PHP

B4X:
function encrypt_decrypt($action, $string) {
    $output = false;
    $encrypt_method = "AES-128-CBC";
    $secret_key = '1234567890123456';
    $secret_iv = '1234567890123456';
  
if (strlen($secret_key) == 16)
        {
            $encrypt_method = "AES-128-CBC";
            }
        else
        {
            $encrypt_method = "AES-256-CBC";
        }

    if ( $action == 'encrypt' ) {
        $output = openssl_encrypt($string, $encrypt_method, $secret_key, 0,$secret_iv);
        //$output is base64 encoded automatically!
    } else if( $action == 'decrypt' ) {
        $output = openssl_decrypt($string, $encrypt_method, $secret_key, 0,$secret_iv);
        //$string must be base64 encoded!
    }
    return $output;
}
B4x

B4X:
Sub AES_Encrypt(input As String, IV As String, pass As String) As String

    Dim inputB() As Byte = input.GetBytes("UTF8")
    Dim passB() As Byte = pass.GetBytes("UTF8")
    Dim IVb() As Byte = IV.GetBytes("UTF8")
 
    Dim kg As KeyGenerator
    Dim C As Cipher
 
    kg.Initialize("AES")
    kg.KeyFromBytes(passB)
 
    C.Initialize("AES/CBC/PKCS5Padding")
    C.InitialisationVector = IVb
 
    Dim datas() As Byte = C.Encrypt(inputB, kg.Key, True)
 
    Return SU.EncodeBase64(datas)
End Sub

Sub AES_Decrypt(input As String, IV As String, pass As String) As String

    Dim inputB() As Byte = SU.DecodeBase64(input)
    Dim passB() As Byte = pass.GetBytes("UTF8")
    Dim IVb() As Byte = IV.GetBytes("UTF8")
 
    Dim kg As KeyGenerator
    Dim C As Cipher
 
    kg.Initialize("AES")
    kg.KeyFromBytes(passB)
 
    C.Initialize("AES/CBC/PKCS5Padding")
    C.InitialisationVector = IVb
 
    Dim datas() As Byte = C.Decrypt(inputB, kg.Key, True)
 
    Return BytesToString(datas, 0, datas.Length, "UTF8")

End Sub
Notes:

- the key length must be a multiple of 16 (16=AES 128 Bit, 32 = AES 256 Bit)
- use a different IV for every message! The IV doesn't need to be kept secret.
 
Last edited:
Top