On Github atroutt / android-beginners
A practical introduction to app development on Android
Course source materials at on github.
Taught by Audrey Troutt / @auditty
You should be..
HINT: Press Down Arrow ↓ to navigate to the sub-sections below.
If you have one, create one for free.
We are going to use this for logging in to the apps we will be creating.
If you don't have it already, download and install Android Studio.
This is the editor we will use to write our apps.
Once Android Studio is installed, go to the Android SDK Manager inside Android Studio (Tools -> Android -> SDK Manager) and check two things:
Under the "SDK Platforms" tab check that Android 7.0 (Nougat) API 24 is installed or download if needed. Then in the "SDK Tools" tab on that screen download the Support Repository / Android Support Repository if not already installed.Download the code for this class:
Go to GitHub atroutt/android-beginners On the top right press "Clone or download" and click "Download ZIP" On your computer, open the zip files with your favorite unzip program and put the 'android-beginners' folder somewhere you can find it later. This repo contains the source for the app we will be using in this class in addition to these slides.If you have an an Android phone enable developer options so that you can install and run your own apps.
An Android phone is not required for this course.
Your phone needs to be running Android Jellybean (4.1-4.3), KitKat (4.4), Lollipop (5.x), or Marshmallow (6.x).
An Android phone is not required for this course, but it is fun to have so you can show your friends what you've built. If you don't have a device you can use the Android emulator with Android Studio. Luckily for you the Android emulators have gotten faster and faster in recent years.
If you just can't wait to get started, here's some resources to start learning now:
HINT: This is the end of the section, press Right Arrow → to navigate to the next section.
HINT: Press ESC to enter the slide overview.
I'm hoping that we will make it through the first two today and the second two tomorrow, but since this is the first time through I'm not sure how it will go. There's a little wiggle room to stretch and shrink the sections if needed.
There are no dumb questions. I am here to help you learn what you want to learn, so anytime you have a question or get stuck, speak up. I'm happy to answer your questions.
Be brave when facing new challenges and boldly poke around in the code. Breaking things is one of my favorite ways to learn about something new. I promise we will all help you fix it if you get really stuck.
Also feel free to take a break anytime you need. We will have 30 minutes for lunch and at least one stretch break in the morning and the afternoon.
Help each other out if you can while we are discussing things or doing our projects. The best way to learn is to teach!
In this section you will learn how to create or import a new project into Android Studio, and build and run it on your device or emulator.
There's a lot to see and do in Android Studio. Let's take a brief tour of the parts we will use in this course.
Introduce these parts of Android Studio:
The toolbar lets you carry out a wide range of actions, including running your app and launching Android tools. The navigation bar helps you navigate through your project and open files for editing. It provides a more compact view of the structure visible in the Project window. The editor window is where you create and modify code. Depending on the current file type, the editor can change. For example, when viewing a layout file, the editor displays the Layout Editor. The tool window bar runs around the outside of the IDE window and contains the buttons that allow you to expand or collapse individual tool windows. The tool windows give you access to specific tasks like project management, search, version control, and more. You can expand them and collapse them. The status bar displays the status of your project and the IDE itself, as well as any warnings or messages.Set the name and package for your app. Choose a project location or use the suggested directory.
Packages are how your app is uniquely identified on the device and in the app stores. Usually they look like backwards urls, but they don't actually have to match a real website.Select Phone and Tablet and press Next
For now, select Empty Activity to start (it's the simplest) and press Next
Leave the default name and press Finish. Your app will be generated!
You should see this error-free code after Gradle, our build tool, finishes syncing.
In the top menu choose "Run 'app'"
Android Studio is going to ask you to "Select Deployment Target" — this means choose the device you would like to run on
PRO TIP: Check the box that says "Use same device for future launches."
You should see this on your device!
Import and run our class app for the first time.
If you haven't already, download the source code for the first app (zip).
Alternatively, download the repo using git:
https://github.com/atroutt/android-beginners.git
Launch Android studio
Choose to open an existing Android Studio Project (File -> New -> Import Project)
Choose the AndroidBeginnersFirstApp folder on your computer.
Before you can run your first app, you need to make sure you have all the dependencies downloaded. To do that select "Sync Project with Gradle Files" from the top menu bar. This may take a minute.
If they haven't downloaded the SDKs that they need yet, there will be a lot of errors.In the top menu choose "Run 'app'"
Android Studio is going to ask you to "Select Deployment Target" — this means choose the device you would like to run on
PRO TIP: Check the box that says "Use same device for future launches."
If you can see this, then you are all set up!
Go ahead and snap a selfie!
If you have some people who finish early, teach them the "Find in Path" (cmd+shift+f) command in Android Studio and see if they can figure out how to change the text that appears on the MainActivity "Take a photo of yourself..."
Now that you have been introduced to Android Studio, let's dissect an app!
This is an app, also known as an Application
Every app has one Application class
An app usually has many screens.
These are called Activities.
Here's a screen from our app. It is defined by an Activity class you'll see later.
An Activity usually has many views
An Activity usually has many views
What does all this mean?
package com.audreytroutt.androidbeginners.firstapp; import android.app.Application; public class MyFirstApplication extends Application { int toastLength = Toast.LENGTH_LONG; @Override public void onCreate() { super.onCreate(); showToast("I have been created!"); } public void showToast(String message) { Toast.makeText(this, message, Toast.LENGTH_LONG).show(); } }
What is all this (press next)
What does all this mean?
package com.audreytroutt.androidbeginners.firstapp; import android.app.Application; // import public class MyFirstApplication extends Application { // class int toastLength = Toast.LENGTH_LONG; // field @Override public void onCreate() { // method super.onCreate(); showToast("I have been created!"); } public void showToast(String message) { // method Toast.makeText(this, message, Toast.LENGTH_LONG).show(); } }Really quick, here's what I mean by class, field, method, and comment.
Anything after // is a comment in Java.
// This is a commentLet's start with something easy: anything after double forward slashes is a comment. Comments should go BEFORE the thing you are talking about, but not on the same line!!
Every line of code must end with a semi-colon;
int numberOfErrors = 0;Every line of code ends with a semi-colon. Android Studio will put a red error on your file if you forget your semi-colons--you can't compile your code without them.
Most things in Java are object.
A class is the definition of an object in Java
// this is a class public class MyFirstApplication extends Application { // your fields and methods go in here }
All of your code goes inside of classes.
Everything in Java is an object and objects are defined by classes. This is an Application class called MyFirstApplication. All of your code goes inside of classes. You can see here that this says MyFirstApplication extends Application. That means MFA *is an* Application--it has all the fields and methods of Application PLUS anything added here in this file. This is known as inheritance.A method defines the behavior of your class.
Everything your app does will be defined inside of methods inside of classes.
public void showToast(String message) { // this is a method Toast.makeText(this, message, toastLength).show(); }A method defines the behavior of your class. Your classes will have many methods. Everything your app does will be defined inside of methods inside of classes. Methods have visibility (public, private), return type (String, void), name (showToast), and maybe parameters (inside the parens) Here is a method called showToast that takes a message as a parameter. Inside the showToast method we call another method on a class called Toast to tell Android to display the little pop up message for the specified duration.
A field holds some data for your object.
public class MyFirstApplication extends Application { int toastLength; // this is a field public void showToast(String message) { Toast.makeText(this, message, toastLength).show(); } }
PRO TIP: always put your fields at the top of your class, above any methods.
A field is where you can hold data for your object. Fields have a type (View, int, String, long) and a name (like toastLength). In this case my Application has a field called toastLength. It's an integer. In my showToast method I'm using the value of toastLength to determine how long the toast message is displayed on the screen.Up at the top above the class body there are two things you will see: the package and import declarations.
package com.audreytroutt.androidbeginners.firstapp; import android.app.Application; import android.widget.Toast; import java.util.Date;
package is like a folder path to your class--it will be auto-generated for you.
import is used to include other code into your class.
Up at the top above the class body you will often see a lot of import statements. This is how you can make other code accessible to you in your class. You usually don't have to type this yourself. I will show you how to automatically import based on Android Studio's suggestions. On a mac the keyboard shortcut is option+return.Let's dissect an app!
This is an app, also known as an Application
Your application is always represented in your Application's Manifest, AndroidManifest.xml.
The Android Manifest is an XML file that declares things like the name of your Application, what Activities it includes, and which Activity should be displayed on launch.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.audreytroutt.androidbeginners.firstapp"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" android:name=".MyFirstApplication"> <activity android:name=".MainActivity" android:label="Welcome to the First App!" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".PaintingListActivity" android:label="Paintings (List)" android:parentActivityName=".MainActivity"> </activity> <activity android:name=".PaintingDetailActivity" android:parentActivityName=".MainActivity"> </activity> </application> </manifest>
An instance of your Application class is created when your app launches and sticks around until your app is stopped.
You are not required to create your own Application class implementation. There is a default that works well enough for simple use cases.
If you do create your own implementation it has to extend android.app.Application. It will look something like this:
package com.audreytroutt.androidbeginners.firstapp; import android.app.Application; public class MyFirstApplication extends Application { @Override public void onCreate() { super.onCreate(); // At this point my first application was just created } }This is a super simple Application implememtation. It doesn't really do anything, but it is there. The one method here is onCreate, which is called by the Android operating system just after your application is first launched. The name of this class "MyFirstApplication" could be anything we want. What makes it our application class is that it extends Application. We might see later some examples where you can extend a class indirectly (extend a class that extends Application, etc.)
As we learned earlier, an app can have many Activities.
Here's a screen from our app.
Activities are registered in your Application Manifest
Here you can see three Activities for my app.
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.audreytroutt.androidbeginners.firstapp"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme" android:name=".MyFirstApplication"> <!-- Here's the first Activity --> <activity android:name=".MainActivity" android:label="Welcome to the First App!" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".PaintingListActivity" android:label="Paintings (List)" android:parentActivityName=".MainActivity"> </activity> <activity android:name=".PaintingDetailActivity" android:parentActivityName=".MainActivity"> </activity> </application> </manifest>
Let's take a look at our MainActivity.
HINT: The name MainActivity can be anything you want. It's only by convention that the name has "Activity" in it. The only requirement is that the class extends Activity.
Inside an Activity class you again see an onCreate method. This is often where you set up what is displayed on the screen.
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Snackbar.make(view, "Hello, Test!", Snackbar.LENGTH_LONG) .setAction("Action", null).show(); } }); DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle( this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); drawer.setDrawerListener(toggle); toggle.syncState(); NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); } @Override public void onBackPressed() { DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } @Override public boolean onNavigationItemSelected(MenuItem item) { // Handle navigation view item clicks here. int id = item.getItemId(); if (id == R.id.nav_camera) { // Handle the camera action } else if (id == R.id.nav_list) { Intent listIntent = new Intent(this, PaintingListActivity.class); startActivity(listIntent); } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); return true; } }
From developer.android.com/reference/android/app/Activity.html#ProcessLifecycle
onCreate is just the first of many standard Android Activity methods. Activities have a lifecycle from creation, through destruction. The android operating system will call each of these methods at the appropriate time, which makes it possible to set up your screen, like we saw in onCreate, or to save things off or pause expensive operations if the screen is about to disappear. You do not have to define these methods in your activity because they are defined in the Activity class that all activities inherit from. That's why we always have to call super.onCreate(...) so that the underlying code gets called.There will also be methods for any of the actions that can be taken with the views (buttons, toggles, menus, etc.) on the screen.
@Override public void onBackPressed() { DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); if (drawer.isDrawerOpen(GravityCompat.START)) { drawer.closeDrawer(GravityCompat.START); } else { super.onBackPressed(); } } @Override public boolean onNavigationItemSelected(MenuItem item) { int id = item.getItemId(); if (id == R.id.nav_camera) { } else if (id == R.id.nav_list) { Intent listIntent = new Intent(this, PaintingListActivity.class); startActivity(listIntent); } DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); drawer.closeDrawer(GravityCompat.START); return true; }
Now we can talk about how you navigate between Activities: Intents!
Navigating between Activities is done with the use of Intents, like this:
Intent listIntent = new Intent(this, PaintingListActivity.class); startActivity(listIntent);
An Intent is a message to the Android operating system that we'd like to start the Activity named in the Intent.
HINT: Intents can only be created with a "context", which for us means an Activity. The startActivity method is defined for us in the Activity base class that we extend.
Here is an intent being created from inside a method on my MainActivity (so this refers to the current MainActivity instance). When this code is executed a new PaintingListActivity will be created and presented to the user.Intents are useful for more than switching between screens. They are the way to kick off all sorts of actions from your app:
Reference: common intents
Here's an example of a method that creates an intent to send an email:
public void composeEmail(String[] addresses, String subject) { Intent intent = new Intent(Intent.ACTION_SENDTO); intent.setData(Uri.parse("mailto:")); // only email apps intent.putExtra(Intent.EXTRA_EMAIL, addresses); intent.putExtra(Intent.EXTRA_SUBJECT, subject); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } }
Here's an example of a method that creates an intent to capture a photo with the camera app:
static final int REQUEST_IMAGE_CAPTURE = 1; private void dispatchTakePictureIntent() { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); } }
When you call startActivityForResult you have to pass a code that you can use to match the response with your request.
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE && resultCode == RESULT_OK) { // A picture was just taken, let's display that in our image view editImage(); updateMainImageFromFile(); } }
Head over to Android Studio for our second project.
For this project, your goal is to create intents so that each of the menu items in the drawer lead to the correct screens. Open MainActivity and find onNavigationItemSelected
nav_camera should fire a Capture Photo Intent. NOTE: I have already implemented the onActivityResult with the code CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE nav_list should display the PaintingListActivity nav_grid should display the PaintingGridActivity nav_web should take you to this website nav_share should fire a social share Intent nav_send should open an email IntentHINT: You can either try to figure those out from the previous lessons, or from the Android docs, or continue down for some copy and paste code snippets.
nav_camera should fire a Capture Photo Intent
if (id == R.id.nav_camera) { // TODO Project 2: create an intent for the MediaStore.ACTION_IMAGE_CAPTURE Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); intent.putExtra(MediaStore.EXTRA_OUTPUT, getAndroidBeginnerImageUri()); // set the image file name startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE); }
nav_list should display the PaintingListActivity
} else if (id == R.id.nav_list) { // TODO Project 2: create an intent for the PaintingListActivity Intent listIntent = new Intent(this, PaintingListActivity.class); startActivity(listIntent); }
nav_grid should display the PaintingGridActivity. This is just like nav_list, but with a different class--see if you can figure this out!
nav_web should take you to this website
} else if (id == R.id.nav_web) { // TODO Project 2: create an intent to open a url Uri webpage = Uri.parse("http://audreytroutt.com/android-beginners/"); Intent intent = new Intent(Intent.ACTION_VIEW, webpage); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } }
nav_share should fire a social share Intent
I've already implemented a shareAction() method because I need it for the Floating Action Button (FAB).
} else if (id == R.id.nav_share) { // TODO Project 2: create an intent to social share about this app shareAction(); }
nav_send should open an email Intent
} else if (id == R.id.nav_send) { // TODO Project 2: create an intent to send an email Intent intent = new Intent(Intent.ACTION_SENDTO); intent.setData(Uri.parse("mailto:")); // only email apps should handle this intent.putExtra(Intent.EXTRA_EMAIL, new String[] { "gdiandroidbeginners@mailinator.com"}); intent.putExtra(Intent.EXTRA_SUBJECT, "Testing out my Email Intent -- Success!"); if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); } }
Most of the code that makes apps work isn't code you write yourself--instead we use libraries.
A library will provide an API for implementing the features you need.
API stands for Application Program Interface, and it means the classes and methods that you need to use to leverage all this code that other people wrote.
You've already learned a lot about the Android APIs when you learned about Application, Activity, and the Intent system.Dependencies are libraries that your project needs to work.
You can add libraries manually by putting a copy of the library (usually a .jar or .aar file) in your project and instructing the gradle build script to include this file when it builds your app.
Or, you can use a dependency management system, like gradle, to automatically download and include the required libraries into your app.
Gradle is both our build tool and our dependency managment system. The build.gradle script gives instructions for how to build the app and which dependencies to download and include.
apply plugin: 'com.android.application' android { compileSdkVersion 24 buildToolsVersion "24.0.3" defaultConfig { applicationId "com.audreytroutt.androidbeginners.firstapp" minSdkVersion 16 targetSdkVersion 24 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' // Support libraries compile 'com.android.support:appcompat-v7:24.2.1' compile 'com.android.support:design:24.2.1' compile 'com.android.support:cardview-v7:24.2.1' compile 'com.android.support:support-v4:24.2.1' // picasso compile 'com.squareup.picasso:picasso:2.5.2' // TODO Project 3: add the firebase auth dependencies here }
Once you've added a library to your project, you need to know how to use it. There are four kinds of documentation for most libraries:
Getting Started Instructions Formal API Documentation Feature Usage Guides Sample appsGetting started instructions usually tell you how to get the library and maybe how to initialize the main class.
Formal API documentation will list every class, method, and field in the API. Often these are in javadoc format.
Feature usage guides give you step by step instructions for how to use a specific feature of the API.
Sample apps are helpful because you can see a working example of the API.
When we use an authentication service there are typically three steps:
Send request for authentication using API Receive success token or failure response from auth service (Optional) Request additional user info with success tokenFirebase is a service that can do authentication AND store data.
Rather than forcing your app users to remember a new username and password you can let them log in with Google instead.
You can authenticate with Google and then use the Google success token to authenticate with Firebase.
For this project, your goal is to add a login screen to our app.
We are going to use Google to log in so that we don't have to remember any usernames or passwords.
Let's add a login screen to our app to learn more about who is using the app.
Reference: Using Firebase Auth
Add the following dependencies to your app's build.gradle file inside the dependencies{...} section
dependencies { // ... // TODO Project 3: add the firebase auth dependencies here compile 'com.google.firebase:firebase-core:9.8.0' compile 'com.google.firebase:firebase-auth:9.8.0' compile 'com.google.android.gms:play-services-auth:9.8.0' }
HINT: There are two build.gradle files for your project. One is labled "Module" and the other "Project". The Module one is the one we want to edit.
Add the following line to your app's build.gradle file outside the dependencies{...} section at the bottom of the file.
// Add to the bottom of the file outside of dependencies apply plugin: 'com.google.gms.google-services'
HINT: Always sync gradle after editing build.gradle. Android Studio will remind you.
Go to File->New->Activity and choose "Empty Activity"
Remove the launch intent filter from MainActivity.
<activity android:name=".MainActivity" android:label="Android Beginners" android:theme="@style/AppTheme.NoActionBar"> <!-- TODO Project 3: Remove this intent when you add the LoginActivity --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
Check that your LoginActivity looks like this:
<activity android:name=".LoginActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
Open activity_login.xml and replace it's contents with this layout.
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.audreytroutt.androidbeginners.firstapp.LoginActivity"> <ImageView android:layout_width="match_parent" android:layout_height="match_parent" android:src="@drawable/cell_bg" android:contentDescription="@string/background_image_description" android:scaleType="fitXY"/> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:gravity="center_horizontal" android:orientation="vertical" android:layout_gravity="center_horizontal"> <com.google.android.gms.common.SignInButton android:id="@+id/sign_in_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:layout_gravity="center_horizontal" /> <TextView android:id="@+id/logged_in_name" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:textSize="40sp"/> </LinearLayout> </FrameLayout>
Findy your LoginActivity onCreate method and set up a reference to your sign in button.
public class LoginActivity extends AppCompatActivity { private TextView mStatusTextView; private SignInButton mSignInButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); mSignInButton = (SignInButton) findViewById(R.id.sign_in_button); mSignInButton.setSize(SignInButton.SIZE_WIDE); mStatusTextView = (TextView) findViewById(R.id.logged_in_name); } }
public class LoginActivity extends AppCompatActivity implements GoogleApiClient.OnConnectionFailedListener { // ... private GoogleApiClient mGoogleApiClient; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); // ... other stuff you already set up // Set up Google Auth and request basic user profile data and email GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(getString(R.string.default_web_client_id)) .requestEmail() .build(); mGoogleApiClient = new GoogleApiClient.Builder(this) .enableAutoManage(this, this) .addApi(Auth.GOOGLE_SIGN_IN_API, gso) .build(); } @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { Log.d(TAG, "onConnectionFailed:" + connectionResult.getErrorMessage()); } }
Inside onCreate also set up Firebase Authentication.
// ... private FirebaseAuth mAuth; private FirebaseAuth.AuthStateListener mAuthListener; private static final String TAG = "LoginActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); // ... other stuff you already set up // Set up Firebase Auth mAuth = FirebaseAuth.getInstance(); mAuthListener = new FirebaseAuth.AuthStateListener() { @Override public void onAuthStateChanged(@NonNull FirebaseAuth firebaseAuth) { FirebaseUser user = firebaseAuth.getCurrentUser(); if (user != null) { // User is signed in Log.d(TAG, "onAuthStateChanged:signed_in:" + user.getUid()); } else { // User is signed out Log.d(TAG, "onAuthStateChanged:signed_out"); } } }; }
We only want to listen for authentication while the login activity is visible.
Add these methods inside your LoginActivity, after onCreate.
@Override public void onStart() { super.onStart(); mAuth.addAuthStateListener(mAuthListener); } @Override public void onStop() { super.onStop(); if (mAuthListener != null) { mAuth.removeAuthStateListener(mAuthListener); } }
Make Your LoginActivity the listener for the sign in button and implement an onClick method.
// # 1 ADD IMPLEMENTS OnClickListener public class LoginActivity extends AppCompatActivity implements GoogleApiClient.OnConnectionFailedListener, OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { // ... // # 3 setOnClickListener mSignInButton.setOnClickListener(this); // ... } // # 2 ADD onClick @Override public void onClick(View v) { if (R.id.sign_in_button == v.getId()) { Toast.makeText(this, "Sign In Button was pressed", Toast.LENGTH_LONG).show(); } } }
Use an intent! This starts a Google Auth activity.
@Override public void onClick(View v) { if (R.id.sign_in_button == v.getId()) { Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(mGoogleApiClient); startActivityForResult(signInIntent, RC_SIGN_IN); } }
When the Google auth is done it will call back to our LoginActivity onActivityResult method. If successful we want to tell Firebase who you are.
private static final int RC_SIGN_IN = 111; // ... @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); // 1. if the result is for our sign in request if (requestCode == RC_SIGN_IN) { GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data); if (result.isSuccess()) { // 2. if Google Sign In was successful, get the Google account GoogleSignInAccount account = result.getSignInAccount(); // 3. Display the welcome text on the LoginActivity status text view String text = getResources().getString(R.string.welcome_on_login, account.getDisplayName()); mStatusTextView.setText(text); // 4. Hide the sign in button so that you can't sign in again mSignInButton.setVisibility(View.INVISIBLE); // 5. Tell Firebase to login with this Google account (this method won't exist until you create it in the next step!) firebaseAuthWithGoogle(account); } else { // 6. Google Sign In failed, update status message mStatusTextView.setText("Login Failed"); } } }
Use Android Studio to automatically create the missing method, firebaseAuthWithGoogle. Add the following implementation:
private void firebaseAuthWithGoogle(final GoogleSignInAccount acct) { Log.d(TAG, "firebaseAuthWithGoogle:" + acct.getId()); AuthCredential credential = GoogleAuthProvider.getCredential(acct.getIdToken(), null); mAuth.signInWithCredential(credential).addOnCompleteListener(this, new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful()); if (!task.isSuccessful()) { // Firebase login failed even though Google login was successful Log.w(TAG, "signInWithCredential", task.getException()); Toast.makeText(LoginActivity.this, "Authentication failed.", Toast.LENGTH_SHORT).show(); mSignInButton.setVisibility(View.VISIBLE); } else { // Firebase login successful FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser(); UserProfileChangeRequest profileUpdates = new UserProfileChangeRequest.Builder() .setDisplayName(acct.getDisplayName()) .setPhotoUri(acct.getPhotoUrl()) .build(); user.updateProfile(profileUpdates).addOnCompleteListener(new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { if (task.isSuccessful()) { // We are all done authenticating with Firebase // TODO show the MainActivity! Do you remember how to create an intent and call startActivity? } } }); } } }); }
In order to use Google Login you need to add your debug certificate fingerprint to our Firebase account.
Follow these instructions to get your SHA-1 developer fingerprint. It will look something like this:
SHA1: DA:39:A3:EE:5E:6B:4B:0D:32:55:BF:EF:95:60:18:90:AF:D8:07:09
Run the app and try logging in with Google!
In MainActivity find setUserInfoInDrawer and add the following implementation:
private void setUserInfoInDrawer() { // TODO Project 3: Get user info and display it at the top of the drawer FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser(); if (user != null) { NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view); View headerView = navigationView.getHeaderView(0); TextView currentUserEmail = (TextView) headerView.findViewById(R.id.current_user_email); currentUserEmail.setText(user.getEmail()); TextView currentUserName = (TextView) headerView.findViewById(R.id.current_user_name); currentUserName.setText(user.getDisplayName()); ImageView currentUserImage = (ImageView) headerView.findViewById(R.id.current_user_photo); Picasso.with(this).load(user.getPhotoUrl()).into(currentUserImage); } }
It's nice to be able to sign out too! We will need to set up a Google API Client in MainActivity.
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener, GoogleApiClient.OnConnectionFailedListener { // ... private GoogleApiClient mGoogleApiClient; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // ... other stuff you already set up // Set up Google Auth and request basic user profile data and email GoogleSignInOptions gso = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(getString(R.string.default_web_client_id)) .requestEmail() .build(); mGoogleApiClient = new GoogleApiClient.Builder(this) .enableAutoManage(this, this) .addApi(Auth.GOOGLE_SIGN_IN_API, gso) .build(); } @Override public void onConnectionFailed(@NonNull ConnectionResult connectionResult) { Log.d(TAG, "onConnectionFailed:" + connectionResult.getErrorMessage()); } }
I've already added a Sign Out menu in the top right of the screen. Find onOptionsItemSelected in MainActivity to implement Sign Out:
if (id == R.id.action_sign_out) { // TODO Project 3: implement sign out Auth.GoogleSignInApi.signOut(mGoogleApiClient).setResultCallback(new ResultCallback<Status>() { @Override public void onResult(Status status) { if (status.isSuccess()) { Intent loginScreenIntent = new Intent(MainActivity.this, LoginActivity.class); startActivity(loginScreenIntent); } } }); return true; }
We already learned how to wire up a photo intent in the second project. There's more fun to be had with images, like editing them!
On Android, images are loaded into memory as Bitmaps
Bitmap bmp = BitmapFactory.decodeFile(getAndroidBeginnerImageUri().getPath(), null);
A bitmap is a representation of the image as a grid of pixels.
You can edit your Bitmap by creating a Canvas and some Paint.
Just like good old MS Paint you can switch colors and draw different shapes, all with code!
Bitmap bitmap = Bitmap.createBitmap(300, 300, null); Canvas canvas = new Canvas(bitmap); Paint paint = new Paint(); paint.setColor(Color.WHITE); canvas.drawCircle(150, 150, 50, paint);
The pixels are numbered by their x and y coordinates on the grid. With 0,0 at the top left.
X increases as you move to the right and Y increases as you move to the bottom.
Most shapes are drawn from the top left, but when drawing text on a bitmap by default text is drawn from the bottom left of the text (not the top left!).
HINT: The bottom of the text is known as the baseline of the text. The tails of letters like y and g hang down below the baseline, so make room for them when placing your text.
For this project, your goal is to update the snapped photo to add the words "Android Developer" on top.
In MainActivity look for the method editImage and your TODO comment there
private void editImage() { if (!isStoragePermissionGranted()) { requestWriteExternalStoragePermission(); } // Load the image into memory from the file Bitmap bmp = BitmapFactory.decodeFile(getAndroidBeginnerImageUri().getPath(), null); // Square up the image from the camera int croppedImageSize = (int)Math.min(bmp.getWidth(), bmp.getHeight()); Bitmap cropped = centerCropBitmapToSquareSize(bmp, croppedImageSize); // TODO Project 4: Draw text on the cropped image // Finally, save the edited image back to the file saveBitmapToFile(cropped); }
Create a Canvas from your bitmap--this is how you can "paint" on your image. Create a Paint object too!
Canvas canvas = new Canvas(cropped); Paint paint = new Paint();
Set the size and color of the text that we want to write on the image
// I want the text to be about 1/10 as tall as the image final int textSize = croppedImageSize / 10; paint.setTextSize(textSize); paint.setColor(ResourcesCompat.getColor(getResources(), R.color.colorPrimaryDark, null));
You will need to tell your Paint where to draw the text on the image in terms of X and Y position.
X is the horizontal position of the text, relative to the left side. Y is the vertical position of the text, measured as how far the BOTTOM of the text is from the top of the image.
// it works out to start the text about 1/10 of the way into the image from the left, which is the same as our text size. final int textXPosition = textSize; // I want the text to be a little above the bottom of the image final int textYPosition = croppedImageSize - (textSize / 2);
Now that your paint and variables are configured you can draw the text to your canvas! Drawing on the canvas updates the image.
final String label = getString(R.string.android_developer_image_label); canvas.drawText(label, textXPosition, textYPosition, paint);
HINT: if you want the text to stand out a little more, try adding a shadow layer to your Paint BEFORE you draw the text. Check out paint.setShadowLayer(...)
Make sure that the call to save the edited bitmap is AFTER you are done painting on your canvas so that your changes will be there the next time you run the app.
// Save the edited image back to the file saveBitmapToFile(cropped);
The next time you snap an image you should see your text added on top. Share that with your friends!
Android tests are based off of JUnit. There are two kinds of tests: unit tests and instrumented tests.
Normally when you create a new project Android Studio will create the unit test and instrumented test directories for you, but I have deleted them!
Switch to Project view in the Project Tool on the left and create test/java and androidTest/java under app/src
Cmd+click (Mac) or Ctrl+click (Windows) on test/java and Select New... -> Java Class.
Create FirstTest in package com.audreytroutt.androidbeginners.firstapp
Paste in this implementation
package com.audreytroutt.androidbeginners.firstapp; import org.junit.Test; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; public class FirstTest { @Test public void trueIsTrue() { assertThat(true, is(true)); assertEquals(true, true); assertTrue(true); } @Test public void falseIsFalse() { assertThat(false, is(false)); assertEquals(false, false); assertFalse(false); } }
We already have the junit 4.12 dependency in our project
dependencies { ... testCompile 'junit:junit:4.12' ... }
Cmd+click (Mac) or Ctrl+click (Windows) on FirstTest and select Run 'FirstTest'
Down in the Run tool, you should see the results of your tests
Cmd+click (Mac) or Ctrl+click (Windows) on androidTest/java and Select New... -> Java Class and create MainActivityTest in package com.audreytroutt.androidbeginners.firstapp
We need to add several dependencies to build Integrated Tests
dependencies { ... // for Instrumented Tests androidTestCompile 'com.android.support:support-annotations:24.2.1' androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.supp