Android LayerDrawable and Drawable.Callback

LayerDrawable is a Drawable that manages an array of Drawables, where each Drawable is a layer of it. It can be used to compose fancy visual effects. However, used incorrectly, it can also introduce difficult to catch bugs. This post discusses a bug that can be caused by the Callback mechanism of Drawable.

Chain of Callbacks

In LayerDrawable, every layer/drawable registers the LayerDrawable as its Drawable.callback. This allows the layer to inform the LayerDrawable when it needs to redraw. As shown in the invalidateSelf method from Drawable.java, the DrawableCallback.invalidateDrawable is called to inform the client (the LayerDrawable, in this case) the drawable needs to redraw.

public void invalidateSelf() {
    final Callback callback = getCallback();
    if (callback != null) {
        callback.invalidateDrawable(this);
    }
}

 

Also, a View registers itself as Drawable.callback, so when the drawable needs to redraw, the View can be informed and invalidated. So if we set background of a View to be LayeredDrawable, we have a chain of DrawableCallbacks. This is illustrated in the figure below.

1.jpg

Figure 1. Chain of Callbacks when Setting LayerDrawable as View’s Background

When the Drawable needs to redraw, it calls its callback.invalidateDrawable, which in turns calls LayerDrawable’s callback.invalidateDrawable, which causes the View to redraw the corresponding region if necessary.

View Breaks Callback of Old Background Drawable

So far all work well, but the setBackgroundDrawable of View.java has the following code snippet.

public void setBackgroundDrawable(Drawable background) {
    ...

    /*
     * Regardless of whether we're setting a new background or not, we want
     * to clear the previous drawable.
     */
    if (mBackground != null) {
        mBackground.setCallback(null);
        unscheduleDrawable(mBackground);
    }

    …
    if (background != null) {
        background.setCallback(this);
    }
    ...
}

This means setting a new background will break the old background drawable’s callback by setting it to null, regardless whether the callback is still set to the View.

The Bug

With all the knowledge above, we can “fabricate” a bug by follow the steps below.

  1. Set a Drawable A as a background of a View V. The A.callback is set to V.

  2. Create a LayerDrawable L with A as one layer. Now A.callback is set to L.

  3. Now set another Drawable (or just null) as background of V. V will set callback of its old background (A in our example) to null, which breaks the link between A and L.

  4. Updates A won’t trigger L to update.

The solution to this issue is to update the background of V before creating L. This is illustrated in the code example here.

In the example, we tried animating the actionbar background. The background consists of two layers, the first one is a green ColorDrawable, and second a bitmap drawable. We want to animate the alpha value of the bitmap drawable, so it appears the image is faded in.

The code contains two methods, one works and the other doesn’t due to the bug we described. Each method can be triggered by a button. Below we extract the key part of the code, one can refer to the link for more details.


Button btn1 = (Button) findViewById(R.id.button1);
btn1.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    // 1. This sets launcherIconDrawable.callback to actionBar
    actionBar.setBackgroundDrawable(launcherIconDrawable);
    animateActionBarWorking();
  }
});
Button btn2 = (Button) findViewById(R.id.button2);
btn2.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    // 1. This sets launcherIconDrawable.callback to actionBar
    actionBar.setBackgroundDrawable(launcherIconDrawable);
    animateActionBarNotWorking();
  }
});

private void animateActionBarNotWorking() {
	Drawable[] layers = new Drawable[] { colorLayer, launcherIconDrawable };
	// 2. This sets launcherIconDrawable.callback to layerDrawable
	LayerDrawable layerDrawable = new LayerDrawable(layers);
	// 3. This sets launcherIconDrawable.callback to null
	actionBar.setBackgroundDrawable(layerDrawable);
	ValueAnimator valueAnimator = ValueAnimator.ofInt(0, 255);
	valueAnimator.setDuration(1000);
	valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
	  @Override
	  public void onAnimationUpdate(ValueAnimator animation) {
		// 4. Updates launcherIconDrawable will not trigger action bar background to update
		// as launcherIconDrawable.callback is null
		launcherIconDrawable.setAlpha((Integer) animation.getAnimatedValue());
	  }
	});
	valueAnimator.start();
}

private void animateActionBarWorking() {
	actionBar.setBackgroundDrawable(null);
	animateActionBarNotWorking();
}

 

References:

  1. LayerDrawable: http://developer.android.com/reference/android/graphics/drawable/LayerDrawable.html

Record WAVE Audio on Android

This post discusses how to record raw audio (PCM) and save it to wave file on Android. If you’re not familiar with WAVE audio file format, please refer to a previous post, WAVE Audio File Format.

The post is a follow up post for Record PCM Audio on Android. The code and working principle are similar. It is strongly suggested you read it first.

WAVE file is used to store PCM data, with 44-byte header. Recording WAVE audio is equivalent to recording PCM audio and adding the 44-byte header in front.

We used a RandomAccessFile to write the data. We first write 44-byte header. Because some fields are not known until we finish the recording, we simply write zeros. This is shown as below.

randomAccessWriter = new RandomAccessFile(filePath, "rw");

