Activez les services de localisation sans accéder à la page des paramètres


122

Contrairement à l'approche traditionnelle consistant à inviter l'utilisateur à accéder à la page des paramètres et à activer les services de localisation et à revenir, j'ai remarqué un moyen plus simple de faire de même dans certaines des dernières applications.

En se référant à la capture d'écran ci-dessous, il invite une boîte de dialogue à l'utilisateur pour activer les services de localisation en un seul clic et cela fonctionne dans ces applications.

Comment puis-je réaliser la même chose?

entrez la description de l'image ici


14
Les électeurs négatifs peuvent-ils en fournir la raison?
GAMA

4
Merci d'avoir posé cette question. Up a voté
Lokesh Pandey

1
@GAMA Voici comment fonctionne le stackoverflow! Les gens n'ont pas besoin d'une raison pour refuser de voter. Une telle communauté hostile et formidable en même temps!

Réponses:


150

Cette boîte de dialogue est créée par LocationSettingsRequest.Builder disponible dans les services Google Play.

Vous devez ajouter une dépendance à votre application build.gradle:

compile 'com.google.android.gms:play-services-location:10.0.1'

Ensuite, vous pouvez utiliser cet exemple minimal:

private void displayLocationSettingsRequest(Context context) {
    GoogleApiClient googleApiClient = new GoogleApiClient.Builder(context)
            .addApi(LocationServices.API).build();
    googleApiClient.connect();

    LocationRequest locationRequest = LocationRequest.create();
    locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
    locationRequest.setInterval(10000);
    locationRequest.setFastestInterval(10000 / 2);

    LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder().addLocationRequest(locationRequest);
    builder.setAlwaysShow(true);

    PendingResult<LocationSettingsResult> result = LocationServices.SettingsApi.checkLocationSettings(googleApiClient, builder.build());
    result.setResultCallback(new ResultCallback<LocationSettingsResult>() {
        @Override
        public void onResult(LocationSettingsResult result) {
            final Status status = result.getStatus();
            switch (status.getStatusCode()) {
                case LocationSettingsStatusCodes.SUCCESS:
                    Log.i(TAG, "All location settings are satisfied.");
                    break;
                case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                    Log.i(TAG, "Location settings are not satisfied. Show the user a dialog to upgrade location settings ");

                    try {
                        // Show the dialog by calling startResolutionForResult(), and check the result
                        // in onActivityResult().
                        status.startResolutionForResult(MainActivity.this, REQUEST_CHECK_SETTINGS);
                    } catch (IntentSender.SendIntentException e) {
                        Log.i(TAG, "PendingIntent unable to execute request.");
                    }
                    break;
                case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                    Log.i(TAG, "Location settings are inadequate, and cannot be fixed here. Dialog not created.");
                    break;
            }
        }
    });
}

Vous pouvez trouver l'exemple complet ici .


1
Je suis désolé mais je n'ai pas compris les deux premières lignes:You need to add a dependency to your app build.gradle: compile 'com.google.android.gms:play-services:8.1.0'
GAMA

Vous pouvez trouver plus d'informations sur la configuration du service Google Play ici
Mattia Maestrini

où se trouve build.gradle ?
GAMA

Dans le répertoire de votre module d'application. Habituellement, le nom du répertoire estapp
Mattia Maestrini

3
SettingsApi est désormais obsolète.
Sagar Kacha

27

Suivez les étapes mentionnées ci-dessous

1) Créez un LocationRequestselon votre souhait

LocationRequest mLocationRequest = LocationRequest.create()
           .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
           .setInterval(10 * 1000)
           .setFastestInterval(1 * 1000);

2) Créez unLocationSettingsRequest.Builder

LocationSettingsRequest.Builder settingsBuilder = new LocationSettingsRequest.Builder()
               .addLocationRequest(mLocationRequest);
settingsBuilder.setAlwaysShow(true);

3) Utilisez le LocationSettingsResponse Taskcode suivant

Task<LocationSettingsResponse> result = LocationServices.getSettingsClient(this)
              .checkLocationSettings(settingsBuilder.build());

Remarque: LocationServices.SettingsApi est obsolète donc, utilisez SettingsClientplutôt.

4) Ajoutez un OnCompleteListenerpour obtenir le résultat de la tâche. Une fois la tâche Taskterminée, le client peut vérifier les paramètres d'emplacement en regardant le code d'état de l' LocationSettingsResponseobjet.

