segunda-feira, 2 de janeiro de 2012

Widgets + Service

Olá povo,

Eu já coloquei aqui no blog dois posts sobre widgets (veja aqui e aqui), mas para quem não os viu, um AppWidget é uma aplicação que roda na HomeScreen dos aparelhos Android. E um dos grandes problemas ao se desenvolver esse tipo de aplicação é que o tratamento dos eventos que são disparados por ele. Eles podem ser feitos com uma subclasse de AppWidgetProvider (que herda de BroadcastReceiver), entretanto eles não mantêm estado. Por ser um BroadcastReceiver, o seu programa só interage com ele durante a execução do método onReceive.

Alguns colegas de trabalho (que não vou citar nomes :) usam classes com atributos estáticos para manter os estado dos AppWidgets. Mas digamos que essa não é a maneira mais elegante :) Por isso, hoje vou mostrar como integrá-los com o componente Service do Android.

O componente Service permite a sua execução em segundo plano mesmo que não haja nenhuma tela da aplicação aberta. Seu ciclo de vida inicia quando é disparada uma Intent para a classe do serviço utilizando o método startService. Caso o serviço ainda não esteja sendo executado, o mesmo será iniciado e ficará ativo até que o método stopService seja chamado ou que o próprio serviço chame o método stopSelf.

Tendo em vista essa característica do serviço, vou utilizá-lo nesse exemplo para guardar o estado do widget, que ficará fazendo chamadas ao serviço para obter informações e se atualizar. Nosso exemplo será um widget que mostrará alguns links favoritos que nós poderemos passar clicando nos botões de navegação.

Vamos começar pelo código do Service, que está na classe MeuServico

public class MeuServico extends Service {

// Sites favoritos
private String[] sites = {
"nglauber.blogspot.com",
"developer.android.com",
"www.unibratec.edu.br",
"www.especializa.com.br",
"www.cesar.edu.br" };

// índice do site que deve ser mostrado no widget
private int indice;

@Override
public int onStartCommand(Intent intent, int flags,
int startId) {
// O Widget vai chamar esse método cada vez que
// clicarmos no botão
// Obtendo o botão clicado (próx. ou ant.)
String acao = intent.getStringExtra(
MeuWidget.EXTRA_ACAO);

// Se foi próximo, incrementa o índice
if (MeuWidget.ACAO_PROXIMO.equals(acao)) {
indice++;

if (indice > sites.length - 1) {
indice = 0;
}

// Se foi anterior, decrementa o índice
}else if(MeuWidget.ACAO_ANTERIOR.equals(acao)){
indice--;
if (indice < 0) {
indice = sites.length - 1;
}
}
// Se veio alguma ação, atualize o widget
if (acao != null) {
RemoteViews views = new RemoteViews(
this.getPackageName(), R.layout.main);

// Atualizando TextView pra exibir o site
views.setTextViewText(
R.id.txtSite, sites[indice]);

// ID do widget que deve ser atualizado
int appWidgetId = intent.getIntExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);

// Obtendo a instância do AppWidgetManager
// para atualizar o Widget
AppWidgetManager appWidgetManager =
AppWidgetManager.getInstance(this);

// atualiza o widget
appWidgetManager.updateAppWidget(
appWidgetId, views);
}

return super.onStartCommand(
intent, flags, startId);
}

@Override
public IBinder onBind(Intent arg0) {
// Não usado aqui
return null;
}
}

O código acima, cria um serviço que tem uma lista de links favoritos, e um índice que controlará qual link deve ser exibido. O widget então fará requisições ao serviço, que serão tratadas pelo método onStartCommand, que mostrará um link da lista baseado na posição do array.
É importante ter em mente que o widget não roda no processo da nossa aplicação, e sim da HomeScreen, então para ter acesso aos componentes gráficos usamos a classe RemoteViews e atualizamos o seu conteúdo com a classe AppWidgetManager.

Uma vez que o Service está pronto, vamos começar a criar o widget. Assim como uma Activity, ele terá um arquivo de layout. Utilizei o próprio res/layout/main.xml que é criado com o projeto. Ele ficou conforme abaixo:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal"
android:background="#000000"
android:layout_margin="20dp">
<Button
android:id="@+id/btnAnterior"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:text="<" />
<TextView
android:id="@+id/txtSite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:layout_weight="1"
android:text="Clique para navegar nos favoritos"/>
<Button
android:id="@+id/btnProximo"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:text=">" />
</LinearLayout>

Nada de especial no Layout acima, então vamos ver como fica o código do Widget.

public class MeuWidget extends AppWidgetProvider {
public static final String EXTRA_ACAO = "acao";
public static final String ACAO_ANTERIOR="anterior";
public static final String ACAO_PROXIMO = "proximo";

@Override
public void onUpdate(Context context,
AppWidgetManager appWidgetManager,
int[] appWidgetIds) {

super.onUpdate(context,
appWidgetManager, appWidgetIds);

// As view estão na aplicação Home e não na
// nossa. Por isso, para obter as referências
// dos componentes é usada a classe RemoteViews
RemoteViews views = new RemoteViews(
context.getPackageName(), R.layout.main);

// Esse método recebe a lista dos widgets que
// devem ser atualizados, pois podem haver várias
// instâncias do mesmo na Home.
// Então devemos atualizar todos.
for (int i = 0; i < appWidgetIds.length; i++){
// Configurando evento dos botões
setButtonClicks(context, appWidgetIds[i], views);
}
appWidgetManager.updateAppWidget(
appWidgetIds, views);
}

private void setButtonClicks(
Context context, int appWidgetId,
RemoteViews views) {
// Os eventos de click só podem disparar
// PendingIntents. No nosso caso,
// dispararemos PendingIntents para o nosso
// serviço. Para setar o evento, passa o id
// do componentes e a PendingIntent
views.setOnClickPendingIntent(
R.id.btnProximo,
servicePendingIntent(
context, ACAO_PROXIMO, appWidgetId));

views.setOnClickPendingIntent(
R.id.btnAnterior,
servicePendingIntent(
context, ACAO_ANTERIOR, appWidgetId));
}

private PendingIntent servicePendingIntent(
Context ctx, String acao, int appWidgetId){

// Criando a Intent para chamar o serviço
Intent serviceIntent = new Intent(
ctx, MeuServico.class);

// Passando a ação (proximo ou anterior)
serviceIntent.putExtra(EXTRA_ACAO, acao);

// Passando o ID do widget que clicou
serviceIntent.putExtra(
AppWidgetManager.EXTRA_APPWIDGET_ID,
appWidgetId);

// O requestId da PendingIntent deve ser único,
// então gero um número aleatório
int requestId = new Random().nextInt();

// criando a PendingIntent para o serviço
PendingIntent pit = PendingIntent.getService(
ctx, requestId, serviceIntent, 0);
return pit;
}
}

No código acima, o método onUpdate é chamado quando o widget é adicionado à tela. Note que a referência aos componentes é feita através da classe RemoteViews, este é outro conceito que não deve ser esquecido: o widget roda na aplicação Home, logo, essas Views lá. Outro detalhe interessante é que os componentes só podem disparar PendingIntents ao serem clicados. E no nosso caso, essas PendingIntents estão chamando o Service, mudando apenas os parâmetros que são passados (próximo e anterior).

Agora vamos criar o arquivo de configuração do widget. Na pasta res, crie a pasta xml (minúsculo) e adicione o arquivo meuwidget.xml e coloque o conteúdo abaixo:

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/main"
android:minHeight="72dp"
android:minWidth="294dp" >
</appwidget-provider>

Assim como todo BroadcastReceiver ele deve ser declarado no AndroidManifest.xml, só que ele deve tratar a ação APPWIDGET_UPDATE e deve ter uma tag meta-data referenciando o arquivo de configuração que criamos anteriormente.

<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="br.edu.cesar.aula12"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk android:minSdkVersion="10" />

<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name" >

<receiver
android:name="MeuWidget"
android:label="Meu Widget">
<!-- IntentFilter obrigatória
para todo widget -->
<intent-filter>
<action
android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
</intent-filter>
<!-- Arquivo de configuração do widget -->
<meta-data android:resource="@xml/meuwidget"
android:name="android.appwidget.provider"/>
</receiver>

<!-- Meu Serviço -->
<service android:name="MeuServico"/>
</application>
</manifest>

Mande rodar a aplicação. Nada irá aparecer, pois você não tem nenhuma Activity no projeto. Para ver se o Widget está funcionando, dê um clique-longo na Home ou clique na tecla "Menu" e em seguida, selecione "Add". No pop-up que for exibido, selecione "Widgets", e depois "Meu Widget". A mensagem inicial do widget será exibida, depois é clicar nos botões para ver o resultado.

Como vocês observaram dexei o código comentado, mas quem tiver dúvidas, deixem seus comentários.

4br4ç0s,
nglauber

2 comentários:

Emiliano Carvalho disse...

Glauber,

Acredito que seja nesse esquema com Widgets e Servicos.

Um aplicativo que vi 'e simples, apenas toca um mp3, mas no caso ele utiliza o seguinte esquema:

Ao instala-lo ele abre normalmente a tela e executa atravez da tela tambem, mas vc pode colocar apenas como atalho onde fica um botao que toca o mp3, ate com um efeito de pressionado, como num gif animado.

No caso seria como se ele tivesse 2 aplicacoes numa so. Tanto com uma intent e como atalho usando um service.

Pra podermos ter essa solucao seria sempre com widgets e service? Ou poderia apenas usar um service que apresentaria um atalho e ao clicar no botao executaria o mp3?

Abraco

Nelson Glauber disse...

Oi Emiliano,

É isso mesmo. O Service seria acessado tanto pela Activity quanto pelo Widget.

4br4ç05,
nglauber