randomAccessWriter.setLength(0); // Set file length to 0, to prevent unexpected behavior in case the file already existed

randomAccessWriter.writeBytes("RIFF");

randomAccessWriter.writeInt(0); // Final file size not known yet, write 0 

randomAccessWriter.writeBytes("WAVE");

randomAccessWriter.writeBytes("fmt ");

randomAccessWriter.writeInt(Integer.reverseBytes(16)); // Sub-chunk size, 16 for PCM

randomAccessWriter.writeShort(Short.reverseBytes((short) 1)); // AudioFormat, 1 for PCM

randomAccessWriter.writeShort(Short.reverseBytes(nChannels));// Number of channels, 1 for mono, 2 for stereo

randomAccessWriter.writeInt(Integer.reverseBytes(sRate)); // Sample rate

randomAccessWriter.writeInt(Integer.reverseBytes(sRate*nChannels*mBitsPersample/8)); // Byte rate, SampleRate*NumberOfChannels*mBitsPersample/8

randomAccessWriter.writeShort(Short.reverseBytes((short)(nChannels*mBitsPersample/8))); // Block align, NumberOfChannels*mBitsPersample/8

randomAccessWriter.writeShort(Short.reverseBytes(mBitsPersample)); // Bits per sample

randomAccessWriter.writeBytes("data");

randomAccessWriter.writeInt(0); // Data chunk size not known yet, write 0

We then write the PCM data. This is discussed in detail in post Record PCM Audio on Android.

After the recording is done, we seek to the header and update a few header fields. This is shown as below.

audioRecorder.stop();

try {

    randomAccessWriter.seek(4); // Write size to RIFF header

    randomAccessWriter.writeInt(Integer.reverseBytes(36+payloadSize));

 

    randomAccessWriter.seek(40); // Write size to Subchunk2Size field

    randomAccessWriter.writeInt(Integer.reverseBytes(payloadSize));

 

    randomAccessWriter.close();

} catch(IOException e) {

    Log.e(WavAudioRecorder.class.getName(), "I/O exception occured while closing output file");

    state = State.ERROR;

}

For the complete source code, one can refer to my github Android tutorial project.

Record PCM Audio on Android

This post discusses how to record PCM audio on Android with the android.media.AudioRecord class. If you’re not familiar with PCM, please read a previous post PCM Audio Format.

For the source code, please refer to AndroidPCMRecorder.

1. State Transition

The source code records the PCM audio with PcmAudioRecorder.java, which uses Android AudioRecord internally. Similar to the Android MediaRecorder class, PcmAudioRecorder follows a simple state machine as shown below.

Untitled drawing

Figure 1. State Transition of PcmAudioRecorder Class

As indicated in the diagram, we initialize a PcmAudioRecorder class by either calling the getInstance static method or the constructor to get into INITIALIZING state. We can then set the output file path and call prepare to get into PERPARED state. We can then start recording by calling  start method and finally call stop to stop the recording. At any state except ERROR, we can call reset to get back to INITIALIZING state. When we’re done with recording, we can call release to discard the PcmAudioRecorder object.

2. Filling the Buffer with Data and Write to File

One particular part of the code requires a bit attention is the updateListener. We register the listener with the AudioRecord object using setRecordPositionUpdateListener method. The listener is an interface with two abstract methods, namely onMarkerReached and onPeriodicNotification. We implemented the onPeriodicNotification method to pull the audio data from AudioRecord object and save it to the output file.

In order for the listener to work, we need to specify the notification period by calling AudioRecord.setPositionNotificationPeriod(int) to specify how frequently we want the listener to be triggered and pull data. The method accepts a single argument, which indicates the update period in number of frames. This leads us to next section.

3. Frame vs Sample

For PCM audio, a frame consists of the set of samples from all channels at a given point of time. In other words, the number of frames in a second is equal to sample rate.

However, when the audio is compressed (encoded further to mp3, aac etc.), a frame consists of compressed data for a whole series of samples with additional, non-sample data. For such audio formats, the sample rate and sample size refer to data after decoded to PCM, and it’s completely different from frame rate and frame size.

In our sample code, we set the update period for setPositionNotificationPeriod as number of frames in every 100 millisecond, therefore the listener will be triggered every 100 milliseconds, and we can pull data and update the recording file every 100 milliseconds.

Note that source code is modified based on ExtAudioRecorder.java.

Android TCP Client and Server Communication Programming–Illustrated with Example

This is a follow up post of the Android UDP Client and Server communication programming. TCP and UDP are the two main transport layer protocols used in today’s Internet. TCP aims to provide connection-oriented, reliable end-to-end communication service.

Similar to UDP, TCP programming in Android also uses  the APIs provided in java.net package.

To program a TCP server, one needs to create a ServerSocket instance, and listen to (call accept method) incoming connections. If there’s a connection established, accept method will return a socket representing the opened connection. One can then receive and send message through the socket. This is illustrated as the code below,

