├── .gitignore ├── CODEOWNERS ├── README.md ├── app ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-web.png │ ├── java │ └── com │ │ └── example │ │ └── android │ │ └── shushme │ │ ├── GeofenceBroadcastReceiver.java │ │ ├── Geofencing.java │ │ ├── MainActivity.java │ │ ├── PlaceListAdapter.java │ │ └── provider │ │ ├── PlaceContentProvider.java │ │ ├── PlaceContract.java │ │ └── PlaceDbHelper.java │ └── res │ ├── drawable │ ├── ic_globe_primary_24dp.xml │ ├── ic_gps_fixed_white_24dp.xml │ ├── ic_my_location_primary_24dp.xml │ ├── ic_notifications_active_primary_24dp.xml │ ├── ic_place_accent_24dp.xml │ ├── ic_volume_off_white_24dp.xml │ └── ic_volume_up_white_24dp.xml │ ├── layout │ ├── activity_main.xml │ └── item_place_card.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 ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── screen_1.png ├── screen_2.png ├── screen_3.png ├── screen_4.png ├── screen_5.png └── screen_6.png ├── settings.gradle └── test_locations.kml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # built application files 4 | *.apk 5 | *.ap_ 6 | 7 | # files for the dex VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # generated files 14 | bin/ 15 | out/ 16 | gen/ 17 | 18 | # Libraries used by the app 19 | # Can explicitly add if we want, but shouldn't do so blindly. Licenses, bloat, etc. 20 | /libs 21 | 22 | 23 | # Build stuff (auto-generated by android update project ...) 24 | build.xml 25 | ant.properties 26 | local.properties 27 | project.properties 28 | 29 | # Eclipse project files 30 | .classpath 31 | .project 32 | 33 | # idea project files 34 | .idea/ 35 | *.iml 36 | *.ipr 37 | *.iws 38 | 39 | # Gradle-based build 40 | .gradle 41 | build/ -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @udacity/active-public-content -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ShushMe 2 | 3 | This is the toy app for the Places lesson of the [Advanced Android App Development course on Udacity](https://www.udacity.com/course/advanced-android-app-development--ud855). 4 | 5 | 6 | ## Screenshots 7 | 8 | ![Screenshot1](screenshots/screen_1.png) ![Screenshot2](screenshots/screen_2.png) ![Screenshot3](screenshots/screen_3.png) 9 | ![Screenshot4](screenshots/screen_4.png) ![Screenshot5](screenshots/screen_5.png) ![Screenshot6](screenshots/screen_6.png) 10 | 11 | 12 | ## How to use this repo while taking the course 13 | 14 | Each code repository in this class has a chain of commits that looks like this: 15 | 16 | ![listofcommits](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe2e_listofcommits/listofcommits.png) 17 | 18 | These commits show every step you'll take to create the app. They include **Exercise** commits and **Solution** commits. 19 | 20 | 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. 21 | 22 | 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. 23 | 24 | Each commit also has a **branch** associated with it of the same name as the commit message, seen below: 25 | 26 | ![branches](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590390fe_branches-ud855/branches-ud855.png 27 | ) 28 | Access all branches from this tab 29 | 30 | ![listofbranches](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe76_listofbranches/listofbranches.png 31 | ) 32 | 33 | 34 | ![branchesdropdown](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590391a3_branches-dropdown-ud855/branches-dropdown-ud855.png 35 | ) 36 | 37 | 38 | The branches are also accessible from the drop-down in the "Code" tab 39 | 40 | 41 | ## Working with the Course Code 42 | 43 | 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. 44 | 45 | The basic steps are: 46 | 47 | 1. Clone the repo 48 | 2. Checkout the exercise branch 49 | 3. Find and complete the TODOs 50 | 4. Optionally commit your code changes 51 | 5. Compare with the solution 52 | 53 | 54 | **Step 1: Clone the repo** 55 | 56 | 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: 57 | 58 | ```bash 59 | git clone https://github.com/udacity/REPOSITORY_NAME.git 60 | ``` 61 | 62 | **Step 2: Checkout the exercise branch** 63 | 64 | As you do different exercises in the code, you'll be told which exercise you're on, as seen below: 65 | ![exerciseexample](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0087_exerciseexample/exerciseexample.png 66 | ) 67 | 68 | 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: 69 | 70 | ```bash 71 | git checkout TFCM.01-Exercise-AddGradleDependencies 72 | ``` 73 | 74 | **Step 3: Find and complete the TODOs** 75 | 76 | 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. 77 | 78 | We've numbered the TODO steps so you can do them in order: 79 | ![todos](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00e7_todos/todos.png 80 | ) 81 | 82 | **Step 4: Optionally commit your code changes** 83 | 84 | 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. 85 | 86 | ```bash 87 | git add . 88 | git commit -m "Your commit message" 89 | ``` 90 | 91 | **Step 5: Compare with the solution** 92 | 93 | 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: 94 | 95 | ![solutionwindow](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00f9_solutionwindow/solutionwindow.png 96 | ) 97 | 98 | The **Diff** link will take you to a Github diff as seen below: 99 | ![diff](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0108_diffsceenshot/diffsceenshot.png 100 | ) 101 | 102 | 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. 103 | ## Report Issues 104 | Notice any issues with a repository? Please file a github issue in the repository. 105 | -------------------------------------------------------------------------------- /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.shushme" 8 | minSdkVersion 16 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:appcompat-v7:25.1.0' 28 | compile 'com.android.support:recyclerview-v7:25.0.1' 29 | compile 'com.google.android.gms:play-services-places:9.8.0' 30 | compile 'com.google.android.gms:play-services-location:9.8.0' 31 | testCompile 'junit:junit:4.12' 32 | } 33 | -------------------------------------------------------------------------------- /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/asser/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/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacity/AdvancedAndroid_Shushme/164e332c47703f6be02632a49c8c8cfad04a8e9e/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/shushme/GeofenceBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | package com.example.android.shushme; 2 | 3 | /* 4 | * Copyright (C) 2017 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.content.BroadcastReceiver; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.util.Log; 23 | 24 | public class GeofenceBroadcastReceiver extends BroadcastReceiver { 25 | 26 | public static final String TAG = GeofenceBroadcastReceiver.class.getSimpleName(); 27 | 28 | /*** 29 | * Handles the Broadcast message sent when the Geofence Transition is triggered 30 | * Careful here though, this is running on the main thread so make sure you start an AsyncTask for 31 | * anything that takes longer than say 10 second to run 32 | * 33 | * @param context 34 | * @param intent 35 | */ 36 | @Override 37 | public void onReceive(Context context, Intent intent) { 38 | // Get the Geofence Event from the Intent sent through 39 | GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); 40 | if (geofencingEvent.hasError()) { 41 | Log.e(TAG, String.format("Error code : %d", geofencingEvent.getErrorCode())); 42 | return; 43 | } 44 | 45 | // Get the transition type. 46 | int geofenceTransition = geofencingEvent.getGeofenceTransition(); 47 | // Check which transition type has triggered this event 48 | if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) { 49 | setRingerMode(context, AudioManager.RINGER_MODE_SILENT); 50 | } else if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { 51 | setRingerMode(context, AudioManager.RINGER_MODE_NORMAL); 52 | } else { 53 | // Log the error. 54 | Log.e(TAG, String.format("Unknown transition : %d", geofenceTransition)); 55 | // No need to do anything else 56 | return; 57 | } 58 | // Send the notification 59 | sendNotification(context, geofenceTransition); 60 | } 61 | 62 | 63 | /** 64 | * Posts a notification in the notification bar when a transition is detected 65 | * Uses different icon drawables for different transition types 66 | * If the user clicks the notification, control goes to the MainActivity 67 | * 68 | * @param context The calling context for building a task stack 69 | * @param transitionType The geofence transition type, can be Geofence.GEOFENCE_TRANSITION_ENTER 70 | * or Geofence.GEOFENCE_TRANSITION_EXIT 71 | */ 72 | private void sendNotification(Context context, int transitionType) { 73 | // Create an explicit content Intent that starts the main Activity. 74 | Intent notificationIntent = new Intent(context, MainActivity.class); 75 | 76 | // Construct a task stack. 77 | TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); 78 | 79 | // Add the main Activity to the task stack as the parent. 80 | stackBuilder.addParentStack(MainActivity.class); 81 | 82 | // Push the content Intent onto the stack. 83 | stackBuilder.addNextIntent(notificationIntent); 84 | 85 | // Get a PendingIntent containing the entire back stack. 86 | PendingIntent notificationPendingIntent = 87 | stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); 88 | 89 | // Get a notification builder 90 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context); 91 | 92 | // Check the transition type to display the relevant icon image 93 | if (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER) { 94 | builder.setSmallIcon(R.drawable.ic_volume_off_white_24dp) 95 | .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), 96 | R.drawable.ic_volume_off_white_24dp)) 97 | .setContentTitle(context.getString(R.string.silent_mode_activated)); 98 | } else if (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT) { 99 | builder.setSmallIcon(R.drawable.ic_volume_up_white_24dp) 100 | .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), 101 | R.drawable.ic_volume_up_white_24dp)) 102 | .setContentTitle(context.getString(R.string.back_to_normal)); 103 | } 104 | 105 | // Continue building the notification 106 | builder.setContentText(context.getString(R.string.touch_to_relaunch)); 107 | builder.setContentIntent(notificationPendingIntent); 108 | 109 | // Dismiss notification once the user touches it. 110 | builder.setAutoCancel(true); 111 | 112 | // Get an instance of the Notification manager 113 | NotificationManager mNotificationManager = 114 | (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 115 | 116 | // Issue the notification 117 | mNotificationManager.notify(0, builder.build()); 118 | } 119 | 120 | /** 121 | * Changes the ringer mode on the device to either silent or back to normal 122 | * 123 | * @param context The context to access AUDIO_SERVICE 124 | * @param mode The desired mode to switch device to, can be AudioManager.RINGER_MODE_SILENT or 125 | * AudioManager.RINGER_MODE_NORMAL 126 | */ 127 | private void setRingerMode(Context context, int mode) { 128 | NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 129 | // Check for DND permissions for API 24+ 130 | if (android.os.Build.VERSION.SDK_INT < 24 || 131 | (android.os.Build.VERSION.SDK_INT >= 24 && !nm.isNotificationPolicyAccessGranted())) { 132 | AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 133 | audioManager.setRingerMode(mode); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/shushme/Geofencing.java: -------------------------------------------------------------------------------- 1 | package com.example.android.shushme; 2 | 3 | /* 4 | * Copyright (C) 2017 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.app.PendingIntent; 20 | import android.content.Context; 21 | import android.content.Intent; 22 | import android.support.annotation.NonNull; 23 | import android.util.Log; 24 | 25 | import com.google.android.gms.common.api.GoogleApiClient; 26 | import com.google.android.gms.common.api.Result; 27 | import com.google.android.gms.common.api.ResultCallback; 28 | import com.google.android.gms.location.Geofence; 29 | import com.google.android.gms.location.GeofencingRequest; 30 | import com.google.android.gms.location.LocationServices; 31 | import com.google.android.gms.location.places.Place; 32 | import com.google.android.gms.location.places.PlaceBuffer; 33 | 34 | import java.util.ArrayList; 35 | import java.util.List; 36 | 37 | public class Geofencing implements ResultCallback { 38 | 39 | // Constants 40 | public static final String TAG = Geofencing.class.getSimpleName(); 41 | private static final float GEOFENCE_RADIUS = 50; // 50 meters 42 | private static final long GEOFENCE_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours 43 | 44 | private List mGeofenceList; 45 | private PendingIntent mGeofencePendingIntent; 46 | private GoogleApiClient mGoogleApiClient; 47 | private Context mContext; 48 | 49 | public Geofencing(Context context, GoogleApiClient client) { 50 | mContext = context; 51 | mGoogleApiClient = client; 52 | mGeofencePendingIntent = null; 53 | mGeofenceList = new ArrayList<>(); 54 | } 55 | 56 | /*** 57 | * Registers the list of Geofences specified in mGeofenceList with Google Place Services 58 | * Uses {@code #mGoogleApiClient} to connect to Google Place Services 59 | * Uses {@link #getGeofencingRequest} to get the list of Geofences to be registered 60 | * Uses {@link #getGeofencePendingIntent} to get the pending intent to launch the IntentService 61 | * when the Geofence is triggered 62 | * Triggers {@link #onResult} when the geofences have been registered successfully 63 | */ 64 | public void registerAllGeofences() { 65 | // Check that the API client is connected and that the list has Geofences in it 66 | if (mGoogleApiClient == null || !mGoogleApiClient.isConnected() || 67 | mGeofenceList == null || mGeofenceList.size() == 0) { 68 | return; 69 | } 70 | try { 71 | LocationServices.GeofencingApi.addGeofences( 72 | mGoogleApiClient, 73 | getGeofencingRequest(), 74 | getGeofencePendingIntent() 75 | ).setResultCallback(this); 76 | } catch (SecurityException securityException) { 77 | // Catch exception generated if the app does not use ACCESS_FINE_LOCATION permission. 78 | Log.e(TAG, securityException.getMessage()); 79 | } 80 | } 81 | 82 | /*** 83 | * Unregisters all the Geofences created by this app from Google Place Services 84 | * Uses {@code #mGoogleApiClient} to connect to Google Place Services 85 | * Uses {@link #getGeofencePendingIntent} to get the pending intent passed when 86 | * registering the Geofences in the first place 87 | * Triggers {@link #onResult} when the geofences have been unregistered successfully 88 | */ 89 | public void unRegisterAllGeofences() { 90 | if (mGoogleApiClient == null || !mGoogleApiClient.isConnected()) { 91 | return; 92 | } 93 | try { 94 | LocationServices.GeofencingApi.removeGeofences( 95 | mGoogleApiClient, 96 | // This is the same pending intent that was used in registerGeofences 97 | getGeofencePendingIntent() 98 | ).setResultCallback(this); 99 | } catch (SecurityException securityException) { 100 | // Catch exception generated if the app does not use ACCESS_FINE_LOCATION permission. 101 | Log.e(TAG, securityException.getMessage()); 102 | } 103 | } 104 | 105 | 106 | /*** 107 | * Updates the local ArrayList of Geofences using data from the passed in list 108 | * Uses the Place ID defined by the API as the Geofence object Id 109 | * 110 | * @param places the PlaceBuffer result of the getPlaceById call 111 | */ 112 | public void updateGeofencesList(PlaceBuffer places) { 113 | mGeofenceList = new ArrayList<>(); 114 | if (places == null || places.getCount() == 0) return; 115 | for (Place place : places) { 116 | // Read the place information from the DB cursor 117 | String placeUID = place.getId(); 118 | double placeLat = place.getLatLng().latitude; 119 | double placeLng = place.getLatLng().longitude; 120 | // Build a Geofence object 121 | Geofence geofence = new Geofence.Builder() 122 | .setRequestId(placeUID) 123 | .setExpirationDuration(GEOFENCE_TIMEOUT) 124 | .setCircularRegion(placeLat, placeLng, GEOFENCE_RADIUS) 125 | .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT) 126 | .build(); 127 | // Add it to the list 128 | mGeofenceList.add(geofence); 129 | } 130 | } 131 | 132 | /*** 133 | * Creates a GeofencingRequest object using the mGeofenceList ArrayList of Geofences 134 | * Used by {@code #registerGeofences} 135 | * 136 | * @return the GeofencingRequest object 137 | */ 138 | private GeofencingRequest getGeofencingRequest() { 139 | GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); 140 | builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER); 141 | builder.addGeofences(mGeofenceList); 142 | return builder.build(); 143 | } 144 | 145 | /*** 146 | * Creates a PendingIntent object using the GeofenceTransitionsIntentService class 147 | * Used by {@code #registerGeofences} 148 | * 149 | * @return the PendingIntent object 150 | */ 151 | private PendingIntent getGeofencePendingIntent() { 152 | // Reuse the PendingIntent if we already have it. 153 | if (mGeofencePendingIntent != null) { 154 | return mGeofencePendingIntent; 155 | } 156 | Intent intent = new Intent(mContext, GeofenceBroadcastReceiver.class); 157 | mGeofencePendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent. 158 | FLAG_UPDATE_CURRENT); 159 | return mGeofencePendingIntent; 160 | } 161 | 162 | @Override 163 | public void onResult(@NonNull Result result) { 164 | Log.e(TAG, String.format("Error adding/removing geofence : %s", 165 | result.getStatus().toString())); 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/shushme/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.example.android.shushme; 2 | 3 | /* 4 | * Copyright (C) 2017 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.app.NotificationManager; 20 | import android.content.ContentValues; 21 | import android.content.Intent; 22 | import android.content.SharedPreferences; 23 | import android.content.pm.PackageManager; 24 | import android.database.Cursor; 25 | import android.net.Uri; 26 | import android.os.Bundle; 27 | import android.support.annotation.NonNull; 28 | import android.support.annotation.Nullable; 29 | import android.support.v4.app.ActivityCompat; 30 | import android.support.v7.app.AppCompatActivity; 31 | import android.support.v7.widget.LinearLayoutManager; 32 | import android.support.v7.widget.RecyclerView; 33 | import android.util.Log; 34 | import android.view.View; 35 | import android.widget.CheckBox; 36 | import android.widget.CompoundButton; 37 | import android.widget.Switch; 38 | import android.widget.Toast; 39 | 40 | import com.example.android.shushme.provider.PlaceContract; 41 | import com.google.android.gms.common.ConnectionResult; 42 | import com.google.android.gms.common.GooglePlayServicesNotAvailableException; 43 | import com.google.android.gms.common.GooglePlayServicesRepairableException; 44 | import com.google.android.gms.common.api.GoogleApiClient; 45 | import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; 46 | import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; 47 | import com.google.android.gms.common.api.PendingResult; 48 | import com.google.android.gms.common.api.ResultCallback; 49 | import com.google.android.gms.location.LocationServices; 50 | import com.google.android.gms.location.places.Place; 51 | import com.google.android.gms.location.places.PlaceBuffer; 52 | import com.google.android.gms.location.places.Places; 53 | import com.google.android.gms.location.places.ui.PlacePicker; 54 | 55 | import java.util.ArrayList; 56 | import java.util.List; 57 | 58 | public class MainActivity extends AppCompatActivity implements 59 | ConnectionCallbacks, 60 | OnConnectionFailedListener { 61 | 62 | // Constants 63 | public static final String TAG = MainActivity.class.getSimpleName(); 64 | private static final int PERMISSIONS_REQUEST_FINE_LOCATION = 111; 65 | private static final int PLACE_PICKER_REQUEST = 1; 66 | 67 | // Member variables 68 | private PlaceListAdapter mAdapter; 69 | private RecyclerView mRecyclerView; 70 | private boolean mIsEnabled; 71 | private GoogleApiClient mClient; 72 | private Geofencing mGeofencing; 73 | 74 | /** 75 | * Called when the activity is starting 76 | * 77 | * @param savedInstanceState The Bundle that contains the data supplied in onSaveInstanceState 78 | */ 79 | @Override 80 | protected void onCreate(Bundle savedInstanceState) { 81 | super.onCreate(savedInstanceState); 82 | setContentView(R.layout.activity_main); 83 | 84 | // Set up the recycler view 85 | mRecyclerView = (RecyclerView) findViewById(R.id.places_list_recycler_view); 86 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 87 | mAdapter = new PlaceListAdapter(this, null); 88 | mRecyclerView.setAdapter(mAdapter); 89 | 90 | // Initialize the switch state and Handle enable/disable switch change 91 | Switch onOffSwitch = (Switch) findViewById(R.id.enable_switch); 92 | mIsEnabled = getPreferences(MODE_PRIVATE).getBoolean(getString(R.string.setting_enabled), false); 93 | onOffSwitch.setChecked(mIsEnabled); 94 | onOffSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 95 | @Override 96 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 97 | SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit(); 98 | editor.putBoolean(getString(R.string.setting_enabled), isChecked); 99 | mIsEnabled = isChecked; 100 | editor.commit(); 101 | if (isChecked) mGeofencing.registerAllGeofences(); 102 | else mGeofencing.unRegisterAllGeofences(); 103 | } 104 | 105 | }); 106 | 107 | // Build up the LocationServices API client 108 | // Uses the addApi method to request the LocationServices API 109 | // Also uses enableAutoManage to automatically when to connect/suspend the client 110 | mClient = new GoogleApiClient.Builder(this) 111 | .addConnectionCallbacks(this) 112 | .addOnConnectionFailedListener(this) 113 | .addApi(LocationServices.API) 114 | .addApi(Places.GEO_DATA_API) 115 | .enableAutoManage(this, this) 116 | .build(); 117 | 118 | mGeofencing = new Geofencing(this, mClient); 119 | 120 | } 121 | 122 | /*** 123 | * Called when the Google API Client is successfully connected 124 | * 125 | * @param connectionHint Bundle of data provided to clients by Google Play services 126 | */ 127 | @Override 128 | public void onConnected(@Nullable Bundle connectionHint) { 129 | refreshPlacesData(); 130 | Log.i(TAG, "API Client Connection Successful!"); 131 | } 132 | 133 | /*** 134 | * Called when the Google API Client is suspended 135 | * 136 | * @param cause cause The reason for the disconnection. Defined by constants CAUSE_*. 137 | */ 138 | @Override 139 | public void onConnectionSuspended(int cause) { 140 | Log.i(TAG, "API Client Connection Suspended!"); 141 | } 142 | 143 | /*** 144 | * Called when the Google API Client failed to connect to Google Play Services 145 | * 146 | * @param result A ConnectionResult that can be used for resolving the error 147 | */ 148 | @Override 149 | public void onConnectionFailed(@NonNull ConnectionResult result) { 150 | Log.e(TAG, "API Client Connection Failed!"); 151 | } 152 | 153 | public void refreshPlacesData() { 154 | Uri uri = PlaceContract.PlaceEntry.CONTENT_URI; 155 | Cursor data = getContentResolver().query( 156 | uri, 157 | null, 158 | null, 159 | null, 160 | null); 161 | 162 | if (data == null || data.getCount() == 0) return; 163 | List guids = new ArrayList(); 164 | while (data.moveToNext()) { 165 | guids.add(data.getString(data.getColumnIndex(PlaceContract.PlaceEntry.COLUMN_PLACE_ID))); 166 | } 167 | PendingResult placeResult = Places.GeoDataApi.getPlaceById(mClient, 168 | guids.toArray(new String[guids.size()])); 169 | placeResult.setResultCallback(new ResultCallback() { 170 | @Override 171 | public void onResult(@NonNull PlaceBuffer places) { 172 | mAdapter.swapPlaces(places); 173 | mGeofencing.updateGeofencesList(places); 174 | if (mIsEnabled) mGeofencing.registerAllGeofences(); 175 | } 176 | }); 177 | } 178 | 179 | /*** 180 | * Button Click event handler to handle clicking the "Add new location" Button 181 | * 182 | * @param view 183 | */ 184 | public void onAddPlaceButtonClicked(View view) { 185 | if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) 186 | != PackageManager.PERMISSION_GRANTED) { 187 | Toast.makeText(this, getString(R.string.need_location_permission_message), Toast.LENGTH_LONG).show(); 188 | return; 189 | } 190 | try { 191 | // Start a new Activity for the Place Picker API, this will trigger {@code #onActivityResult} 192 | // when a place is selected or with the user cancels. 193 | PlacePicker.IntentBuilder builder = new PlacePicker.IntentBuilder(); 194 | Intent i = builder.build(this); 195 | startActivityForResult(i, PLACE_PICKER_REQUEST); 196 | } catch (GooglePlayServicesRepairableException e) { 197 | Log.e(TAG, String.format("GooglePlayServices Not Available [%s]", e.getMessage())); 198 | } catch (GooglePlayServicesNotAvailableException e) { 199 | Log.e(TAG, String.format("GooglePlayServices Not Available [%s]", e.getMessage())); 200 | } catch (Exception e) { 201 | Log.e(TAG, String.format("PlacePicker Exception: %s", e.getMessage())); 202 | } 203 | } 204 | 205 | 206 | /*** 207 | * Called when the Place Picker Activity returns back with a selected place (or after canceling) 208 | * 209 | * @param requestCode The request code passed when calling startActivityForResult 210 | * @param resultCode The result code specified by the second activity 211 | * @param data The Intent that carries the result data. 212 | */ 213 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 214 | if (requestCode == PLACE_PICKER_REQUEST && resultCode == RESULT_OK) { 215 | Place place = PlacePicker.getPlace(this, data); 216 | if (place == null) { 217 | Log.i(TAG, "No place selected"); 218 | return; 219 | } 220 | 221 | // Extract the place information from the API 222 | String placeName = place.getName().toString(); 223 | String placeAddress = place.getAddress().toString(); 224 | String placeID = place.getId(); 225 | 226 | // Insert a new place into DB 227 | ContentValues contentValues = new ContentValues(); 228 | contentValues.put(PlaceContract.PlaceEntry.COLUMN_PLACE_ID, placeID); 229 | getContentResolver().insert(PlaceContract.PlaceEntry.CONTENT_URI, contentValues); 230 | 231 | // Get live data information 232 | refreshPlacesData(); 233 | } 234 | } 235 | 236 | @Override 237 | public void onResume() { 238 | super.onResume(); 239 | 240 | // Initialize location permissions checkbox 241 | CheckBox locationPermissions = (CheckBox) findViewById(R.id.location_permission_checkbox); 242 | if (ActivityCompat.checkSelfPermission(MainActivity.this, 243 | android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { 244 | locationPermissions.setChecked(false); 245 | } else { 246 | locationPermissions.setChecked(true); 247 | locationPermissions.setEnabled(false); 248 | } 249 | 250 | // Initialize ringer permissions checkbox 251 | CheckBox ringerPermissions = (CheckBox) findViewById(R.id.ringer_permissions_checkbox); 252 | NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 253 | // Check if the API supports such permission change and check if permission is granted 254 | if (android.os.Build.VERSION.SDK_INT >= 24 && !nm.isNotificationPolicyAccessGranted()) { 255 | ringerPermissions.setChecked(false); 256 | } else { 257 | ringerPermissions.setChecked(true); 258 | ringerPermissions.setEnabled(false); 259 | } 260 | } 261 | 262 | public void onRingerPermissionsClicked(View view) { 263 | Intent intent = new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS); 264 | startActivity(intent); 265 | } 266 | 267 | public void onLocationPermissionClicked(View view) { 268 | ActivityCompat.requestPermissions(MainActivity.this, 269 | new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, 270 | PERMISSIONS_REQUEST_FINE_LOCATION); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/shushme/PlaceListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.example.android.shushme; 2 | 3 | /* 4 | * Copyright (C) 2017 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.content.Context; 20 | import android.support.v7.widget.RecyclerView; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.ViewGroup; 24 | import android.widget.TextView; 25 | 26 | import com.google.android.gms.location.places.PlaceBuffer; 27 | 28 | public class PlaceListAdapter extends RecyclerView.Adapter { 29 | 30 | private Context mContext; 31 | private PlaceBuffer mPlaces; 32 | 33 | /** 34 | * Constructor using the context and the db cursor 35 | * 36 | * @param context the calling context/activity 37 | */ 38 | public PlaceListAdapter(Context context, PlaceBuffer places) { 39 | this.mContext = context; 40 | this.mPlaces = places; 41 | } 42 | 43 | /** 44 | * Called when RecyclerView needs a new ViewHolder of the given type to represent an item 45 | * 46 | * @param parent The ViewGroup into which the new View will be added 47 | * @param viewType The view type of the new View 48 | * @return A new PlaceViewHolder that holds a View with the item_place_card layout 49 | */ 50 | @Override 51 | public PlaceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 52 | // Get the RecyclerView item layout 53 | LayoutInflater inflater = LayoutInflater.from(mContext); 54 | View view = inflater.inflate(R.layout.item_place_card, parent, false); 55 | return new PlaceViewHolder(view); 56 | } 57 | 58 | /** 59 | * Binds the data from a particular position in the cursor to the corresponding view holder 60 | * 61 | * @param holder The PlaceViewHolder instance corresponding to the required position 62 | * @param position The current position that needs to be loaded with data 63 | */ 64 | @Override 65 | public void onBindViewHolder(PlaceViewHolder holder, int position) { 66 | String placeName = mPlaces.get(position).getName().toString(); 67 | String placeAddress = mPlaces.get(position).getAddress().toString(); 68 | holder.nameTextView.setText(placeName); 69 | holder.addressTextView.setText(placeAddress); 70 | } 71 | 72 | public void swapPlaces(PlaceBuffer newPlaces){ 73 | mPlaces = newPlaces; 74 | if (mPlaces != null) { 75 | // Force the RecyclerView to refresh 76 | this.notifyDataSetChanged(); 77 | } 78 | } 79 | 80 | /** 81 | * Returns the number of items in the cursor 82 | * 83 | * @return Number of items in the cursor, or 0 if null 84 | */ 85 | @Override 86 | public int getItemCount() { 87 | if(mPlaces==null) return 0; 88 | return mPlaces.getCount(); 89 | } 90 | 91 | /** 92 | * PlaceViewHolder class for the recycler view item 93 | */ 94 | class PlaceViewHolder extends RecyclerView.ViewHolder { 95 | 96 | TextView nameTextView; 97 | TextView addressTextView; 98 | 99 | public PlaceViewHolder(View itemView) { 100 | super(itemView); 101 | nameTextView = (TextView) itemView.findViewById(R.id.name_text_view); 102 | addressTextView = (TextView) itemView.findViewById(R.id.address_text_view); 103 | } 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/shushme/provider/PlaceContentProvider.java: -------------------------------------------------------------------------------- 1 | package com.example.android.shushme.provider; 2 | 3 | /* 4 | * Copyright (C) 2017 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.content.ContentProvider; 20 | import android.content.ContentUris; 21 | import android.content.ContentValues; 22 | import android.content.Context; 23 | import android.content.UriMatcher; 24 | import android.database.Cursor; 25 | import android.database.sqlite.SQLiteDatabase; 26 | import android.net.Uri; 27 | import android.support.annotation.NonNull; 28 | 29 | import static com.example.android.shushme.provider.PlaceContract.PlaceEntry; 30 | 31 | 32 | public class PlaceContentProvider extends ContentProvider { 33 | 34 | // Define final integer constants for the directory of places and a single item. 35 | // It's convention to use 100, 200, 300, etc for directories, 36 | // and related ints (101, 102, ..) for items in that directory. 37 | public static final int PLACES = 100; 38 | public static final int PLACE_WITH_ID = 101; 39 | 40 | // Declare a static variable for the Uri matcher that you construct 41 | private static final UriMatcher sUriMatcher = buildUriMatcher(); 42 | private static final String TAG = PlaceContentProvider.class.getName(); 43 | 44 | // Define a static buildUriMatcher method that associates URI's with their int match 45 | public static UriMatcher buildUriMatcher() { 46 | // Initialize a UriMatcher 47 | UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 48 | // Add URI matches 49 | uriMatcher.addURI(PlaceContract.AUTHORITY, PlaceContract.PATH_PLACES, PLACES); 50 | uriMatcher.addURI(PlaceContract.AUTHORITY, PlaceContract.PATH_PLACES + "/#", PLACE_WITH_ID); 51 | return uriMatcher; 52 | } 53 | 54 | // Member variable for a PlaceDbHelper that's initialized in the onCreate() method 55 | private PlaceDbHelper mPlaceDbHelper; 56 | 57 | @Override 58 | public boolean onCreate() { 59 | Context context = getContext(); 60 | mPlaceDbHelper = new PlaceDbHelper(context); 61 | return true; 62 | } 63 | 64 | /*** 65 | * Handles requests to insert a single new row of data 66 | * 67 | * @param uri 68 | * @param values 69 | * @return 70 | */ 71 | @Override 72 | public Uri insert(@NonNull Uri uri, ContentValues values) { 73 | final SQLiteDatabase db = mPlaceDbHelper.getWritableDatabase(); 74 | 75 | // Write URI matching code to identify the match for the places directory 76 | int match = sUriMatcher.match(uri); 77 | Uri returnUri; // URI to be returned 78 | switch (match) { 79 | case PLACES: 80 | // Insert new values into the database 81 | long id = db.insert(PlaceEntry.TABLE_NAME, null, values); 82 | if (id > 0) { 83 | returnUri = ContentUris.withAppendedId(PlaceContract.PlaceEntry.CONTENT_URI, id); 84 | } else { 85 | throw new android.database.SQLException("Failed to insert row into " + uri); 86 | } 87 | break; 88 | // Default case throws an UnsupportedOperationException 89 | default: 90 | throw new UnsupportedOperationException("Unknown uri: " + uri); 91 | } 92 | 93 | // Notify the resolver if the uri has been changed, and return the newly inserted URI 94 | getContext().getContentResolver().notifyChange(uri, null); 95 | 96 | // Return constructed uri (this points to the newly inserted row of data) 97 | return returnUri; 98 | } 99 | 100 | /*** 101 | * Handles requests for data by URI 102 | * 103 | * @param uri 104 | * @param projection 105 | * @param selection 106 | * @param selectionArgs 107 | * @param sortOrder 108 | * @return 109 | */ 110 | @Override 111 | public Cursor query(@NonNull Uri uri, String[] projection, String selection, 112 | String[] selectionArgs, String sortOrder) { 113 | 114 | // Get access to underlying database (read-only for query) 115 | final SQLiteDatabase db = mPlaceDbHelper.getReadableDatabase(); 116 | 117 | // Write URI match code and set a variable to return a Cursor 118 | int match = sUriMatcher.match(uri); 119 | Cursor retCursor; 120 | 121 | switch (match) { 122 | // Query for the places directory 123 | case PLACES: 124 | retCursor = db.query(PlaceEntry.TABLE_NAME, 125 | projection, 126 | selection, 127 | selectionArgs, 128 | null, 129 | null, 130 | sortOrder); 131 | break; 132 | // Default exception 133 | default: 134 | throw new UnsupportedOperationException("Unknown uri: " + uri); 135 | } 136 | 137 | // Set a notification URI on the Cursor and return that Cursor 138 | retCursor.setNotificationUri(getContext().getContentResolver(), uri); 139 | 140 | // Return the desired Cursor 141 | return retCursor; 142 | } 143 | 144 | /*** 145 | * Deletes a single row of data 146 | * 147 | * @param uri 148 | * @param selection 149 | * @param selectionArgs 150 | * @return number of rows affected 151 | */ 152 | @Override 153 | public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) { 154 | // Get access to the database and write URI matching code to recognize a single item 155 | final SQLiteDatabase db = mPlaceDbHelper.getWritableDatabase(); 156 | int match = sUriMatcher.match(uri); 157 | // Keep track of the number of deleted places 158 | int placesDeleted; // starts as 0 159 | switch (match) { 160 | // Handle the single item case, recognized by the ID included in the URI path 161 | case PLACE_WITH_ID: 162 | // Get the place ID from the URI path 163 | String id = uri.getPathSegments().get(1); 164 | // Use selections/selectionArgs to filter for this ID 165 | placesDeleted = db.delete(PlaceEntry.TABLE_NAME, "_id=?", new String[]{id}); 166 | break; 167 | default: 168 | throw new UnsupportedOperationException("Unknown uri: " + uri); 169 | } 170 | // Notify the resolver of a change and return the number of items deleted 171 | if (placesDeleted != 0) { 172 | // A place (or more) was deleted, set notification 173 | getContext().getContentResolver().notifyChange(uri, null); 174 | } 175 | // Return the number of places deleted 176 | return placesDeleted; 177 | } 178 | 179 | /*** 180 | * Updates a single row of data 181 | * 182 | * @param uri 183 | * @param selection 184 | * @param selectionArgs 185 | * @return number of rows affected 186 | */ 187 | @Override 188 | public int update(@NonNull Uri uri, ContentValues values, String selection, 189 | String[] selectionArgs) { 190 | // Get access to underlying database 191 | final SQLiteDatabase db = mPlaceDbHelper.getWritableDatabase(); 192 | int match = sUriMatcher.match(uri); 193 | // Keep track of the number of updated places 194 | int placesUpdated; 195 | 196 | switch (match) { 197 | case PLACE_WITH_ID: 198 | // Get the place ID from the URI path 199 | String id = uri.getPathSegments().get(1); 200 | // Use selections/selectionArgs to filter for this ID 201 | placesUpdated = db.update(PlaceEntry.TABLE_NAME, values, "_id=?", new String[]{id}); 202 | break; 203 | // Default exception 204 | default: 205 | throw new UnsupportedOperationException("Unknown uri: " + uri); 206 | } 207 | 208 | // Notify the resolver of a change and return the number of items updated 209 | if (placesUpdated != 0) { 210 | // A place (or more) was updated, set notification 211 | getContext().getContentResolver().notifyChange(uri, null); 212 | } 213 | // Return the number of places deleted 214 | return placesUpdated; 215 | } 216 | 217 | 218 | @Override 219 | public String getType(@NonNull Uri uri) { 220 | throw new UnsupportedOperationException("Not yet implemented"); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/shushme/provider/PlaceContract.java: -------------------------------------------------------------------------------- 1 | package com.example.android.shushme.provider; 2 | 3 | /* 4 | * Copyright (C) 2017 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.net.Uri; 20 | import android.provider.BaseColumns; 21 | 22 | public class PlaceContract { 23 | 24 | // The authority, which is how your code knows which Content Provider to access 25 | public static final String AUTHORITY = "com.example.android.shushme"; 26 | 27 | // The base content URI = "content://" + 28 | public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY); 29 | 30 | // Define the possible paths for accessing data in this contract 31 | // This is the path for the "places" directory 32 | public static final String PATH_PLACES = "places"; 33 | 34 | public static final class PlaceEntry implements BaseColumns { 35 | 36 | // TaskEntry content URI = base content URI + path 37 | public static final Uri CONTENT_URI = 38 | BASE_CONTENT_URI.buildUpon().appendPath(PATH_PLACES).build(); 39 | 40 | public static final String TABLE_NAME = "places"; 41 | public static final String COLUMN_PLACE_ID = "placeID"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/com/example/android/shushme/provider/PlaceDbHelper.java: -------------------------------------------------------------------------------- 1 | package com.example.android.shushme.provider; 2 | 3 | /* 4 | * Copyright (C) 2017 The Android Open Source Project 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | import android.content.Context; 20 | import android.database.sqlite.SQLiteDatabase; 21 | import android.database.sqlite.SQLiteOpenHelper; 22 | 23 | import com.example.android.shushme.provider.PlaceContract.PlaceEntry; 24 | 25 | public class PlaceDbHelper extends SQLiteOpenHelper { 26 | 27 | // The database name 28 | private static final String DATABASE_NAME = "shushme.db"; 29 | 30 | // If you change the database schema, you must increment the database version 31 | private static final int DATABASE_VERSION = 1; 32 | 33 | // Constructor 34 | public PlaceDbHelper(Context context) { 35 | super(context, DATABASE_NAME, null, DATABASE_VERSION); 36 | } 37 | 38 | @Override 39 | public void onCreate(SQLiteDatabase sqLiteDatabase) { 40 | 41 | // Create a table to hold the places data 42 | final String SQL_CREATE_PLACES_TABLE = "CREATE TABLE " + PlaceEntry.TABLE_NAME + " (" + 43 | PlaceEntry._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 44 | PlaceEntry.COLUMN_PLACE_ID + " TEXT NOT NULL, " + 45 | "UNIQUE (" + PlaceEntry.COLUMN_PLACE_ID + ") ON CONFLICT REPLACE" + 46 | "); "; 47 | 48 | sqLiteDatabase.execSQL(SQL_CREATE_PLACES_TABLE); 49 | } 50 | 51 | @Override 52 | public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { 53 | // For now simply drop the table and create a new one. 54 | sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " + PlaceEntry.TABLE_NAME); 55 | onCreate(sqLiteDatabase); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_globe_primary_24dp.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_gps_fixed_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_my_location_primary_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_notifications_active_primary_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_place_accent_24dp.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_volume_off_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_volume_up_white_24dp.xml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 17 | 18 | 25 | 26 | 33 | 34 | 39 | 40 | 44 | 45 | 52 | 53 | 60 | 61 | 62 | 66 | 67 | 74 | 75 | 82 | 83 | 86 | 87 | 94 | 95 | 96 | 97 | 98 | 102 | 103 | 110 | 111 | 118 | 119 | 122 | 123 | 130 | 131 | 132 | 133 | 134 | 135 |