result.addOnCompleteListener(new OnCompleteListener<LocationSettingsResponse>() {
    @Override
    public void onComplete(@NonNull Task<LocationSettingsResponse> task) {
    try {
        LocationSettingsResponse response = 
                          task.getResult(ApiException.class);
        } catch (ApiException ex) {
            switch (ex.getStatusCode()) {
                case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                    try {
                        ResolvableApiException resolvableApiException = 
                                 (ResolvableApiException) ex;
                            resolvableApiException
                                   .startResolutionForResult(MapsActivity.this, 
                                         LOCATION_SETTINGS_REQUEST);
                    } catch (IntentSender.SendIntentException e) {

                    }
                    break;
                case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:

                    break;
            }
        }
    }
});  

CAS 1:: L' LocationSettingsStatusCodes.RESOLUTION_REQUIRED emplacement n'est pas activé mais, nous pouvons demander à l'utilisateur d'activer l'emplacement en lui demandant d'activer l'emplacement avec la boîte de dialogue (en appelant startResolutionForResult).

Demande de paramètres de localisation Google Map

CAS 2:: LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE Les paramètres de localisation ne sont pas satisfaits. Cependant, nous n'avons aucun moyen de corriger les paramètres, nous n'afficherons donc pas la boîte de dialogue.

5) OnActivityResult nous pouvons obtenir l'action de l'utilisateur dans la boîte de dialogue des paramètres d'emplacement. RESULT_OK=> L'utilisateur a activé l'emplacement. RESULT_CANCELLED- L'utilisateur a refusé la demande de réglage de l'emplacement.


11

Son fonctionnement est similaire à Google Maps

Ajouter une dépendance dans le fichier build.gradle

compile 'com.google.android.gms:play-services:8.3.0'

Ceci ou cela

compile 'com.google.android.gms:play-services-location:10.0.1'

entrez la description de l'image ici

import android.content.Context;
import android.content.IntentSender;
import android.location.LocationManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;

import com.google.android.gms.common.ConnectionResult;
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.common.api.Status;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.LocationSettingsRequest;
import com.google.android.gms.location.LocationSettingsResult;
import com.google.android.gms.location.LocationSettingsStatusCodes;

import java.util.List;

public class LocationOnOff_Similar_To_Google_Maps extends AppCompatActivity {

    protected static final String TAG = "LocationOnOff";

    private GoogleApiClient googleApiClient;
    final static int REQUEST_LOCATION = 199;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.setFinishOnTouchOutside(true);

        // Todo Location Already on  ... start
        final LocationManager manager = (LocationManager) LocationOnOff_Similar_To_Google_Maps.this.getSystemService(Context.LOCATION_SERVICE);
        if (manager.isProviderEnabled(LocationManager.GPS_PROVIDER) && hasGPSDevice(LocationOnOff_Similar_To_Google_Maps.this)) {
            Toast.makeText(LocationOnOff_Similar_To_Google_Maps.this,"Gps already enabled",Toast.LENGTH_SHORT).show();
            finish();
        }
        // Todo Location Already on  ... end

        if(!hasGPSDevice(LocationOnOff_Similar_To_Google_Maps.this)){
            Toast.makeText(LocationOnOff_Similar_To_Google_Maps.this,"Gps not Supported",Toast.LENGTH_SHORT).show();
        }

