Android: como posso obter a atividade atual em primeiro plano (de um serviço)?

180

Existe uma maneira nativa do Android de obter uma referência à atividade atualmente em execução de um serviço?

Eu tenho um serviço em execução em segundo plano e gostaria de atualizar minha Atividade atual quando ocorrer um evento (no serviço). Existe uma maneira fácil de fazer isso (como o que sugeri acima)?

George
fonte
talvez isso possa lhe dar uma idéia stackoverflow.com/a/28423385/185022
AZ_

Respostas:

87

Existe uma maneira nativa do Android de obter uma referência à atividade atualmente em execução de um serviço?

Você não pode ser o proprietário da "Atividade em execução no momento".

Eu tenho um serviço em execução em segundo plano e gostaria de atualizar minha Atividade atual quando ocorrer um evento (no serviço). Existe uma maneira fácil de fazer isso (como o que sugeri acima)?

  1. Envie uma transmissão Intentpara a atividade - aqui está um projeto de amostra demonstrando esse padrão
  2. Faça com que a atividade forneça um PendingIntent(por exemplo, via createPendingResult()) que o serviço chama
  3. Faça com que a atividade registre um objeto de retorno de chamada ou ouvinte no serviço via bindService()e faça com que o serviço chame um método de evento nesse objeto de retorno de chamada / ouvinte
  4. Envie uma transmissão ordenada Intentpara a atividade, com uma prioridade baixa BroadcastReceivercomo backup (para aumentar Notificationse a atividade não estiver na tela) - aqui está uma postagem de blog com mais informações sobre esse padrão
CommonsWare
fonte
3
Obrigado, mas como tenho uma série de atividades e não quero atualizá-las, estava procurando uma maneira do Android de obter a atividade em primeiro plano. Como você diz, eu não deveria ser capaz de fazer isso. Significa que tenho que contornar isso.
George
1
@ George: O que você quer não ajuda em nada. Como você aponta, você tem "uma série de atividades". Portanto, essas são todas classes separadas. Portanto, não há nada que seu serviço possa fazer com eles sem um enorme bloco if de instanceofverificações ou refatoração das atividades para compartilhar uma superclasse ou interface comum. E se você for refatorar as atividades, é melhor fazê-lo de uma maneira que melhor se adapte à estrutura e cubra mais cenários, como nenhuma de suas atividades sendo ativa. # 4 é provavelmente o menos trabalhoso e mais flexível.
CommonsWare
2
Obrigado, mas tenho uma solução melhor. Todas as minhas atividades estendem uma classe BaseActivity personalizada. Eu configurei um ContextRegister que registra a atividade como atual sempre que está em primeiro plano, com literalmente 3 linhas na minha classe BaseActivity. Ainda assim, obrigado pelo apoio.
George
3
@ George: Cuidado com vazamentos de memória. Membros de dados estáticos mutáveis ​​devem ser evitados em Java sempre que possível.
CommonsWare
Encapsulei firmemente o objeto, apesar de manter isso em mente. Obrigado.
George
139

Atualização : isso não funciona mais com as atividades de outros aplicativos a partir do Android 5.0


Aqui está uma boa maneira de fazer isso usando o gerenciador de atividades. Você basicamente recebe as tarefas em execução do gerenciador de atividades. Ele sempre retornará a tarefa atualmente ativa primeiro. De lá, você pode obter a topActivity.

Exemplo aqui

Existe uma maneira fácil de obter uma lista de tarefas em execução no serviço ActivityManager. Você pode solicitar um número máximo de tarefas em execução no telefone e, por padrão, a tarefa atualmente ativa é retornada primeiro.

Depois de conseguir, você pode obter um objeto ComponentName solicitando o topActivity da sua lista.

Aqui está um exemplo.

    ActivityManager am = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    List<ActivityManager.RunningTaskInfo> taskInfo = am.getRunningTasks(1);
    Log.d("topActivity", "CURRENT Activity ::" + taskInfo.get(0).topActivity.getClassName());
    ComponentName componentInfo = taskInfo.get(0).topActivity;
    componentInfo.getPackageName();

Você precisará da seguinte permissão no seu manifesto:

<uses-permission android:name="android.permission.GET_TASKS"/>
Nelson Ramirez
fonte
3
A sua resposta é 100% correct.Thanxx 4 it.Its melhor que u postar a mesma resposta aqui
Shahzad Imam
1
O @ArtOfWarfare não foi verificado, mas sim, o número de fragmentos que você possui é irrelevante, pois eles sempre são hospedados por uma única atividade.
Nelson Ramirez
2
Se eu precisar da atividade atual com mais frequência, tenho que pesquisar com muita frequência, certo? Existe uma maneira de obter um evento de retorno de chamada sempre que a atividade em primeiro plano é alterada?
Nsq.sp 28/03
43
Para que todos saibam, os documentos afirmam isso sobre o método getRunningTasks (). -------- Nota: esse método destina-se apenas à depuração e apresentação de interfaces com o usuário de gerenciamento de tarefas. Isso nunca deve ser usado para a lógica principal de um aplicativo, como decidir entre diferentes comportamentos com base nas informações encontradas aqui. Tais usos não são suportados e provavelmente serão interrompidos no futuro. Por exemplo, se vários aplicativos puderem ser executados ativamente ao mesmo tempo, as suposições feitas sobre o significado dos dados aqui para fins de fluxo de controle estarão incorretas. ------------
Ryan
30
semifake em api 21As of LOLLIPOP, this method is no longer available to third party applications: the introduction of document-centric recents means it can leak person information to the caller. For backwards compatibility, it will still return a small subset of its data: at least the caller's own tasks, and possibly some other tasks such as home that are known to not be sensitive.
sherpya
115

Aviso: violação do Google Play

O Google ameaçou remover aplicativos da Play Store se eles usassem serviços de acessibilidade para fins de não acessibilidade. No entanto, isso está sendo reconsiderado .


Use um AccessibilityService

Benefícios

  • Testado e funcionando no Android 2.2 (API 8) através do Android 7.1 (API 25).
  • Não requer pesquisa.
  • Não requer a GET_TASKSpermissão.

Desvantagens

  • Cada usuário deve ativar o serviço nas configurações de acessibilidade do Android.
  • Isso não é 100% confiável. Ocasionalmente, os eventos ocorrem fora de ordem.
  • O serviço está sempre em execução.
  • Quando um usuário tenta ativar AccessibilityService, ele não pode pressionar o botão OK se um aplicativo tiver colocado uma sobreposição na tela. Alguns aplicativos que fazem isso são Velis Auto Brightness e Lux. Isso pode ser confuso porque o usuário pode não saber por que não pode pressionar o botão ou como contornar esse problema.
  • Eles AccessibilityServicenão conhecerão a atividade atual até a primeira alteração de atividade.

Exemplo

Serviço

public class WindowChangeDetectingService extends AccessibilityService {

    @Override
    protected void onServiceConnected() {
        super.onServiceConnected();

        //Configure these here for compatibility with API 13 and below.
        AccessibilityServiceInfo config = new AccessibilityServiceInfo();
        config.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED;
        config.feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC;

        if (Build.VERSION.SDK_INT >= 16)
            //Just in case this helps
            config.flags = AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS;

        setServiceInfo(config);
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {
        if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
            if (event.getPackageName() != null && event.getClassName() != null) {
                ComponentName componentName = new ComponentName(
                    event.getPackageName().toString(),
                    event.getClassName().toString()
                );

                ActivityInfo activityInfo = tryGetActivity(componentName);
                boolean isActivity = activityInfo != null;
                if (isActivity)
                    Log.i("CurrentActivity", componentName.flattenToShortString());
            }
        }
    }

    private ActivityInfo tryGetActivity(ComponentName componentName) {
        try {
            return getPackageManager().getActivityInfo(componentName, 0);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    @Override
    public void onInterrupt() {}
}

AndroidManifest.xml

Mesclar isso em seu manifesto:

<application>
    <service
        android:label="@string/accessibility_service_name"
        android:name=".WindowChangeDetectingService"
        android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
        <intent-filter>
            <action android:name="android.accessibilityservice.AccessibilityService"/>
        </intent-filter>
        <meta-data
            android:name="android.accessibilityservice"
            android:resource="@xml/accessibilityservice"/>
    </service>
</application>

Informações de serviço

Coloque isso em res/xml/accessibilityservice.xml:

<?xml version="1.0" encoding="utf-8"?>
<!-- These options MUST be specified here in order for the events to be received on first
 start in Android 4.1.1 -->
<accessibility-service
    xmlns:tools="http://schemas.android.com/tools"
    android:accessibilityEventTypes="typeWindowStateChanged"
    android:accessibilityFeedbackType="feedbackGeneric"
    android:accessibilityFlags="flagIncludeNotImportantViews"
    android:description="@string/accessibility_service_description"
    xmlns:android="http://schemas.android.com/apk/res/android"
    tools:ignore="UnusedAttribute"/>

Habilitando o serviço

Cada usuário do aplicativo precisará ativar explicitamente o AccessibilityServicepara que ele seja usado. Veja esta resposta StackOverflow para saber como fazer isso.

Observe que o usuário não poderá pressionar o botão OK ao tentar ativar o serviço de acessibilidade se um aplicativo tiver colocado uma sobreposição na tela, como Velis Auto Brightness ou Lux.

Sam
fonte
1
@lovemint, o que eu quis dizer é: eu especifiquei um exemplo settingsActivityde your.app.ServiceSettingsActivity, então você deve alterar isso para sua própria atividade de configurações para o seu serviço de acessibilidade. Eu acho que a atividade de configurações é opcional, então removi essa parte da minha resposta para simplificá-la.
Sam
1
Muito obrigado, apenas a última pergunta. Se eu precisar lidar da API 8 para a atual, devo fazer isso funcionar no código, usando XML?
precisa saber é o seguinte
1
@lovemint, isso mesmo. Acabei de atualizar o código de exemplo para fazer isso.
Sam
1
@ Sam Posso confirmar que este serviço funciona perfeitamente. Um comportamento estranho, usei uma intenção na atividade principal para ativar o Serviço de Acessibilidade. Após essa primeira ativação, o serviço é ativado, mas onAccessibilityEventnão recebe nenhum evento, mas se eu desabilitar e habilitar novamente o Serviço de Acessibilidade, o serviço é reativado e onAccessibilityEventcomeça a funcionar.
paolo2988
2
Obrigado pelo seu código. Criei este aplicativo para você :)
vault
20

Isso pode ser feito por:

  1. Implemente sua própria classe de aplicativo, registre-se no ActivityLifecycleCallbacks - dessa forma, você poderá ver o que está acontecendo com nosso aplicativo. Em cada retorno, o retorno de chamada atribui a atividade visível atual na tela e, na pausa, remove a atribuição. Ele usa o método registerActivityLifecycleCallbacks()que foi adicionado na API 14.

    public class App extends Application {
    
    private Activity activeActivity;
    
    @Override
    public void onCreate() {
        super.onCreate();
        setupActivityListener();
    }
    
    private void setupActivityListener() {
    registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
            }
            @Override
            public void onActivityStarted(Activity activity) {
            }
            @Override
            public void onActivityResumed(Activity activity) {
                activeActivity = activity;
            }
            @Override
            public void onActivityPaused(Activity activity) {
                activeActivity = null;
            }
            @Override
            public void onActivityStopped(Activity activity) {
            }
            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
            }
            @Override
            public void onActivityDestroyed(Activity activity) {
            }
        });
    }
    
    public Activity getActiveActivity(){
        return activeActivity;
    }
    
    }
  2. Na sua chamada de serviço getApplication()e transmita-o para o nome da classe do seu aplicativo (neste caso, aplicativo). Do que você pode chamar app.getActiveActivity()- isso fornecerá uma Atividade visível atual (ou nula quando nenhuma atividade estiver visível). Você pode obter o nome da Atividade chamandoactiveActivity.getClass().getSimpleName()

Vit Veres
fonte
1
Estou recebendo a exceção do Nullpointer em activeActivity.getClass (). GetSimpleName (). Você pode por favor ajudar
iniciante
Bem, como você pode ver em ActivityLifecycleCallbacks - caso a atividade não seja visível, application.getActiveActivity () retorna nulo. Isso significa que nenhuma atividade é visível. Você precisa verificar isso em seu serviço ou em qualquer outro lugar em que o usar.
Vit Veres
ok..mas caso a atividade seja retomada, isso não deve retornar nulo ?? Minha atividade havia sido lançada e, em seu currículo, eu fiquei assim
iniciante
Difícil de adivinhar, tente colocar pontos de interrupção em onActivityResumed(), onActivityPaused()e getActiveActivity()para ver como é que Callet, se sempre assim.
Vit Veres
@beginner deve-se verificar se activitye activeActivitysão os mesmos antes de atribuir activeActivitycom null, a fim de erros a evitar, devido à ordem chamando entremeadas de métodos de ciclo de vida de várias atividades.
Rahul Tiwari
16

Não consegui encontrar uma solução com a qual nossa equipe ficasse feliz, então criamos a nossa. Usamos ActivityLifecycleCallbackspara acompanhar a atividade atual e, em seguida, expô-la através de um serviço:

public interface ContextProvider {
    Context getActivityContext();
}

public class MyApplication extends Application implements ContextProvider {
    private Activity currentActivity;

    @Override
    public Context getActivityContext() {
         return currentActivity;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
            @Override
            public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
                MyApplication.this.currentActivity = activity;
            }

            @Override
            public void onActivityStarted(Activity activity) {
                MyApplication.this.currentActivity = activity;
            }