private void runTcpServer() {

    ServerSocket ss = null;

    try {

        ss = new ServerSocket(TCP_SERVER_PORT);

        //ss.setSoTimeout(10000);

        //accept connections

        Socket s = ss.accept();

        BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));

        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

        //receive a message

        String incomingMsg = in.readLine() + System.getProperty("line.separator");

        Log.i("TcpServer", "received: " + incomingMsg);

        textDisplay.append("received: " + incomingMsg);

        //send a message

        String outgoingMsg = "goodbye from port " + TCP_SERVER_PORT + System.getProperty("line.separator");

        out.write(outgoingMsg);

        out.flush();

        Log.i("TcpServer", "sent: " + outgoingMsg);

        textDisplay.append("sent: " + outgoingMsg);

        //SystemClock.sleep(5000);

        s.close();

    } catch (InterruptedIOException e) {

        //if timeout occurs

        e.printStackTrace();

    } catch (IOException e) {

        e.printStackTrace();

    } finally {

        if (ss != null) {

            try {

                ss.close();

            } catch (IOException e) {

                e.printStackTrace();

            }

        }

    }

}

To program a TCP client, one needs to open a socket with server ip and port number. Once it’s connected, one can receive and send message through the socket. The code is as below,

private void runTcpClient() {

    try {

        Socket s = new Socket("localhost", TCP_SERVER_PORT);

        BufferedReader in = new BufferedReader(new InputStreamReader(s.getInputStream()));

        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

        //send output msg

        String outMsg = "TCP connecting to " + TCP_SERVER_PORT + System.getProperty("line.separator"); 

        out.write(outMsg);

        out.flush();

        Log.i("TcpClient", "sent: " + outMsg);

        //accept server response

        String inMsg = in.readLine() + System.getProperty("line.separator");

        Log.i("TcpClient", "received: " + inMsg);

        //close connection

        s.close();

    } catch (UnknownHostException e) {

        e.printStackTrace();

    } catch (IOException e) {

        e.printStackTrace();

    } 

}

Note that to use the network socket (ServerSocket and Socket, in our case), you’ll need to request for INTERNET permission. Adds the following line to AndroidManifest.xml will do,

<uses-permission android:name="android.permission.INTERNET"/>

Download and Screenshots
You can download the complete example (client) and (server) or from my github android tutorial repo. If you run server and client on your android device (server first), you’ll get something like this,

Untitled

Figure 1. Screenshot for TCP Communication Client and Server

The TCP server receives the TCP packet from TCP client and outputs the message to a TextView.

Android UDP Client and Server Communication Programming–Illustrated with Example

UDP and TCP are the two main transport protocols used in today’s Internet. This post illustrates how to program UDP client and server in Android.

Android UDP communication uses the APIs provided in java.net package, which are also available in Java standard edition etc.

To program a UDP server, one can use the code below,

private void runUdpServer() {

    String lText;

    byte[] lMsg = new byte[MAX_UDP_DATAGRAM_LEN];

    DatagramPacket dp = new DatagramPacket(lMsg, lMsg.length);

    DatagramSocket ds = null;

    try {

        ds = new DatagramSocket(UDP_SERVER_PORT);

        //disable timeout for testing

        //ds.setSoTimeout(100000);

        ds.receive(dp);

        lText = new String(lMsg, 0, dp.getLength());

        Log.i("UDP packet received", lText);

        textView.setText(lText);

    } catch (SocketException e) {

        e.printStackTrace();

    } catch (IOException e) {

        e.printStackTrace();

    } finally {

        if (ds != null) {

            ds.close();

        }

    }

}

The code opens a DatagramSocket and calls the receive method. Note that if setSoTimeout() is not called, the receive method is a block call, which means the execution will block until a udp message is received. If you don’t want this behavior, set the timeout to a value you want and catch for timeout exception.

For the client side, it’s also simple,

private void runUdpClient()  {

    String udpMsg = "hello world from UDP client " + UDP_SERVER_PORT;

    DatagramSocket ds = null;

    try {

        ds = new DatagramSocket();

        InetAddress serverAddr = InetAddress.getByName("127.0.0.1");

        DatagramPacket dp;

        dp = new DatagramPacket(udpMsg.getBytes(), udpMsg.length(), serverAddr, UDP_SERVER_PORT);

        ds.send(dp);

    } catch (SocketException e) {

        e.printStackTrace();

    }catch (UnknownHostException e) {

        e.printStackTrace();

    } catch (IOException e) {

        e.printStackTrace();

    } catch (Exception e) {

        e.printStackTrace();

    } finally {

        if (ds != null) {

            ds.close();

        }

    }

}

The code also opens a DatagramSocket, and create a DatagramPacket with destination address and port number. It calls send method to send the DatagramPacket out.

Note that to use the network socket (DatagramSocket, in our case), you’ll need to request for INTERNET permission. Adds the following line to AndroidManifest.xml will do,

<uses-permission android:name="android.permission.INTERNET"/>

Download and Screenshots

You can download the complete example here (client) and here (server) or from my github android tutorial repo here. If you run server and client on your android device (server first), you’ll get something like this,

Untitled

Figure 1. Screenshot for UDP Communication Client and Server

The UDP server receives the UDP datagram from UDP client and outputs the message to a TextView.

