One would think displaying a ticking time clock and locale information in a live recording video clip on Android must be a readily available built-in option. Well, not really, but there are a number of Android apps out there that allow users to add the wanted info. But what if you want to add such info to your own custom video recording app? That’s a mobile app component required in a recent R&D project.
The required work turns out to be less trivial than expected. Android covers a wide range of hardware profiles, and adding text to video recorded or being recorded requires the video to be re-encoded, which is hardware dependent. Fortunately, the video content required by the project doesn’t need to be of super high resolution. Given that contemporary mobile devices have decent screen resolution (e.g. even an old Android Pixel-3 phone has a screen resolution of 1080 x 2160), I decided to go for a much simpler approach by displaying the time and locale on screen while recording the screen view as the video content.
Android CameraX
The latest Android API for camera functionality is CameraX. As we know, Google has made Kotlin (as opposed to Java) the preferred programming language on Android since 2019. While Kotlin seems like a neat programming language with improvement (e.g. null safety) over Java, I’m not quite ready to justify diving into another new language for a simple mobile app, hence I’m going to stick to the good old Java and repurpose from this nice Java implementation repo on GitHub.
Let’s get right into the coding work. First, do a git clone
of the base CameraX Java app.
git clone https://github.com/farazxsiddiqui/CameraX.git
From the cloned repo, create an Android Studio project, then, if wanted, modify the project name and path. To do that, either use Android Studio
‘s refactor
or manually modify paths and content of a few relevant files under ${projectDir}/app/ including build.gradle
, main/AndroidManifest.xml
, main app MainActivity.java
, etc.
CameraX dependencies
Next, we review app/build.gradle
to make sure the CameraX
dependencies are there and upgrade cameraxVersion
to a later version, if preferred.
build.gradle:
... dependencies { ... def cameraxVersion = "1.1.0-alpha05" implementation "androidx.camera:camera-core:${cameraxVersion}" implementation "androidx.camera:camera-camera2:${cameraxVersion}" implementation "androidx.camera:camera-lifecycle:${cameraxVersion}" implementation 'androidx.camera:camera-view:1.0.0-alpha25' }
Android TextClock
The Android API provides TextClock for constructing timestamp in a string of various formats as TextView
. The following example UI layout highlights how to use it.
Let’s say we have the main display content content_main.xml
in XML included from within the standard activity_main.xml
.
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout ... <include layout="@layout/content_main" /> </androidx.coordinatorlayout.widget.CoordinatorLayout> content_main.xml: <?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".MainActivity" tools:showIn="@layout/activity_main"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> ... <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="16dp" android:paddingRight="16dp" android:orientation="vertical" > ... <TextClock android:id="@+id/textClock" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:format12Hour="yyyy-MM-dd hh:mm:ss a z" android:text="Date-time" android:textColor="#cccccc" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/button_some_action" /> ... </LinearLayout> ... </RelativeLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
In this example, we set the timestamp format as follows so as to display the date/time/timezone like “2024-01-15 09:30:00 AM -0800”.
<TextClock android:format12Hour="yyyy-MM-dd hh:mm:ss a z" />
From within MainActivity.java
, we then import android.widget.TextClock
and get the view from the XML layout via findViewById(R.id.textClock)
.
MainActivity.java:
import android.widget.TextClock; public class MainActivity extends AppCompatActivity { ... private TextClock clockTC; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ... clockTC = findViewById(R.id.textClock); // clockTC.setFormat12Hour("yyyy-MM-dd hh:mm:ss a z"); } ... }
Android Location / Geocoder
For locale info, we use Android’s Location
, LocationManager, LocationListener and Geocoder APIs to access system location services, get location change notifications and display live locale info as TextView
. On the UI layout, we expand the content to include the address/latitude/longitude info.
content_main.xml:
<?xml version="1.0" encoding="utf-8"?> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".MainActivity" tools:showIn="@layout/activity_main"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> ... <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="16dp" android:paddingRight="16dp" android:orientation="vertical" > ... <androidx.appcompat.widget.AppCompatTextView android:id="@+id/textLoc" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:text="Location" android:textColor="#cccccc" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textClock" /> <androidx.appcompat.widget.AppCompatTextView android:id="@+id/textLatLon" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:text="Lat/Long" android:textColor="#cccccc" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textLoc" /> ... </LinearLayout> ... </RelativeLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
From within MainActivity.java
, we make class MainActivity.java
implement LocationListener
and override onLocationChanged()
to report live location of the mobile device the app resides on. Note that the double-typed lat/lon values are being truncated to 4 decimal places via String.format("%.4f", ...)
. That gives a location precision of down to ~10 meters, which is about the proximity of a building. Depending on the specific use case, one can adjust the precision limit accordingly.
MainActivity.java:
import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.Address; import android.location.Geocoder; import java.util.List; import java.util.Locale; public class MainActivity extends AppCompatActivity implements LocationListener { ... protected String provider; protected LocationManager locationManager; protected LocationListener locationListener; protected TextView textLocation; protected TextView textLatLon; protected String latitude,longitude; protected Geocoder geocoder; protected List<Address> addresses; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ... textLocation = (TextView) findViewById(R.id.textLoc); locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED) { locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, this); } geocoder = new Geocoder(this, Locale.getDefault()); ... } ... public String addressOf(double latitude, double longitude) { try { addresses = geocoder.getFromLocation(latitude, longitude, 1); String address = addresses.get(0).getAddressLine(0); /* String city = addresses.get(0).getLocality(); String state = addresses.get(0).getAdminArea(); String country = addresses.get(0).getCountryName(); return address + ", " + city + ", " + state + ", " + country; */ return address; } catch (IOException e) { e.printStackTrace(); return e.getMessage(); } } @Override public void onLocationChanged(Location location) { double lat = location.getLatitude(); double lon = location.getLongitude(); textLocation = (TextView) findViewById(R.id.textLoc); textLocation.setText(addressOf(lat, lon)); textLatLon = (TextView) findViewById(R.id.textLatLon); textLatLon.setText("( " + String.format("%.4f", lat) + " , " + String.format("%.4f", lon) + " )"); } @Override public void onProviderDisabled(String provider) { Log.d("Latitude","disable"); } @Override public void onProviderEnabled(String provider) { Log.d("Latitude","enable"); } @Override public void onStatusChanged(String provider, int status, Bundle extras) { Log.d("Latitude","status"); } }
The Android app is now capable of displaying live time and locale info on the camera screen. To record videos that are overlaid with the displayed info, the simplest way would be to install a 3rd-party screen recorder such as AZ Recorder. To make the feature a part of a custom mobile app, integrating the screen recording functionality into the app might be necessary. That’s what we’re going to do in the next blog post. On top of that, we’ll explore how such features can be useful in some interesting use cases.