        if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER) && hasGPSDevice(LocationOnOff_Similar_To_Google_Maps.this)) {
            Log.e("keshav","Gps already enabled");
            Toast.makeText(LocationOnOff_Similar_To_Google_Maps.this,"Gps not enabled",Toast.LENGTH_SHORT).show();
            enableLoc();
        }else{
            Log.e("keshav","Gps already enabled");
            Toast.makeText(LocationOnOff_Similar_To_Google_Maps.this,"Gps already enabled",Toast.LENGTH_SHORT).show();
        }
    }


    private boolean hasGPSDevice(Context context) {
        final LocationManager mgr = (LocationManager) context
                .getSystemService(Context.LOCATION_SERVICE);
        if (mgr == null)
            return false;
        final List<String> providers = mgr.getAllProviders();
        if (providers == null)
            return false;
        return providers.contains(LocationManager.GPS_PROVIDER);
    }

    private void enableLoc() {

        if (googleApiClient == null) {
            googleApiClient = new GoogleApiClient.Builder(LocationOnOff_Similar_To_Google_Maps.this)
                    .addApi(LocationServices.API)
                    .addConnectionCallbacks(new GoogleApiClient.ConnectionCallbacks() {
                        @Override
                        public void onConnected(Bundle bundle) {

                        }

                        @Override
                        public void onConnectionSuspended(int i) {
                            googleApiClient.connect();
                        }
                    })
                    .addOnConnectionFailedListener(new GoogleApiClient.OnConnectionFailedListener() {
                        @Override
                        public void onConnectionFailed(ConnectionResult connectionResult) {

                            Log.d("Location error","Location error " + connectionResult.getErrorCode());
                        }
                    }).build();
            googleApiClient.connect();

            LocationRequest locationRequest = LocationRequest.create();
            locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
            locationRequest.setInterval(30 * 1000);
            locationRequest.setFastestInterval(5 * 1000);
            LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder()
                    .addLocationRequest(locationRequest);

            builder.setAlwaysShow(true);

            PendingResult<LocationSettingsResult> result =
                    LocationServices.SettingsApi.checkLocationSettings(googleApiClient, builder.build());
            result.setResultCallback(new ResultCallback<LocationSettingsResult>() {
                @Override
                public void onResult(LocationSettingsResult result) {
                    final Status status = result.getStatus();
                    switch (status.getStatusCode()) {
                        case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                            try {
                                // Show the dialog by calling startResolutionForResult(),
                                // and check the result in onActivityResult().
                                status.startResolutionForResult(LocationOnOff_Similar_To_Google_Maps.this, REQUEST_LOCATION);

                                finish();
                            } catch (IntentSender.SendIntentException e) {
                                // Ignore the error.
                            }
                            break;
                    }
                }
            });
        }
    }

}

2
SettingsApi est maintenant obsolète
Sagar Kacha

2
SettingsApi est désormais obsolète. Maintenant, utilisez SettingsClient: développeurs.google.com
android

9
implementation 'com.google.android.gms:play-services-location:16.0.0'

Déclaration de variable

private SettingsClient mSettingsClient;
private LocationSettingsRequest mLocationSettingsRequest;
private static final int REQUEST_CHECK_SETTINGS = 214;
private static final int REQUEST_ENABLE_GPS = 516;

Ouvrir la boîte de dialogue en utilisant le code ci-dessous

LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder();
builder.addLocationRequest(new LocationRequest().setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY));
builder.setAlwaysShow(true);
mLocationSettingsRequest = builder.build();

mSettingsClient = LocationServices.getSettingsClient(YourActivity.this);

mSettingsClient
    .checkLocationSettings(mLocationSettingsRequest)
    .addOnSuccessListener(new OnSuccessListener<LocationSettingsResponse>() {
        @Override
        public void onSuccess(LocationSettingsResponse locationSettingsResponse) {
            //Success Perform Task Here
        }
    })
    .addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception e) {
            int statusCode = ((ApiException) e).getStatusCode();
            switch (statusCode) {
                case LocationSettingsStatusCodes.RESOLUTION_REQUIRED:
                    try {
                        ResolvableApiException rae = (ResolvableApiException) e;
                        rae.startResolutionForResult(YourActivity.this, REQUEST_CHECK_SETTINGS);
                    } catch (IntentSender.SendIntentException sie) {
                        Log.e("GPS","Unable to execute request.");
                    }
                    break;
                case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE:
                    Log.e("GPS","Location settings are inadequate, and cannot be fixed here. Fix in Settings.");
            }
        }
    })
    .addOnCanceledListener(new OnCanceledListener() {
        @Override
        public void onCanceled() {
            Log.e("GPS","checkLocationSettings -> onCanceled");
        }
    });

onActivityResult

@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    if (requestCode == REQUEST_CHECK_SETTINGS) {
        switch (resultCode) {
            case Activity.RESULT_OK:
                //Success Perform Task Here
                break;
            case Activity.RESULT_CANCELED:
                Log.e("GPS","User denied to access location");
                openGpsEnableSetting();
                break;
        }
    } else if (requestCode == REQUEST_ENABLE_GPS) {
        LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
        boolean isGpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);

        if (!isGpsEnabled) {
            openGpsEnableSetting();
        } else {
            navigateToUser();
        }
    }
}

private void openGpsEnableSetting() {
    Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
    startActivityForResult(intent, REQUEST_ENABLE_GPS);
}   

Merci! Si vous utilisez ce code dans un fragment, consultez stackoverflow.com/a/39579124/2914140 : au lieu d' rae.startResolutionForResult(activity, REQUEST_CHECK_SETTINGS)appeler startIntentSenderForResult(rae.getResolution().getIntentSender(), REQUEST_CHECK_SETTINGS, null, 0, 0, 0, null), sinon onActivityResult()ne sera pas appelé.
CoolMind