            @Override
            public void onActivityResumed(Activity activity) {
                MyApplication.this.currentActivity = activity;
            }

            @Override
            public void onActivityPaused(Activity activity) {
                MyApplication.this.currentActivity = null;
            }

            @Override
            public void onActivityStopped(Activity activity) {
                // don't clear current activity because activity may get stopped after
                // the new activity is resumed
            }

            @Override
            public void onActivitySaveInstanceState(Activity activity, Bundle outState) {

            }

            @Override
            public void onActivityDestroyed(Activity activity) {
                // don't clear current activity because activity may get destroyed after
                // the new activity is resumed
            }
        });
    }
}

Em seguida, configure seu contêiner de DI para retornar a instância de MyApplicationpara ContextProvider, por exemplo

public class ApplicationModule extends AbstractModule {    
    @Provides
    ContextProvider provideMainActivity() {
        return MyApplication.getCurrent();
    }
}

(Observe que a implementação de getCurrent()é omitida no código acima. É apenas uma variável estática definida no construtor do aplicativo)

Muxa
fonte
4
é necessário verificar se activitye currentActivitysão os mesmos antes de atribuir currentActivitya null, a fim de evitar erros devido a ordem de chamada entrelaçada dos métodos do ciclo de vida de várias atividades.
Rahul Tiwari
14

Usar ActivityManager

Se você deseja conhecer apenas o aplicativo que contém a atividade atual, pode fazê-lo usando ActivityManager. A técnica que você pode usar depende da versão do Android:

Benefícios

  • Deve funcionar em todas as versões do Android atualizadas.

Desvantagens

  • Não funciona no Android 5.1 ou superior (ele retorna apenas seu próprio aplicativo)
  • A documentação dessas APIs diz que elas são destinadas apenas à depuração e gerenciamento de interfaces de usuário.
  • Se você quiser atualizações em tempo real, precisará usar a pesquisa.
  • Confia em uma API oculta: ActivityManager.RunningAppProcessInfo.processState
  • Esta implementação não capta a atividade do alternador de aplicativos.

Exemplo (baseado no código do KNaito )

public class CurrentApplicationPackageRetriever {

    private final Context context;

    public CurrentApplicationPackageRetriever(Context context) {
        this.context = context;
    }

    public String get() {
        if (Build.VERSION.SDK_INT < 21)
            return getPreLollipop();
        else
            return getLollipop();
    }

    private String getPreLollipop() {
        @SuppressWarnings("deprecation")
        List<ActivityManager.RunningTaskInfo> tasks =
            activityManager().getRunningTasks(1);
        ActivityManager.RunningTaskInfo currentTask = tasks.get(0);
        ComponentName currentActivity = currentTask.topActivity;
        return currentActivity.getPackageName();
    }

    private String getLollipop() {
        final int PROCESS_STATE_TOP = 2;

        try {
            Field processStateField = ActivityManager.RunningAppProcessInfo.class.getDeclaredField("processState");

            List<ActivityManager.RunningAppProcessInfo> processes =
                activityManager().getRunningAppProcesses();
            for (ActivityManager.RunningAppProcessInfo process : processes) {
                if (
                    // Filters out most non-activity processes
                    process.importance <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
                    &&
                    // Filters out processes that are just being
                    // _used_ by the process with the activity
                    process.importanceReasonCode == 0
                ) {
                    int state = processStateField.getInt(process);

                    if (state == PROCESS_STATE_TOP) {
                        String[] processNameParts = process.processName.split(":");
                        String packageName = processNameParts[0];

                        /*
                         If multiple candidate processes can get here,
                         it's most likely that apps are being switched.
                         The first one provided by the OS seems to be
                         the one being switched to, so we stop here.
                         */
                        return packageName;
                    }
                }
            }
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }

        return null;
    }