Android Video Recording API–Illustrated with an Example

Android has made video recording very simply by providing a few high level classes, including Camera, SurfaceView and MediaRecorder. This tutorial illustrates the Video Recording API by providing a workable example as shown in the screenshots.

cam1cam2

Figure 1. Screenshots of Android Video Capturing Sample Program

The Camera class allows us to access the Camera on an Android device; the SurfaceView class provides a drawing surface, it is used to present a live preview in our application; the MediaRecoder contains the API to configure and record the video, it uses Camera class to access the hardware camera.

0. Live Preview Before Recording

Before users start to record video, they want to see what they’re capturing. That’s what the preview function does, it allows one to see what the camera is able to capture. The preview implementation is briefly illustrated as the code below,

SurfaceView prSurfaceView = (SurfaceView) findViewById(R.id.surface_camera);

SurfaceHolder prSurfaceHolder = prSurfaceView.getHolder();

prSurfaceHolder.addCallback(this);

prSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

Camera prCamera = Camera.open();        

Camera.Parameters lParam = prCamera.getParameters();

prCamera.setParameters(lParam);

prCamera.setPreviewDisplay(prSurfaceHolder);

prCamera.startPreview();

prCamera.stopPreview();

1. Video Recording and Live Preview at Recording

The state transition diagram for Android MediaRecorder class is as below,

image

Figure 2. State Transition of Android MediaRecorder

The implementation follows the state transition diagram closely. Starting from Initial state, our code sets the audio source as microphone, video source as camera, and the program enters Initialized state. The output format can be set to mpeg4 or 3gp, which indicates the container format for the video recorded. At DataSourceConfigured state, we set the audio encoder as AMR_NB (Later versions of Android supports AAC), and video encoder as MPEG4,H263 or H264 based on user configurations. We also set the frame rate, and output file here. After that, we call prepare method to transit to Prepared state, and finally a start method call will start the recording.

The description above doesn’t contain every detail but just for illustration. The actual code in feipeng.yacamcorder.Main.java contains lots of error handling and code for remembering and setting user configurations.

Note that the encoders a device supports may differ.

2. Download

You can download the entire source code for the example above from here, or from my github repo.

References:

1. http://developer.android.com/guide/appendix/media-formats.html

2. http://developer.android.com/guide/topics/media/camera.html

Android PhoneStateListener Illustration–with Auto Logging Calls to Google Calendar Example

android.telephony.PhoneStateListener class is provided to monitor various state changes related to phone function of your Android device.

In order to use it, one needs to complete the following steps.

0. Extends the PhoneStateListener listener class, and override the methods for the state you want to receive updates.

1. Pass your customized PhoneStateListener instance to TelephonyManager.listen() function with a flag to indicate which states you want to monitor.

I’ll use a calltrack implementation (auto logging call logs to google calendar, used in my app Advanced Phone Log) to illustrate it. You can download the entire sample app source code at the end of this post. The UI of  the sample app and a sample log to Calendar is as below,

sc0sc1

The idea of the calltrack app is  to use a broadcast receiver to receive the Android system broadcast for a call state change. There’re three call states, CALL_STATE_IDLE, CALL_STATE_OFFHOOK and CALL_STATE_RINGING.

Based on the call state transitions, we can decide whether there’s call ended. In other words, if call state changes from CALL_STATE_OFFHOOK to CALL_STATE_IDLE, we know there’s a outgoing call; if it’s from CALL_STATE_RINGING to CALL_STATE_IDLE, we know it’s a incoming call or miss call.

Once we detected a call ends, we know there’ll be a new call log generated in Android system contacts database. We read the phone log and use the method illustrated in Android Tutorial – Programming with Calendar to create a new event to google calendar.

Below illustrates the implementation details.

0. Extend the PhoneStateListener.

The code is as below,

class MyPhoneStateListener extends PhoneStateListener {