@CoolMind Merci peut être utilisé pour quelqu'un qui en a besoin pour fragment
Ketan Ramani

@CoolMind, est-il possible d'utiliser quelque chose de similaire à startIntentSenderForResultdans la méthode openGpsEnableSetting()?
Aliton Oliveira

@AlitonOliveira, pourriez-vous décrire en détails? Dans le code openGpsEnableSetting()démarre simplement une boîte de dialogue pour activer les paramètres GPS. Une fois terminé, onActivityResult()est appelé avec requestCode == REQUEST_ENABLE_GPS.
CoolMind

onActivityResult()est appelé depuis Activity et je me demandais s'il est possible de renvoyer le résultat à un résultat Fragmentsimilaire startIntentSenderForResult.
Aliton Oliveira

8

Merci à Mattia Maestrini +1

Solution Xamarin:

using Android.Gms.Common.Apis;
using Android.Gms.Location;

public const int REQUEST_CHECK_SETTINGS = 0x1;

private void DisplayLocationSettingsRequest()
{
    var googleApiClient = new GoogleApiClient.Builder(this).AddApi(LocationServices.API).Build();
    googleApiClient.Connect();

    var locationRequest = LocationRequest.Create();
    locationRequest.SetPriority(LocationRequest.PriorityHighAccuracy);
    locationRequest.SetInterval(10000);
    locationRequest.SetFastestInterval(10000 / 2);

    var builder = new LocationSettingsRequest.Builder().AddLocationRequest(locationRequest);
    builder.SetAlwaysShow(true);

    var result = LocationServices.SettingsApi.CheckLocationSettings(googleApiClient, builder.Build());
    result.SetResultCallback((LocationSettingsResult callback) =>
    {
        switch (callback.Status.StatusCode)
        {
            case LocationSettingsStatusCodes.Success:
                {
                    DoStuffWithLocation();
                    break;
                }
            case LocationSettingsStatusCodes.ResolutionRequired:
                {
                    try
                    {
                        // Show the dialog by calling startResolutionForResult(), and check the result
                        // in onActivityResult().
                        callback.Status.StartResolutionForResult(this, REQUEST_CHECK_SETTINGS);
                    }
                    catch (IntentSender.SendIntentException e)
                    {
                    }

                    break;
                }
            default:
                {
                    // If all else fails, take the user to the android location settings
                    StartActivity(new Intent(Android.Provider.Settings.ActionLocationSourceSettings));
                    break;
                }
        }
    });
}

protected override void OnActivityResult(int requestCode, Android.App.Result resultCode, Intent data)
{
    switch (requestCode)
    {
        case REQUEST_CHECK_SETTINGS:
            {
                switch (resultCode)
                {
                    case Android.App.Result.Ok:
                        {
                            DoStuffWithLocation();
                            break;
                        }
                    case Android.App.Result.Canceled:
                        {
                            //No location
                            break;
                        }
                }
                break;
            }
    }
}

REMARQUE:

Cela ne fonctionnera pas avec Huawei ou d'autres appareils sur lesquels les services Google ne sont pas installés.


Ça ne fonctionne pas!! Pouvez-vous s'il vous plaît partager le code complet
Omkar

J'essaie d'appeler la méthode DisplayLocationSettingsRequest () à partir de la méthode OnCreate de l'activité Android. Mais malheureusement, je ne suis pas en mesure d'afficher la fenêtre contextuelle de demande de localisation qui active la localisation. Pouvez vous me donner un coup de main.
Omkar

@Omkar avez-vous installé Xamarin.GooglePlayServices.Location via Nuget? Avez-vous inclus les deux lignes ci using android.Gms.Common.Apis; using Android.Gms.Location;- dessus ? Après avoir appelé LocationServices.SettingsApi.CheckLocationSettings, recevez-vous un rappel à l'intérieur result.SetResultCallback(? Placez un point d'arrêt sur chacun d'eux et vérifiez ce que fait le code
Pierre

Oui j'ai ajouté tous les prérequis. Et j'ai reçu le résultat comme Id = 1, Status = WaitingForActivation, Method = (null). Mais ce temps d'attente est infini car attendu depuis longtemps, et n'a reçu aucun résultat.
Omkar

4

Android Marshmallow 6 prend en charge les autorisations d'exécution. Les autorisations d'exécution ne fonctionnent que sur Marshmallow et sur pré-Marshmallow, cela fonctionne toujours à l'ancienne.