    private ActivityManager activityManager() {
        return (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
    }

}

Manifesto

Adicione a GET_TASKSpermissão para AndroidManifest.xml:

<!--suppress DeprecatedClassUsageInspection -->
<uses-permission android:name="android.permission.GET_TASKS" />
Sam
fonte
10
isso não funciona mais no Android M. getRunningAppProcesses () agora retorna único pacote do seu aplicativo
Lior Iluz
1
Também não funciona para o nível 22 da API (Android 5.1). Testado na compilação LPB23
sumitb.mdi 29/01
9

Estou usando isso para meus testes. É API> 19, e apenas para atividades do seu aplicativo.

@TargetApi(Build.VERSION_CODES.KITKAT)
public static Activity getRunningActivity() {
    try {
        Class activityThreadClass = Class.forName("android.app.ActivityThread");
        Object activityThread = activityThreadClass.getMethod("currentActivityThread")
                .invoke(null);
        Field activitiesField = activityThreadClass.getDeclaredField("mActivities");
        activitiesField.setAccessible(true);
        ArrayMap activities = (ArrayMap) activitiesField.get(activityThread);
        for (Object activityRecord : activities.values()) {
            Class activityRecordClass = activityRecord.getClass();
            Field pausedField = activityRecordClass.getDeclaredField("paused");
            pausedField.setAccessible(true);
            if (!pausedField.getBoolean(activityRecord)) {
                Field activityField = activityRecordClass.getDeclaredField("activity");
                activityField.setAccessible(true);
                return (Activity) activityField.get(activityRecord);
            }
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

    throw new RuntimeException("Didn't find the running activity");
}
espinchi
fonte
Substitua ArrayMap pelo mapa e ele funcionará no 4.3 também. Não testado versões anteriores do Android.
Oliver Jonas
2

Use este código para a API 21 ou superior. Isso funciona e fornece melhores resultados em comparação com as outras respostas, ele detecta perfeitamente o processo em primeiro plano.

if (Build.VERSION.SDK_INT >= 21) {
    String currentApp = null;
    UsageStatsManager usm = (UsageStatsManager) this.getSystemService(Context.USAGE_STATS_SERVICE);
    long time = System.currentTimeMillis();
    List<UsageStats> applist = usm.queryUsageStats(UsageStatsManager.INTERVAL_DAILY, time - 1000 * 1000, time);
    if (applist != null && applist.size() > 0) {
        SortedMap<Long, UsageStats> mySortedMap = new TreeMap<Long, UsageStats>();
        for (UsageStats usageStats : applist) {
            mySortedMap.put(usageStats.getLastTimeUsed(), usageStats);

        }
        if (mySortedMap != null && !mySortedMap.isEmpty()) {
            currentApp = mySortedMap.get(mySortedMap.lastKey()).getPackageName();
        }
    }
Manish Godhani
fonte
Você pode, por favor, explicar como os resultados desse método são melhores do que as outras respostas?
21416 Sam
Isso funciona também com o Menu de notificação. Estou enfrentando um problema no qual, se eu receber uma ligação ou mensagem. O UsageStatsManager começa a me fornecer o nome do pacote systemUi em vez do meu aplicativo.
kukroid
@kukroid, poste uma pergunta separada e inclua seu código-fonte, informações do dispositivo e quais aplicativos de mensagens e telefone você está usando.
Sam
0

Aqui está a minha resposta que funciona muito bem ...

Você deve conseguir a Atividade atual dessa maneira ... Se você estruturar seu aplicativo com algumas Atividades com muitos fragmentos e quiser acompanhar qual é a sua Atividade atual, levaria muito trabalho. Meu senario foi que eu tenho uma atividade com vários fragmentos. Para que eu possa acompanhar a Atividade Atual através do Objeto Aplicativo, que pode armazenar todo o estado atual das variáveis ​​Globais.

Aqui está um caminho. Quando você inicia sua Atividade, você a armazena por Application.setCurrentActivity (getIntent ()); Este aplicativo irá armazená-lo. Na sua classe de serviço, você pode simplesmente fazer como Intent currentIntent = Application.getCurrentActivity (); getApplication (). startActivity (currentIntent);

John3328
fonte
2
Isso pressupõe que o serviço seja executado no mesmo processo que a Atividade, certo?
helleye
0

Não sei se é uma resposta estúpida, mas resolvi esse problema armazenando um sinalizador em preferências compartilhadas toda vez que inseri oCreate () de qualquer atividade, então usei o valor das preferências shered para descobrir qual é a atividade em primeiro plano.

Vlad Vladescu
fonte
1
Estamos à procura de uma maneira nativa para fazê-lo
IgniteCoders
-3

Recentemente descobri sobre isso. Com apis como:

  • minSdkVersion 19
  • targetSdkVersion 26

    ActivityManager.getCurrentActivity (contexto)

Espero que isso seja útil.

user3709410
fonte
1
Tinha minhas esperanças. Talvez quando isso foi respondido, ele existisse, mas a partir de agora não há nenhuma getCurrentActivity()função na ActivityManagerclasse ( developer.android.com/reference/android/app/ActivityManager ). Engraçado que esta função não existe: ActivityManager.isUserAMonkey().
ByteSlinger
1
Isso detecta se você está usando um agente de teste de macaco para testar sua atividade. Apesar de ser um nome engraçado, pode ser bastante útil para testar (e fazer outras pessoas rirem, obvs.) Developer.android.com/studio/test/monkeyrunner
Ken Corey