Alright so we're talking about the Uri
Matcher in the last video, and there's
the link to that UriMatcher class in
the description under Content URI
patterns, on the documentation page for
creating Content Providers. So let's go
and click on that and open it in a new
tab. Let's have a look at this Uri
Matcher class. So this class maps
different content URIs to integer
values, that we can use to decide what
code to execute for various URIs.
And you can see down here that it's
described as a utility class to aid in
matching URIs in content providers.
And you can see there that the example
defines a number of constant int values;
PEOPLE is 1, PEOPLE_ID is
2 and so forth. Most of Google's
example code is still in Java, but it
should be easy to see what's going on
here. If the URI just referred to the
PEOPLE table, the UriMatcher would
return 1. If the URI also had an ID on
the end, then Matcher would return 2. Now
we can't do that magically, so further
down in the example code, as we scroll down
there, there's a lot of calls to the add
URI method. These associate the various
URI patterns with the values
that we want the Matcher to return. For
the people table without an ID, the Matcher
is told to return the value of the
people constant, which is 1. But if an
ID is provided, then this code tells it to
return PEOPLE_ID, which is two.
So I'll talk about that hash in a minute,
but it's just a wild card that'll match
any number. And this example goes a
bit further, and allows all the phone
numbers for a specified person to be
returned - that's PEOPLE_PHONES -
or a specific phone number for that
person - PEOPLE_PHONES
_ID. So you might be asking why
do we need an integer value for each URI?
Well if we didn't use the UriMatcher,
then it would have to do things like
counting how many segments the URI path
contains, then checking the value of each
segment to decide what action to take.
Now a bit further down the page - if I
scroll down - you can see over here that
Google have provided an example
of how we can use a switch statement
with the values returned by the Matcher,
compared to an alternative if we don't
use the Matcher. I can't quite get that
all on the screen but you can see the
gist of it there, and Java uses switch in
case instead of Kotlons when, and the
arrow, the right arrow. But hopefully, you
get the general idea with his code even
if you don't know Java. Now don't actually
worry about these return values at
the moment - we're coming back to those
in a minute.
The important thing here, is that the
code that doesn't use the UriMatcher
is a lot more complicated. So there's a
UriMatcher code, and if I scroll down
you can see there. I think clearly you'd
agree that looks a lot more
complicated, and that's because it has to
check the number of segments, then check
specific segments against a table name.
So it's just more complicated. The Uri
Matcher is making our lives easier by
taking care of the matching, and just
giving us an integer value to use in the
switch statement, and obviously in the
when statement if we're talking Kotlin. So
now that we've been through that we can
add the code for the buildUriMatcher
function now, or rather add the rest of
the code.
Now Google have done it slightly
differently here in the code just above
this. I'll scroll up and have a look there,
and you can see here they've added a static
initializer block to call the addURI
method. Tt'll be interesting to see
what they suggest when they produce a
Kotlin version of this example, but I
suspect they'll do the same as what
we're about to do. So going back to our code,
so our lazy delegate that we defined on
line 34, make sure that the UriMatcher
instance is created when we need it, and
lazy also ensures that there's only one
instance created. Now if you read that
article by Christophe Beyls, you'll
see that he based the singleton holder
class on the Kotlin lazy code. Alright so
let's write a bit of code here now and
expand on the buildUriMatcher
method. So at the moment we've got, just
pretty well an empty bit of code there -
I'm going to start. Now firstly we'll
look at the first URI we want to
match, and it'll be e.g. content colon
//learn
programming.academy
dot tasktimer.provider/Tasks
with a capital T. So to add that to the
Matcher we're going to type matcher
dot addURI, and then in
parentheses we'll start with our CONTENT
_AUTHORITY comma. Then it'll be
tasksContract dot tables TABLE
_NAME comma, then TASKS. Then the
next thing we want to match, if I just
copy that comment. This time it'll be for
a specific task, /8
for argument's sake, and that would be
mature.addURI, and in parentheses
CONTENT_AUTHORITY comma. This time
it's going to be, in double quotes, dollar
sign left and right curly braces Tasks
Contract.TABLE_NAME, then right,
closing off the curly brace, the right
curly brace there, forward slash and the
hash for the wild card comma. This time
it's going to be TASKS_ID. Then
what I'm going to do is take a copy of
these. We're going to change this - instead
of TasksContract here we're going to go
with, in this next part,
TimingsContract - then that'll be TIMINGS
on the end there.
And don't worry about these errors, I'll
talk about those shortly. And in the
string it's going to be TimingsContract
TABLE_NAME, and instead of TASKS_ID it's
going to be TIMINGS_ID. And
we'll add one more group here, and this
will be for the DurationsContract.
So I'll come over to there, type Durations, , 149
Durations contract, and then on the end
instead of TIMINGS it's going to be
TASK_DURATIONS. And on the end
here I'll just make that change - that's
TASK_DURATIONS_ID.
And then the actual string - instead of
TimingsCcontract - is going to be
DurationsContract.
Okay, and again don't worry about the errors.
I've included URIs for the timings
and durations table that we'll be using
later, because without them it's less
obvious what this function's doing. Al
right so when the function starts, it
creates a new UriMatcher object and we
can see that on line 38. The constructors
parameter is a value to return if the
route URI is matched. Now we don't
want to re-match the route. A table name must be
specified in the URL. Well that's why
we're specifying NO_MATCH as
the argument on line 38. Alright, so then
we're adding the URIs that we want
to match. So if the Tasks table is
specified without an ID, the matcher will
return 100, the value of our Tasks
constant - that's the code on line 41. Now
if an ID is also provided it'll return
101 - the TASKS_ID constant, and
that's the code on line 44. And for the
Timings table we'll get 200 and 201, which
is the value of TIMINGS and TIMINGS
_ID - the constants that we've
defined. Now we can scroll up and see
those values again, the ones that we've
defined up here earlier in the file, at
the top of the file. Now just to be clear
here, these numbers are totally arbitrary.
We could have just set our constants to
1, 2, 3 etc, which is what that Google
example we just looked at, does. Now
obviously we shouldn't use negative 1
because that's the value of the Uri
Matcher.NO_MATCH constant. And
if you're wondering you can see that -
I'll hover over it and click - you can see
there that no matter is set defined to
be negative 1. And if you haven't already
done so, what I suggest you do is click
on the documentation for one of these
links. So I'm going to do the one for the
first addURI, and it gives us some
information. So you can see that the
first parameter here's the authority, and
the second one is the path that we want
to match. So the path that we want to match
can contain wild cards, either a hash
or an asterisk. In this example we used
a hash because we want to match any
numeric ID. If you want to match any
text, then you could use an asterisk
which will match numbers as well as text.
Now the use of the hash here is maybe
unfortunate, as I mentioned in the
previous video when were looking at
URIs, because the hash character is used
to separate a URI fragment in the
specification that we looked at. So here
though, the hash is something that the
uriMatcher class interprets to mean any
numeric value. And it's not used in the
same way as the hash that separates a
fragment from the rest of the URI that,
again, we saw in the specification
relating to URIs. Now there's no
confusion as far as the class is
concerned, because this second parameter
is only matching the path part of the
URI. So the third parameter is the code
that we want the matcher to return, if it
matches the URI. So go back to our code
here; if the URI just refers to the Tasks
table then the matcher would return 100,
and if there's an ID on the end of the
URI
it'll return 101. And I've also included
the code to match the timings and
duration URIs, just so you can see
how we get different values returned
when dealing with the different
URIs. Now at this point we haven't
created the TimingsContract and
DurationContract classes, so I'm going
to comment out those lines just to
remove the errors, but we'll uncomment
this code once the classes have been
written in later videos. So hopefully now,
the reason for these constants that we
defined at the top of the file should
now be apparent. They're the values that
the uriMatcher will be returning for us
to ultimately match the URIs against.
Alright so let's scroll down and look at the
code, start implementing some code now.
And we'll start by getting the onCreate
method out of the way, then we can see
how to use this your uriMatcher. Now
although it's not important, I prefer to
arrange these functions in a
more logical order. So onCreate, to mind,
should come first, and I expect to find
it to the top of the class code. So I'm
just going to move these functions
around a little bit, so start with
onCreate. So I'm just going to put that
right at the top here, and the next one
I'd expect to see there is getType - grab
that as well -
and put that below the onCreate. And the
next one would be query, then we're going
to move that. And then the other three;
insert, update and delete, are fine in
that order. And again, that's more a
personal preference thing, but generally
I would expect to be using things in
that order, and therefore seeing the
functions defined in that order as well.
And what I'll also do is take the
opportunity to remove the nullable type
marker from all the Uri parameters. So let's
go ahead and do that. Now these functions
won't be called with a null Uri.
If they are, something's gone wrong,
and I'd much rather that was picked up
by the compiler rather than having the
app crash. And the same's actually true
of the content value parameters - these
ones here from the insert and update
functions. There's really no point
passing null for the values to insert,
for example. It's just not going to work.
So I'm going to remove the question mark
there -
the nullable type marker - from both the
insert method for ContentValues, as well
as the ContentValues for update. Alright,
so getting back to our onCreate
function, it's very simple - very, very
simple. I'm not going to do anything in
fact, in the code, but I'll start by
deleting this TODO. Now we could create
our database class in here, but as we've
implemented it as a singleton, we'll just
use the getInstance function whenever
we need a database instance. But this on
Create function has to be implemented
though, and should return true. And just
out of interest, it's worth demonstrating
that we can't create instances of App
Database, so I'll try and do that now. But
I'll start with a log, Log.d
parentheses TAG comma onCreate colon
starts, and just to be consistent I'm going
to add a colon up here - buildUri
Matcher: starts as well, for the
log. Alright, so I'm trying to create an
instance of our AppDatabase; val app
Database equals AppDatabase, and in
parentheses context, and if I hover over
it you can see, you "Cannot access init:
it's
private in AppDatabase". So that's
actually what we want here: any attempt
to create an AppDatabase instance will
fail, and that's because we have to use
the getInstance function. So I'm going
to delete that line now just to remove
that error, but what I will do is return true.
Alright, so I mentioned that on
Create's easy - it certainly was. I've added logging
so that we can see the flow events in
the logcat. But let's get back to using
the uriMatcher - this time in the query
function, which is the third implemented
function. And I'm going to get back to
getType, the getType function later. So
the first parameter in the query, the
query function here - and I'll delete the TO
DO - so again, the first parameter is the
URI. So we're going to start by
using our uriMatcher to work out what
kind of URI we've been given here.
So I'm going to type Log.d
parentheses TAG comma, as a comment
query colon called with uri dollar
uri, so I can see in logcat what it
looks like. But the actual code will
be val match equals uriMatcher dot
match, in the parentheses, uri.
Then we'll do a log for that as well; Log
.d parentheses TAG comma, and it'll
be query again :
match is $match. Alright, at this
point now we can use the value of match
to decide which URI was passed into
this query method, so that we know which
table we should be using, for example. So
the next thing we want to do, is use a
queryBuilder to build the query that
will be executed against the database. So
to do that I'm going to type val query
Builder colon and it's going to be SQLite
QueryBuilder equals, and it's going to be
SQLiteQueryBuilder. Now to get the
documentation for the SQLiteQuery
Builder,
make sure you click on the type over here
and not the constructor call. And you can
see here that the documentation tells us
that this is a convenience class that
helps build SQL queries, to be sent to
SQLite database
subjects. So that's pretty useful. It's
always easier to use a builtin object
for building things like URIs or
queries. The alternative is parsing and
concatenating strings, which while not
being terribly difficult, can quickly get
messy. Alright to see how we use the
SQLiteQueryBuilder, I'm going to
paste in the rest of the code for this
function, so that we can see what it does
while I explain it. So let me just grab
that code, and we'll actually get some
errors here, and I'll deal with them once
we've talked about what the code's doing.
So I'm going to put this below the query
Builder variable, the variable definition
there. Now I'm also going to take the
opportunity up here, to remove the
redundant type declaration. I only added
that so we had something to click on to
see the documentation. So I mentioned
we're going to get some errors. They're
there mainly because, mostly because this
includes the code for querying the
timings and durations tables that we
haven't created Contract classes for. And
by the way, if you want to grab the code
for this that I've just, the code for
what I've just pasted in, it is in the
resources section for this video if you
want to go ahead and type that in.
So again, I've added the code here for
the timings and duration tables, because
if there was only one table, it may not
have been as obvious why we need to
bother with the URI in the first
place. I'm going to comment out the
timings and durations code in a minute,
but seeing all the code does make it
easier, hopefully, to see why a Uri
Matcher class is so useful. Alright so
we're using a when here, a when statement to
choose different blocks of code,
depending on the result of matching the
URI. If the URI just contained a
table name, the basic query's pretty
simple. It's just a SQL select with a
TABLE_NAME. You can see that there - the
code on line 74. And same for timings,
line 82, and durations, line 90. So
basically, when the URI matches TASKS,
TIMINGS or TASKS_DURATIONS, we'll just
call the setTables method to tell the
queryBuilder which table we want to
query. So the URI matches TASKS, TIMINGS
or TASKS_DURATIONS. We're just
using, we're just setting the tables
property here, to tell the queryBuilder
which table we want to use.
And if we check the documentation there
for that, you'll see that we can also
specify more than one table and we can
perform a join. So that's certainly one
way of doing things, but another approach
is to perform the join in the database
using a view. So using a view made
life easier in a SQL tutorial
earlier in this course, and it also
simplifies the code here in the provider.
Now you can do it either way of course,
but there's really no reason why the
content provider should care about how
the data's organized in the database.
Implementation details like that really
belong in the database, so the less our
code relies on knowing about the
implementation, the easier it is to
change things in the future. This code
though, as it stands, does have a
potentially serious flaw though. Using
queryBuilder.appendWhere, like
we're using here, for example, on line 79,
may introduce the potential for SQL
injection attacks. In fact, if you've
uploaded code like this to the Google
Play Store, you'll get a warning and be
told to fix it. So rather than using
appendWhere, we'll use, the suggestion is to use
appendWhereEscapeString, so I'm going
to change that. And I'll take the
opportunity to do that for all three. So
let's call the, call up the documentation
for that function. So the relevant part, and
the difference from the appendWhere
method that we're using, is this final
sentence of the documentation, "it will be
escaped to avoid SQL injection
attacks". Now at the moment the lint
checker doesn't pick up on this, and
allows calls to appendWhere to get
through with no warning. Until that
changes be vigilant, and use the append
WhereEscapeString function instead of
appendWhere. Alright, so moving on
now. If an ID is included in the URI,
then the matcher will return TASK
_ID, TIMINGS_ID or TASK
_DURATIONS_ID. So in
that case, we call the getId method of
our Contracts class, and there's an example there
on line 78, and that'll extract the ID
for the URI, for us. Now this is another
example of delegating responsibility
where it belongs. Our Contract
classes have intimate knowledge of the
URIs, so it should be down to them to
pass information out of the URIs.
There's no need for the content provider
to know that the ID appears in the
second segment of the path - it just needs
to get the ID from the URI. Now
we'll add that function to the
Contract class in the next video. Now if
there's an ID included in the URI, we'll
call the queryBuilders appendWhere
EscapeString function, and this adds a
where clause to the query, and here the
where clause will just be _ID
equals whatever the actual ID value was.
Alright, so I'm going to take this opportunity
just to comment out the code now, for the
tables that we haven't created yet. And
in the next video we'll start work on
getting that getId function in a Task
contract class written, and make a few
other changes. So I'll see you in the
next video.