Vous pouvez en savoir plus à ce sujet dans cette vidéo officielle du développeur Android:

https://www.youtube.com/watch?v=C8lUdPVSzDk

Et demander la permission: http://developer.android.com/training/permissions/requesting.html


L'API Marshmallow est-elle disponible à des fins de développement?
GAMA

Cela signifie-t-il que le flux de travail montré par capture d'écran ne peut être réalisé que Marshmallow et plus tard?
GAMA

Oui, comme indiqué dans la documentation, il s'agit du niveau d'API 23, il ne fonctionnera donc que sur Marshmallow et plus récent.
Sharj

n'importe quel moyen d'obtenir le même comportement dans ma propre boîte de dialogue personnalisée
Srishti Roy

4

Merci Mattia Maestrini pour la réponse, je voudrais ajouter qu'en utilisant

compile 'com.google.android.gms:play-services-location:8.1.0'

suffirait. Cela empêche votre application d'inclure des bibliothèques inutiles et aide à garder votre nombre de méthodes bas.


2

Solution Kotlin

Ajouter build.gradle(Module:app)

implementation 'com.google.android.gms:play-services-location:17.0.0'
implementation 'com.google.android.gms:play-services-maps:17.0.0'

après cela, créez cette fonction

fun enablegps() {

    val mLocationRequest = LocationRequest.create()
        .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
        .setInterval(2000)
        .setFastestInterval(1000)

    val settingsBuilder = LocationSettingsRequest.Builder()
        .addLocationRequest(mLocationRequest)
    settingsBuilder.setAlwaysShow(true)

    val result = LocationServices.getSettingsClient(this).checkLocationSettings(settingsBuilder.build())
    result.addOnCompleteListener { task ->

        //getting the status code from exception
        try {
            task.getResult(ApiException::class.java)
        } catch (ex: ApiException) {

            when (ex.statusCode) {

                LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> try {

                    Toast.makeText(this,"GPS IS OFF",Toast.LENGTH_SHORT).show()

                    // Show the dialog by calling startResolutionForResult(), and check the result
                    // in onActivityResult().
                    val resolvableApiException = ex as ResolvableApiException
                    resolvableApiException.startResolutionForResult(this,REQUEST_CHECK_SETTINGS
                    )
                } catch (e: IntentSender.SendIntentException) {
                    Toast.makeText(this,"PendingIntent unable to execute request.",Toast.LENGTH_SHORT).show()

                }

                LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> {

                    Toast.makeText(
                        this,
                        "Something is wrong in your GPS",
                        Toast.LENGTH_SHORT
                    ).show()

                }


            }
        }



    }


}

1

Avec la mise à jour récente de Marshmallow, même lorsque le paramètre de localisation est activé, votre application devra demander explicitement l'autorisation. La méthode recommandée pour ce faire est d'afficher la section Autorisations de votre application dans laquelle l'utilisateur peut basculer l'autorisation selon les besoins. L'extrait de code pour cela est comme ci-dessous:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {if (this.checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {final AlertDialog.Builder builder = new AlertDialog.Builder(this);
        builder.setTitle("Location Permission");
        builder.setMessage("The app needs location permissions. Please grant this permission to continue using the features of the app.");
        builder.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialogInterface, int i) {
                requestPermissions(new String[]{Manifest.permission.ACCESS_COARSE_LOCATION}, PERMISSION_REQUEST_COARSE_LOCATION);}
        });
        builder.setNegativeButton(android.R.string.no, null);
        builder.show();
    }
} else {
    // do programatically as show in the other answer 
}

Et remplacez la onRequestPermissionsResultméthode comme ci-dessous:

@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        switch (requestCode) {
            case PERMISSION_REQUEST_COARSE_LOCATION: {
                if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    Log.d(TAG, "coarse location permission granted");
                } else {
                    Intent intent = new Intent();
                    intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                    Uri uri = Uri.fromParts("package", getPackageName(), null);
                    intent.setData(uri);
                    startActivity(intent);
                }
            }
        }
    }

Une autre approche est que vous pouvez également utiliser SettingsApi pour savoir quels fournisseurs de localisation sont activés. Si aucun n'est activé, vous pouvez inviter une boîte de dialogue pour modifier le paramètre à partir de l'application.


1

Le moyen le plus simple que j'ai trouvé au cours de mes recherches est de créer une classe Util pour ce processus de demande de localisation, puis de l'appeler pour activer le GPS pour nous.

Veuillez consulter ce blog! Il a raconté toute l'histoire.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.