        public void onCallStateChanged(int state,String incomingNumber){

            AdvancedPhoneLogRecord mQueryRecord;

            switch(state) {

              case TelephonyManager.CALL_STATE_IDLE:

                  if (mLastState != TelephonyManager.CALL_STATE_IDLE) {

                      AdvancedPhoneLogRecord lQueryRecord = null;

                      int lSize = mQueryRecords.size();

                      for (int i = 0; i < lSize; ++i) {

                          if (mQueryRecords.get(i).aplr_number.compareTo(incomingNumber)==0) {

                              //Log.e("query-all-match", mQueryRecords.get(i).aplr_number + ":" + mQueryRecords.get(i).aplr_time.toString());

                              lQueryRecord = mQueryRecords.get(i);

                              mRecord = getLastCallLog(mQueryRecords.get(i));

                              break;

                          }

                      }

                      List<AdvancedPhoneLogRecord> lQueryRecords = new ArrayList<AdvancedPhoneLogRecord>();

                      if (lQueryRecord!=null) {

                          for (int i = 0; i < lSize; ++i) {

                              if (mQueryRecords.get(i).aplr_number.compareTo(lQueryRecord.aplr_number)!=0) {

                                  lQueryRecords.add(mQueryRecords.get(i));

                              } 

                          }

                          mQueryRecords = lQueryRecords;

                          lQueryRecords.clear();

                      }

//                      for (int i = 0; i < mQueryRecords.size(); ++i) {

//                          Log.e("query-all----", mQueryRecords.get(i).aplr_number + ":" + mQueryRecords.get(i).aplr_time.toString());

//                      }

                      if (CalltrackSettingsStatic.getTrackEnable(mContext) != 0) {

                          switch (mRecord.aplr_type) {

                          case android.provider.CallLog.Calls.INCOMING_TYPE:

                              if (CalltrackSettingsStatic.getTrackInEnable(mContext) != 0) {

                                  addEvent(mRecord);

                              }

                              break;

                          case android.provider.CallLog.Calls.OUTGOING_TYPE:

                              if (CalltrackSettingsStatic.getTrackOutEnable(mContext) != 0) {

                                  addEvent(mRecord);

                              }

                              break;

                          case android.provider.CallLog.Calls.MISSED_TYPE:

                              if (CalltrackSettingsStatic.getTrackMissEnable(mContext) != 0) {

                                  addEvent(mRecord);

                              }

                              break;

                          }

                      }

                      Log.v("PhoneIntentReceiver", mRecord.aplr_name + ":" + mRecord.aplr_number + ":" + mRecord.aplr_dataId + ":" + mRecord.aplr_duration + ":" + mRecord.aplr_id + ":" + mRecord.aplr_type);

                  }

                  mLastState = TelephonyManager.CALL_STATE_IDLE;

                  break;

              case TelephonyManager.CALL_STATE_OFFHOOK:

                  mLastState = TelephonyManager.CALL_STATE_OFFHOOK;

                  mQueryRecord = new AdvancedPhoneLogRecord();

                  mQueryRecord.aplr_number = incomingNumber;

                  mQueryRecord.aplr_time = new Date(System.currentTimeMillis());

                  mQueryRecords.add(mQueryRecord);

                  break;

              case TelephonyManager.CALL_STATE_RINGING:

                  mLastState = TelephonyManager.CALL_STATE_RINGING;

                  mQueryRecord = new AdvancedPhoneLogRecord();

                  mQueryRecord.aplr_number = incomingNumber;

                  mQueryRecord.aplr_time = new Date(System.currentTimeMillis());

                  mQueryRecords.add(mQueryRecord);

                  break;

             }

        } 

   }

When a CALL_STATE_RINGING or CALL_STATE_OFFHOOK even is detected, we know that there’s new call starts (outgoing, incoming or miss call). So we create a new Query and remember the state.

Note that sometimes the broadcast receiver receives mutliple broadcast events when there’s a single state change, and sometimes there’re incoming calls when you’re in a call. So we use a queue to store all the queries.

When a CALL_STATE_IDLE event is detected, we know a call ends. We go through all the query records in the queue, and get the one that has the same number the current call state change number. We call a method getLastCallLog (details in source code) to retrieve the call log from the system database and then clear all entries in the queue with this number.

Next step is just to log the call to calendar based on user settings.

One thing note-worthy is that getLastCallLog queries the system database multiple times to get the latest call log as the Android system takes some time to update its database.

1. Register your customized PhoneStateListener

The code is simple,

MyPhoneStateListener phoneListener=new MyPhoneStateListener();

TelephonyManager telephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);

telephony.listen(phoneListener,PhoneStateListener.LISTEN_CALL_STATE);

2. Register Broadcast Receiver

Add lines below to AndroidManifest.xml,

<receiver android:name=".PhoneIntentReceiver" android:process=":remote">

    <intent-filter>

        <action android:name="android.intent.action.PHONE_STATE" />

    </intent-filter>

</receiver>

Also don’t forget the permissions,

<uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>

<uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>

<uses-permission android:name="android.permission.WRITE_CALENDAR"></uses-permission>

<uses-permission android:name="android.permission.READ_CALENDAR"></uses-permission>

More Notes

Android also supports lots of other phone state change detection, including,

Flags

Event Handler you should overwrite

LISTEN_CALL_FORWARDING_INDICATOR

onCallForwardingIndicatorChanged

LISTEN_CALL_STATE

onCallStateChanged

LISTEN_CELL_LOCATION

onCellLocationChanged

LISTEN_DATA_ACTIVITY

onDataActivity

LISTEN_DATA_CONNECTION_STATE

onDataConnectionStateChanged

LISTEN_MESSAGE_WAITING_INDICATOR

onMessageWaitingIndicatorChanged

LISTEN_NONE

 

LISTEN_SERVICE_STATE

onServiceStateChanged

LISTEN_SIGNAL_STRENGTH

onSignalStrengthChanged(int asu)

LISTEN_SIGNAL_STRNEGTHS

onSignalStrengthChanged(SignalStrength signalStrength)

Just like using the OnCallStateChanged can write a calltrack function, you can use the event handlers in the above list to write very useful apps.

Here is the download link for the the sample calltrack app code.

Android Tutorial–Programming with Calendar

