We have one more step! It's the practical use of our database classes and performing CRUD operations. We will extend the application to create notes, and we will focus on insertion.
Before we insert anything into the database, we must provide a mechanism to obtain the current user location since it's required for both notes and todos. Create a new class called LocationProvider and locate it in the location package as follows:
object LocationProvider { private val tag = "Location provider" private val listeners = CopyOnWriteArrayList
<WeakReference<LocationListener>>() private val locationListener = object : LocationListener { ... } fun subscribe(subscriber: LocationListener): Boolean { val result = doSubscribe(subscriber) turnOnLocationListening() return result } fun unsubscribe(subscriber: LocationListener): Boolean { val result = doUnsubscribe(subscriber) if (listeners.isEmpty()) { turnOffLocationListening() } return result } private fun turnOnLocationListening() { ... } private fun turnOffLocationListening() { ... } private fun doSubscribe(listener: LocationListener): Boolean { ... } private fun doUnsubscribe(listener: LocationListener): Boolean { ... } }
We exposed the main structure for the LocationProvider object. Let's take a look at the rest of the implementation:
The locationListener instance code is as follows:
private val locationListener = object : LocationListener { override fun onLocationChanged(location: Location) { Log.i( tag, String.format( Locale.ENGLISH, "Location [ lat: %s ][ long: %s ]",
location.latitude, location.longitude ) ) val iterator = listeners.iterator() while (iterator.hasNext()) { val reference = iterator.next() val listener = reference.get() listener?.onLocationChanged(location) } } override fun onStatusChanged(provider: String, status: Int,
extras: Bundle) { Log.d( tag, String.format(Locale.ENGLISH, "Status changed [ %s
][ %d ]", provider, status) ) val iterator = listeners.iterator() while (iterator.hasNext()) { val reference = iterator.next() val listener = reference.get() listener?.onStatusChanged(provider, status, extras) } } override fun onProviderEnabled(provider: String) { Log.i(tag, String.format("Provider [ %s ][ ENABLED ]",
provider)) val iterator = listeners.iterator() while (iterator.hasNext()) { val reference = iterator.next() val listener = reference.get() listener?.onProviderEnabled(provider) } } override fun onProviderDisabled(provider: String) { Log.i(tag, String.format("Provider [ %s ][ ENABLED ]",
provider)) val iterator = listeners.iterator() while (iterator.hasNext()) { val reference = iterator.next() val listener = reference.get() listener?.onProviderDisabled(provider) } } }
LocationListener is Android's interface whose purpose is to be executed on location events. We created our concretization that will basically notify all subscribed parties about those events. The most important for us is onLocationChanged():
turnOnLocationListening(): private fun turnOnLocationListening() { Log.v(tag, "We are about to turn on location listening.") val ctx = Journaler.ctx if (ctx != null) { Log.v(tag, "We are about to check location permissions.") val permissionsOk = ActivityCompat.checkSelfPermission(ctx,
Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(ctx,
Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED if (!permissionsOk) { throw IllegalStateException( "Permissions required [ ACCESS_FINE_LOCATION ]
[ ACCESS_COARSE_LOCATION ]" ) } Log.v(tag, "Location permissions are ok.
We are about to request location changes.") val locationManager =
ctx.getSystemService(Context.LOCATION_SERVICE)
as LocationManager val criteria = Criteria() criteria.accuracy = Criteria.ACCURACY_FINE criteria.powerRequirement = Criteria.POWER_HIGH criteria.isAltitudeRequired = false criteria.isBearingRequired = false criteria.isSpeedRequired = false criteria.isCostAllowed = true locationManager.requestLocationUpdates( 1000, 1F, criteria, locationListener,
Looper.getMainLooper() ) } else { Log.e(tag, "No application context available.") } }
To turn on location listening, we must check if permissions are properly fulfilled. If that is the case, then we are obtaining Android's LocationManager and defined Criteria for location updates. We defined our criteria to be very precise and accurate. Finally, we request location updates by passing the following arguments:
- long minTime
- float minDistance
- Criteria criteria
- LocationListener listener
- Looper looper
As you can see, we passed our LocationListener concretization that will notify all subscribed third parties about location events:
turnOffLocationListening():private fun turnOffLocationListening()
{ Log.v(tag, "We are about to turn off location listening.") val ctx = Journaler.ctx if (ctx != null) { val locationManager = ctx.getSystemService(Context.LOCATION_SERVICE)
as LocationManager locationManager.removeUpdates(locationListener) } else { Log.e(tag, "No application context available.") } }
- We stopped listening for location by simply removing our listener instance.doSubscribe():
private fun doSubscribe(listener: LocationListener): Boolean { val iterator = listeners.iterator() while (iterator.hasNext()) { val reference = iterator.next() val refListener = reference.get() if (refListener != null && refListener === listener) { Log.v(tag, "Already subscribed: " + listener) return false } } listeners.add(WeakReference(listener)) Log.v(tag, "Subscribed, subscribers count: " + listeners.size) return true }
- The doUnsubscribe() method code is as follows:
private fun doUnsubscribe(listener: LocationListener): Boolean { var result = true val iterator = listeners.iterator() while (iterator.hasNext()) { val reference = iterator.next() val refListener = reference.get() if (refListener != null && refListener === listener) { val success = listeners.remove(reference) if (!success) { Log.w(tag, "Couldn't un subscribe, subscribers
count: " + listeners.size) } else { Log.v(tag, "Un subscribed, subscribers count: " +
listeners.size) } if (result) { result = success } } } return result }
These two methods are responsible for subscribing and unsubscribing in location updates to interested third parties.
We have all we need. Open the NoteActivity class and extend it as follows:
class NoteActivity : ItemActivity() { private var note: Note? = null override val tag = "Note activity" private var location: Location? = null override fun getLayout() = R.layout.activity_note private val textWatcher = object : TextWatcher { override fun afterTextChanged(p0: Editable?) { updateNote() } override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2:
Int, p3: Int) {} override fun onTextChanged(p0: CharSequence?, p1: Int, p2:
Int, p3: Int) {} } private val locationListener = object : LocationListener { override fun onLocationChanged(p0: Location?) { p0?.let { LocationProvider.unsubscribe(this) location = p0 val title = getNoteTitle() val content = getNoteContent() note = Note(title, content, p0) val task = object : AsyncTask<Note, Void, Boolean>() { override fun doInBackground(vararg params: Note?):
Boolean { if (!params.isEmpty()) { val param = params[0] param?.let { return Db.NOTE.insert(param) > 0 } } return false } override fun onPostExecute(result: Boolean?) { result?.let { if (result) { Log.i(tag, "Note inserted.") } else { Log.e(tag, "Note not inserted.") } } } } task.execute(note) } } override fun onStatusChanged(p0: String?, p1: Int, p2:
Bundle?) {} override fun onProviderEnabled(p0: String?) {} override fun onProviderDisabled(p0: String?) {} } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) note_title.addTextChangedListener(textWatcher) note_content.addTextChangedListener(textWatcher) } private fun updateNote() { if (note == null) { if (!TextUtils.isEmpty(getNoteTitle()) &&
!TextUtils.isEmpty(getNoteContent())) { LocationProvider.subscribe(locationListener) } } else { note?.title = getNoteTitle() note?.message = getNoteContent() val task = object : AsyncTask<Note, Void, Boolean>() { override fun doInBackground(vararg params: Note?):
Boolean { if (!params.isEmpty()) { val param = params[0] param?.let { return Db.NOTE.update(param) > 0 } } return false } override fun onPostExecute(result: Boolean?) { result?.let { if (result) { Log.i(tag, "Note updated.") } else { Log.e(tag, "Note not updated.") } } } } task.execute(note) } } private fun getNoteContent(): String { return note_content.text.toString() } private fun getNoteTitle(): String { return note_title.text.toString() } }
What did we do here? Let's go from top to bottom and explain everything! We added two fields--one that contains the current Note instance we are editing and one that holds information about the current user's location. Then, we defined a TextWatcher instance. TextWatcher is a listener that we will assign to our EditText views, and, on each change, the proper update method will be triggered. That method will create a new note class and persist it into a database if it does not exist, or perform a data update if it exists.
Since we will not insert note until there is no location data available, we defined our locationListener to put received location into the location field and to unsubscribe itself. Then, we will get the current value for the note title and its main content and create a new note instance. Since the database operations can take some time, we will execute them asynchronously. For that purpose, we will use the AsyncTask class. The AsyncTask class is Android's class that is intended to be used for the most async operations. Class defines input type, progress type, and result type. In our case, input type is Note. We do not have a progress type, but we have a result type Boolean, that is, if the operation is successful or not.
The main work is done in the doInBackground() concretization while the result is handled in onPostExecute(). As you can see, we are performing insertion in the background using classes we recently defined for database management.
If you keep looking, the next thing we do is assign textWatcher to EditText views in the onCreate() method. Then, we define our most important method--updateNote(). It will update an existing note or insert a new one if it does not exist. Again, we used AsyncTask to perform an operation in the background.
Build your application and run it. Try to insert note. Observe your Logcat. You will notice database-related logs as your type:
I/Note activity: Note inserted. I/Note activity: Note updated. I/Note activity: Note updated. I/Note activity: Note updated.
If you can see these logs, you have successfully implemented your first database in Android. We encourage you to extend the code for the rest of the CRUD operations. Make sureĀ NoteActivity supports the select and delete operations.