diff --git a/.gitignore b/.gitignore index acc2157a..720422ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .DS_Store +# local keystore files +keystore.properties + # built application files *.apk *.ap_ diff --git a/README.md b/README.md index aebbd9a9..5cc518f8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,110 @@ # ShushMe -Google Places API demo app + +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). + ## Screenshots ![Screenshot1](screenshots/screen_1.png) ![Screenshot2](screenshots/screen_2.png) ![Screenshot3](screenshots/screen_3.png) ![Screenshot4](screenshots/screen_4.png) ![Screenshot5](screenshots/screen_5.png) ![Screenshot6](screenshots/screen_6.png) + + +## How to use this repo while taking the course + +First create **keystore.properties** file in root folder and add following line - + +``` +GOOGLE_API_KEY="insert-your-api-key-here" +``` + +Each code repository in this class has a chain of commits that looks like this: + +![listofcommits](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe2e_listofcommits/listofcommits.png) + +These commits show every step you'll take to create the app. They include **Exercise** commits and **Solution** commits. + +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. + +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. + +Each commit also has a **branch** associated with it of the same name as the commit message, seen below: + +![branches](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590390fe_branches-ud855/branches-ud855.png +) +Access all branches from this tab + +![listofbranches](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58befe76_listofbranches/listofbranches.png +) + + +![branchesdropdown](https://d17h27t6h515a5.cloudfront.net/topher/2017/April/590391a3_branches-dropdown-ud855/branches-dropdown-ud855.png +) + + +The branches are also accessible from the drop-down in the "Code" tab + + +## Working with the Course Code + +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. + +The basic steps are: + +1. Clone the repo +2. Checkout the exercise branch +3. Find and complete the TODOs +4. Optionally commit your code changes +5. Compare with the solution + + +**Step 1: Clone the repo** + +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: + +```bash +git clone https://github.com/udacity/REPOSITORY_NAME.git +``` + +**Step 2: Checkout the exercise branch** + +As you do different exercises in the code, you'll be told which exercise you're on, as seen below: +![exerciseexample](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0087_exerciseexample/exerciseexample.png +) + +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: + +```bash +git checkout TFCM.01-Exercise-AddGradleDependencies +``` + +**Step 3: Find and complete the TODOs** + +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. + +We've numbered the TODO steps so you can do them in order: +![todos](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00e7_todos/todos.png +) + +**Step 4: Optionally commit your code changes** + +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. + +```bash +git add . +git commit -m "Your commit message" +``` + +**Step 5: Compare with the solution** + +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: + +![solutionwindow](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf00f9_solutionwindow/solutionwindow.png +) + +The **Diff** link will take you to a Github diff as seen below: +![diff](https://d17h27t6h515a5.cloudfront.net/topher/2017/March/58bf0108_diffsceenshot/diffsceenshot.png +) + +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. +## Report Issues +Notice any issues with a repository? Please file a github issue in the repository. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 66172bad..596f4a16 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,12 +1,16 @@ apply plugin: 'com.android.application' +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + android { - compileSdkVersion 25 - buildToolsVersion "25.0.2" + compileSdkVersion 26 + buildToolsVersion "26.0.3" defaultConfig { applicationId "com.example.android.shushme" minSdkVersion 16 - targetSdkVersion 25 + targetSdkVersion 26 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" @@ -17,6 +21,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + buildTypes.each { + it.resValue 'string', 'GOOGLE_API_KEY', keystoreProperties['GOOGLE_API_KEY'] + } } dependencies { @@ -24,8 +32,12 @@ dependencies { androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) - compile 'com.android.support:appcompat-v7:25.1.0' - compile 'com.android.support:recyclerview-v7:25.0.1' - // TODO (3) Add play-services-places and play-services-location dependencies + compile 'com.android.support:appcompat-v7:26.1.0' + compile 'com.android.support:recyclerview-v7:26.1.0' + + //Google Play Services + compile 'com.google.android.gms:play-services-places:11.6.2' + compile 'com.google.android.gms:play-services-location:11.6.2' + testCompile 'junit:junit:4.12' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 764cd012..71ec1cb8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,12 @@ + + + + + + - + @@ -23,8 +31,8 @@ android:authorities="com.example.android.shushme" android:exported="false"/> - + - + \ No newline at end of file diff --git a/app/src/main/java/com/example/android/shushme/GeofenceBroadcastReceiver.java b/app/src/main/java/com/example/android/shushme/GeofenceBroadcastReceiver.java new file mode 100644 index 00000000..b06870d5 --- /dev/null +++ b/app/src/main/java/com/example/android/shushme/GeofenceBroadcastReceiver.java @@ -0,0 +1,163 @@ +package com.example.android.shushme; + +/* +* Copyright (C) 2017 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.support.v4.app.TaskStackBuilder; +import android.support.v7.app.NotificationCompat; +import android.util.Log; + +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingEvent; + +public class GeofenceBroadcastReceiver extends BroadcastReceiver { + + public static final String TAG = GeofenceBroadcastReceiver.class.getSimpleName(); + + /*** + * Handles the Broadcast message sent when the Geofence Transition is triggered + * Careful here though, this is running on the main thread so make sure you start an AsyncTask for + * anything that takes longer than say 10 second to run + * + * @param context + * @param intent + */ + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "-> onReceive called"); + + // TODO COMPLETED (4) Use GeofencingEvent.fromIntent to retrieve the GeofencingEvent that caused the transition + // Get the Geofence Event from the Intent sent through + + GeofencingEvent geofencingEvent = GeofencingEvent.fromIntent(intent); + if (geofencingEvent.hasError()) { + Log.e(TAG, String.format("-> Error code : %d", geofencingEvent.getErrorCode())); + return; + } + + // TODO COMPLETED (5) Call getGeofenceTransition to get the transition type and use AudioManager to set the + // phone ringer mode based on the transition type. Feel free to create a helper method (setRingerMode) + + // Get the transition type. + int geofenceTransition = geofencingEvent.getGeofenceTransition(); + // Check which transition type has triggered this event + if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_ENTER) { + setRingerMode(context, AudioManager.RINGER_MODE_SILENT); + } else if (geofenceTransition == Geofence.GEOFENCE_TRANSITION_EXIT) { + setRingerMode(context, AudioManager.RINGER_MODE_NORMAL); + } else { + // Log the error. + Log.e(TAG, String.format("-> Unknown transition : %d", geofenceTransition)); + // No need to do anything else + return; + } + + // TODO COMPLETED (6) Show a notification to alert the user that the ringer mode has changed. + // Feel free to create a helper method (sendNotification) + + // Send the notification + sendNotification(context, geofenceTransition); + } + + + /** + * Posts a notification in the notification bar when a transition is detected + * Uses different icon drawables for different transition types + * If the user clicks the notification, control goes to the MainActivity + * + * @param context The calling context for building a task stack + * @param transitionType The geofence transition type, can be Geofence.GEOFENCE_TRANSITION_ENTER + * or Geofence.GEOFENCE_TRANSITION_EXIT + */ + private void sendNotification(Context context, int transitionType) { + Log.v(TAG, "-> sendNotification -> transitionType = " + transitionType); + + // Create an explicit content Intent that starts the main Activity. + Intent notificationIntent = new Intent(context, MainActivity.class); + + // Construct a task stack. + TaskStackBuilder stackBuilder = TaskStackBuilder.create(context); + + // Add the main Activity to the task stack as the parent. + stackBuilder.addParentStack(MainActivity.class); + + // Push the content Intent onto the stack. + stackBuilder.addNextIntent(notificationIntent); + + // Get a PendingIntent containing the entire back stack. + PendingIntent notificationPendingIntent = + stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); + + // Get a notification builder + NotificationCompat.Builder builder = new NotificationCompat.Builder(context); + + // Check the transition type to display the relevant icon image + if (transitionType == Geofence.GEOFENCE_TRANSITION_ENTER) { + builder.setSmallIcon(R.drawable.ic_volume_off_white_24dp) + .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), + R.drawable.ic_volume_off_white_24dp)) + .setContentTitle(context.getString(R.string.silent_mode_activated)); + } else if (transitionType == Geofence.GEOFENCE_TRANSITION_EXIT) { + builder.setSmallIcon(R.drawable.ic_volume_up_white_24dp) + .setLargeIcon(BitmapFactory.decodeResource(context.getResources(), + R.drawable.ic_volume_up_white_24dp)) + .setContentTitle(context.getString(R.string.back_to_normal)); + } + + // Continue building the notification + builder.setContentText(context.getString(R.string.touch_to_relaunch)); + builder.setContentIntent(notificationPendingIntent); + + // Dismiss notification once the user touches it. + builder.setAutoCancel(true); + + // Get an instance of the Notification manager + NotificationManager mNotificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + // Issue the notification + mNotificationManager.notify(0, builder.build()); + } + + /** + * Changes the ringer mode on the device to either silent or back to normal + * + * @param context The context to access AUDIO_SERVICE + * @param mode The desired mode to switch device to, can be AudioManager.RINGER_MODE_SILENT or + * AudioManager.RINGER_MODE_NORMAL + */ + private void setRingerMode(Context context, int mode) { + Log.i(TAG, "-> setRingerMode -> mode = " + mode); + + NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + // TODO COMPLETED - BUG FIXED check https://github.com/udacity/AdvancedAndroid_Shushme/issues/2 + + // Check for DND permissions for API 24+ + if (android.os.Build.VERSION.SDK_INT < 24 || + (android.os.Build.VERSION.SDK_INT >= 24 && nm.isNotificationPolicyAccessGranted())) { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + audioManager.setRingerMode(mode); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/shushme/Geofencing.java b/app/src/main/java/com/example/android/shushme/Geofencing.java new file mode 100644 index 00000000..bde65a9b --- /dev/null +++ b/app/src/main/java/com/example/android/shushme/Geofencing.java @@ -0,0 +1,177 @@ +package com.example.android.shushme; + +/* +* Copyright (C) 2017 The Android Open Source Project +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.Result; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.location.Geofence; +import com.google.android.gms.location.GeofencingRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.places.Place; +import com.google.android.gms.location.places.PlaceBuffer; + +import java.util.ArrayList; +import java.util.List; + +public class Geofencing implements ResultCallback { + + // Constants + public static final String TAG = Geofencing.class.getSimpleName(); + private static final float GEOFENCE_RADIUS = 50; // 50 meters + private static final long GEOFENCE_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours + + private List mGeofenceList; + private PendingIntent mGeofencePendingIntent; + private GoogleApiClient mGoogleApiClient; + private Context mContext; + + public Geofencing(Context context, GoogleApiClient client) { + mContext = context; + mGoogleApiClient = client; + mGeofencePendingIntent = null; + mGeofenceList = new ArrayList<>(); + } + + /*** + * Registers the list of Geofences specified in mGeofenceList with Google Place Services + * Uses {@code #mGoogleApiClient} to connect to Google Place Services + * Uses {@link #getGeofencingRequest} to get the list of Geofences to be registered + * Uses {@link #getGeofencePendingIntent} to get the pending intent to launch the IntentService + * when the Geofence is triggered + * Triggers {@link #onResult} when the geofences have been registered successfully + */ + public void registerAllGeofences() { + Log.v(TAG, "-> registerAllGeofences"); + + // Check that the API client is connected and that the list has Geofences in it + if (mGoogleApiClient == null || !mGoogleApiClient.isConnected() || + mGeofenceList == null || mGeofenceList.size() == 0) { + return; + } + try { + LocationServices.GeofencingApi.addGeofences( + mGoogleApiClient, + getGeofencingRequest(), + getGeofencePendingIntent() + ).setResultCallback(this); + } catch (SecurityException securityException) { + // Catch exception generated if the app does not use ACCESS_FINE_LOCATION permission. + Log.e(TAG, securityException.getMessage()); + } + } + + /*** + * Unregisters all the Geofences created by this app from Google Place Services + * Uses {@code #mGoogleApiClient} to connect to Google Place Services + * Uses {@link #getGeofencePendingIntent} to get the pending intent passed when + * registering the Geofences in the first place + * Triggers {@link #onResult} when the geofences have been unregistered successfully + */ + public void unRegisterAllGeofences() { + Log.v(TAG, "-> unRegisterAllGeofences"); + + if (mGoogleApiClient == null || !mGoogleApiClient.isConnected()) { + return; + } + try { + LocationServices.GeofencingApi.removeGeofences( + mGoogleApiClient, + // This is the same pending intent that was used in registerGeofences + getGeofencePendingIntent() + ).setResultCallback(this); + } catch (SecurityException securityException) { + // Catch exception generated if the app does not use ACCESS_FINE_LOCATION permission. + Log.e(TAG, securityException.getMessage()); + } + } + + /*** + * Updates the local ArrayList of Geofences using data from the passed in list + * Uses the Place ID defined by the API as the Geofence object Id + * + * @param places the PlaceBuffer result of the getPlaceById call + */ + public void updateGeofencesList(PlaceBuffer places) { + Log.v(TAG, "-> updateGeofencesList"); + + mGeofenceList = new ArrayList<>(); + if (places == null || places.getCount() == 0) return; + for (Place place : places) { + // Read the place information from the DB cursor + String placeUID = place.getId(); + double placeLat = place.getLatLng().latitude; + double placeLng = place.getLatLng().longitude; + // Build a Geofence object + Geofence geofence = new Geofence.Builder() + .setRequestId(placeUID) + .setExpirationDuration(GEOFENCE_TIMEOUT) + .setCircularRegion(placeLat, placeLng, GEOFENCE_RADIUS) + .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT) + .build(); + // Add it to the list + mGeofenceList.add(geofence); + } + } + + /*** + * Creates a GeofencingRequest object using the mGeofenceList ArrayList of Geofences + * Used by {@code #registerGeofences} + * + * @return the GeofencingRequest object + */ + private GeofencingRequest getGeofencingRequest() { + Log.v(TAG, "-> getGeofencingRequest"); + + GeofencingRequest.Builder builder = new GeofencingRequest.Builder(); + builder.setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER); + builder.addGeofences(mGeofenceList); + return builder.build(); + } + + /*** + * Creates a PendingIntent object using the GeofenceTransitionsIntentService class + * Used by {@code #registerGeofences} + * + * @return the PendingIntent object + */ + private PendingIntent getGeofencePendingIntent() { + Log.v(TAG, "-> getGeofencePendingIntent"); + + // Reuse the PendingIntent if we already have it. + if (mGeofencePendingIntent != null) { + return mGeofencePendingIntent; + } + Intent intent = new Intent(mContext, GeofenceBroadcastReceiver.class); + mGeofencePendingIntent = PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent. + FLAG_UPDATE_CURRENT); + return mGeofencePendingIntent; + } + + @Override + public void onResult(@NonNull Result result) { + Log.e(TAG, String.format("-> Error adding/removing geofence : %s", + result.getStatus().toString())); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/android/shushme/MainActivity.java b/app/src/main/java/com/example/android/shushme/MainActivity.java index a33cf634..19866cde 100644 --- a/app/src/main/java/com/example/android/shushme/MainActivity.java +++ b/app/src/main/java/com/example/android/shushme/MainActivity.java @@ -16,19 +16,58 @@ * limitations under the License. */ +import android.app.NotificationManager; +import android.content.ContentValues; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; +import android.util.Log; +import android.view.View; +import android.widget.CheckBox; +import android.widget.CompoundButton; +import android.widget.Switch; +import android.widget.Toast; -public class MainActivity extends AppCompatActivity { +import com.example.android.shushme.provider.PlaceContract; +import com.google.android.gms.common.ConnectionResult; +import com.google.android.gms.common.GooglePlayServicesNotAvailableException; +import com.google.android.gms.common.GooglePlayServicesRepairableException; +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.PendingResult; +import com.google.android.gms.common.api.ResultCallback; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.places.Place; +import com.google.android.gms.location.places.PlaceBuffer; +import com.google.android.gms.location.places.Places; +import com.google.android.gms.location.places.ui.PlacePicker; + +import java.util.ArrayList; +import java.util.List; + +public class MainActivity extends AppCompatActivity implements + GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener { // Constants public static final String TAG = MainActivity.class.getSimpleName(); + private static final int PERMISSIONS_REQUEST_FINE_LOCATION = 111; + private static final int PLACE_PICKER_REQUEST = 1; // Member variables private PlaceListAdapter mAdapter; private RecyclerView mRecyclerView; + private GoogleApiClient mClient; + private boolean mIsEnabled; + private Geofencing mGeofencing; /** * Called when the activity is starting @@ -38,20 +77,215 @@ public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Log.v(TAG, "-> onCreate"); + setContentView(R.layout.activity_main); // Set up the recycler view mRecyclerView = (RecyclerView) findViewById(R.id.places_list_recycler_view); mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); - mAdapter = new PlaceListAdapter(this); + + mAdapter = new PlaceListAdapter(this, null); mRecyclerView.setAdapter(mAdapter); - // TODO (4) Create a GoogleApiClient with the LocationServices API and GEO_DATA_API + // Initialize the switch state and Handle enable/disable switch change + Switch onOffSwitch = (Switch) findViewById(R.id.enable_switch); + mIsEnabled = getPreferences(MODE_PRIVATE).getBoolean(getString(R.string.setting_enabled), false); + onOffSwitch.setChecked(mIsEnabled); + + onOffSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + + SharedPreferences.Editor editor = getPreferences(MODE_PRIVATE).edit(); + editor.putBoolean(getString(R.string.setting_enabled), isChecked); + mIsEnabled = isChecked; + editor.commit(); + if (isChecked) mGeofencing.registerAllGeofences(); + else mGeofencing.unRegisterAllGeofences(); + } + }); + + // Build up the LocationServices API client + // Uses the addApi method to request the LocationServices API + // Also uses enableAutoManage to automatically when to connect/suspend the client + mClient = new GoogleApiClient.Builder(this) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .addApi(LocationServices.API) + .addApi(Places.GEO_DATA_API) + .enableAutoManage(this, this) + .build(); + + mGeofencing = new Geofencing(this, mClient); + } + + /*** + * Called when the Google API Client is successfully connected + * + * @param connectionHint Bundle of data provided to clients by Google Play services + */ + @Override + public void onConnected(@Nullable Bundle connectionHint) { + Log.i(TAG, "-> API Client Connection Successful!"); + refreshPlacesData(); + } + + /*** + * Called when the Google API Client is suspended + * + * @param cause cause The reason for the disconnection. Defined by constants CAUSE_*. + */ + @Override + public void onConnectionSuspended(int cause) { + Log.i(TAG, "-> API Client Connection Suspended!"); + } + + /*** + * Called when the Google API Client failed to connect to Google Play Services + * + * @param result A ConnectionResult that can be used for resolving the error + */ + @Override + public void onConnectionFailed(@NonNull ConnectionResult result) { + Log.e(TAG, "-> API Client Connection Failed!"); + } + + public void refreshPlacesData() { + Log.v(TAG, "-> refreshPlacesData"); + + Uri uri = PlaceContract.PlaceEntry.CONTENT_URI; + Cursor data = getContentResolver().query( + uri, + null, + null, + null, + null); + + if (data == null || data.getCount() == 0) return; + List guids = new ArrayList(); + while (data.moveToNext()) { + guids.add(data.getString(data.getColumnIndex(PlaceContract.PlaceEntry.COLUMN_PLACE_ID))); + } + PendingResult placeResult = Places.GeoDataApi.getPlaceById(mClient, + guids.toArray(new String[guids.size()])); + placeResult.setResultCallback(new ResultCallback() { + @Override + public void onResult(@NonNull PlaceBuffer places) { + Log.v(TAG, "-> ResultCallback -> onResult"); + + mAdapter.swapPlaces(places); + + mGeofencing.updateGeofencesList(places); + if (mIsEnabled) mGeofencing.registerAllGeofences(); + } + }); + } + + /*** + * Button Click event handler to handle clicking the "Add new location" Button + * + * @param view + */ + public void onAddPlaceButtonClicked(View view) { + Log.v(TAG, "-> onAddPlaceButtonClicked"); + + if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, getString(R.string.need_location_permission_message), Toast.LENGTH_LONG).show(); + return; + } + + try { + // Start a new Activity for the Place Picker API, this will trigger {@code #onActivityResult} + // when a place is selected or with the user cancels. + PlacePicker.IntentBuilder builder = new PlacePicker.IntentBuilder(); + Intent i = builder.build(this); + startActivityForResult(i, PLACE_PICKER_REQUEST); + } catch (GooglePlayServicesRepairableException e) { + Log.e(TAG, String.format("-> GooglePlayServices Not Available [%s]", e.getMessage())); + } catch (GooglePlayServicesNotAvailableException e) { + Log.e(TAG, String.format("-> GooglePlayServices Not Available [%s]", e.getMessage())); + } catch (Exception e) { + Log.e(TAG, String.format("-> PlacePicker Exception: %s", e.getMessage())); + } } - // TODO (5) Override onConnected, onConnectionSuspended and onConnectionFailed for GoogleApiClient - // TODO (7) Override onResume and inside it initialize the location permissions checkbox - // TODO (8) Implement onLocationPermissionClicked to handle the CheckBox click event - // TODO (9) Implement the Add Place Button click event to show a toast message with the permission status + /*** + * Called when the Place Picker Activity returns back with a selected place (or after canceling) + * + * @param requestCode The request code passed when calling startActivityForResult + * @param resultCode The result code specified by the second activity + * @param data The Intent that carries the result data. + */ + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.v(TAG, "-> onActivityResult"); + + if (requestCode == PLACE_PICKER_REQUEST && resultCode == RESULT_OK) { + Place place = PlacePicker.getPlace(this, data); + if (place == null) { + Log.i(TAG, "-> No place selected"); + return; + } + // Extract the place information from the API + String placeName = place.getName().toString(); + String placeAddress = place.getAddress().toString(); + String placeID = place.getId(); + + // Insert a new place into DB + ContentValues contentValues = new ContentValues(); + contentValues.put(PlaceContract.PlaceEntry.COLUMN_PLACE_ID, placeID); + getContentResolver().insert(PlaceContract.PlaceEntry.CONTENT_URI, contentValues); + + // Get live data information + refreshPlacesData(); + } + } + + @Override + public void onResume() { + super.onResume(); + Log.v(TAG, "-> onResume"); + + // Initialize location permissions checkbox + CheckBox locationPermissions = (CheckBox) findViewById(R.id.location_permission_checkbox); + if (ActivityCompat.checkSelfPermission(MainActivity.this, + android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + locationPermissions.setChecked(false); + } else { + locationPermissions.setChecked(true); + locationPermissions.setEnabled(false); + } + + //TODO COMPLETED (3) Initialize ringer permissions checkbox + + // Initialize ringer permissions checkbox + CheckBox ringerPermissions = (CheckBox) findViewById(R.id.ringer_permissions_checkbox); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + // Check if the API supports such permission change and check if permission is granted + if (android.os.Build.VERSION.SDK_INT >= 24 && !nm.isNotificationPolicyAccessGranted()) { + ringerPermissions.setChecked(false); + } else { + ringerPermissions.setChecked(true); + ringerPermissions.setEnabled(false); + } + } + + // TODO COMPLETED (2) Implement onRingerPermissionsClicked to launch ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS + + public void onRingerPermissionsClicked(View view) { + Log.v(TAG, "-> onRingerPermissionsClicked"); + + Intent intent = new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS); + startActivity(intent); + } + + public void onLocationPermissionClicked(View view) { + Log.v(TAG, "-> onLocationPermissionClicked"); + + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{android.Manifest.permission.ACCESS_FINE_LOCATION}, + PERMISSIONS_REQUEST_FINE_LOCATION); + } } diff --git a/app/src/main/java/com/example/android/shushme/PlaceListAdapter.java b/app/src/main/java/com/example/android/shushme/PlaceListAdapter.java index 793ac1d5..2ceb2321 100644 --- a/app/src/main/java/com/example/android/shushme/PlaceListAdapter.java +++ b/app/src/main/java/com/example/android/shushme/PlaceListAdapter.java @@ -18,22 +18,28 @@ import android.content.Context; import android.support.v7.widget.RecyclerView; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.TextView; +import com.google.android.gms.location.places.PlaceBuffer; + public class PlaceListAdapter extends RecyclerView.Adapter { private Context mContext; + private PlaceBuffer mPlaces; + private static final String TAG = PlaceListAdapter.class.getSimpleName(); /** * Constructor using the context and the db cursor * * @param context the calling context/activity */ - public PlaceListAdapter(Context context) { + public PlaceListAdapter(Context context, PlaceBuffer places) { this.mContext = context; + this.mPlaces = places; } /** @@ -60,8 +66,21 @@ public PlaceViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { @Override public void onBindViewHolder(PlaceViewHolder holder, int position) { + String placeName = mPlaces.get(position).getName().toString(); + String placeAddress = mPlaces.get(position).getAddress().toString(); + holder.nameTextView.setText(placeName); + holder.addressTextView.setText(placeAddress); } + public void swapPlaces(PlaceBuffer newPlaces){ + Log.v(TAG, "-> swapPlaces"); + + mPlaces = newPlaces; + if (mPlaces != null) { + // Force the RecyclerView to refresh + this.notifyDataSetChanged(); + } + } /** * Returns the number of items in the cursor @@ -70,7 +89,8 @@ public void onBindViewHolder(PlaceViewHolder holder, int position) { */ @Override public int getItemCount() { - return 0; + if(mPlaces==null) return 0; + return mPlaces.getCount(); } /** diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 101323e4..70dc28e4 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -59,14 +59,86 @@ android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + +