Exemplo: Comunicação entre Atividade e Serviço usando o Sistema de Mensagens


584

Não consegui encontrar exemplos de como enviar mensagens entre uma atividade e um serviço e passei muitas horas tentando descobrir isso. Aqui está um exemplo de projeto para outras pessoas fazerem referência.

Este exemplo permite iniciar ou parar um serviço diretamente e vincular / desvincular separadamente do serviço. Quando o serviço está em execução, ele incrementa um número em 10 Hz. Se a atividade estiver vinculada a Service, exibirá o valor atual. Os dados são transferidos como um número inteiro e como uma string, para que você possa ver como fazer isso de duas maneiras diferentes. Também existem botões na atividade para enviar mensagens ao serviço (altera o valor incrementado por).

Captura de tela:

Captura de tela do exemplo de mensagens de serviço do Android

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.exampleservice"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name=".MainActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    <service android:name=".MyService"></service>
    </application>
    <uses-sdk android:minSdkVersion="8" />
</manifest>

res \ values ​​\ strings.xml:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">ExampleService</string>
    <string name="service_started">Example Service started</string>
    <string name="service_label">Example Service Label</string>
</resources>

res \ layout \ main.xml:

<RelativeLayout
    android:id="@+id/RelativeLayout01"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

    <Button
        android:id="@+id/btnStart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Start Service" >
    </Button>

    <Button
        android:id="@+id/btnStop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="Stop Service" >
    </Button>
</RelativeLayout>

<RelativeLayout
    android:id="@+id/RelativeLayout02"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

    <Button
        android:id="@+id/btnBind"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Bind to Service" >
    </Button>

    <Button
        android:id="@+id/btnUnbind"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="Unbind from Service" >
    </Button>
</RelativeLayout>

<TextView
    android:id="@+id/textStatus"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="Status Goes Here"
    android:textSize="24sp" />

<TextView
    android:id="@+id/textIntValue"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="Integer Value Goes Here"
    android:textSize="24sp" />

<TextView
    android:id="@+id/textStrValue"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:text="String Value Goes Here"
    android:textSize="24sp" />

<RelativeLayout
    android:id="@+id/RelativeLayout03"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content" >

    <Button
        android:id="@+id/btnUpby1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Increment by 1" >
    </Button>

    <Button
        android:id="@+id/btnUpby10"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentRight="true"
        android:text="Increment by 10" >
    </Button>
</RelativeLayout>

src \ com.exampleservice \ MainActivity.java:

package com.exampleservice;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends Activity {
    Button btnStart, btnStop, btnBind, btnUnbind, btnUpby1, btnUpby10;
    TextView textStatus, textIntValue, textStrValue;
    Messenger mService = null;
    boolean mIsBound;
    final Messenger mMessenger = new Messenger(new IncomingHandler());

    class IncomingHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MyService.MSG_SET_INT_VALUE:
                textIntValue.setText("Int Message: " + msg.arg1);
                break;
            case MyService.MSG_SET_STRING_VALUE:
                String str1 = msg.getData().getString("str1");
                textStrValue.setText("Str Message: " + str1);
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            mService = new Messenger(service);
            textStatus.setText("Attached.");
            try {
                Message msg = Message.obtain(null, MyService.MSG_REGISTER_CLIENT);
                msg.replyTo = mMessenger;
                mService.send(msg);
            }
            catch (RemoteException e) {
                // In this case the service has crashed before we could even do anything with it
            }
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been unexpectedly disconnected - process crashed.
            mService = null;
            textStatus.setText("Disconnected.");
        }
    };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        btnStart = (Button)findViewById(R.id.btnStart);
        btnStop = (Button)findViewById(R.id.btnStop);
        btnBind = (Button)findViewById(R.id.btnBind);
        btnUnbind = (Button)findViewById(R.id.btnUnbind);
        textStatus = (TextView)findViewById(R.id.textStatus);
        textIntValue = (TextView)findViewById(R.id.textIntValue);
        textStrValue = (TextView)findViewById(R.id.textStrValue);
        btnUpby1 = (Button)findViewById(R.id.btnUpby1);
        btnUpby10 = (Button)findViewById(R.id.btnUpby10);

        btnStart.setOnClickListener(btnStartListener);
        btnStop.setOnClickListener(btnStopListener);
        btnBind.setOnClickListener(btnBindListener);
        btnUnbind.setOnClickListener(btnUnbindListener);
        btnUpby1.setOnClickListener(btnUpby1Listener);
        btnUpby10.setOnClickListener(btnUpby10Listener);

        restoreMe(savedInstanceState);

        CheckIfServiceIsRunning();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("textStatus", textStatus.getText().toString());
        outState.putString("textIntValue", textIntValue.getText().toString());
        outState.putString("textStrValue", textStrValue.getText().toString());
    }
    private void restoreMe(Bundle state) {
        if (state!=null) {
            textStatus.setText(state.getString("textStatus"));
            textIntValue.setText(state.getString("textIntValue"));
            textStrValue.setText(state.getString("textStrValue"));
        }
    }
    private void CheckIfServiceIsRunning() {
        //If the service is running when the activity starts, we want to automatically bind to it.
        if (MyService.isRunning()) {
            doBindService();
        }
    }

    private OnClickListener btnStartListener = new OnClickListener() {
        public void onClick(View v){
            startService(new Intent(MainActivity.this, MyService.class));
        }
    };
    private OnClickListener btnStopListener = new OnClickListener() {
        public void onClick(View v){
            doUnbindService();
            stopService(new Intent(MainActivity.this, MyService.class));
        }
    };
    private OnClickListener btnBindListener = new OnClickListener() {
        public void onClick(View v){
            doBindService();
        }
    };
    private OnClickListener btnUnbindListener = new OnClickListener() {
        public void onClick(View v){
            doUnbindService();
        }
    };
    private OnClickListener btnUpby1Listener = new OnClickListener() {
        public void onClick(View v){
            sendMessageToService(1);
        }
    };
    private OnClickListener btnUpby10Listener = new OnClickListener() {
        public void onClick(View v){
            sendMessageToService(10);
        }
    };
    private void sendMessageToService(int intvaluetosend) {
        if (mIsBound) {
            if (mService != null) {
                try {
                    Message msg = Message.obtain(null, MyService.MSG_SET_INT_VALUE, intvaluetosend, 0);
                    msg.replyTo = mMessenger;
                    mService.send(msg);
                }
                catch (RemoteException e) {
                }
            }
        }
    }


    void doBindService() {
        bindService(new Intent(this, MyService.class), mConnection, Context.BIND_AUTO_CREATE);
        mIsBound = true;
        textStatus.setText("Binding.");
    }
    void doUnbindService() {
        if (mIsBound) {
            // If we have received the service, and hence registered with it, then now is the time to unregister.
            if (mService != null) {
                try {
                    Message msg = Message.obtain(null, MyService.MSG_UNREGISTER_CLIENT);
                    msg.replyTo = mMessenger;
                    mService.send(msg);
                }
                catch (RemoteException e) {
                    // There is nothing special we need to do if the service has crashed.
                }
            }
            // Detach our existing connection.
            unbindService(mConnection);
            mIsBound = false;
            textStatus.setText("Unbinding.");
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        try {
            doUnbindService();
        }
        catch (Throwable t) {
            Log.e("MainActivity", "Failed to unbind from the service", t);
        }
    }
}

src \ com.exampleservice \ MyService.java:

package com.exampleservice;

import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;

public class MyService extends Service {
    private NotificationManager nm;
    private Timer timer = new Timer();
    private int counter = 0, incrementby = 1;
    private static boolean isRunning = false;

    ArrayList<Messenger> mClients = new ArrayList<Messenger>(); // Keeps track of all current registered clients.
    int mValue = 0; // Holds last value set by a client.
    static final int MSG_REGISTER_CLIENT = 1;
    static final int MSG_UNREGISTER_CLIENT = 2;
    static final int MSG_SET_INT_VALUE = 3;
    static final int MSG_SET_STRING_VALUE = 4;
    final Messenger mMessenger = new Messenger(new IncomingHandler()); // Target we publish for clients to send messages to IncomingHandler.


    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }
    class IncomingHandler extends Handler { // Handler of incoming messages from clients.
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MSG_REGISTER_CLIENT:
                mClients.add(msg.replyTo);
                break;
            case MSG_UNREGISTER_CLIENT:
                mClients.remove(msg.replyTo);
                break;
            case MSG_SET_INT_VALUE:
                incrementby = msg.arg1;
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }
    private void sendMessageToUI(int intvaluetosend) {
        for (int i=mClients.size()-1; i>=0; i--) {
            try {
                // Send data as an Integer
                mClients.get(i).send(Message.obtain(null, MSG_SET_INT_VALUE, intvaluetosend, 0));

                //Send data as a String
                Bundle b = new Bundle();
                b.putString("str1", "ab" + intvaluetosend + "cd");
                Message msg = Message.obtain(null, MSG_SET_STRING_VALUE);
                msg.setData(b);
                mClients.get(i).send(msg);

            }
            catch (RemoteException e) {
                // The client is dead. Remove it from the list; we are going through the list from back to front so this is safe to do inside the loop.
                mClients.remove(i);
            }
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("MyService", "Service Started.");
        showNotification();
        timer.scheduleAtFixedRate(new TimerTask(){ public void run() {onTimerTick();}}, 0, 100L);
        isRunning = true;
    }
    private void showNotification() {
        nm = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
        // In this sample, we'll use the same text for the ticker and the expanded notification
        CharSequence text = getText(R.string.service_started);
        // Set the icon, scrolling text and timestamp
        Notification notification = new Notification(R.drawable.icon, text, System.currentTimeMillis());
        // The PendingIntent to launch our activity if the user selects this notification
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0);
        // Set the info for the views that show in the notification panel.
        notification.setLatestEventInfo(this, getText(R.string.service_label), text, contentIntent);
        // Send the notification.
        // We use a layout id because it is a unique number.  We use it later to cancel.
        nm.notify(R.string.service_started, notification);
    }
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i("MyService", "Received start id " + startId + ": " + intent);
        return START_STICKY; // run until explicitly stopped.
    }

    public static boolean isRunning()
    {
        return isRunning;
    }


    private void onTimerTick() {
        Log.i("TimerTick", "Timer doing work." + counter);
        try {
            counter += incrementby;
            sendMessageToUI(counter);

        }
        catch (Throwable t) { //you should always ultimately catch all exceptions in timer tasks.
            Log.e("TimerTick", "Timer Tick Failed.", t);
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (timer != null) {timer.cancel();}
        counter=0;
        nm.cancel(R.string.service_started); // Cancel the persistent notification.
        Log.i("MyService", "Service Stopped.");
        isRunning = false;
    }
}

53
Ótimo exemplo! Outro recurso interessante: se você android:process=:myservicenameatribuir um atributo à servicetag do seu serviço no manifest.xml, como <service android:name="sname" android:process=":myservicename" />:, ele executará o serviço como um processo diferente - portanto, em um thread diferente. Isso significa que qualquer cálculo pesado feito / solicitação longa do serviço não interromperá o encadeamento da interface do usuário.
sydd

28
Sei que você se esforçou para fazer isso, mas faria mais sentido publicá-lo no github ou em um site semelhante de compartilhamento de código-fonte e postar o link aqui. É mais fácil para as pessoas colocá-lo em funcionamento dessa maneira.
Ehtesh Choudhury

25
Bom exemplo. I colocar esse código em um repo on-line (com pequenas modificações) para aqueles que gostariam de clone: bitbucket.org/alexfu/androidserviceexample/src
Alex Fu

7
As mensagens são realmente necessárias apenas se o seu serviço puder ser chamado por outros aplicativos. Caso contrário, você pode ficar com um fichário que retorna uma referência ao Serviço e apenas chama métodos públicos dele.
Tipo-a1pha

13
Você deveria ter feito a pergunta e, em seguida, criar uma resposta, não para responder o problema na pergunta. Porém, ótimo exemplo;)
7hi4g0

Respostas:


46

Veja o exemplo LocalService .

Você Serviceretorna uma instância para os consumidores que ligam onBind. Em seguida, você pode interagir diretamente com o serviço, por exemplo, registrando sua própria interface de ouvinte no serviço, para obter retornos de chamada.


2
O único problema disso é que ele não usaria um Messenger, portanto não responderia a essa pseudo-pergunta. Eu usei um LocalService, mas fiquei feliz em encontrar um exemplo de Messenger / Handler. Não acredito que um LocalService possa ser colocado em outro processo.
Ben

@ Christoper-Orr: Estou muito agradecido por você ter postado o link do Android BroadcastReceivertutorial. Eu usei um LocalBroadcastManagerpara trocar dados continuamente entre duas Activityinstâncias.
Dirk

O problema LocalBroadcastManageré que não é bloqueador e você precisa esperar pelos resultados. Às vezes você deseja resultados imediatos.
TheRealChx101

Você pode por favor me ajudar com esta pergunta stackoverflow.com/questions/51508046/…
Rajesh K

20

Para enviar dados para um serviço, você pode usar:

Intent intent = new Intent(getApplicationContext(), YourService.class);
intent.putExtra("SomeData","ItValue");
startService(intent);

E depois em serviço em onStartCommand () obtenha dados da intenção.

Para enviar dados ou evento de um serviço para um aplicativo (para uma ou mais atividades):

private void sendBroadcastMessage(String intentFilterName, int arg1, String extraKey) {
    Intent intent = new Intent(intentFilterName);
    if (arg1 != -1 && extraKey != null) {
        intent.putExtra(extraKey, arg1);
    }
    sendBroadcast(intent);
}

Este método está chamando do seu serviço. Você pode simplesmente enviar dados para sua Atividade.

private void someTaskInYourService(){

    //For example you downloading from server 1000 files
    for(int i = 0; i < 1000; i++) {
        Thread.sleep(5000) // 5 seconds. Catch in try-catch block
        sendBroadCastMessage(Events.UPDATE_DOWNLOADING_PROGRESSBAR, i,0,"up_download_progress");
    }

Para receber um evento com dados, crie e registre o método registerBroadcastReceivers () em sua atividade:

private void registerBroadcastReceivers(){
    broadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            int arg1 = intent.getIntExtra("up_download_progress",0);
            progressBar.setProgress(arg1);
        }
    };
    IntentFilter progressfilter = new IntentFilter(Events.UPDATE_DOWNLOADING_PROGRESS);
    registerReceiver(broadcastReceiver,progressfilter);

Para enviar mais dados, você pode modificar o método sendBroadcastMessage();. Lembre-se: você deve registrar transmissões nos métodos onResume () e cancelar o registro nos métodos onStop ()!

ATUALIZAR

Por favor, não use meu tipo de comunicação entre Atividade e Serviço. Este é o caminho errado. Para uma melhor experiência, use bibliotecas especiais, como:

1) EventBus da greenrobot

2) Otto da Square Inc

PS: estou usando apenas o EventBus da greenrobot em meus projetos,


2
onde devo registrar o receptor no onResume e no onStop que preciso fazer em atividade?
user3233280

Sim. Para receber um evento do serviço, você deve registrar a transmissão no onResume. Lembre-se de que você deve cancelar o registro da transmissão no onStop. Agora, eu não recomendo usar meu método. Utilize libs especiais para se comunicar com outros views / atividades / serviços, tais como EventBus github.com/greenrobot/EventBus ou Otto github.com/square/otto
a.black13

1
pode u pls me ajudar como eu posso usar isso eu preso no meu projeto em comunicação serviço
user3233280

8
O Google recomendou isso ou você está dizendo que está "errado" porque acha que as outras soluções são melhores?
21415 Kevin Krumwiede

mais um para fornecer links para EventBuse Otto.
Mohammed Ali

14

Nota: Você não precisa verificar se o serviço está em execução CheckIfServiceIsRunning(), porque bindService()o iniciará se não estiver em execução.

Além disso: se você girar o telefone, não o desejará bindService()novamente, porque onCreate()será chamado novamente. Certifique-se de definir onConfigurationChanged()para evitar isso.


No meu caso, não preciso do serviço em execução o tempo todo. Se o serviço já estiver em execução quando a atividade for iniciada, quero vincular a ele. Se o serviço não estiver em execução quando a atividade for iniciada, quero deixar o serviço parado.
Lance Lefebure

1
Não sei se isso é verdade, o bindService não inicia o serviço, você pode apontar para a documentação?
Calin

1
developer.android.com/reference/android/app/Service.html O primeiro parágrafo declaraServices can be started with Context.startService() and Context.bindService()
Someone Somewhere

8
Message msg = Message.obtain(null, 2, 0, 0);
                    Bundle bundle = new Bundle();
                    bundle.putString("url", url);
                    bundle.putString("names", names);
                    bundle.putString("captions",captions); 
                    msg.setData(bundle);

Então você envia para o serviço. Depois receba.


8

Está tudo bem. Bom exemplo de activity/servicecomunicação usando o Messenger .

Um comentário: o método MyService.isRunning()não é necessário. bindService()Pode ser feito inúmeras vezes. nenhum dano nisso.

Se o MyService estiver sendo executado em um processo diferente, a função estática MyService.isRunning()sempre retornará false. Portanto, não há necessidade dessa função.


2

Foi assim que implementei a Atividade-> Comunicação de Serviço: na minha Atividade eu tive

private static class MyResultReciever extends ResultReceiver {
     /**
     * Create a new ResultReceive to receive results.  Your
     * {@link #onReceiveResult} method will be called from the thread running
     * <var>handler</var> if given, or from an arbitrary thread if null.
     *
     * @param handler
     */
     public MyResultReciever(Handler handler) {
         super(handler);
     }

     @Override
     protected void onReceiveResult(int resultCode, Bundle resultData) {
         if (resultCode == 100) {
             //dostuff
         }
     }

E então eu usei isso para iniciar meu serviço

protected void onCreate(Bundle savedInstanceState) {
MyResultReciever resultReciever = new MyResultReciever(handler);
        service = new Intent(this, MyService.class);
        service.putExtra("receiver", resultReciever);
        startService(service);
}

No meu serviço eu tinha

public int onStartCommand(Intent intent, int flags, int startId) {
    if (intent != null)
        resultReceiver = intent.getParcelableExtra("receiver");
    return Service.START_STICKY;
}

Espero que isto ajude


0

Parece-me que você poderia ter economizado um pouco de memória declarando sua atividade com "implementa Handler.Callback"


0

Ótimo tutorial, apresentação fantástica. Puro, simples, curto e muito explicativo. Embora, o notification.setLatestEventInfo(this, getText(R.string.service_label), text, contentIntent);método não existe mais. Como o trante afirmou aqui , uma boa abordagem seria:

private static final int NOTIFICATION_ID = 45349;

private void showNotification() {
    NotificationCompat.Builder builder =
            new NotificationCompat.Builder(this)
                    .setSmallIcon(R.mipmap.ic_launcher)
                    .setContentTitle("My Notification Title")
                    .setContentText("Something interesting happened");

    Intent targetIntent = new Intent(this, MainActivity.class);
    PendingIntent contentIntent = PendingIntent.getActivity(this, 0, targetIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    builder.setContentIntent(contentIntent);
    _nManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    _nManager.notify(NOTIFICATION_ID, builder.build());
}

@Override
public void onDestroy() {
    super.onDestroy();
    if (_timer != null) {_timer.cancel();}
    _counter=0;
    _nManager.cancel(NOTIFICATION_ID); // Cancel the persistent notification.
    Log.i("PlaybackService", "Service Stopped.");
    _isRunning = false;
}

Eu me verifiquei, tudo funciona como um encanto (os nomes de atividades e serviços podem ser diferentes dos originais).


0

Eu já vi todas as respostas. Quero dizer a maneira mais robusta hoje em dia . Isso fará você se comunicar entre Activity - Service - Dialog - Fragments(Tudo).

EventBus

Essa lib que estou usando em meus projetos tem ótimos recursos relacionados a mensagens.

EventBus em 3 etapas

  1. Defina eventos:

    public static class MessageEvent { /* Additional fields if needed */ }

  2. Prepare assinantes:

Declare e anote seu método de inscrição, especifique opcionalmente um modo de encadeamento :

@Subscribe(threadMode = ThreadMode.MAIN) 
public void onMessageEvent(MessageEvent event) {/* Do something */};

Registre e cancele o registro de seu assinante. Por exemplo, no Android, atividades e fragmentos geralmente devem ser registrados de acordo com seu ciclo de vida:

@Override
public void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
}

@Override
public void onStop() {
    super.onStop();
    EventBus.getDefault().unregister(this);
}
  1. Publicar eventos:

    EventBus.getDefault().post(new MessageEvent());

Basta adicionar essa dependência na nota no nível do aplicativo

compile 'org.greenrobot:eventbus:3.1.1'
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.