diff --git a/ActivityRecognition/app/build.gradle b/ActivityRecognition/app/build.gradle index e27fca5a..0ea869b6 100644 --- a/ActivityRecognition/app/build.gradle +++ b/ActivityRecognition/app/build.gradle @@ -22,7 +22,7 @@ dependencies { androidTestImplementation('androidx.test.espresso:espresso-core:3.1.1', { exclude group: 'com.android.support', module: 'support-annotations' }) - implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.appcompat:appcompat:1.2.0' testImplementation 'junit:junit:4.12' implementation 'com.google.android.material:material:1.0.0' diff --git a/BasicLocationKotlin/app/build.gradle b/BasicLocationKotlin/app/build.gradle index b4cc2ab2..933cc389 100644 --- a/BasicLocationKotlin/app/build.gradle +++ b/BasicLocationKotlin/app/build.gradle @@ -5,7 +5,6 @@ apply plugin: 'kotlin-android-extensions' android { compileSdkVersion parent.ext.compileSdkVersion - defaultConfig { applicationId "com.google.android.gms.location.sample.basiclocationsample" minSdkVersion parent.ext.minSdkVersion @@ -19,11 +18,35 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + buildFeatures { + viewBinding true + } +} + +ext { + // Android Jetpack + // AppCompat + appCompatVersion = "1.2.0" + // MaterialComponents + materialComponentsVersion = "1.2.1" + + // Google PlayServices + // Location + locationVersion = "17.1.0" } dependencies { - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'com.google.android.material:material:1.0.0' - implementation "com.google.android.gms:play-services-location:17.0.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.3.50" + // Android Jetpack + // AppCompat + implementation "androidx.appcompat:appcompat:$appCompatVersion" + // MaterialComponents + implementation "com.google.android.material:material:$materialComponentsVersion" + + // Google PlayServices + // Location + implementation "com.google.android.gms:play-services-location:$locationVersion" + + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" } diff --git a/BasicLocationKotlin/app/src/main/java/com/google/android/gms/location/sample/basiclocationsample/MainActivity.kt b/BasicLocationKotlin/app/src/main/java/com/google/android/gms/location/sample/basiclocationsample/MainActivity.kt index 9231ab4d..e57d8d93 100644 --- a/BasicLocationKotlin/app/src/main/java/com/google/android/gms/location/sample/basiclocationsample/MainActivity.kt +++ b/BasicLocationKotlin/app/src/main/java/com/google/android/gms/location/sample/basiclocationsample/MainActivity.kt @@ -16,7 +16,9 @@ package com.google.android.gms.location.sample.basiclocationsample +import android.Manifest import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.Manifest.permission.ACCESS_FINE_LOCATION import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageManager @@ -24,26 +26,22 @@ import android.content.pm.PackageManager.PERMISSION_GRANTED import android.location.Location import android.net.Uri import android.os.Bundle +import android.os.Looper import android.provider.Settings -import com.google.android.material.snackbar.Snackbar -import com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE -import com.google.android.material.snackbar.Snackbar.LENGTH_LONG -import androidx.core.app.ActivityCompat -import androidx.appcompat.app.AppCompatActivity import android.util.Log import android.view.View -import android.widget.TextView - -import com.google.android.gms.location.FusedLocationProviderClient -import com.google.android.gms.location.LocationServices -import com.google.android.gms.location.sample.basiclocationsample.BuildConfig.APPLICATION_ID -import com.google.android.gms.tasks.Task +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.viewbinding.BuildConfig +import com.google.android.gms.location.* +import com.google.android.gms.location.sample.basiclocationsample.databinding.MainActivityBinding +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.snackbar.Snackbar.LENGTH_INDEFINITE /** * Demonstrates use of the Location API to retrieve the last known location for a device. */ -class MainActivity : AppCompatActivity() { - +class MainActivity: AppCompatActivity() { private val TAG = "MainActivity" private val REQUEST_PERMISSIONS_REQUEST_CODE = 34 @@ -51,20 +49,23 @@ class MainActivity : AppCompatActivity() { * Provides the entry point to the Fused Location Provider API. */ private lateinit var fusedLocationClient: FusedLocationProviderClient - - private lateinit var latitudeText: TextView - private lateinit var longitudeText: TextView + + /** + * ViewBinding + */ + private lateinit var binding: MainActivityBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.main_activity) - - latitudeText = findViewById(R.id.latitude_text) - longitudeText = findViewById(R.id.longitude_text) - + + // ViewBinding initialization + binding = MainActivityBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) } - + override fun onStart() { super.onStart() @@ -74,75 +75,23 @@ class MainActivity : AppCompatActivity() { getLastLocation() } } - - /** - * Provides a simple way of getting a device's location and is well suited for - * applications that do not require a fine-grained location and that do not need location - * updates. Gets the best and most recent location currently available, which may be null - * in rare cases when a location is not available. - * - * Note: this method should be called after location permission has been granted. - */ - @SuppressLint("MissingPermission") - private fun getLastLocation() { - fusedLocationClient.lastLocation - .addOnCompleteListener { taskLocation -> - if (taskLocation.isSuccessful && taskLocation.result != null) { - - val location = taskLocation.result - - latitudeText.text = resources - .getString(R.string.latitude_label, location?.latitude) - longitudeText.text = resources - .getString(R.string.longitude_label, location?.longitude) - } else { - Log.w(TAG, "getLastLocation:exception", taskLocation.exception) - showSnackbar(R.string.no_location_detected) - } - } - } - - /** - * Shows a [Snackbar]. - * - * @param snackStrId The id for the string resource for the Snackbar text. - * @param actionStrId The text of the action item. - * @param listener The listener associated with the Snackbar action. - */ - private fun showSnackbar( - snackStrId: Int, - actionStrId: Int = 0, - listener: View.OnClickListener? = null - ) { - val snackbar = Snackbar.make(findViewById(android.R.id.content), getString(snackStrId), - LENGTH_INDEFINITE) - if (actionStrId != 0 && listener != null) { - snackbar.setAction(getString(actionStrId), listener) - } - snackbar.show() - } - /** * Return the current state of the permissions needed. */ - private fun checkPermissions() = - ActivityCompat.checkSelfPermission(this, ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED - - private fun startLocationPermissionRequest() { - ActivityCompat.requestPermissions(this, arrayOf(ACCESS_COARSE_LOCATION), - REQUEST_PERMISSIONS_REQUEST_CODE) - } - + private fun checkPermissions() = ActivityCompat.checkSelfPermission(this, + ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + this, ACCESS_FINE_LOCATION) == PERMISSION_GRANTED private fun requestPermissions() { - if (ActivityCompat.shouldShowRequestPermissionRationale(this, ACCESS_COARSE_LOCATION)) { + if (ActivityCompat.shouldShowRequestPermissionRationale(this, ACCESS_COARSE_LOCATION) + && ActivityCompat.shouldShowRequestPermissionRationale(this, ACCESS_FINE_LOCATION)) { // Provide an additional rationale to the user. This would happen if the user denied the // request previously, but didn't check the "Don't ask again" checkbox. Log.i(TAG, "Displaying permission rationale to provide additional context.") - showSnackbar(R.string.permission_rationale, android.R.string.ok, View.OnClickListener { + showSnackbar(R.string.permission_rationale, android.R.string.ok) { // Request permission startLocationPermissionRequest() - }) - + } + } else { // Request permission. It's possible this can be auto answered if device policy // sets the permission in a given state or the user denied the permission @@ -151,7 +100,10 @@ class MainActivity : AppCompatActivity() { startLocationPermissionRequest() } } - + private fun startLocationPermissionRequest() { + ActivityCompat.requestPermissions(this, arrayOf(ACCESS_COARSE_LOCATION, + ACCESS_FINE_LOCATION), REQUEST_PERMISSIONS_REQUEST_CODE) + } /** * Callback received when a permissions request has been completed. */ @@ -166,35 +118,118 @@ class MainActivity : AppCompatActivity() { // If user interaction was interrupted, the permission request is cancelled and you // receive empty arrays. grantResults.isEmpty() -> Log.i(TAG, "User interaction was cancelled.") - + // Permission granted. - (grantResults[0] == PackageManager.PERMISSION_GRANTED) -> getLastLocation() - + (grantResults[0] == PERMISSION_GRANTED) -> getLastLocation() + // Permission denied. - + // Notify the user via a SnackBar that they have rejected a core permission for the // app, which makes the Activity useless. In a real app, core permissions would // typically be best requested during a welcome-screen flow. - + // Additionally, it is important to remember that a permission might have been // rejected without asking the user for permission (device policy or "Never ask // again" prompts). Therefore, a user interface affordance is typically implemented // when permissions are denied. Otherwise, your app could appear unresponsive to // touches or interactions which have required permissions. else -> { - showSnackbar(R.string.permission_denied_explanation, R.string.settings, - View.OnClickListener { - // Build intent that displays the App settings screen. - val intent = Intent().apply { - action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - data = Uri.fromParts("package", APPLICATION_ID, null) - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - startActivity(intent) - }) + showSnackbar(R.string.permission_denied_explanation, R.string.settings) { + // Build intent that displays the App settings screen. + val intent = Intent().apply { + action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + data = Uri.fromParts("package", + BuildConfig.LIBRARY_PACKAGE_NAME, null) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + startActivity(intent) + } } } } } - + /** + * Provides a simple way of getting a device's location and is well suited for + * applications that do not require a fine-grained location and that do not need location + * updates. Gets the best and most recent location currently available, which may be null + * in rare cases when a location is not available. + * + * Note: this method should be called after location permission has been granted. + */ + @SuppressLint("MissingPermission") + private fun getLastLocation() { + Log.d(TAG, "getLastLocation") + fusedLocationClient.lastLocation.addOnCompleteListener { taskLocation -> + if (taskLocation.isSuccessful && taskLocation.result != null) { + updateViews(taskLocation.result) + } else { + requestNewLocationData() + /*Log.w(TAG, "getLastLocation:exception", taskLocation.exception) + showSnackbar(R.string.no_location_detected)*/ + } + } + } + fun updateViews(currentLocation: Location) { + Log.d(TAG, "updateViews") + binding.currentLatitude.text = resources + .getString(R.string.latitude_label, currentLocation.latitude) + binding.currentLongitude.text = resources + .getString(R.string.longitude_label, currentLocation.longitude) + } + fun requestNewLocationData() { + Log.d(TAG, "requestNewLocationData") + // Initializing LocationRequest + // object with appropriate methods + val locationRequest = LocationRequest().apply { + // For a high level accuracy use PRIORITY_HIGH_ACCURACY argument. + // For a low level accuracy (city), use PRIORITY_LOW_POWER + priority = LocationRequest.PRIORITY_HIGH_ACCURACY + interval = 3 + fastestInterval = 1 + numUpdates = 2 + } + + // setting LocationRequest on a FusedLocationClient + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + + /*if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_DENIED && ActivityCompat.checkSelfPermission(this, + Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_DENIED) { + ActivityCompat.requestPermissions( + requireActivity(), arrayOf(Manifest.permission_group.LOCATION), PERMISSIONS_ALLOW_USING_LOCATION_ID + ) + } else { + locationClient?.requestLocationUpdates(locationRequest, + locationCallback, Looper.myLooper()) + }*/ + + if (ActivityCompat.checkSelfPermission(this, ACCESS_FINE_LOCATION) + == PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, + ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED) { + fusedLocationClient.requestLocationUpdates(locationRequest, + locationCallback, Looper.myLooper()) + } + } + private val locationCallback: LocationCallback = object : LocationCallback() { + override fun onLocationResult(locationResult: LocationResult) { + Log.d(TAG, "onLocationResult") + updateViews(locationResult.lastLocation) + } + } + /** + * Shows a [Snackbar]. + * + * @param snackStrId The id for the string resource for the Snackbar text. + * @param actionStrId The text of the action item. + * @param listener The listener associated with the Snackbar action. + */ + private fun showSnackbar(snackStrId: Int, actionStrId: Int = 0, + listener: View.OnClickListener? = null) { + val snackBar = Snackbar.make(findViewById(android.R.id.content), getString(snackStrId), + LENGTH_INDEFINITE) + if (actionStrId != 0 && listener != null) { + snackBar.setAction(getString(actionStrId), listener) + } + snackBar.show() + } } diff --git a/BasicLocationKotlin/app/src/main/res/layout/main_activity.xml b/BasicLocationKotlin/app/src/main/res/layout/main_activity.xml index 85398403..15d15479 100644 --- a/BasicLocationKotlin/app/src/main/res/layout/main_activity.xml +++ b/BasicLocationKotlin/app/src/main/res/layout/main_activity.xml @@ -26,7 +26,7 @@ android:paddingTop="@dimen/activity_vertical_margin"> = Build.VERSION_CODES.O) { - builder.setChannelId(CHANNEL_ID); // Channel ID + builder = new NotificationCompat.Builder( + this, CHANNEL_ID + ); + // Builder initialization for other versions of Android + } else { + builder = new NotificationCompat.Builder( + this, "" + ); } + + builder.addAction(R.drawable.ic_launch, getString(R.string.launch_activity), + activityPendingIntent); + builder.addAction(R.drawable.ic_cancel, getString(R.string.remove_location_updates), + servicePendingIntent); + builder.setContentText(text); + builder.setContentTitle(Utils.getLocationTitle(this)); + builder.setOngoing(true); + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setSmallIcon(R.mipmap.ic_launcher); + builder.setTicker(text); + builder.setWhen(System.currentTimeMillis()); return builder.build(); } @@ -301,14 +307,11 @@ private Notification getNotification() { private void getLastLocation() { try { mFusedLocationClient.getLastLocation() - .addOnCompleteListener(new OnCompleteListener() { - @Override - public void onComplete(@NonNull Task task) { - if (task.isSuccessful() && task.getResult() != null) { - mLocation = task.getResult(); - } else { - Log.w(TAG, "Failed to get location."); - } + .addOnCompleteListener(task -> { + if (task.isSuccessful() && task.getResult() != null) { + mLocation = task.getResult(); + } else { + Log.w(TAG, "Failed to get location."); } }); } catch (SecurityException unlikely) { diff --git a/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.java b/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.java index 5244c7a7..3d4e5f91 100644 --- a/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.java +++ b/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.java @@ -16,6 +16,7 @@ package com.google.android.gms.location.sample.locationupdatesforegroundservice; +import android.Manifest; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -23,28 +24,25 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.location.Location; -import android.os.IBinder; -import android.preference.PreferenceManager; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import androidx.appcompat.app.AppCompatActivity; +import android.net.Uri; import android.os.Bundle; +import android.os.IBinder; +import android.provider.Settings; import android.util.Log; +import android.view.View; +import android.widget.Toast; -import android.Manifest; - -import android.content.pm.PackageManager; - -import android.net.Uri; - -import android.provider.Settings; import androidx.annotation.NonNull; -import com.google.android.material.snackbar.Snackbar; +import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; +import androidx.viewbinding.BuildConfig; -import android.view.View; -import android.widget.Button; -import android.widget.Toast; +import com.google.android.gms.location.sample.locationupdatesforegroundservice.databinding.ActivityMainBinding; +import com.google.android.material.snackbar.Snackbar; /** * The only activity in this sample. @@ -98,8 +96,7 @@ public class MainActivity extends AppCompatActivity implements private boolean mBound = false; // UI elements. - private Button mRequestLocationUpdatesButton; - private Button mRemoveLocationUpdatesButton; + private ActivityMainBinding mBinding; // Monitors the state of the connection to the service. private final ServiceConnection mServiceConnection = new ServiceConnection() { @@ -121,12 +118,17 @@ public void onServiceDisconnected(ComponentName name) { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // ViewBinding initialization + mBinding = ActivityMainBinding.inflate(getLayoutInflater()); + View view = mBinding.getRoot(); + setContentView(view); + myReceiver = new MyReceiver(); - setContentView(R.layout.activity_main); // Check that the user hasn't revoked permissions by going to Settings. if (Utils.requestingLocationUpdates(this)) { - if (!checkPermissions()) { + if (checkPermissions()) { requestPermissions(); } } @@ -138,26 +140,16 @@ protected void onStart() { PreferenceManager.getDefaultSharedPreferences(this) .registerOnSharedPreferenceChangeListener(this); - mRequestLocationUpdatesButton = (Button) findViewById(R.id.request_location_updates_button); - mRemoveLocationUpdatesButton = (Button) findViewById(R.id.remove_location_updates_button); - - mRequestLocationUpdatesButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (!checkPermissions()) { - requestPermissions(); - } else { - mService.requestLocationUpdates(); - } + mBinding.requestLocationUpdatesButton.setOnClickListener(view -> { + if (checkPermissions()) { + requestPermissions(); + } else { + mService.requestLocationUpdates(); } }); - mRemoveLocationUpdatesButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mService.removeLocationUpdates(); - } - }); + mBinding.removeLocationUpdatesButton.setOnClickListener( + view -> mService.removeLocationUpdates()); // Restore the state of the buttons when the activity (re)launches. setButtonsState(Utils.requestingLocationUpdates(this)); @@ -199,8 +191,8 @@ protected void onStop() { * Returns the current state of the permissions needed. */ private boolean checkPermissions() { - return PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(this, - Manifest.permission.ACCESS_FINE_LOCATION); + return PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this, + Manifest.permission.ACCESS_FINE_LOCATION); } private void requestPermissions() { @@ -213,17 +205,14 @@ private void requestPermissions() { if (shouldProvideRationale) { Log.i(TAG, "Displaying permission rationale to provide additional context."); Snackbar.make( - findViewById(R.id.activity_main), + mBinding.getRoot(), R.string.permission_rationale, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.ok, new View.OnClickListener() { - @Override - public void onClick(View view) { - // Request permission - ActivityCompat.requestPermissions(MainActivity.this, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, - REQUEST_PERMISSIONS_REQUEST_CODE); - } + .setAction(R.string.ok, view -> { + // Request permission + ActivityCompat.requestPermissions(MainActivity.this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + REQUEST_PERMISSIONS_REQUEST_CODE); }) .show(); } else { @@ -256,22 +245,19 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis // Permission denied. setButtonsState(false); Snackbar.make( - findViewById(R.id.activity_main), + mBinding.getRoot(), R.string.permission_denied_explanation, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.settings, new View.OnClickListener() { - @Override - public void onClick(View view) { - // Build intent that displays the App settings screen. - Intent intent = new Intent(); - intent.setAction( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", - BuildConfig.APPLICATION_ID, null); - intent.setData(uri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - } + .setAction(R.string.settings, view -> { + // Build intent that displays the App settings screen. + Intent intent = new Intent(); + intent.setAction( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + Uri uri = Uri.fromParts("package", + BuildConfig.LIBRARY_PACKAGE_NAME, null); + intent.setData(uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); }) .show(); } @@ -293,9 +279,9 @@ public void onReceive(Context context, Intent intent) { } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String s) { + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String string) { // Update the buttons state depending on whether location updates are being requested. - if (s.equals(Utils.KEY_REQUESTING_LOCATION_UPDATES)) { + if (string.equals(Utils.KEY_REQUESTING_LOCATION_UPDATES)) { setButtonsState(sharedPreferences.getBoolean(Utils.KEY_REQUESTING_LOCATION_UPDATES, false)); } @@ -303,11 +289,11 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin private void setButtonsState(boolean requestingLocationUpdates) { if (requestingLocationUpdates) { - mRequestLocationUpdatesButton.setEnabled(false); - mRemoveLocationUpdatesButton.setEnabled(true); + mBinding.requestLocationUpdatesButton.setEnabled(false); + mBinding.removeLocationUpdatesButton.setEnabled(true); } else { - mRequestLocationUpdatesButton.setEnabled(true); - mRemoveLocationUpdatesButton.setEnabled(false); + mBinding.requestLocationUpdatesButton.setEnabled(true); + mBinding.removeLocationUpdatesButton.setEnabled(false); } } } diff --git a/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/Utils.java b/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/Utils.java index 01dc5887..df98f6ca 100644 --- a/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/Utils.java +++ b/LocationUpdatesForegroundService/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/Utils.java @@ -19,7 +19,8 @@ import android.content.Context; import android.location.Location; -import android.preference.PreferenceManager; + +import androidx.preference.PreferenceManager; import java.text.DateFormat; import java.util.Date; diff --git a/LocationUpdatesForegroundService/build.gradle b/LocationUpdatesForegroundService/build.gradle index 095a4ff4..3bd97cd3 100644 --- a/LocationUpdatesForegroundService/build.gradle +++ b/LocationUpdatesForegroundService/build.gradle @@ -1,15 +1,17 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext { + // Android Gradle plugin + gradlePluginVersion = "4.0.1" + } repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.1' - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files + // Android Gradle plugin + classpath "com.android.tools.build:gradle:$gradlePluginVersion" } } diff --git a/LocationUpdatesForegroundService/gradle.properties b/LocationUpdatesForegroundService/gradle.properties index aac7c9b4..01236edf 100644 --- a/LocationUpdatesForegroundService/gradle.properties +++ b/LocationUpdatesForegroundService/gradle.properties @@ -15,3 +15,5 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true + +android.useAndroidX=true diff --git a/LocationUpdatesForegroundService/gradle/wrapper/gradle-wrapper.properties b/LocationUpdatesForegroundService/gradle/wrapper/gradle-wrapper.properties index 0793d0b0..d936ae58 100644 --- a/LocationUpdatesForegroundService/gradle/wrapper/gradle-wrapper.properties +++ b/LocationUpdatesForegroundService/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/LocationUpdatesForegroundServiceKotlin/.gitignore b/LocationUpdatesForegroundServiceKotlin/.gitignore new file mode 100644 index 00000000..e3ee5f5f --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/.gitignore @@ -0,0 +1,9 @@ +# Directories +build/ +.idea +.gradle + +# Files +*.iml +local.properties +.DS_Store \ No newline at end of file diff --git a/LocationUpdatesForegroundServiceKotlin/.google/packaging.yaml b/LocationUpdatesForegroundServiceKotlin/.google/packaging.yaml new file mode 100644 index 00000000..eaa5334c --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/.google/packaging.yaml @@ -0,0 +1,25 @@ +# Copyright 2018 Google Inc. +# +# 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. +# +# This file is used by Google as part of our samples packaging process. +# End users may safely ignore this file. It has no relevance to other systems. +--- +status: PUBLISHED +technologies: [Android] +categories: [Location] +languages: [Java] +solutions: [Mobile] +github: android/location-samples +level: BEGINNER +license: apache2 diff --git a/LocationUpdatesForegroundServiceKotlin/app/.gitignore b/LocationUpdatesForegroundServiceKotlin/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/LocationUpdatesForegroundServiceKotlin/app/build.gradle b/LocationUpdatesForegroundServiceKotlin/app/build.gradle new file mode 100644 index 00000000..e6c0760b --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/app/build.gradle @@ -0,0 +1,79 @@ +apply plugin: 'com.android.application' + +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 29 + + defaultConfig { + applicationId "com.google.android.gms.location.sample.locationupdatesforegroundservice" + minSdkVersion 16 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + buildFeatures { + viewBinding true + } +} + +ext { + // Jetpack + // AppCompat + appCompatVersion = "1.2.0" + // LocalBroadcastManager + localBroadcastManagerVersion = "1.0.0" + // Preference + preferenceVersion = "1.1.1" + // MaterialComponents + materialComponentsVersion = "1.2.1" + + // Google + // Location + locationVersion = "17.0.0" + + // Testing + // Junit + junitVersion = "4.13" + // Espresso + espressoVersion = "3.3.0" +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + // Jetpack + // AppCompat + implementation "androidx.appcompat:appcompat:$appCompatVersion" + // LocalBroadcastManager + implementation "androidx.localbroadcastmanager:localbroadcastmanager:$localBroadcastManagerVersion" + // Preference + implementation "androidx.preference:preference-ktx:$preferenceVersion" + // MaterialComponents + implementation "com.google.android.material:material:$materialComponentsVersion" + + // Google + implementation "com.google.android.gms:play-services-location:$locationVersion" + + // Kotlin + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion" + + // Testing + // Junit + testImplementation "junit:junit:$junitVersion" + // Espresso + androidTestImplementation("androidx.test.espresso:espresso-core:$espressoVersion", { + exclude group: 'com.android.support', module: 'support-annotations' + }) +} diff --git a/LocationUpdatesForegroundServiceKotlin/app/proguard-rules.pro b/LocationUpdatesForegroundServiceKotlin/app/proguard-rules.pro new file mode 100644 index 00000000..ec87fa03 --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/app/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/shailentuli/Library/Android/sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/AndroidManifest.xml b/LocationUpdatesForegroundServiceKotlin/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..415ff0f8 --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/LocationUpdatesService.java b/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/LocationUpdatesService.java new file mode 100644 index 00000000..848ce706 --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/LocationUpdatesService.java @@ -0,0 +1,376 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ + +package com.google.android.gms.location.sample.locationupdatesforegroundservice; + +import android.app.ActivityManager; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.location.Location; +import android.os.Binder; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; +import com.google.android.gms.location.LocationServices; + +/** + * A bound and started service that is promoted to a foreground service when location updates have + * been requested and all clients unbind. + * + * For apps running in the background on "O" devices, location is computed only once every 10 + * minutes and delivered batched every 30 minutes. This restriction applies even to apps + * targeting "N" or lower which are run on "O" devices. + * + * This sample show how to use a long-running service for location updates. When an activity is + * bound to this service, frequent location updates are permitted. When the activity is removed + * from the foreground, the service promotes itself to a foreground service, and location updates + * continue. When the activity comes back to the foreground, the foreground service stops, and the + * notification associated with that service is removed. + */ +public class LocationUpdatesService extends Service { + + private static final String PACKAGE_NAME = + "com.google.android.gms.location.sample.locationupdatesforegroundservice"; + + private static final String TAG = LocationUpdatesService.class.getSimpleName(); + + /** + * The name of the channel for notifications. + */ + private static final String CHANNEL_ID = "channel_01"; + + static final String ACTION_BROADCAST = PACKAGE_NAME + ".broadcast"; + + static final String EXTRA_LOCATION = PACKAGE_NAME + ".location"; + private static final String EXTRA_STARTED_FROM_NOTIFICATION = PACKAGE_NAME + + ".started_from_notification"; + + private final IBinder mBinder = new LocalBinder(); + + /** + * The desired interval for location updates. Inexact. Updates may be more or less frequent. + */ + private static final long UPDATE_INTERVAL_IN_MILLISECONDS = 10000; + + /** + * The fastest rate for active location updates. Updates will never be more frequent + * than this value. + */ + private static final long FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = + UPDATE_INTERVAL_IN_MILLISECONDS / 2; + + /** + * The identifier for the notification displayed for the foreground service. + */ + private static final int NOTIFICATION_ID = 12345678; + + /** + * Used to check whether the bound activity has really gone away and not unbound as part of an + * orientation change. We create a foreground service notification only if the former takes + * place. + */ + private boolean mChangingConfiguration = false; + + private NotificationManager mNotificationManager; + + /** + * Contains parameters used by {@link com.google.android.gms.location.FusedLocationProviderClient}. + */ + private LocationRequest mLocationRequest; + + /** + * Provides access to the Fused Location Provider API. + */ + private FusedLocationProviderClient mFusedLocationClient; + + /** + * Callback for changes in location. + */ + private LocationCallback mLocationCallback; + + private Handler mServiceHandler; + + /** + * The current location. + */ + private Location mLocation; + + public LocationUpdatesService() { + } + + @Override + public void onCreate() { + mFusedLocationClient = LocationServices.getFusedLocationProviderClient(this); + + mLocationCallback = new LocationCallback() { + @Override + public void onLocationResult(LocationResult locationResult) { + super.onLocationResult(locationResult); + onNewLocation(locationResult.getLastLocation()); + } + }; + + createLocationRequest(); + getLastLocation(); + + HandlerThread handlerThread = new HandlerThread(TAG); + handlerThread.start(); + mServiceHandler = new Handler(handlerThread.getLooper()); + mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + + // Android O requires a Notification Channel. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + CharSequence name = getString(R.string.app_name); + // Create the channel for the notification + NotificationChannel mChannel = + new NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT); + + // Set the Notification Channel for the Notification Manager. + mNotificationManager.createNotificationChannel(mChannel); + } + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.i(TAG, "Service started"); + boolean startedFromNotification = intent.getBooleanExtra(EXTRA_STARTED_FROM_NOTIFICATION, + false); + + // We got here because the user decided to remove location updates from the notification. + if (startedFromNotification) { + removeLocationUpdates(); + stopSelf(); + } + // Tells the system to not try to recreate the service after it has been killed. + return START_NOT_STICKY; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mChangingConfiguration = true; + } + + @Override + public IBinder onBind(Intent intent) { + // Called when a client (MainActivity in case of this sample) comes to the foreground + // and binds with this service. The service should cease to be a foreground service + // when that happens. + Log.i(TAG, "in onBind()"); + stopForeground(true); + mChangingConfiguration = false; + return mBinder; + } + + @Override + public void onRebind(Intent intent) { + // Called when a client (MainActivity in case of this sample) returns to the foreground + // and binds once again with this service. The service should cease to be a foreground + // service when that happens. + Log.i(TAG, "in onRebind()"); + stopForeground(true); + mChangingConfiguration = false; + super.onRebind(intent); + } + + @Override + public boolean onUnbind(Intent intent) { + Log.i(TAG, "Last client unbound from service"); + + // Called when the last client (MainActivity in case of this sample) unbinds from this + // service. If this method is called due to a configuration change in MainActivity, we + // do nothing. Otherwise, we make this service a foreground service. + if (!mChangingConfiguration && Utils.requestingLocationUpdates(this)) { + Log.i(TAG, "Starting foreground service"); + + startForeground(NOTIFICATION_ID, getNotification()); + } + return true; // Ensures onRebind() is called when a client re-binds. + } + + @Override + public void onDestroy() { + mServiceHandler.removeCallbacksAndMessages(null); + } + + /** + * Makes a request for location updates. Note that in this sample we merely log the + * {@link SecurityException}. + */ + public void requestLocationUpdates() { + Log.i(TAG, "Requesting location updates"); + Utils.setRequestingLocationUpdates(this, true); + startService(new Intent(getApplicationContext(), LocationUpdatesService.class)); + try { + mFusedLocationClient.requestLocationUpdates(mLocationRequest, + mLocationCallback, Looper.myLooper()); + } catch (SecurityException unlikely) { + Utils.setRequestingLocationUpdates(this, false); + Log.e(TAG, "Lost location permission. Could not request updates. " + unlikely); + } + } + + /** + * Removes location updates. Note that in this sample we merely log the + * {@link SecurityException}. + */ + public void removeLocationUpdates() { + Log.i(TAG, "Removing location updates"); + try { + mFusedLocationClient.removeLocationUpdates(mLocationCallback); + Utils.setRequestingLocationUpdates(this, false); + stopSelf(); + } catch (SecurityException unlikely) { + Utils.setRequestingLocationUpdates(this, true); + Log.e(TAG, "Lost location permission. Could not remove updates. " + unlikely); + } + } + + /** + * Returns the {@link NotificationCompat} used as part of the foreground service. + */ + private Notification getNotification() { + Intent intent = new Intent(this, LocationUpdatesService.class); + + CharSequence text = Utils.getLocationText(mLocation); + + // Extra to help us figure out if we arrived in onStartCommand via the notification or not. + intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true); + + // The PendingIntent that leads to a call to onStartCommand() in this service. + PendingIntent servicePendingIntent = PendingIntent.getService(this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT); + + // The PendingIntent to launch activity. + PendingIntent activityPendingIntent = PendingIntent.getActivity(this, 0, + new Intent(this, MainActivity.class), 0); + + NotificationCompat.Builder builder; + + // Builder initialization for Android O. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder = new NotificationCompat.Builder( + this, CHANNEL_ID + ); + // Builder initialization for other versions of Android + } else { + builder = new NotificationCompat.Builder( + this, "" + ); + } + + builder.addAction(R.drawable.ic_launch, getString(R.string.launch_activity), + activityPendingIntent); + builder.addAction(R.drawable.ic_cancel, getString(R.string.remove_location_updates), + servicePendingIntent); + builder.setContentText(text); + builder.setContentTitle(Utils.getLocationTitle(this)); + builder.setOngoing(true); + builder.setPriority(NotificationCompat.PRIORITY_HIGH); + builder.setSmallIcon(R.mipmap.ic_launcher); + builder.setTicker(text); + builder.setWhen(System.currentTimeMillis()); + + return builder.build(); + } + + private void getLastLocation() { + try { + mFusedLocationClient.getLastLocation() + .addOnCompleteListener(task -> { + if (task.isSuccessful() && task.getResult() != null) { + mLocation = task.getResult(); + } else { + Log.w(TAG, "Failed to get location."); + } + }); + } catch (SecurityException unlikely) { + Log.e(TAG, "Lost location permission." + unlikely); + } + } + + private void onNewLocation(Location location) { + Log.i(TAG, "New location: " + location); + + mLocation = location; + + // Notify anyone listening for broadcasts about the new location. + Intent intent = new Intent(ACTION_BROADCAST); + intent.putExtra(EXTRA_LOCATION, location); + LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent); + + // Update notification content if running as a foreground service. + if (serviceIsRunningInForeground(this)) { + mNotificationManager.notify(NOTIFICATION_ID, getNotification()); + } + } + + /** + * Sets the location request parameters. + */ + private void createLocationRequest() { + mLocationRequest = new LocationRequest(); + mLocationRequest.setInterval(UPDATE_INTERVAL_IN_MILLISECONDS); + mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS); + mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + } + + /** + * Class used for the client Binder. Since this service runs in the same process as its + * clients, we don't need to deal with IPC. + */ + public class LocalBinder extends Binder { + LocationUpdatesService getService() { + return LocationUpdatesService.this; + } + } + + /** + * Returns true if this is a foreground service. + * + * @param context The {@link Context}. + */ + public boolean serviceIsRunningInForeground(Context context) { + ActivityManager manager = (ActivityManager) context.getSystemService( + Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices( + Integer.MAX_VALUE)) { + if (getClass().getName().equals(service.service.getClassName())) { + if (service.foreground) { + return true; + } + } + } + return false; + } +} diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.kt b/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.kt new file mode 100644 index 00000000..6027f176 --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/MainActivity.kt @@ -0,0 +1,283 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ +package com.google.android.gms.location.sample.locationupdatesforegroundservice + +import android.Manifest +import android.content.* +import android.content.pm.PackageManager +import android.location.Location +import android.net.Uri +import android.os.Bundle +import android.os.IBinder +import android.provider.Settings +import android.util.Log +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.localbroadcastmanager.content.LocalBroadcastManager +import androidx.preference.PreferenceManager +import androidx.viewbinding.BuildConfig +import com.google.android.gms.location.sample.locationupdatesforegroundservice.databinding.ActivityMainBinding +import com.google.android.material.snackbar.Snackbar + +/** + * The only activity in this sample. + * + * Note: Users have three options in "Q" regarding location: + * + * * Allow all the time + * * Allow while app is in use, i.e., while app is in foreground + * * Not allow location at all + * + * Because this app creates a foreground service (tied to a Notification) when the user navigates + * away from the app, it only needs location "while in use." That is, there is no need to ask for + * location all the time (which requires additional permissions in the manifest). + * + * "Q" also now requires developers to specify foreground service type in the manifest (in this + * case, "location"). + * + * Note: For Foreground Services, "P" requires additional permission in manifest. Please check + * project manifest for more information. + * + * Note: for apps running in the background on "O" devices (regardless of the targetSdkVersion), + * location may be computed less frequently than requested when the app is not in the foreground. + * Apps that use a foreground service - which involves displaying a non-dismissable + * notification - can bypass the background location limits and request location updates as before. + * + * This sample uses a long-running bound and started service for location updates. The service is + * aware of foreground status of this activity, which is the only bound client in + * this sample. After requesting location updates, when the activity ceases to be in the foreground, + * the service promotes itself to a foreground service and continues receiving location updates. + * When the activity comes back to the foreground, the foreground service stops, and the + * notification associated with that foreground service is removed. + * + * While the foreground service notification is displayed, the user has the option to launch the + * activity from the notification. The user can also remove location updates directly from the + * notification. This dismisses the notification and stops the service. + */ +class MainActivity : AppCompatActivity(), SharedPreferences.OnSharedPreferenceChangeListener { + companion object { + private val TAG = MainActivity::class.java.simpleName + + // Used in checking for runtime permissions. + private const val REQUEST_PERMISSIONS_REQUEST_CODE = 34 + } + + // The BroadcastReceiver used to listen from broadcasts from the service. + private var myReceiver: MyReceiver? = null + + // A reference to the service used to get location updates. + private var mService: LocationUpdatesService? = null + + // Tracks the bound state of the service. + private var mBound = false + + // UI elements. + private lateinit var mBinding: ActivityMainBinding + + // Monitors the state of the connection to the service. + private val mServiceConnection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName, service: IBinder) { + val binder = service as LocationUpdatesService.LocalBinder + mService = binder.service + mBound = true + } + + override fun onServiceDisconnected(name: ComponentName) { + mService = null + mBound = false + } + } + + /** + * Receiver for broadcasts sent by [LocationUpdatesService]. + */ + private inner class MyReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val location = intent.getParcelableExtra(LocationUpdatesService.EXTRA_LOCATION) + if (location != null) { + Toast.makeText(this@MainActivity, Utils.getLocationText(location), + Toast.LENGTH_SHORT).show() + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // ViewBinding initialization + mBinding = ActivityMainBinding.inflate(layoutInflater) + val view = mBinding.root + setContentView(view) + myReceiver = MyReceiver() + + // Check that the user hasn't revoked permissions by going to Settings. + if (Utils.requestingLocationUpdates(this)) { + if (checkPermissions()) { + requestPermissions() + } + } + } + + override fun onStart() { + super.onStart() + PreferenceManager.getDefaultSharedPreferences(this) + .registerOnSharedPreferenceChangeListener(this) + + mBinding.requestLocationUpdatesButton.setOnClickListener { + if (checkPermissions()) { + requestPermissions() + } else { + mService?.requestLocationUpdates() + } + } + + mBinding.removeLocationUpdatesButton.setOnClickListener { + mService?.removeLocationUpdates() + } + + // Restore the state of the buttons when the activity (re)launches. + setButtonsState(Utils.requestingLocationUpdates(this)) + + // Bind to the service. If the service is in foreground mode, this signals to the service + // that since this activity is in the foreground, the service can exit foreground mode. + bindService(Intent(this, LocationUpdatesService::class.java), mServiceConnection, + Context.BIND_AUTO_CREATE) + } + + override fun onResume() { + super.onResume() + myReceiver?.let { + LocalBroadcastManager.getInstance(this) + .registerReceiver(it, IntentFilter(LocationUpdatesService.ACTION_BROADCAST)) + } + } + + override fun onPause() { + myReceiver?.let { LocalBroadcastManager.getInstance(this) + .unregisterReceiver(it) } + super.onPause() + } + + override fun onStop() { + if (mBound) { + // Unbind from the service. This signals to the service that this activity is no longer + // in the foreground, and the service can respond by promoting itself to a foreground + // service. + unbindService(mServiceConnection) + mBound = false + } + PreferenceManager.getDefaultSharedPreferences(this) + .unregisterOnSharedPreferenceChangeListener(this) + super.onStop() + } + + /** + * Returns the current state of the permissions needed. + */ + private fun checkPermissions(): Boolean { + return PackageManager.PERMISSION_GRANTED != ActivityCompat.checkSelfPermission(this, + Manifest.permission.ACCESS_FINE_LOCATION) + } + + private fun requestPermissions() { + val shouldProvideRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, + Manifest.permission.ACCESS_FINE_LOCATION) + + // Provide an additional rationale to the user. This would happen if the user denied the + // request previously, but didn't check the "Don't ask again" checkbox. + if (shouldProvideRationale) { + Log.i(TAG, "Displaying permission rationale to provide additional context.") + Snackbar.make( + mBinding.root, + R.string.permission_rationale, + Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.ok) { + // Request permission + ActivityCompat.requestPermissions(this@MainActivity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + REQUEST_PERMISSIONS_REQUEST_CODE) + } + .show() + } else { + Log.i(TAG, "Requesting permission") + // Request permission. It's possible this can be auto answered if device policy + // sets the permission in a given state or the user denied the permission + // previously and checked "Never ask again". + ActivityCompat.requestPermissions(this@MainActivity, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + REQUEST_PERMISSIONS_REQUEST_CODE) + } + } + + /** + * Callback received when a permissions request has been completed. + */ + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, + grantResults: IntArray) { + Log.i(TAG, "onRequestPermissionResult") + if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) { + when { + grantResults.isEmpty() -> { + // If user interaction was interrupted, the permission request is cancelled and you + // receive empty arrays. + Log.i(TAG, "User interaction was cancelled.") + } + grantResults[0] == PackageManager.PERMISSION_GRANTED -> { + // Permission was granted. + mService?.requestLocationUpdates() + } + else -> { + // Permission denied. + setButtonsState(false) + Snackbar.make( + mBinding.root, + R.string.permission_denied_explanation, + Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.settings) { + // Build intent that displays the App settings screen. + val intent = Intent() + intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS + val uri = Uri.fromParts("package", + BuildConfig.LIBRARY_PACKAGE_NAME, null) + intent.data = uri + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + startActivity(intent) + } + .show() + } + } + } + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, string: String) { + // Update the buttons state depending on whether location updates are being requested. + if (string == Utils.KEY_REQUESTING_LOCATION_UPDATES) { + setButtonsState(sharedPreferences.getBoolean(Utils.KEY_REQUESTING_LOCATION_UPDATES, + false)) + } + } + + private fun setButtonsState(requestingLocationUpdates: Boolean) { + if (requestingLocationUpdates) { + mBinding.requestLocationUpdatesButton.isEnabled = false + mBinding.removeLocationUpdatesButton.isEnabled = true + } else { + mBinding.requestLocationUpdatesButton.isEnabled = true + mBinding.removeLocationUpdatesButton.isEnabled = false + } + } +} \ No newline at end of file diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/Utils.kt b/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/Utils.kt new file mode 100644 index 00000000..50216589 --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/app/src/main/java/com/google/android/gms/location/sample/locationupdatesforegroundservice/Utils.kt @@ -0,0 +1,65 @@ +/** + * Copyright 2017 Google Inc. All Rights Reserved. + * + * 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. + */ +package com.google.android.gms.location.sample.locationupdatesforegroundservice + +import android.content.Context +import android.location.Location +import androidx.preference.PreferenceManager +import java.text.DateFormat +import java.util.* + +internal object Utils { + const val KEY_REQUESTING_LOCATION_UPDATES = "requesting_location_updates" + + /** + * Returns true if requesting location updates, otherwise returns false. + * + * @param context The [Context]. + */ + @JvmStatic + fun requestingLocationUpdates(context: Context?): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(KEY_REQUESTING_LOCATION_UPDATES, false) + } + + /** + * Stores the location updates state in SharedPreferences. + * @param requestingLocationUpdates The location updates state. + */ + @JvmStatic + fun setRequestingLocationUpdates(context: Context?, requestingLocationUpdates: Boolean) { + PreferenceManager.getDefaultSharedPreferences(context) + .edit() + .putBoolean(KEY_REQUESTING_LOCATION_UPDATES, requestingLocationUpdates) + .apply() + } + + /** + * Returns the `location` object as a human readable string. + * @param location The [Location]. + */ + @JvmStatic + fun getLocationText(location: Location?): String { + return if (location == null) "Unknown location" + else "(${location.latitude}, ${location.longitude})" + } + + @JvmStatic + fun getLocationTitle(context: Context): String { + return context.getString(R.string.location_updated, + DateFormat.getDateTimeInstance().format(Date())) + } +} \ No newline at end of file diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-hdpi/ic_cancel.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-hdpi/ic_cancel.png new file mode 100644 index 00000000..4ccdcad5 Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-hdpi/ic_cancel.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-hdpi/ic_launch.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-hdpi/ic_launch.png new file mode 100644 index 00000000..93ba8835 Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-hdpi/ic_launch.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-mdpi/ic_cancel.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-mdpi/ic_cancel.png new file mode 100644 index 00000000..0b17795d Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-mdpi/ic_cancel.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-mdpi/ic_launch.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-mdpi/ic_launch.png new file mode 100644 index 00000000..80821b15 Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-mdpi/ic_launch.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xhdpi/ic_cancel.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xhdpi/ic_cancel.png new file mode 100644 index 00000000..a4d0600c Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xhdpi/ic_cancel.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xhdpi/ic_launch.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xhdpi/ic_launch.png new file mode 100644 index 00000000..59a9abf5 Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xhdpi/ic_launch.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxhdpi/ic_cancel.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxhdpi/ic_cancel.png new file mode 100644 index 00000000..17c25dac Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxhdpi/ic_cancel.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxhdpi/ic_launch.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxhdpi/ic_launch.png new file mode 100644 index 00000000..268ef626 Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxhdpi/ic_launch.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxxhdpi/ic_cancel.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxxhdpi/ic_cancel.png new file mode 100644 index 00000000..65195d22 Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxxhdpi/ic_cancel.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxxhdpi/ic_launch.png b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxxhdpi/ic_launch.png new file mode 100644 index 00000000..205f22d1 Binary files /dev/null and b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/drawable-xxxhdpi/ic_launch.png differ diff --git a/LocationUpdatesForegroundServiceKotlin/app/src/main/res/layout/activity_main.xml b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..e9975e45 --- /dev/null +++ b/LocationUpdatesForegroundServiceKotlin/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,39 @@ + + + + + + +