One of the popular apps on Android device is the Calendar application. As one of the android app development philosophy is to make use of functionalities of other apps, it is expected by many developers that we can access the calendar data easily. However, the truth is NO!

Android doesn’t provide an official API for read/write Calendar data, probably because Android is going to change the calendar data format in future release.

The good news is there’re workarounds, more than one. This tutorial will list out those methods and provide an example at the end of the tutorial.

1. Google Calendar APIs

Google doesn’t provide Calendar API specifically for Android, but it does provide an Calendar API for more general use. The API supports .NET, Java and Python, so it is expected that it can be used in Android.

However, this method requires the app to establish a network connection through Internet (probably requires some kind of authentication), and then access the calendar data. As android already has Calendar app built in, it sounds dumb that we’ll need to connect to Internet, create a connection and access the data.

This approach is not explored in detail here. Interested readers could refer reference 2 for more details.

2. Android Calendar Private APIs

Android is open source, so it’s not difficult to find out that android actually has undocumented APIs for calendar (it’s just not public APIs).

This part will cover how to get list of calendars, how to read event, and how to add event.

2.0 Permission Declaration

In order to read calendar data, one needs to declare the “READ_CALENDAR” permission in AndroidManifest.xml file.

<uses-permission android:name="android.permission.READ_CALENDAR"></uses-permission>

 

For write to Calendar, there’s a another “WRITE_CALENDAR” permission.

<uses-permission android:name="android.permission.WRITE_CALENDAR"></uses-permission>

 

You can refer to the complete code at the end of the tutorial for exact position of the declaration.

2.1 List Calendars

One may have multiple calendars created in Calendar application. For me, I have the default calendar, a Singapore Holiday Calendar, and a third calendar from my other Gmail account.

The code to get a list of all calendars are as below,

class MyCalendar {
    public String name;
    public String id;
    public MyCalendar(String _name, String _id) {
        name = _name;
        id = _id;
    }
    @Override
    public String toString() {
        return name;
    }
}
private MyCalendar m_calendars[];
private void getCalendars() {
    String[] l_projection = new String[]{"_id", "displayName"};
    Uri l_calendars;
    if (Build.VERSION.SDK_INT >= 8 ) {
        l_calendars = Uri.parse("content://com.android.calendar/calendars");
    } else {
        l_calendars = Uri.parse("content://calendar/calendars");
    }
    Cursor l_managedCursor = this.managedQuery(l_calendars, l_projection, null, null, null);    //all calendars
    //Cursor l_managedCursor = this.managedQuery(l_calendars, l_projection, "selected=1", null, null);   //active calendars
    if (l_managedCursor.moveToFirst()) {
        m_calendars = new MyCalendar[l_managedCursor.getCount()];
        String l_calName;
        String l_calId;
        int l_cnt = 0;
        int l_nameCol = l_managedCursor.getColumnIndex(l_projection[1]);
        int l_idCol = l_managedCursor.getColumnIndex(l_projection[0]);
        do {
            l_calName = l_managedCursor.getString(l_nameCol);
            l_calId = l_managedCursor.getString(l_idCol);
            m_calendars[l_cnt] = new MyCalendar(l_calName, l_calId);
            ++l_cnt;
        } while (l_managedCursor.moveToNext());
    }
}

The code essentially queries the android calendar content provider and goes through the calendar list returned. The name and calendar id are stored in m_calendars[].

For android version less than 8, the uri for query calendars is content://calendar/*, while content://com.android.calendar/* is used for android version 8 and above.

2.2 Read Events

To read a list of Calendar events, one can create another query as illustrated below,

private void getLastThreeEvents() {
    Uri l_eventUri;
    if (Build.VERSION.SDK_INT >= 8 ) {
        l_eventUri = Uri.parse("content://com.android.calendar/events");
    } else {
        l_eventUri = Uri.parse("content://calendar/events");
    }
    String[] l_projection = new String[]{"title", "dtstart", "dtend"};
    Cursor l_managedCursor = this.managedQuery(l_eventUri, l_projection, "calendar_id=" + m_selectedCalendarId, null, "dtstart DESC, dtend DESC");
    //Cursor l_managedCursor = this.managedQuery(l_eventUri, l_projection, null, null, null);
    if (l_managedCursor.moveToFirst()) {
        int l_cnt = 0;
        String l_title;
        String l_begin;
        String l_end;
        StringBuilder l_displayText = new StringBuilder();
        int l_colTitle = l_managedCursor.getColumnIndex(l_projection[0]);
        int l_colBegin = l_managedCursor.getColumnIndex(l_projection[1]);
        int l_colEnd = l_managedCursor.getColumnIndex(l_projection[1]);
        do {
            l_title = l_managedCursor.getString(l_colTitle);
            l_begin = getDateTimeStr(l_managedCursor.getString(l_colBegin));
            l_end = getDateTimeStr(l_managedCursor.getString(l_colEnd));
            l_displayText.append(l_title + "n" + l_begin + "n" + l_end + "n----------------n");
            ++l_cnt;
        } while (l_managedCursor.moveToNext() && l_cnt < 3);
        m_text_event.setText(l_displayText.toString());
    }
}

The method above get the last three events from the selected calendar. Note that m-selectedCalendarId variable and getDateTimeStr() method are defined by us. You can refer to code at the end of the tutorial for a complete picture.

2.3 Create an Event

Similarly, one can insert a new event into Calendar database.

/*add an event to calendar*/
private void addEvent() {
    ContentValues l_event = new ContentValues();
    l_event.put("calendar_id", m_selectedCalendarId);
    l_event.put("title", "roman10 calendar tutorial test");
    l_event.put("description", "This is a simple test for calendar api");
    l_event.put("eventLocation", "@home");
    l_event.put("dtstart", System.currentTimeMillis());
    l_event.put("dtend", System.currentTimeMillis() + 1800*1000);
    l_event.put("allDay", 0);
    //status: 0~ tentative; 1~ confirmed; 2~ canceled
    l_event.put("eventStatus", 1);
    //0~ default; 1~ confidential; 2~ private; 3~ public
    l_event.put("visibility", 0);
    //0~ opaque, no timing conflict is allowed; 1~ transparency, allow overlap of scheduling
    l_event.put("transparency", 0);
    //0~ false; 1~ true
    l_event.put("hasAlarm", 1);
    Uri l_eventUri;
    if (Build.VERSION.SDK_INT >= 8 ) {
        l_eventUri = Uri.parse("content://com.android.calendar/events");
    } else {
        l_eventUri = Uri.parse("content://calendar/events");
    }
    Uri l_uri = this.getContentResolver().insert(l_eventUri, l_event);
    Log.v("++++++test", l_uri.toString());
}

The code above simply create a ContentValues and insert it into the database URI.

3. Sending Intent to Calendar

This method doesn’t require the READ/WRITE_CALENDAR permission. It sends out an intent to calendar, and users will be redirected to Calendar app to create the event. Note that this is not a documented method either.

Below is a method that adds an event to Android Calendar.

/*add an event through intent, this doesn't require any permission
* just send intent to android calendar
*/
private void addEvent2() {
   Intent l_intent = new Intent(Intent.ACTION_EDIT);
   l_intent.setType("vnd.android.cursor.item/event");
   //l_intent.putExtra("calendar_id", m_selectedCalendarId);  //this doesn't work
   l_intent.putExtra("title", "roman10 calendar tutorial test");
   l_intent.putExtra("description", "This is a simple test for calendar api");
   l_intent.putExtra("eventLocation", "@home");
   l_intent.putExtra("beginTime", System.currentTimeMillis());
   l_intent.putExtra("endTime", System.currentTimeMillis() + 1800*1000);
   l_intent.putExtra("allDay", 0);
   //status: 0~ tentative; 1~ confirmed; 2~ canceled
   l_intent.putExtra("eventStatus", 1);
   //0~ default; 1~ confidential; 2~ private; 3~ public
   l_intent.putExtra("visibility", 0);
   //0~ opaque, no timing conflict is allowed; 1~ transparency, allow overlap of scheduling
   l_intent.putExtra("transparency", 0);
   //0~ false; 1~ true
   l_intent.putExtra("hasAlarm", 1);
   try {
       startActivity(l_intent);
   } catch (Exception e) {
       Toast.makeText(this.getApplicationContext(), "Sorry, no compatible calendar is found!", Toast.LENGTH_LONG).show();
   }
}

This method is used in my android app, Advanced Phone Log.

4. A Complete and Runnable Example

The code snippet shown above are retrieved from an example. You can download the source code here, or find it at github.

Below are screenshots of the app.

234

Figure 1. Screenshots of Calendar Sample App

Reference:

1. Working with the Android Calendar: http://www.developer.com/ws/article.php/3850276/Working-with-the-Android-Calendar.htm

2. Google Calendar APIs and Tools: http://code.google.com/apis/calendar/data/2.0/developers_guide.html

Android FileObserver–The Underlying inotify Mechanism and an Example

Android FileObserver is an interface provided by Android OS to monitor file access and modification. It’s based on the Linux inotify file change notification system.

This blog will give a brief introduction to inotify and illustrate the usage of Android FileObserver API with an example.

inotify

inotify is a simple but powerful file change notification system. It’s a mechanism built into the Linux 2.6.13 kernel and above.

The mechanism is exposed to the userspace program through a set of 3 system calls, including inotify_init, inotify_add_watch and inotify_rm_watch.

These system calls are declared in /usr/include/sys/inotify.h. One needs to include the header file as below in order to use them.

#include <sys/inotify.h>

 

The inotify_init function has the following prototype,

int inotify_init (void);

 

This function will create an inotify instance and return a file descriptor which all events are read from. Each instance is associated with a unique, ordered queue for events.

The inotify_add_watch function is declared as,

int inotify_add_watch (int __fd, const char *__name, uint32_t __mask);

 

where __fd is the file descriptor returned from the inotify_init function, __name is the path to the object (either file or directory) to watch, and __mask indicates the types of event to watch. The return value can be used to refer to the watch added, called watch descriptor.

The inotify_rm_watch system call is declared as,

