Onorato: I’m Joe Onorato.
This is Romain Guy. We’re both framework engineers
on the Android team. We’re gonna talk about
supporting multiple devices. Guy: But not quite.
Onorato: But not quite. Well, if the code’s gonna run
on more than one device, we’ve got some–well, we’re
gonna do it on the emulator. And the reason
I sort of didn’t want to talk too much
about multiple devices is all the devices we have now
are pretty much the same. There’s– Guy: I also didn’t really
know the subject, so. Onorato: Well, that too. [laughter] So it’s– There’s the Dre–the G1. I’ll probably call it
the Dream at some point. Guy: That’s okay,
you can call it the Dream. Onorato: We’re used to that. Then there’s the Ion,
or the Magic, or whatever
we’re calling that, and, um… they’re almost identical
in terms of hardware. One has a keyboard, obviously,
but other than that, the screen size
is the same. That’s far and away
the hardest thing that you guys
have to deal with. Guy: The same physical
screen size and physi– resolution and same density,
so the same screen. Onorato: Same part number
for the screen, so. Okay. Oh, I’m supposed to say that if
you want to provide feedback– Or please provide feedback
on this presentation at haveasec.com/IO. Guy: And I was supposed to say
that for my previous session, so now you know. [laughter] Onorato: How many of you– How many of you guys came
from Romain’s previous–oh, God. Okay.
[laughter] Well, good–
good plugging, I guess. All right, so our motto
is “write once.” No, that’s not. But we do have– We do want you to not
have to write a different version of your app
for every single thing. Guy: Except we don’t promise
that you will have nothing to do to make your app work
across the bases. You can write it once, but you might have
more work the first time. Onorato: In fact,
right now the market– You can actually load–
upload one copy of your app, so you actually do
have to have it working with one binary
for the whole–whole world. But I know
they’re working on that too. So here’s some– some of the things
you have to worry about. There’s OS versions 1.0, 1.1, it’s on there,
Cupcake, Donut, so. I think we let it loose that
it’s called Eclair last night. Guy: Not just last night.
It’s public. Do you guys know
what Eclair is? Onorato: It’s E. Guy: I’m not talking
about the pastry. Okay, good. Onorato: They had eclairs
at the party last night too. So I think that
that gave it away too. The… So there’s Donut,
and luckily most people– All the devices,
as far as I know of, are getting
updated to cupcake. So we don’t have
too much of this multiple version thing
to worry about yet, but this is the mobile industry;
the phones don’t last forever. So at some point people
will have old versions. Guy: Well, and let’s
admit it. You like to change phones
every now and then. Onorato: You know, I have one
every, like, two months or so. Then I drop it
or break it or something. Guy: Yeah, you drop it. Onorato: Drop it. Cupc– Okay, so the other thing
we have is configuration. So there’s– Let me get the– There’s locales, orientations,
which operator–that’s carrier. A lot of other stuff. Guy: Actually, we have
good documentation on that. Onorato: We do, actually. That’s the other reason I didn’t want to talk
too much about this. Because the docs on this
are actually pretty good. I have a slide
for that later, so. As the URL. You know, these animations
are, like, killer. Guy: Powerpoint. Onorato: So this is
configuration object. It’s “android.content.
res.configuration” in the docs. And it’s got these
different parameters here. These are the– I’m not gonna read them all,
but you can– you can see them whether
it’s a portrait, or a landscape, or just density of the screen;
we’ll get to density. Guy: Yes, basically
what you see here like the values on the right,
yeah, like you said, it’s for the G1, but that should
give you a fair– a fairly good idea
of, you know, the kind of things
that might change from one device
to the other. So you know,
obviously maybe you don’t care, but when another keyboard
is visible when you want
to display button on screen, but then maybe
the resolution will matter. Onorato: Might need
to make a smaller button. Guy: Yeah.
Onorato: Or a bigger button. Guy: It depends
on your app. And we’re gonna focus primarily
on density and screen resolution ’cause those are the ones
we think that, you know, will be the most difficult
for you guys to handle. Onorato: Yeah,
so for the stuff like locale, just read the docs;
it’s in there. And I have a URL
for that later. All right, so this is the app
that we’re gonna work with. Looks kind of like
the launcher. It’s based on the code
that Romain had– Guy: It’s a very
useful app. Onorato: Well, it has
little bug droids on there. All right, so this
is running in HVGA. That’s 320×480. Guy: So that’s G1.
Onorato: This is what G1 is. Guy: On the phone
that you got yesterday. Onorato: And so what if
it ran on a QVG device? That’s… Oh, AbsoluteLayout.
Okay. You could’ve written that
with AbsoluteLayout. I don’t even know why
we have AbsoluteLayout– Guy: But if you do that,
I’m gonna hurt you, so don’t. [laughter] Onorato: The worst part
about it, though, is that it’s got this–if you
see down there at the bottom, you have to list the position
of every single view. So that’s tedious, right? For this view here, that’s–
what is that– 12 different coordinates
you have to come up just for that one thing. This file’s
sort of annoying to write. All right, so this is what
it would look like with QVGA if you used
AbsoluteLayout. You can see that we’re
missing one of them. So that’s pretty much
what we’re gonna talk about is how to write–
how to write this view, and have all of your
bug droids show up. Guy: Yeah, how to
do it correctly. Onorato: How to
do it correctly. Another thing. For Donut,
one of the– Well, for Donut
we’re gonna be supporting three different
screen resolutions. Guy: Well, we haven’t
decided yet. Let’s say densities. Onorato: Densities. Okay. But for all these,
the screen is gonna be about the same
physical size, so we’re not gonna– If we have a WVGA device
that’s 480×800, it’s, you know, gonna be
about this big still. It might be–it’ll be prettier
because it’s got more pixels, but it’s–it’s not
gonna be this big. So if you use
that AbsoluteLayout, what you’d see
is tiny, little ones that might be too small
to hit with your finger. And they didn’t
wrap correctly, either. Guy: Yeah, and if you
use a QVGA device, then you have
the opposite problem where everything
is too big. So if you think the buttons
are too big already on the G1, wait till you try that
on a QVGA device– Onorato: You get
two emails per screen, yeah. Guy: So screen density is–
is a complex issue. And actually in the framework,
we built a bunch of things to help you
deal with that. You probably already encountered
something called DIPs that we’re gonna
talk about. The DIPs are–are our special
way to define pixels that will scale correctly
across all the densities, and everything will
look the same size– the same physical size,
across the devices. But let’s get going. Onorato: All right.
Absolutely, that sucks, okay. Guy: Yeah, and actually you
can’t really use AbsoluteLayout anymore ’cause
I duplicated it in Cupcake, and people already
hate me for that. The layout will not disappear,
so you can keep using it, but it’s there to show you that
it’s probably not what you want. Onorato: All right. So Romain talked a bit
in his last talk about writing a custom–
custom view. He called it
custom view layout. Or what did you call it? Guy: Custom view,
and then custom ViewGroup. So here it’s
a custom ViewGroup. Onorato: This is actually
gonna be a custom ViewGroup. Guy: Or custom layout. Onorato: They’re all
subclasses of views, so what are you
gonna do? All right,
these are the links. These are all on
developer.android.com, which is the homepage
for all the developer– all the SDK docs
and stuff. And you have a pretty much
complete copy of that in the SDK itself, too, so if
you’re on a plane or something it still works. Guy: And also
if you look, we did– sometimes we do write
good documentation. So if you look at the Java doc
for the view class, there’s actually a huge block
of text at the beginning, and it explains many things
about layouts and custom views. But apparently people
ignore that kind of stuff. So that’s why we have our docs.
Onorato: But it is there, yeah. All right, so… All right, so we’re gonna
go and write our custom view. Four things we’re
gonna worry about. The first thing, we got to figure out
how big our view wants to be. How big it’s gonna tell
its parent that it wants to be. Although its parent
is free to ignore that. Guy: Yeah, often they do.
Onorato: Often they do. The lay–Then we’re gonna
do the layout, and the first iteration here
we’re actually gonna ignore what our children said
and just do what we want, which is valid. Then there’s units,
and the last thing, we want to make sure that you
guys can use your view– this custom view
from an XML file. It’s not hard, it’s actually less lines of code
than I thought it was. Guy: It’s not hard,
you just–It’s– Onorato: It’s just
ten lines of code that you have to know
what they are. Guy: They’re confusing. Even, you know, ourselves, like,
we can’t remember what they are, and we always have to
look at samples that we wrote. Onorato: But luckily now we can
watch the YouTube video of it. Guy: Yeah, or other sites. Onorato: Okay,
let’s talk about units. Actually, no, let’s not
talk about units. Let’s write the code. Guy: Yeah, units are boring.
Onorato: Okay. All right. So first thing we’re gonna do–
I’ve got my little script here– is go into our SDK folder,
which is– it’s one on my desktop
right here. And create the project… Guy: So we’re gonna take you
through the different steps of creating a new layout, and
we’ll start with the–you know, The basic, dumb layout,
and we’ll iterate on that until we get something
that actually works and that can be reused,
and that works across devices. And at the end we’re gonna
submit that to the Android
open source project, and we’re gonna commit it
in the actual source code base. So you can see
how it’s done, and also see
what kind of stuff you have to do–if you ever want to, you
know, commit code that ends up
in your project, like, shat kind of things
you have to support. Onorato: So, uh… It didn’t work.
Perfect. Guy: [clicks tongue] Onorato: “-Target.” Is it working?
audience member: Can’t see. Onorato: You can’t see that? Is it too low
or is it too small? Guy: Too low. Onorato: How’s that? There we go. Okay.
We created our file. So we’re gonna go in there.
That’s back here. I’ll move that
up for you. All right, do you want
to talk while I’m typing? Guy: Yeah.
Onorato: Okay. Guy: So yeah, so Joe– I don’t know if you’ve used
the Generate 1.5 SDK You probably used the Eclipse
plug-in ’cause, you know, it’s easy, it’s shiny,
you click buttons and stuff. But Joe just created the project
from the comment line, so we have
this historical Android, a simple executable that will
generate the project for you. Or the directories,
the ant build file, uh, the Manifest, and even,
I think, a couple of Java files. So now that we have
the project created, we can, you know, go ahead
and just add back ant install or entry install and it’s gonna
compile the project, ship it to whatever device you
have connected to the computer. And when I say device,
it can also be the emulator. Like anything we do,
it’s the same for the actual physical phones
and the emulator. Onorato: So I just started
the emulator, here. Guy: So we’re starting
the emulator. And hopefully it’s not
gonna take too long. ‘Cause we have
nice machines at Google. Next time start it
before the talk. Onorato: I know, I meant to.
Sorry. And once–See, the command here
is “ant reinstall.” I don’t know why
it’s reinstall. Guy: It is reinstall
because– Okay, there’s a difference
between installing the APK and reinstalling it. If you try to install an APK
that’s already on the phone it’s gonna fail, so you have to
use a flag called -r if you use the ADB install
command and reinstall. Make sure that
even if the application is already on the phone,
the install will work. Usually when you’re in Eclipse
and you click the “Run” button, that’s actually
what Eclipse does. It trans–it’s reinstall command
behind the scene. Onorato: All right,
well, I was gonna run the project we just created, but
it’s taking its time to boot up. So it’s the hello world one that
you guys have probably all seen. Guy: Yeah, let’s go
to next step. Onorato: Yeah, let’s just
go to the next step. I’m gonna get–I’m gonna
open up my file, here. This is the image.
Our little, like, bug droid guy. And I’m gonna copy that
as “drawable.” Copy that
into my “drawable.” Into there. And… Same thing with the… XML layout file. So you guys have seen this,
probably from the GUI, there. Let’s open that up now. And for this–
just to get started, I always like to start
with just a, you know, some simple layout
that’s already there. So we picked
linear layout. It’s got these… Should be up
by now. Good. We can run it,
so let’s ant reinstall. It’s compiling it. And it wor–failed. Guy: Device not found.
Interesting. Try again.
Onorato: Perfect. Oh, you know what,
let’s do… Guy: Yeah, that always works.
Onorato: This always happens. I don’t know why ADB doesn’t
work, but it doesn’t, so. Let’s–let’s kill
the server. Guy: But you–At least you
get to learn, you know, how– Onorato: This is my life. Guy: Yeah, this is actually how
we do things on the Android team ’cause we don’t have the luxury
of using the Eclipse plug-in. Pardon me?
audience member: [indistinct] Onorato: That’s what I’m doing.
Oh, there we go. Kill all ADB. Guy: Or ADB kill-server.
Your best friend. So, yeah, I was saying,
we don’t have the– you know,
the fancy Eclipse plug-in, and I’m always jealous
of you guys whenever, you know, I see a demo
of the Eclipse plug-in ’cause it’s so easy. And we have to do all that crap
in the command line. But it works.
Onorato: Okay. Should be here now. Guy: Yay. Onorato: Let’s put it
on our desktop. There it is.
They’re in a row. All right. That took a while, but… Okay, so the next–
the next thing– Guy: You wanted a grid,
not a column. Onorato: Yeah, we want a grid. So let me– Let me copy
that other file over. Guy: Now you get to
see the code. Onorato: Yeah, now you get
to see the code. That was–Maybe we should’ve
done that beforehand. Sorry. We wanted to show you the–
sort of the whole step. I mean,
it’s a little boring, but… All right,
here’s the view. Guy: Feel our pain,
damn it. Onorato: You want
to talk about these? Guy: Yeah, so that’s–
for those of you who were
at the previous session, this is, like,
how you create a layout. So you extend
the ViewGroup class. So for some weird reason–
it’s probably his fault– we have this
classical ViewGroup, and the children–I mean,
the subclasses of ViewGroup are called BlahLayout. That makes no sense at all,
but that’s how it is. So you extend
the ViewGroup class when you want to create
your layout class– Onorato: That is actually
my fault. Yeah, sorry. Guy: Yeah, I’m pretty
sure it is. [laughter] And you need at least
two constructors if you want to use
the layout from XML. Which is what we want
’cause we all love XML. It’s so easy and nice. The first constructor
is the one you have to use when you want to instantiate
a view or ViewGroup from code. But we don’t care about that.
Like, you know. Writing code for UI is bad;
we want everything in XML because from XML we can support
different configurations. And, you know, if you don’t know
what I’m talking about, refer to the documentation to
the links that Joe talked about. So the constructor that you’re
interested in is the second one. So you have a second parameter
called the attribute set, and it’s basically
just a big bag of values that come from XML. So later we’re gonna
use those attributes to extract the information
we want from the XML file. Right now we’re gonna,
you know, keep using the attributes
from the parent class, so we have nothing to do,
which is always nice. And that fixed grid layout
is pretty dumb. You have to specify the width
and the height of each cell, and we’re gonna
force the children to have those
dimensions. So here you have
two methods. Set cell width,
and set cell height. You specify the dimension pixel,
and you can see that we have this request layout call. So whenever you change
the size of the cells, we want the screen
to be refreshed so we call a request layout
that will trigger a re-layout
of the whole screen, and of that layout
in particular. And if we scroll down,
we have– We have the fun part. So onMeasure– We said before that before you
want to do a layout, you have to measure
the children because you want to know
how big they want to be. It can actually matter. In that case, you can see
the middle with the loop that go through
all the children and call measure
on the children. So we–we–
So we said that before. Then at the end
we have the usual setMeasuredDimension
that tells our parents how big we want to be. And you can see
a lot of, you know, weird stuff going on
with the MeasureSpec and the makeMeasureSpec. And do we want
to go into details about that right now
or later? So basically–
So let’s talk about– a little bit
about the MeasureSpec. So when you–
In the onMeasure method, what you get are actually
not just dimensions. Like the two parameters
widthMeasureSpec and heightMeasureSpec, are not how many pixels
your parents want you to be. They’re hints
from your parent telling you what size
you should be. So–And we have to do
the same with our children. So let’s ignore
the parameter for now, and look at the–
look at the first two lines. So we create
two new specs. So we use
the makeMeasureSpec method and we use the cellWidth
and the cellHeight that are defined
by the two setter methods that we just saw before. And we pass– as a second parameter
to the makeMeasureSpec method, the operation we want to use
with these dimensions, there are three
in Android. That’s three,
that’s two. Three operations
in Android. We have EXACTLY,
AT_MOST, and UNDEFINED. So here, we know
the size of the cells, so we want to tell our children that they can be
AT_MOST 80 pixels, for instance, or AT_MOST, you know,
whatever the user specified. If we wanted
to force the children to be exactly 80 pixels, we would say,
“MeasureSpec.EXACTLY.” And if we don’t know
what we want– it actually happens
sometimes; the Internet loves to do
that kind of stuff– we would just say,
“UNSPECIFIED,” and then the size you pass
doesn’t matter. So here we just
create our MeasureSpec, and we say, please be
AT_MOST, you know, whatever cell width
and cell height have. We call measure
on the children, the children will look
at those units, and will set their dimension
according to that. And the way you do it is
at the end of your onMeasure you can see
that we call resolveSize. So resolveSize is a method
of the view class. You pass it the size
that you want to be, so here the grid layout
is saying, okay, I have a number of–
I have N children, so that’s
the count variable– I know that each child can be
AT_MOST cell width in pixels, and so that’s the size
I want to have. Onorato: What we’re
telling here, though, is we’re telling
the children that–that– that–what would
they like– We’re asking them,
“What would you guys like “if these were
your constraints? “So if you could be
AT_MOST 80 pixels, in this case, what would you do?” And we’re giving them
a chance to sort of traverse down the hierarchy
and figure out what they would do in that case,
and then we’ll bubble up. Guy: Yeah, but so at the end we
do what our children do with us. Like we tell our parents, you
know, what size we want to be. And the resolveSize– Yeah, I know, it’s… Onorato: So the one–
So this is a simple one ’cause we’re sort of
ignoring all the details, but for a linear layout
it’s actually– takes into consideration
what its children say, and comes up
with a size for itself. And that’s–
that’s how you– That’s how the wrap content
mode is implemented. Guy: And so this
resolveSize thing– we pass the widths
that we think we want to be, and we also pass the MeasureSpec
that our parent gave us. And that MeasureSpec can be
AT_MOST, EXACTLY, whatever. And the resolveSize, we simply
look at those two values and figure out
what we should be. So you can ignore
the magic that’s inside, basically always
use resolveSize, pass the MeasureSpec
that your parent gave you, and just indicate what
you think you would like to be. It will, you know, figure out
some magic value that works. Onorato: So if they
said EXACTLY, you’ll get exactly what–
it’ll, you know, use that one. Guy: So when you’re
done with the measurements– and I know the measurements
is very confusing. Believe me, the first two months
I was on Android, my first task
was to write layouts, and I was like,
oh, God, I don’t understand. So when you’re done
with the measurement we can do the layout, so we need to position
all the children on screen, and we need
to give them a size so it can be
the size they wanted, or, you know, the size
that we think is best for them. So here it’s
a very simple algorithm. We go through
all the children, and we simply– we simply
wrap the children. So we put as many children
as we can on the row, and at the end of the row
we go to the next row. Actually, here we only put
three children per row. So you just
call layout on the child, you give it–you give the child
the X and Y coordinates, and you also give it the size
you want the child to be. And here,
as you can see, we’re passing
the cell width that we got from the setter
methods at the beginning, so we are totally
ignoring the size that the children figured out
when we called Measure. So that layout will always
lay out the children with a very–with an exact size
no matter what the child wanted. Onorato: But keep in mind
that that might be okay. If all of these views
are your views, and you know exactly how big
the bitmaps are, like, just go ahead
and use it. Guy: Yes, like I was saying
before, like, if you can, if you want,
take shortcuts. Like, if that’s all you need,
then stop right there, and, you know, your code
will be more efficient because it does
less work. So that’s actually okay
to, you know, fix the size of the children
and ignore what they told you. But that’s not correct, like,
we can’t cheat that on Android, so we need
to improve it. And also that’s not gonna
work really well across different resolutions
and different screens. So let’s see the–
the next step. Onorato: The other thing
we’re missing here, I actually realized
during your talk, is we’re not
skipping the gone ones. But we don’t have any gone ones,
so that’s not a problem. Guy: We need bugs,
you know. Onorato: All right. So the other thing
to get this actually running is we have to go into our–
our view here, and use– and use the right one,
com.example.android… Guy: So if you ever
wondered how to use your custom views in the XML,
that’s how you do it. Onorato: You type in
class name. Guy: Yeah, but the fully
qualified class name– how do they say
in Java– so you need to prefix
with the packaging. And to make things easier,
when you use the standard views, you don’t have to say
android.widget.ListView. But you can
if you want. Onorato: All we do is search in
those packages for those things. Makes it a little bit
slower, actually, but this thing isn’t
linear layout anymore, so we’ll
take that out. Guy: So let’s run them. Onorato: Get a–
down here too. This is one thing I always
forget to do, actually, in XML. Guy: Ah, the beauty of XML.
Onorato: Every single time. And one more thing. In here–
Shouldn’t be too bad. Is we actually have to set
the size of the cell. It starts out at zero. So we’ll d–
Guy: Oh, yeah. You want to say the–
Onorato: Yeah. Guy: So here,
because we can’t specify the width and the height
of the cells in XML yet, we’re doing that
from code. So the usual, you know,
boilerplate of finding the ViewById,
casting it, and then calling
some methods on it. Onorato: Okay. So we’ll run it again. Guy: [imitates small explosion] You did not specify
the ID in the XML file. Onorato: That is what I did. So the syntax– Don’t forget android.id,
too, that’s another– that’s another common one. If you just say ID,
it doesn’t– Guy: Well, at least
it’s not too painful when you do that
in a project ’cause when you
forget the ID and you’re working on the source
code of Android itself, you have to reveal
the whole tree. You know, take some time,
and your ID goes all crazy because it has to
refresh all the, like, tens of thousands of files
we have. So usually we’re better
at not forgetting that. Onorato: Yeah. Guy: Okay.
Onorato: So here we have it. Guy: But, uh, so they are
exactly 80 pixels across– Onorato: And ImageView
stretches by default. Guy: Yeah,
so I don’t know if it’s really visible
on the projector, but you can see that
the droids are much bigger than the original picture. And they don’t look really good,
like, you know, their scale. And we don’t want that,
so we need to honor the measured dimensions
from the children. Onorato: So we’ll do that.
This is a little bit of typing. Guy: As long as you do
the typing, it’s okay. Onorato: That’s fine. All right, so this is all, uh,
all in a layout here. Guy: Yeah, so we don’t have
to chance the measure– the measure method
because we’ve already done it. And usually, actually,
you will probably end up not doing much work
in the measure method because all you want to do,
really, is you measure the children. The hardest parts usually
is to figure out whether you want the children
to be AT_MOST, EXACTLY, UNSPECIFIED,
and all that stuff. And if you’re really
courageous, you can go look
at the source code of LinearLayout. And if you’re still reading it
after a couple minutes, then please apply for a job
and we’d love to give you LinearLayout to maintain. And so here, Droid’s modifying
the layout method so we’re actually asking
the children “What are the measured widths
and measured height?” And that’s something
that you have to keep in mind. When you want to know
the width and the height of a view for some reason,
you’re on your Y, you have two different
methods. You have get Width
and get measuredWidth. And they
can actually be different. In many situations,
the value will be the same, but they may be different. So the one you want
most of the time is get Width because it gives you
the actual size on screen. Uh, get measuredWidths
is really useful only for layouts because they’re the ones
who care about what the child thinks– thinks it wants to be. Yeah, so here, Joe
is just updating the call to layouts. Onorato: I’m going
to have it centered in the view too. Guy: Yeah, and you’re actually
centering it. So because we said
to the children, “okay, we want you to be at most
80 pixels large,” they might be smaller. So we have a couple lines
of code here “int left” and “int top”=that center the child
within the cell if they’re smaller
than the 80 pixels that we specified. Onorato: And we’ll make it
wrap correctly instead of hardcoding it
to four columns too. Guy: Which is always nice.
So let’s give that a try. Onorato:
See if that actually runs. Guy: Just “ant reinstall.”
Onorato: Yeah. Guy: The padding.
Onorato: Again? Guy: Okay, no, you don’t
take care of the padding of your children
in the layout ’cause the padding
is inside the child. So it’s part
of the measured width. Onorato: Well, if the layout
itself had padding, then we would. Guy: Yeah, if the layout
has padding, you have to, you know,
deal with it. What you will deal with
is the margin of the children. ‘Cause the margins
are outside of the bounds of the children. If you want a good example
of that, you can look at FrameLayout,
LinearLayout, RelativeLayout, I think, to deal with that kind
of stuff. And usually, you know,
if you don’t care about padding and margin, just forget about them. Like, take the shortcut. ‘Cause as soon as you start
taking the padding in the margin into account– Onorato: It doubles
the amount of code. Guy: Yeah, the code
becomes much more complicated. So be glad that we do
that kind of stuff for you. Onorato: So here they are,
the right size and centered in their cells. Guy: Okay, and, yeah,
and if we’re to inspect that layout
in Hierarchy Viewer, we would see that the items
are smaller than the cells
in which they’re contained. Onorato:
You want to do that? Guy: Yeah, no.
Let’s go ahead. Onorato: Okay. All right, so next, we’re going
to actually make the density. So if we ran this
in wide VGA, what we would see– I won’t do it ’cause it takes
so long to start the emulator– but what we would see
is all six of these androids right in a line. Guy: Yeah. Yeah, that’s exactly– the screenshots that you saw
at the beginning, we specified– in this example, we specified
the size of the cells in pixels. So there are actually
80 pixels. And if you want to devise it
as a higher density, then 80 pixels
are much smaller, physically, for you as the user. And that’s what we want
to fix. It’s not an issue
right now, but it’s going
to become an issue for all of your applications
really quickly. We have stuff in the framework
that take care of compatibility for older applications. But it’s best
if you guys do it. Onorato: So we were going–
what we’re going to do is sort of show you how
to hardcode it. But I think I want
to just skip that. Guy: You want to skip that?
Okay. Onorato: ‘Cause it takes
a little while. So while this is going, I’ll–when I restart
the emulator here… Guy: Yeah, I’m pretty sure
you will have the slides available somewhere. So we can–
it’s not much code. It’s like two lines of code. Onorato:
It’s like 54 lines of code. Guy: Yeah, and if you want
to know more about supporting the density and how to compute
the right size, then, you know,
just send us emails. We’ll point you
to the examples. And we’ll probably talk
about it really soon on the blog,
on the forums. Onorato: So I think
what well do is show you how to use resources
then instead. Guy: Yeah.
The attributes one? Onorato: Yeah. Attributes. Guy: So let’s see–
so again, so let’s see how to get the attributes
from XML. Onorato: So I said
it was about ten lines of code. It’s really about that. So there’s res–this is–
you have to add a file called res/values/attrs.xml. Guy: It doesn’t have
to be called attrs.xml. Onorato: Really?
Guy: Yeah. It’s a convention. Onorato: Okay. Guy: And I would love
that we change this convention ’cause I can never
pronounce attrs.xml. [laughter] I–you should try
with a French accent. You’ll see. [laughter] Onorato: You don’t want me
to do my French accent. Guy: No, you don’t. Onorato: So there’s–
go ahead. Guy: Yeah. Well, yeah,
so Joe here is just creating a new resources–
new series of resources. And something that called
the “styleable” is basically a set of resources
that can be modified by the theme. So if you ever created a theme
and you changed, you know, like, the fullscreen mode,
the window background, the appearance of your text
views or stuff like that, that’s because they are declared
as styleables in the framework. And then in the styleable,
you define all the attributes that you want to be able
to use in XML. So just a name and a format. So here dimension. So dimension is a fancy name
for all the units we support. So that means that when you have
a dimension attribute, you can say 1 px
for pixel, 1 dip for device
independent pixel. You can say 1 mm
for millimeter. You can use 1 inch. We have a bunch
of different units. And we support a bunch
of formats so you can–
you can have floats, you can have enums,
you can have flags, integers, booleans,
strings, references. Onorato: Enums are kind of fun,
actually. They–they let you–they–
I mean, they do exactly what enums would do. They give you a name
in the XML file, but it becomes an integer. Actually becom–yeah,
becomes any integer in the–
in the data structure. Guy: So if you ever use
a LinearLayout, for instance, and you specify
the orientation, you say orientation equals
vertical or horizontal, that’s an enum defined
as a–as an attribute. So here we just defined
the two attributes we need to specify the width
and the height of our cells. And let’s see how to use
that from code. Onorato: Well, we’ll add
to the XML too. Guy: Yeah, the XML. Onorato: So this is the–
this is the magic part that I… Guy: Always forget about.
Onorato: Always forget. So, yeah, you–
you probably always– you’ve already seen
that namespace in the root tag of all your XML layout files. And you probably wonder, like, “why do I have
to have that thing that, you know,
it’s pretty weird to look at?” Onorato: So there’s actually
two parts to it. This is just the prefix
that tells us that this is going
to be an Android… Guy: Attribute.
Onorato: Attribute, yeah. And then there’s
your packaging. You know, Android is the one
that it’s sort of regularly in. Guy: Yeah. And you want
to change the namespace now. Onorato: Oh, yeah. Guy: And so when you create
your own attributes from XML, because the name
of your attributes could potentially,
you know, get in conflict
with the attributes that we define, you have to specify a namespace
for your application just like you have to use
the Android namespace. So, you know, does– this will prefix then
the package name of your app. Then when you specify
the attribute, instead of using
android:whatever, you use the name
of the namespace you just used. Here Joe just declared
the widths of the cells to be 80 device
independent pixels, which will scale automatically
with the density of the device. That’s some framework magic. And the height is just 100. Onorato: Yeah. Guy: So it’s, you know,
like we said, it’s actually pretty easy to create your own
XML attributes. The only thing is that you have
to remember the syntax. And I don’t know
how you can– you might not even remember
the syntax. So it’s nice to have–
to have examples. In the–actually, in the SDK,
you have a bunch of examples
that–that use that. So if you look
at the home sample, for instance, we use heav– we use custom attributes
heavily. Have you recompiled the… Onorato: I haven’t yet
’cause we needed to put the Java code in too. Guy: Okay. Onorato: So I’m just going
to copy and paste it. Guy: So now this–yeah,
there’s some more width syntax. Onorato:
So what–what this– so we’re going to add
this big hunk of code to the constructor here. And what that–
what all that XML did, there’s your–
your R.java file. and that generated–
inside–inside that, it generated a static,
final array of resource IDs that can then be used to this obtainStyledAttributes
function. And it–and it’ll get you
this thing called the typed array,
which is–is magic. Guy: So the typed array
is–is basically all the attributes
and all the values that you’ve defined in XML. Onorato: But just for this one–
just for this one thing. Guy: Styleable. Onorato: One–
for this one styleable, yeah. And then we’re going to get
the width and the height out of that. The thing we skipped
was the pixel size thing. Just briefly, there’s–there’s–
this typed array thing has four or so
different versions of getPixel–
or getDimension. And the one we have here
is used for widths. So if you–if the rounding
would cause your pixel to go down to zero, it actually… Guys: Forces it to be– Onorato: It actually forces it
to be 1 just so your widget doesn’t
completely disappear. It’s–it’s not so much useful
for the width of your actual widget. But if you’re going
to do something like draw a border, you probably still want
to have a border even if it’s
a really small border. Guy: Yeah, it’s
the second-worst API name ever. Onorato: Yes.
What’s the… Guy: [indistinct] That’s the first one. Again, so you saw
that it’s pretty easy to get the attributes
from XML. But we have to admit that,
you know, the API like TypedArray,
getStyleable, it’s under intuitive, so we wanted
to–to show you guys, you know, the code
that you have to type so that at least
you can remember what it looks like
and, you know, so that you can know
what to look at in the source code
of the examples when you want to do the same
in your applications. Onorato: So here,
while we were doing all that, we restarted
the emulator here. And it’s running at wide VGA,
which is 400–480×800. And you can see that Android
isn’t really ready for it yet either. The home screen
sort of ends. [laughter] And the icons are small. So here’s our little button. Guy: My androids are small too.
Onorato: What? Guy: The androids
are small too. Onorato: Yeah, the androids
are small too. Well, shouldn’t be,
actually. Guy: You have to modify
the DPI property of the system. Onorato: I should’ve done that.
Guy: Yeah. Onorato: Crap.
Guy: That’s okay. Onorato: All right. Well, if this were to run
on a real device, which don’t exist, which is why this talk
is all theoretical anyway, then this would’ve
looked right. There–there–there’s a way
to get the emulator to build it. I’m not going to show it ’cause it took me
about 20 minutes to figure out. Guy: Yeah,
so one of these days, I’ll write an article
on the blog to show you how you can
start the emulator with the appropriate density. And you will see that things
actually work correctly so that you can at least test
your application. It’s not that hard. It’s another one
of those things where you have to find
a weird configuration file somewhere in the system and change
one of the properties. So we’ll document that. Onorato: And if–if somebody
were actually making a device that was–
that was that size, they would’ve done that,
obviously. Guy: Yes. If they want to ship
with device. So we’re done with the code? Onorato: So I think we’re done
with the code, yeah. Guy: So let’s go
to the fun part. Onorato: The fun part.
Yeah. Guy: Okay. So now we’ll show you how– you know, like, this code
is awesome, obviously, ’cause we just wrote it. Joe’s a great developer. And we’re going to now com–
you know, submit that for review to the Android
Open Source Project. So Joe will show you
the commands that you have to do
on your side to submit a patch. And then I will show you
how on our side, we look at your patches
and how we approve them. And the cool thing about it
is everything that we do right– that we’re going to do
right now, it’s exactly what we do
every day, several times a day, and so on. So we use
the exact same tools. We have to do the exact
same process. The only difference
between you and us is that you don’t have the right
to submit something in the tree whenever you want
and we do. You’ll see.
It will be obvious. We have a cool button
that you will never see on your screen. [laughter] We–we got to have some perks,
you know, working on the Android team. Onorato: So we’re going
to check this into the samples directory
on the tree. And we’re actually really going
to check it in right now. So if we break everything,
then–the build is broken? Guy: I think the build
is broken already. So that’s okay. Onorato: I think
the public one isn’t. All right.
So we copied it in there. And now we’re going to go into development/samples/ This whole thing is completely
command line in our–in our world. And we’re going
to show you Git. Guy: I won’t say anything bad
about Git today. Onorato: Git is great. Actually, you know what, Linus had a nice talk
at Google where he slammed everybody
but Git. Linus is a jerk
and Git is annoying. Guy: Can I say that Git
is a pain in the butt? Onorato: Yeah. Cool. Guy: So Git is a pain
in the butt. No, it is actually awesome. Let’s just say that to me,
you know, as a user that likes Macs, they forgot
about the user part. So it’s not very
user friendly. And we’re currently
transitioning internally. We used to be on Perforce
and we’re switching to Git so that we use the same tools
that we–you do. You know, so that we can also
feel your pain. And believe me,
we do feel it. Onorato:
So I just deleted– what I had just done
is deleted all the files that we don’t actually
want to check in. Our sample structure
doesn’t have all the build… Guy: Yeah. Onorato: ‘Cause they have
hardcoded paths and everything. Guy: Okay, let’s send
the repo. Onorato: Yeah. Guy: So we have this tool
called repo. It’s basically a smart wrapper
on Git because the Android project
is, you know– we like to make things
complicated. So instead of having
one repository, we have currently 152
or something like that. Oh, and obviously,
you know, handling 152 Git repositories
by hand would be, you know, quite tedious
to really impolite. So we created this tool
called repo that takes care of mo–most
of the work for you. So when you type “repo sync,” it goes through the hundreds
of repositories and syncs them. So when you do “repo start”–
let’s back up a little. Onorato: It’s way back
up here, yeah. Guy: Yeah.
Stop typing stuff. So when you do “repo start,” you’re just starting
a new branch shown here called “simple.” So when you want
to commit something to the Android Project, you have to start
a new branch. Don’t ask me why. Onorato: This is all
documented somewhere. Guy: Yeah, it’s because
of the way Git works. Anyway, so you have
to start a branch. And if you don’t,
I can’t help you. Onorato:
It’s about ten commands and you have to rebase something
somewhere. Guy: Yeah, so just create
the branch. Never forget about creating
a branch. Onorato: So we’re going
to add the file to Git. Guy:
Yeah, so now that we– Onorato: Or add
the directory rather. Guy: Yeah, so– Onorato: Status shows us
all our new files. Guy: Let’s back up
right now. Onorato: Oh, we didn’t do that.
Guy: No. Oh, did you show
the [indistinct]? Onorato: Yeah,
we did the commit. It’s just that’s all
we had to do. Guy: Okay. Anyway,
for those of you who know Git, we just staged the files
in UNIX. For those who don’t,
just, I don’t know, go read a book about Git
and you’ll understand. Anyway, so the–the point is,
you know, in Git,
there are several stages where your files live. So you modify the files. Then you have to go Git add
to put them in something called the index. So you repo start,
my branch, then you–you–you modify
your files. You do Git add. You put them in the index. And when you’re ready,
you do a Git commit. The fun part
about a Git commit is that it commits the files
on your machine. So they’re still
on your machine for some reason. So here, Joe is just adding
the commitments edge and you can see the comments
at the bottom, everything that we did. So we just added
a bunch of files. That’s all the files
in the project. And when we’re ready,
we can finish the commit. So now files
are committed locally. And we need to upload them
to the Android Project. So for that, we use the comment
called “repo upload.” And repo upload is really smart
and figures out that you have commits
in your current branch. It backs–
Onorato: It asks you– it says, “would you like
to upload those?” Guy: Yes. So yes. And then–go ahead. Onorato:
That’s interesting. Guy: The remote–oh, do you have
the SSH key? Onorato: Yeah.
Guy: Huh. Try again. [laughter] Isn’t working out. Onorato: Huh. All right.
Well, uh… Guy: Uh, you don’t have
the key. Onorato: I don’t have
the key. Guy: Well, let’s do that
right now. Go to the web site
and put the key in. Onorato: Oh, yeah.
Guy: Uh… Onorato: Here’s my public key
if you guys all want to see it. [laughter] Guy: I’m pretty sure
the country– Oh, but they will have
the video. They’ll show your key. Onorato: I’m not going
to open my private key. Just the public one. I hope that’s the public one.
Yeah. [laughter] Guy: You’ll have
to do that yourself. So when you want
to–to–to deal with repo– and I’m sure
there’s a good reason, but I was never told
the reason– you need
to put your public SSH key in your account. So let’s do that. Onorato: So, yeah,
if you go to the settings tab… Guy: So let’s review the–
Onorato: SSH Keys… Guy: Yeah,
review.source.android.com. Onorato: Oh, yeah,
or r.android.com also works too. Paste that in there. Click “Add.” Let’s go back
and try that again. Guy: So all the–
all of that, like, you know, the SSH Keys
that explain– if you go to
source.android.com they explain everything. Onorato: Here we go.
Okay, it worked. Guy: Okay, it worked. So now
the code review is online. Onorato: The code review’s
online, yeah. If anybody here wants
to go comment on it, you can heckle me. Guy: Actually you can. Onorato: Yeah, you really can.
This is live. Guy: We welcome comments. You know, you can look
at changes from other people and say that you disagree with
them, that you agree with them, so, you know,
if we see, like, that 20 people from the outside
said yes to a change, then, you know, maybe it’s a
good idea to accept the change. So now it’s my turn.
Joe’s work is done. Onorato: Actually I need
to add you as a reviewer. Guy: Oh, yeah. Well–
Onorato: So this– Guy: You don’t re–
Okay, you can. Onorato:
Yeah, let me do it. Because this is
what you guys would do. You’d see it’s up–
I’ve uploaded it. The owner is me. And I’m gonna
put in… in here. Guy: And if you don’t know who
should be reviewing your change, just don’t
put anything. We do receive emails,
we know that, you know, new changes
are coming in. And from time to time
we go look at them, and we add ourselves
as reviewers. Anyway, so– Onorato: I’ll add him;
it’ll send him an email. Guy: So add reviewer.
There you go. Onorato: All right.
Go ahead. Guy: I’m now a reviewer. So I switch to my machine. I’m already logged in
on Gerrit, so the exact same tool. And if I go
to my changes… Reviewable… Yeah, it doesn’t
like Safari sometimes. Anyway. Onorato: Oh, this whole thing,
by the way, is written using
Google Web Toolkit, too. It’s by a guy,
Sean Pierce. He’s on our team,
he’s fantastic. Guy: Okay, so where is
your change? Here we go. So that’s Joe’s change,
and you can see that all those
other changes are changes that people made
to the framework that I could review, but it’s not necessarily my job
to review those changes. But they still appear for me
as changes I can review. So let’s go
look at Joe’s change. And it looks exactly the same
as Joe’s UIs. And actually, someone commented
on the–on the change and gave it a +1. Onorato: Good for you. Guy: Thanks,
whoever you are, Frank. [laughter] But actually, you know,
that’s nice ’cause– Oh, you can see that– So his comment says, “Looks good to me,
but someone else must approve.” It’s a canned comment. So that means
he thinks that, you know, it’s a good thing to have
in the platform, but he doesn’t have
the approval rights. So somebody like,
you know, Joe or I, have to go ahead
and give the approval. So here now I can look
at all the files I have– Looks good. But actually I see here that
Joe forgot to put the makefile. ‘Cause we use Make
in the open source project, so it probably tells me that Joe never built that code
in our platform. [laughter] I promise
we never do that. So let’s– I will say “no score” because
I do appreciate the patch, I want the patch to end up
in the–in the buil– in the framework,
so I say “no score,” and I will
leave a comment. Here I could say
that I would prefer that you didn’t
submit this. That means I disagree with you,
but, you know, you could
convince me otherwise. “Do not submit”– You’re really wrong and I don’t
want to hear about it anymore. Onorato: And actually, the
“do not submit” actually force– the system won’t allow it
to be submitted if somebody’s
reviewed it that way. Guy: It’s gone. We have total control
over your stuff. Okay. Joe, geez, you didn’t even
compile your stuff. Add the makefile. So I publish my comment. You can see here
that my comment was added. And the score is still zero.
Let’s go back to Joe’s machine. Onorato: So I’ve got
my makefile all ready. Guy: Yeah,
especially because maybe you know how to write
a makefile, but I have no idea. Onorato: If you guys use the
build system that’s my fault. If home screen is his fault,
the build scr– build system is my fault. Here’s what
the app make files look like. This is sample code,
here’s the name of the app. This one here says it’s being
built against the SDK as opposed
to the private APIs. Guy: Which is a good thing.
Onorato: Which is a good thing. Guy: Okay, so now
we’ve done that. We need to modify the–
the patch that Joe applauded, and that’s where many people
make the mistake. And I do the same
quite often, actually. Because of the way Git works,
if you create– if you do a new commit to commit
the changes you just did, then repo will treat
the old commit and the new one as two different changes,
which is not what we want. Onorato: So we’re
gonna amend this. We’re just gonna do git– Guy: So we add the file
to the staging area, and then we do commit with
the magic flag called “–amend.” And that modifies the previous
commit with those new changes. So we still have only one commit
with all of our changes. And there’s still one mistake
we can make after that. And that we often do. Onorato: We’ll see
if we make it, ’cause I don’t know
what he’s talking about. Guy: I’m sure you do.
Repo upload replace? Onorato: Oh, yeah. Guy: So if you do
a repo upload right now, it will create
another change, and, you know,
that will piss us off ’cause now we have two changes
that are pretty much the same and we have to
go ahead and– Onorato: Figure out
what the other one is. Guy: Yeah, figure out
what the other one is, you know, take care of rejecting
one of them. So what you have to do is
“repo upload –replace.” That means that you want
to modify the current change that you–
that you have on repo. Onorato: And you say
which project you want, which is the one
we’re in right now. Guy: So here
you can indicate– You can see that
the number between brackets is the number
of the change in the Gerrit tool
on the web. It’s prepopulated for you. Just make sure
that it’s the right number. Onorato: It usually is.
Guy: It usually is. Onorato: It guesses, though. It’s heuristic,
so it’s not 100%. Now it’s re-uploaded.
Guy: Now it’s re-uploaded. My turn. And if I refresh the page… If Gerrit refreshes. Popular. A bunch of people
like to comment on bugs. Onorato: You can
see it reset the– Go back up
to the grid, there. You can see it reset the–
all the statuses. Guy: Yeah, because
we changed the code, we contrast, you know,
what people said the last time. And here you can see that
we have the patch set number 2, so we can look
at the patch set number 1, and we can look at
the difference between the two. So now the patch set number two
has the makefile, so I am happy with it. I won’t go through
the whole code. So when I’m happy
with it, I– I publish comments, and I say,
“Looks good to me, approved.” Onorato: We’re gonna
have to verify it too. Guy: Yes, but let’s
approve first. So now it’s approved.
It just means we can submit it. That doesn’t mean that the code
will compile, or, you know, that–that it’s not gonna
do bad things to the build. So at this stage,
what we’re doing internally is we want to try
the patch locally. So here we have
those two comments we can use, either repo download
or the Git pull. And all I would have to do
is copy that, paste into my shell… Like, for instance,
here I’m in my source tree, I just paste
the comment I executed. This will bring the change that
Joe made into my source tree. I can compile it, I can run it,
I can see if it works, I can see if the build
is still going. And when it does,
I go back to “Publish Comment,” and I say “Verified.” So I verified
that the change is correct. Onorato: This is actually
the big bottleneck process. It’s not looking at the code,
it’s actually downloading it all and building it. Guy: And now that the change
is both approved and verified, I have a new,
fancy button here that you will never
see on your machine, unless we start,
you know, trusting you guys
to become contributors. Proof contributors. So this is
a new submit button. So the cool thing here is that
once we are on the website, I don’t have to,
you know, grab the code, do the patches locally
on my machine to patch our tree and then, you know,
do another commit. Joe doesn’t have
anything to do. I just press this button
and that’s it. The code is in the tree. We can see here, “Change has been
successfully merged,” and Joe’s code is now available
on source.android.com. There’s one last thing that
you want to do on your machine. The abandon.
Onorato: The abandon. Oh, yes. So, remember,
we started a branch. and if I continue
working on that branch, Oh, God, I don’t even know what
happens. It just doesn’t work. What happens is that I work
for about two weeks and then realize
that I’m on a branch and try to submit and… Guy: And you’re screwed. Onorato: Then I’m completely
screwed, yeah. So this is repo abandon–this
project was called “sample,” and it’s
in this project, so dot. And it did it, I guess. Guy: And now–
Can you go to the Git web? To see your code?
Onorato: Uh, yeah. Guy: So now everything’s fine
on Joe’s machine, we accepted the change. When you abandon
your change, the change really disappears
from your machine, so you want
to do a repo sync to bring back the change
that you just sent, and remove
from your machines. It’s weird at first,
but that’s how it works. So if we go
to development samples… So that’s
the Web interface to Git. And you want to look
at the master tree. Onorato: This is master,
I think. Guy: Okay. Onorato: Tree, samples. And, uh… Guy: Are you sure
you’re in master? Onorato: No. Well, anyway,
it’s probably there. Guy: Let’s prove
it’s there. Onorato: Do you know
how to find it? Guy: Go back. Scroll down. Uh… Are you in master? Onorato: So this is
another thing. I worked for about three weeks,
and I was submitting to master, and I thought
I was working on Donut. So that was
a little bit hard, too. Guy: There we go, master.
Onorato: Oh, look at that. Guy: Doesn’t even know
how to use a tool he wrote. Uh, well, somewhere. Onorato: Well,
there’s some code somewhere. Guy: [laughs] This may be in Donut,
this may be in… Onorato: I wonder
if I put it in Donut. [laughter] Onorato: Huh. Well, I have
some homework to do after this. And find out
what build I broke. Guy: Well,
like we said, we are in the transition
to the new tools, but at least, you know,
that gives you an idea of what the workflow is. And again, this is exactly
what we do internally. The only difference is that
we don’t use the public website. We have another instance
of this Gerrit thing, and we have another
Git tree internally, but the tools are the same,
the workflow’s the same… Onorato: The scratching
of our heads is the same, so… Guy: The only difference
is that, you know, Joe could just upload his patch
and say, “Hey, I trust myself,” and submit it. Onorato: Yeah, and then
I break some branch. Okay. Guy: Like I did today. Onorato: So we’re
just about out of time, but if you guys
have any questions… Guy: Why has nobody left us?
Onorato: Come up to the mic. Guy: Yeah, go to the mic
for the questions. [man speaking indistinctly] Okay, so it’s– Yeah, it’s a Git rip,
so use Git-fetch. [man speaking indistinctly] Tesh.
Stage? Stash? [man speaking indistinctly] Onorato: Oh, you mean you saw
how I was doing that there. Yeah, yeah. Guy: Okay. man: Um, when you’ve submitted
your first patch, and you’re waiting
to get approved or not, is there a way that
you can do additional work, or are you kind of frozen in
case you have to amend it later? Onorato: That’s actually–
Guy: The nice thing about Git. Onorato: What you don’t
have to do in that case is do repo abandon. You can continue working
on top of your changes. The only thing is it gets
a little bit hairy if you have to
make changes. Guy: Well, you can
switch to another branch. Onorato: Or you could switch–
If it’s totally unrelated, you can switch
to another branch. man: Okay,
I just want to make sure you get in a state where
you can amend without adding… Onorato: Yeah, you can– What you end up having to do–
and I’ve done this– is you– instead of doing amend, you make changes and you rebase
them into a different order and squash them
and everything and it works. All right.
Thank you. [applause]


3 Comments

ShreddingSoobs · November 14, 2009 at 1:21 am

wow

DeliverLikeUPSTrucks · December 19, 2009 at 12:04 am

DORKS!

whitney coleman · July 18, 2010 at 2:26 am

SAD. REALLY WANTED TO HEAR> GOOGLE CANT FIND SPEAKERS THAT WORK.

Leave a Reply

Your email address will not be published. Required fields are marked *