├── .gitignore ├── CODEOWNERS ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── example │ │ └── android │ │ └── emojify │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── android │ │ │ └── emojify │ │ │ ├── BitmapUtils.java │ │ │ ├── Emojifier.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable │ │ ├── closed_frown.png │ │ ├── closed_smile.png │ │ ├── frown.png │ │ ├── ic_clear.xml │ │ ├── ic_delete.xml │ │ ├── ic_save.xml │ │ ├── ic_share.xml │ │ ├── leftwink.png │ │ ├── leftwinkfrown.png │ │ ├── rightwink.png │ │ ├── rightwinkfrown.png │ │ └── smile.png │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── file_paths.xml │ └── test │ └── java │ └── com │ └── example │ └── android │ └── emojify │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea/ 3 | *.iml 4 | build/ 5 | /local.properties 6 | .DS_Store 7 | /captures 8 | google-services.json 9 | 10 | 11 | # Created by https://www.gitignore.io/api/intellij,android,java 12 | 13 | ### Intellij ### 14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 16 | 17 | # User-specific stuff: 18 | .idea/workspace.xml 19 | .idea/tasks.xml 20 | 21 | # Sensitive or high-churn files: 22 | .idea/dataSources/ 23 | .idea/dataSources.ids 24 | .idea/dataSources.xml 25 | .idea/dataSources.local.xml 26 | .idea/sqlDataSources.xml 27 | .idea/dynamic.xml 28 | .idea/uiDesigner.xml 29 | 30 | # Gradle: 31 | .idea/gradle.xml 32 | .idea/libraries 33 | 34 | # Mongo Explorer plugin: 35 | .idea/mongoSettings.xml 36 | 37 | ## File-based project format: 38 | *.iws 39 | 40 | ## Plugin-specific files: 41 | 42 | # IntelliJ 43 | /out/ 44 | 45 | # mpeltonen/sbt-idea plugin 46 | .idea_modules/ 47 | 48 | # JIRA plugin 49 | atlassian-ide-plugin.xml 50 | 51 | # Crashlytics plugin (for Android Studio and IntelliJ) 52 | com_crashlytics_export_strings.xml 53 | crashlytics.properties 54 | crashlytics-build.properties 55 | fabric.properties 56 | 57 | ### Intellij Patch ### 58 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 59 | 60 | # *.iml 61 | # modules.xml 62 | # .idea/misc.xml 63 | # *.ipr 64 | 65 | 66 | ### Android ### 67 | # Built application files 68 | *.apk 69 | *.ap_ 70 | 71 | # Files for the ART/Dalvik VM 72 | *.dex 73 | 74 | # Java class files 75 | *.class 76 | 77 | # Generated files 78 | bin/ 79 | gen/ 80 | out/ 81 | 82 | # Gradle files 83 | .gradle/ 84 | build/ 85 | 86 | # Local configuration file (sdk path, etc) 87 | local.properties 88 | 89 | # Proguard folder generated by Eclipse 90 | proguard/ 91 | 92 | # Log Files 93 | *.log 94 | 95 | # Android Studio Navigation editor temp files 96 | .navigation/ 97 | 98 | # Android Studio captures folder 99 | captures/ 100 | 101 | # Intellij 102 | *.iml 103 | 104 | # Keystore files 105 | *.jks 106 | 107 | # External native build folder generated in Android Studio 2.2 and later 108 | .externalNativeBuild 109 | 110 | ### Android Patch ### 111 | gen-external-apklibs 112 | 113 | 114 | ### Java ### 115 | 116 | # BlueJ files 117 | *.ctxt 118 | 119 | # Mobile Tools for Java (J2ME) 120 | .mtj.tmp/ 121 | 122 | # Package Files # 123 | *.jar 124 | *.war 125 | *.ear 126 | 127 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 128 | hs_err_pid* 129 | 130 | # End of https://www.gitignore.io/api/intellij,android,java -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @udacity/active-public-content -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AdvancedAndroid_Emojify 2 | 3 | This is the toy app for the Libraries lesson of the [Advanced Android App Development course on Udacity](https://www.udacity.com/course/advanced-android-app-development--ud855). 4 | 5 | ## How to use this repo while taking the course 6 | 7 | Each code repository in this class has a chain of commits that looks like this: 8 | 9 | ![listofcommits](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe2e_listofcommits/listofcommits.png) 10 | 11 | These commits show every step you'll take to create the app. They include **Exercise** commits and **Solution** commits. 12 | 13 | Exercise commits contain instructions for completing the exercise, while solution commits show the completed exercise. You can tell what a commit is by looking at its commit message. 14 | 15 | For example, **TFCM.01-Exercise-AddGradleDependencies** is the first code step in the Firebase Cloud Messaging (FCM) lesson. This is the exercise commit, and the exercise is called Add Gradle Dependencies. 16 | 17 | Each commit also has a **branch** associated with it of the same name as the commit message, seen below: 18 | 19 | ![branches](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590390fe_branches-ud855/branches-ud855.png 20 | ) 21 | Access all branches from this tab 22 | 23 | ![listofbranches](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe76_listofbranches/listofbranches.png 24 | ) 25 | 26 | 27 | ![branchesdropdown](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590391a3_branches-dropdown-ud855/branches-dropdown-ud855.png 28 | ) 29 | 30 | 31 | The branches are also accessible from the drop-down in the "Code" tab 32 | 33 | 34 | ## Working with the Course Code 35 | 36 | Here are the basic steps for working with and completing exercises in the repo. This information is linked whenever you start a new exercise project, so don't feel you need to memorize all of this! In fact, skim it now, make sure that you know generally how to do the different tasks, and then come back when you start your first exercise. 37 | 38 | The basic steps are: 39 | 40 | 1. Clone the repo 41 | 2. Checkout the exercise branch 42 | 3. Find and complete the TODOs 43 | 4. Optionally commit your code changes 44 | 5. Compare with the solution 45 | 46 | 47 | **Step 1: Clone the repo** 48 | 49 | As you go through the course, you'll be instructed to clone the different exercise repositories, so you don't need to set these up now. You can clone a repository from github in a folder of your choice with the command: 50 | 51 | ```bash 52 | git clone https://github.com/udacity/REPOSITORY_NAME.git 53 | ``` 54 | 55 | **Step 2: Checkout the exercise branch** 56 | 57 | As you do different exercises in the code, you'll be told which exercise you're on, as seen below: 58 | ![exerciseexample](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0087_exerciseexample/exerciseexample.png 59 | ) 60 | 61 | To complete an exercise, you'll want to check out the branch associated with that exercise. For the exercise above, the command to check out that branch would be: 62 | 63 | ```bash 64 | git checkout TFCM.01-Exercise-AddGradleDependencies 65 | ``` 66 | 67 | **Step 3: Find and complete the TODOs** 68 | 69 | This branch should always have **Exercise** in the title. Once you've checked out the branch, you'll have the code in the exact state you need. You'll even have TODOs, which are special comments that tell you all the steps you need to complete the exercise. You can easily navigate to all the TODOs using Android Studio's TODO tool. To open the TODO tool, click the button at the bottom of the screen that says TODO. This will display a list of all comments with TODO in the project. 70 | 71 | We've numbered the TODO steps so you can do them in order: 72 | ![todos](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00e7_todos/todos.png 73 | ) 74 | 75 | **Step 4: Optionally commit your code changes** 76 | 77 | After You've completed the TODOs, you can optionally commit your changes. This will allow you to see the code you wrote whenever you return to the branch. The following git code will add and save **all** your changes. 78 | 79 | ```bash 80 | git add . 81 | git commit -m "Your commit message" 82 | ``` 83 | 84 | **Step 5: Compare with the solution** 85 | 86 | Most exercises will have a list of steps for you to check off in the classroom. Once you've checked these off, you'll see a pop up window with a link to the solution code. Note the **Diff** link: 87 | 88 | ![solutionwindow](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00f9_solutionwindow/solutionwindow.png 89 | ) 90 | 91 | The **Diff** link will take you to a Github diff as seen below: 92 | ![diff](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0108_diffsceenshot/diffsceenshot.png 93 | ) 94 | 95 | All of the code that was added in the solution is in green, and the removed code (which will usually be the TODO comments) is in red. 96 | ## Report Issues 97 | Notice any issues with a repository? Please file a github issue in the repository. 98 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 25 5 | buildToolsVersion "25.0.2" 6 | defaultConfig { 7 | applicationId "com.example.android.emojify" 8 | minSdkVersion 15 9 | targetSdkVersion 25 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:design:25.2.0' 28 | compile 'com.android.support:appcompat-v7:25.2.0' 29 | compile 'com.google.android.gms:play-services-vision:10.2.0' 30 | compile 'com.jakewharton:butterknife:8.4.0' 31 | annotationProcessor 'com.jakewharton:butterknife-compiler:8.4.0' 32 | compile 'com.jakewharton.timber:timber:4.5.0' 33 | testCompile 'junit:junit:4.12' 34 | } 35 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/ngamolsky/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/example/android/emojify/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.example.android.emojify; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.example.android.emojify", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 19 | 20 | 23 | 24 | 25 | 26 | 32 | 37 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/emojify/BitmapUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.emojify; 18 | 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.graphics.Bitmap; 22 | import android.graphics.BitmapFactory; 23 | import android.net.Uri; 24 | import android.os.Environment; 25 | import android.support.v4.content.FileProvider; 26 | import android.util.DisplayMetrics; 27 | import android.view.WindowManager; 28 | import android.widget.Toast; 29 | 30 | import java.io.File; 31 | import java.io.FileOutputStream; 32 | import java.io.IOException; 33 | import java.io.OutputStream; 34 | import java.text.SimpleDateFormat; 35 | import java.util.Date; 36 | import java.util.Locale; 37 | 38 | class BitmapUtils { 39 | 40 | private static final String FILE_PROVIDER_AUTHORITY = "com.example.android.fileprovider"; 41 | 42 | 43 | /** 44 | * Resamples the captured photo to fit the screen for better memory usage. 45 | * 46 | * @param context The application context. 47 | * @param imagePath The path of the photo to be resampled. 48 | * @return The resampled bitmap 49 | */ 50 | static Bitmap resamplePic(Context context, String imagePath) { 51 | 52 | // Get device screen size information 53 | DisplayMetrics metrics = new DisplayMetrics(); 54 | WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 55 | manager.getDefaultDisplay().getMetrics(metrics); 56 | 57 | int targetH = metrics.heightPixels; 58 | int targetW = metrics.widthPixels; 59 | 60 | // Get the dimensions of the original bitmap 61 | BitmapFactory.Options bmOptions = new BitmapFactory.Options(); 62 | bmOptions.inJustDecodeBounds = true; 63 | BitmapFactory.decodeFile(imagePath, bmOptions); 64 | int photoW = bmOptions.outWidth; 65 | int photoH = bmOptions.outHeight; 66 | 67 | // Determine how much to scale down the image 68 | int scaleFactor = Math.min(photoW / targetW, photoH / targetH); 69 | 70 | // Decode the image file into a Bitmap sized to fill the View 71 | bmOptions.inJustDecodeBounds = false; 72 | bmOptions.inSampleSize = scaleFactor; 73 | 74 | return BitmapFactory.decodeFile(imagePath); 75 | } 76 | 77 | /** 78 | * Creates the temporary image file in the cache directory. 79 | * 80 | * @return The temporary image file. 81 | * @throws IOException Thrown if there is an error creating the file 82 | */ 83 | static File createTempImageFile(Context context) throws IOException { 84 | String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", 85 | Locale.getDefault()).format(new Date()); 86 | String imageFileName = "JPEG_" + timeStamp + "_"; 87 | File storageDir = context.getExternalCacheDir(); 88 | 89 | return File.createTempFile( 90 | imageFileName, /* prefix */ 91 | ".jpg", /* suffix */ 92 | storageDir /* directory */ 93 | ); 94 | } 95 | 96 | /** 97 | * Deletes image file for a given path. 98 | * 99 | * @param context The application context. 100 | * @param imagePath The path of the photo to be deleted. 101 | */ 102 | static boolean deleteImageFile(Context context, String imagePath) { 103 | // Get the file 104 | File imageFile = new File(imagePath); 105 | 106 | // Delete the image 107 | boolean deleted = imageFile.delete(); 108 | 109 | // If there is an error deleting the file, show a Toast 110 | if (!deleted) { 111 | String errorMessage = context.getString(R.string.error); 112 | Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show(); 113 | } 114 | 115 | return deleted; 116 | } 117 | 118 | /** 119 | * Helper method for adding the photo to the system photo gallery so it can be accessed 120 | * from other apps. 121 | * 122 | * @param imagePath The path of the saved image 123 | */ 124 | private static void galleryAddPic(Context context, String imagePath) { 125 | Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); 126 | File f = new File(imagePath); 127 | Uri contentUri = Uri.fromFile(f); 128 | mediaScanIntent.setData(contentUri); 129 | context.sendBroadcast(mediaScanIntent); 130 | } 131 | 132 | 133 | /** 134 | * Helper method for saving the image. 135 | * 136 | * @param context The application context. 137 | * @param image The image to be saved. 138 | * @return The path of the saved image. 139 | */ 140 | static String saveImage(Context context, Bitmap image) { 141 | 142 | String savedImagePath = null; 143 | 144 | // Create the new file in the external storage 145 | String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", 146 | Locale.getDefault()).format(new Date()); 147 | String imageFileName = "JPEG_" + timeStamp + ".jpg"; 148 | File storageDir = new File( 149 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) 150 | + "/Emojify"); 151 | boolean success = true; 152 | if (!storageDir.exists()) { 153 | success = storageDir.mkdirs(); 154 | } 155 | 156 | // Save the new Bitmap 157 | if (success) { 158 | File imageFile = new File(storageDir, imageFileName); 159 | savedImagePath = imageFile.getAbsolutePath(); 160 | try { 161 | OutputStream fOut = new FileOutputStream(imageFile); 162 | image.compress(Bitmap.CompressFormat.JPEG, 100, fOut); 163 | fOut.close(); 164 | } catch (Exception e) { 165 | e.printStackTrace(); 166 | } 167 | 168 | // Add the image to the system gallery 169 | galleryAddPic(context, savedImagePath); 170 | 171 | // Show a Toast with the save location 172 | String savedMessage = context.getString(R.string.saved_message, savedImagePath); 173 | Toast.makeText(context, savedMessage, Toast.LENGTH_SHORT).show(); 174 | } 175 | 176 | return savedImagePath; 177 | } 178 | 179 | /** 180 | * Helper method for sharing an image. 181 | * 182 | * @param context The image context. 183 | * @param imagePath The path of the image to be shared. 184 | */ 185 | static void shareImage(Context context, String imagePath) { 186 | // Create the share intent and start the share activity 187 | File imageFile = new File(imagePath); 188 | Intent shareIntent = new Intent(Intent.ACTION_SEND); 189 | shareIntent.setType("image/*"); 190 | Uri photoURI = FileProvider.getUriForFile(context, FILE_PROVIDER_AUTHORITY, imageFile); 191 | shareIntent.putExtra(Intent.EXTRA_STREAM, photoURI); 192 | context.startActivity(shareIntent); 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/emojify/Emojifier.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.emojify; 18 | 19 | import android.content.Context; 20 | import android.graphics.Bitmap; 21 | import android.graphics.BitmapFactory; 22 | import android.graphics.Canvas; 23 | import android.util.SparseArray; 24 | import android.widget.Toast; 25 | 26 | import com.google.android.gms.vision.Frame; 27 | import com.google.android.gms.vision.face.Face; 28 | import com.google.android.gms.vision.face.FaceDetector; 29 | 30 | import timber.log.Timber; 31 | 32 | class Emojifier { 33 | 34 | 35 | private static final float EMOJI_SCALE_FACTOR = .9f; 36 | private static final double SMILING_PROB_THRESHOLD = .15; 37 | private static final double EYE_OPEN_PROB_THRESHOLD = .5; 38 | 39 | /** 40 | * Method for detecting faces in a bitmap, and drawing emoji depending on the facial 41 | * expression. 42 | * 43 | * @param context The application context. 44 | * @param picture The picture in which to detect the faces. 45 | */ 46 | static Bitmap detectFacesandOverlayEmoji(Context context, Bitmap picture) { 47 | 48 | // Create the face detector, disable tracking and enable classifications 49 | FaceDetector detector = new FaceDetector.Builder(context) 50 | .setTrackingEnabled(false) 51 | .setClassificationType(FaceDetector.ALL_CLASSIFICATIONS) 52 | .build(); 53 | 54 | // Build the frame 55 | Frame frame = new Frame.Builder().setBitmap(picture).build(); 56 | 57 | // Detect the faces 58 | SparseArray faces = detector.detect(frame); 59 | 60 | // Log the number of faces 61 | Timber.d("detectFaces: number of faces = " + faces.size()); 62 | 63 | // Initialize result bitmap to original picture 64 | Bitmap resultBitmap = picture; 65 | 66 | // If there are no faces detected, show a Toast message 67 | if (faces.size() == 0) { 68 | Toast.makeText(context, R.string.no_faces_message, Toast.LENGTH_SHORT).show(); 69 | } else { 70 | 71 | // Iterate through the faces 72 | for (int i = 0; i < faces.size(); ++i) { 73 | Face face = faces.valueAt(i); 74 | 75 | Bitmap emojiBitmap; 76 | switch (whichEmoji(face)) { 77 | case SMILE: 78 | emojiBitmap = BitmapFactory.decodeResource(context.getResources(), 79 | R.drawable.smile); 80 | break; 81 | case FROWN: 82 | emojiBitmap = BitmapFactory.decodeResource(context.getResources(), 83 | R.drawable.frown); 84 | break; 85 | case LEFT_WINK: 86 | emojiBitmap = BitmapFactory.decodeResource(context.getResources(), 87 | R.drawable.leftwink); 88 | break; 89 | case RIGHT_WINK: 90 | emojiBitmap = BitmapFactory.decodeResource(context.getResources(), 91 | R.drawable.rightwink); 92 | break; 93 | case LEFT_WINK_FROWN: 94 | emojiBitmap = BitmapFactory.decodeResource(context.getResources(), 95 | R.drawable.leftwinkfrown); 96 | break; 97 | case RIGHT_WINK_FROWN: 98 | emojiBitmap = BitmapFactory.decodeResource(context.getResources(), 99 | R.drawable.rightwinkfrown); 100 | break; 101 | case CLOSED_EYE_SMILE: 102 | emojiBitmap = BitmapFactory.decodeResource(context.getResources(), 103 | R.drawable.closed_smile); 104 | break; 105 | case CLOSED_EYE_FROWN: 106 | emojiBitmap = BitmapFactory.decodeResource(context.getResources(), 107 | R.drawable.closed_frown); 108 | break; 109 | default: 110 | emojiBitmap = null; 111 | Toast.makeText(context, R.string.no_emoji, Toast.LENGTH_SHORT).show(); 112 | } 113 | 114 | // Add the emojiBitmap to the proper position in the original image 115 | resultBitmap = addBitmapToFace(resultBitmap, emojiBitmap, face); 116 | } 117 | } 118 | 119 | 120 | // Release the detector 121 | detector.release(); 122 | 123 | return resultBitmap; 124 | } 125 | 126 | 127 | /** 128 | * Determines the closest emoji to the expression on the face, based on the 129 | * odds that the person is smiling and has each eye open. 130 | * 131 | * @param face The face for which you pick an emoji. 132 | */ 133 | 134 | private static Emoji whichEmoji(Face face) { 135 | // Log all the probabilities 136 | Timber.d("whichEmoji: smilingProb = " + face.getIsSmilingProbability()); 137 | Timber.d("whichEmoji: leftEyeOpenProb = " 138 | + face.getIsLeftEyeOpenProbability()); 139 | Timber.d("whichEmoji: rightEyeOpenProb = " 140 | + face.getIsRightEyeOpenProbability()); 141 | 142 | 143 | boolean smiling = face.getIsSmilingProbability() > SMILING_PROB_THRESHOLD; 144 | 145 | boolean leftEyeClosed = face.getIsLeftEyeOpenProbability() < EYE_OPEN_PROB_THRESHOLD; 146 | boolean rightEyeClosed = face.getIsRightEyeOpenProbability() < EYE_OPEN_PROB_THRESHOLD; 147 | 148 | 149 | // Determine and log the appropriate emoji 150 | Emoji emoji; 151 | if(smiling) { 152 | if (leftEyeClosed && !rightEyeClosed) { 153 | emoji = Emoji.LEFT_WINK; 154 | } else if(rightEyeClosed && !leftEyeClosed){ 155 | emoji = Emoji.RIGHT_WINK; 156 | } else if (leftEyeClosed){ 157 | emoji = Emoji.CLOSED_EYE_SMILE; 158 | } else { 159 | emoji = Emoji.SMILE; 160 | } 161 | } else { 162 | if (leftEyeClosed && !rightEyeClosed) { 163 | emoji = Emoji.LEFT_WINK_FROWN; 164 | } else if(rightEyeClosed && !leftEyeClosed){ 165 | emoji = Emoji.RIGHT_WINK_FROWN; 166 | } else if (leftEyeClosed){ 167 | emoji = Emoji.CLOSED_EYE_FROWN; 168 | } else { 169 | emoji = Emoji.FROWN; 170 | } 171 | } 172 | 173 | 174 | // Log the chosen Emoji 175 | Timber.d("whichEmoji: " + emoji.name()); 176 | 177 | // return the chosen Emoji 178 | return emoji; 179 | } 180 | 181 | /** 182 | * Combines the original picture with the emoji bitmaps 183 | * 184 | * @param backgroundBitmap The original picture 185 | * @param emojiBitmap The chosen emoji 186 | * @param face The detected face 187 | * @return The final bitmap, including the emojis over the faces 188 | */ 189 | private static Bitmap addBitmapToFace(Bitmap backgroundBitmap, Bitmap emojiBitmap, Face face) { 190 | 191 | // Initialize the results bitmap to be a mutable copy of the original image 192 | Bitmap resultBitmap = Bitmap.createBitmap(backgroundBitmap.getWidth(), 193 | backgroundBitmap.getHeight(), backgroundBitmap.getConfig()); 194 | 195 | // Scale the emoji so it looks better on the face 196 | float scaleFactor = EMOJI_SCALE_FACTOR; 197 | 198 | // Determine the size of the emoji to match the width of the face and preserve aspect ratio 199 | int newEmojiWidth = (int) (face.getWidth() * scaleFactor); 200 | int newEmojiHeight = (int) (emojiBitmap.getHeight() * 201 | newEmojiWidth / emojiBitmap.getWidth() * scaleFactor); 202 | 203 | 204 | // Scale the emoji 205 | emojiBitmap = Bitmap.createScaledBitmap(emojiBitmap, newEmojiWidth, newEmojiHeight, false); 206 | 207 | // Determine the emoji position so it best lines up with the face 208 | float emojiPositionX = 209 | (face.getPosition().x + face.getWidth() / 2) - emojiBitmap.getWidth() / 2; 210 | float emojiPositionY = 211 | (face.getPosition().y + face.getHeight() / 2) - emojiBitmap.getHeight() / 3; 212 | 213 | // Create the canvas and draw the bitmaps to it 214 | Canvas canvas = new Canvas(resultBitmap); 215 | canvas.drawBitmap(backgroundBitmap, 0, 0, null); 216 | canvas.drawBitmap(emojiBitmap, emojiPositionX, emojiPositionY, null); 217 | 218 | return resultBitmap; 219 | } 220 | 221 | 222 | // Enum for all possible Emojis 223 | private enum Emoji { 224 | SMILE, 225 | FROWN, 226 | LEFT_WINK, 227 | RIGHT_WINK, 228 | LEFT_WINK_FROWN, 229 | RIGHT_WINK_FROWN, 230 | CLOSED_EYE_SMILE, 231 | CLOSED_EYE_FROWN 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/emojify/MainActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.example.android.emojify; 18 | 19 | 20 | import android.Manifest; 21 | import android.content.Intent; 22 | import android.content.pm.PackageManager; 23 | import android.graphics.Bitmap; 24 | import android.net.Uri; 25 | import android.os.Bundle; 26 | import android.provider.MediaStore; 27 | import android.support.annotation.NonNull; 28 | import android.support.design.widget.FloatingActionButton; 29 | import android.support.v4.app.ActivityCompat; 30 | import android.support.v4.content.ContextCompat; 31 | import android.support.v4.content.FileProvider; 32 | import android.support.v7.app.AppCompatActivity; 33 | import android.view.View; 34 | import android.widget.Button; 35 | import android.widget.ImageView; 36 | import android.widget.TextView; 37 | import android.widget.Toast; 38 | 39 | import java.io.File; 40 | import java.io.IOException; 41 | 42 | import butterknife.BindView; 43 | import butterknife.ButterKnife; 44 | import butterknife.OnClick; 45 | import timber.log.Timber; 46 | 47 | public class MainActivity extends AppCompatActivity { 48 | 49 | 50 | private static final int REQUEST_IMAGE_CAPTURE = 1; 51 | private static final int REQUEST_STORAGE_PERMISSION = 1; 52 | 53 | private static final String FILE_PROVIDER_AUTHORITY = "com.example.android.fileprovider"; 54 | 55 | @BindView(R.id.image_view) ImageView mImageView; 56 | 57 | @BindView(R.id.emojify_button) Button mEmojifyButton; 58 | @BindView(R.id.share_button) FloatingActionButton mShareFab; 59 | @BindView(R.id.save_button) FloatingActionButton mSaveFab; 60 | @BindView(R.id.clear_button) FloatingActionButton mClearFab; 61 | 62 | @BindView(R.id.title_text_view) TextView mTitleTextView; 63 | 64 | private String mTempPhotoPath; 65 | 66 | private Bitmap mResultsBitmap; 67 | 68 | 69 | @Override 70 | protected void onCreate(Bundle savedInstanceState) { 71 | super.onCreate(savedInstanceState); 72 | setContentView(R.layout.activity_main); 73 | 74 | // Bind the views 75 | ButterKnife.bind(this); 76 | 77 | // Set up Timber 78 | Timber.plant(new Timber.DebugTree()); 79 | } 80 | 81 | /** 82 | * OnClick method for "Emojify Me!" Button. Launches the camera app. 83 | */ 84 | @OnClick(R.id.emojify_button) 85 | public void emojifyMe() { 86 | // Check for the external storage permission 87 | if (ContextCompat.checkSelfPermission(this, 88 | Manifest.permission.WRITE_EXTERNAL_STORAGE) 89 | != PackageManager.PERMISSION_GRANTED) { 90 | 91 | // If you do not have permission, request it 92 | ActivityCompat.requestPermissions(this, 93 | new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 94 | REQUEST_STORAGE_PERMISSION); 95 | } else { 96 | // Launch the camera if the permission exists 97 | launchCamera(); 98 | } 99 | } 100 | 101 | @Override 102 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 103 | @NonNull int[] grantResults) { 104 | // Called when you request permission to read and write to external storage 105 | switch (requestCode) { 106 | case REQUEST_STORAGE_PERMISSION: { 107 | if (grantResults.length > 0 108 | && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 109 | // If you get permission, launch the camera 110 | launchCamera(); 111 | } else { 112 | // If you do not get permission, show a Toast 113 | Toast.makeText(this, R.string.permission_denied, Toast.LENGTH_SHORT).show(); 114 | } 115 | break; 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Creates a temporary image file and captures a picture to store in it. 122 | */ 123 | private void launchCamera() { 124 | 125 | // Create the capture image intent 126 | Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 127 | 128 | // Ensure that there's a camera activity to handle the intent 129 | if (takePictureIntent.resolveActivity(getPackageManager()) != null) { 130 | // Create the temporary File where the photo should go 131 | File photoFile = null; 132 | try { 133 | photoFile = BitmapUtils.createTempImageFile(this); 134 | } catch (IOException ex) { 135 | // Error occurred while creating the File 136 | ex.printStackTrace(); 137 | } 138 | // Continue only if the File was successfully created 139 | if (photoFile != null) { 140 | 141 | // Get the path of the temporary file 142 | mTempPhotoPath = photoFile.getAbsolutePath(); 143 | 144 | // Get the content URI for the image file 145 | Uri photoURI = FileProvider.getUriForFile(this, 146 | FILE_PROVIDER_AUTHORITY, 147 | photoFile); 148 | 149 | // Add the URI so the camera can store the image 150 | takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); 151 | 152 | // Launch the camera activity 153 | startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); 154 | } 155 | } 156 | } 157 | 158 | 159 | @Override 160 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 161 | // If the image capture activity was called and was successful 162 | if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) { 163 | // Process the image and set it to the TextView 164 | processAndSetImage(); 165 | } else { 166 | 167 | // Otherwise, delete the temporary image file 168 | BitmapUtils.deleteImageFile(this, mTempPhotoPath); 169 | } 170 | } 171 | 172 | /** 173 | * Method for processing the captured image and setting it to the TextView. 174 | */ 175 | private void processAndSetImage() { 176 | 177 | // Toggle Visibility of the views 178 | mEmojifyButton.setVisibility(View.GONE); 179 | mTitleTextView.setVisibility(View.GONE); 180 | mSaveFab.setVisibility(View.VISIBLE); 181 | mShareFab.setVisibility(View.VISIBLE); 182 | mClearFab.setVisibility(View.VISIBLE); 183 | 184 | // Resample the saved image to fit the ImageView 185 | mResultsBitmap = BitmapUtils.resamplePic(this, mTempPhotoPath); 186 | 187 | 188 | // Detect the faces and overlay the appropriate emoji 189 | mResultsBitmap = Emojifier.detectFacesandOverlayEmoji(this, mResultsBitmap); 190 | 191 | // Set the new bitmap to the ImageView 192 | mImageView.setImageBitmap(mResultsBitmap); 193 | } 194 | 195 | 196 | /** 197 | * OnClick method for the save button. 198 | */ 199 | @OnClick(R.id.save_button) 200 | public void saveMe() { 201 | // Delete the temporary image file 202 | BitmapUtils.deleteImageFile(this, mTempPhotoPath); 203 | 204 | // Save the image 205 | BitmapUtils.saveImage(this, mResultsBitmap); 206 | } 207 | 208 | /** 209 | * OnClick method for the share button, saves and shares the new bitmap. 210 | */ 211 | @OnClick(R.id.share_button) 212 | public void shareMe() { 213 | // Delete the temporary image file 214 | BitmapUtils.deleteImageFile(this, mTempPhotoPath); 215 | 216 | // Save the image 217 | BitmapUtils.saveImage(this, mResultsBitmap); 218 | 219 | // Share the image 220 | BitmapUtils.shareImage(this, mTempPhotoPath); 221 | } 222 | 223 | /** 224 | * OnClick for the clear button, resets the app to original state. 225 | */ 226 | @OnClick(R.id.clear_button) 227 | public void clearImage() { 228 | // Clear the image and toggle the view visibility 229 | mImageView.setImageResource(0); 230 | mEmojifyButton.setVisibility(View.VISIBLE); 231 | mTitleTextView.setVisibility(View.VISIBLE); 232 | mShareFab.setVisibility(View.GONE); 233 | mSaveFab.setVisibility(View.GONE); 234 | mClearFab.setVisibility(View.GONE); 235 | 236 | // Delete the temporary image file 237 | BitmapUtils.deleteImageFile(this, mTempPhotoPath); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/closed_frown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Emojify/e3835a34c9c1d0781d8764117e080432fe685eb4/app/src/main/res/drawable/closed_frown.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/closed_smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Emojify/e3835a34c9c1d0781d8764117e080432fe685eb4/app/src/main/res/drawable/closed_smile.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/frown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Emojify/e3835a34c9c1d0781d8764117e080432fe685eb4/app/src/main/res/drawable/frown.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clear.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/leftwink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Emojify/e3835a34c9c1d0781d8764117e080432fe685eb4/app/src/main/res/drawable/leftwink.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/leftwinkfrown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Emojify/e3835a34c9c1d0781d8764117e080432fe685eb4/app/src/main/res/drawable/leftwinkfrown.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rightwink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Emojify/e3835a34c9c1d0781d8764117e080432fe685eb4/app/src/main/res/drawable/rightwink.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/rightwinkfrown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Emojify/e3835a34c9c1d0781d8764117e080432fe685eb4/app/src/main/res/drawable/rightwinkfrown.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/smile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Emojify/e3835a34c9c1d0781d8764117e080432fe685eb4/app/src/main/res/drawable/smile.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 29 | 30 | 37 | 38 | 47 | 48 |