int inotify_rm_watch (int __fd, int __wd);

 

where __fd is the return value of inotify_init, and __wd is the return value of inotify_add_watch. This function will remove the watch from the queue.

With this set of system call and normal file IO operations, one can monitor the file changes in Linux system. Please refer reference 1 and 2 for further details of inotify.

Android FileObserver

Android FileObserver is based on FileObserver. It provides similar monitoring mechanism as inotify does. One thing worth-mentioning is that the API documentation says “If a directory is monitored, events will be triggered for all files and subdirectories (recursively) inside the monitored directory”, but the FileObserver API is actually not recursive.

In other words, a FileObserver is attached to a folder, then only the files and sub-folders inside it are monitored, but its sub sub-folders and files are not. For example, there is a folder B inside folder A, and a folder C inside folder B. If a FileObserver is created to monitor folder A, then any modifications done for folder B is detected, but not for folder C.

An Android Sample App Based on FileObserver

As FileObserver is an abstract class, one must create a subclass that extends FileObserver and implement the event handler onEvent(int, string).

Below is an example,

package roman10.tutorial.fileobserver;

import android.os.FileObserver;

public class MyFileObserver extends FileObserver {
    public String absolutePath;

    public MyFileObserver(String path) {
        super(path, FileObserver.ALL_EVENTS);
        absolutePath = path;
    }

    @Override
    public void onEvent(int event, String path) {
        if (path == null) {
            return;
        }
        //a new file or subdirectory was created under the monitored directory
        if ((FileObserver.CREATE & event)!=0) {
            FileAccessLogStatic.accessLogMsg += absolutePath + "/" + path + " is createdn";
        }
        //a file or directory was opened
        if ((FileObserver.OPEN & event)!=0) {
            FileAccessLogStatic.accessLogMsg += path + " is openedn";
        }
        //data was read from a file
        if ((FileObserver.ACCESS & event)!=0) {
            FileAccessLogStatic.accessLogMsg += absolutePath + "/" + path + " is accessed/readn";
        }
        //data was written to a file
        if ((FileObserver.MODIFY & event)!=0) {
            FileAccessLogStatic.accessLogMsg += absolutePath + "/" + path + " is modifiedn";
        }
        //someone has a file or directory open read-only, and closed it
        if ((FileObserver.CLOSE_NOWRITE & event)!=0) {
            FileAccessLogStatic.accessLogMsg += path + " is closedn";
        }
        //someone has a file or directory open for writing, and closed it 
        if ((FileObserver.CLOSE_WRITE & event)!=0) {
            FileAccessLogStatic.accessLogMsg += absolutePath + "/" + path + " is written and closedn";
        }
        //[todo: consider combine this one with one below]
        //a file was deleted from the monitored directory
        if ((FileObserver.DELETE & event)!=0) {
            //for testing copy file
// FileUtils.copyFile(absolutePath + "/" + path);
            FileAccessLogStatic.accessLogMsg += absolutePath + "/" + path + " is deletedn";
        }
        //the monitored file or directory was deleted, monitoring effectively stops
        if ((FileObserver.DELETE_SELF & event)!=0) {
            FileAccessLogStatic.accessLogMsg += absolutePath + "/" + " is deletedn";
        }
        //a file or subdirectory was moved from the monitored directory
        if ((FileObserver.MOVED_FROM & event)!=0) {
            FileAccessLogStatic.accessLogMsg += absolutePath + "/" + path + " is moved to somewhere " + "n";
        }
        //a file or subdirectory was moved to the monitored directory
        if ((FileObserver.MOVED_TO & event)!=0) {
            FileAccessLogStatic.accessLogMsg += "File is moved to " + absolutePath + "/" + path + "n";
        }
        //the monitored file or directory was moved; monitoring continues
        if ((FileObserver.MOVE_SELF & event)!=0) {
            FileAccessLogStatic.accessLogMsg += path + " is movedn";
        }
        //Metadata (permissions, owner, timestamp) was changed explicitly
        if ((FileObserver.ATTRIB & event)!=0) {
            FileAccessLogStatic.accessLogMsg += absolutePath + "/" + path + " is changed (permissions, owner, timestamp)n";
        }
    }
}

Once the subclass is defined, one needs to create instances of this class in order to use it.

MyFileObserver fileOb = new MyFileObserver("/sdcard/testf/");

 

Similar to inotify_add_watch and inotify_remove_watch, one can call startWatching() and stopWatching() methods to start or stop monitoring.

fileOb.startWatching();
fileOb.stopWatching();

For a complete sample app which monitors the /sdcard directory of the Android OS, and output the results to screen, you can refer to my github repo here, or simply click here to download the source code zip file.

Below is a screenshot of the sample app.

device

Figure 1. Screenshot of File Modification Monitor

References

1. Kernel Korner – Intro to inotify: http://www.linuxjournal.com/article/8478

2. inotify – a powerful yet simple file change notification system: http://www.kernel.org/doc/Documentation/filesystems/inotify.txt

3. Android FileObserver API documentation: http://developer.android.com/reference/android/os/FileObserver.html