[Android] UsageStatsManager를 이용해 현재 실행 중인 앱 확인하고 패키지 이름 가져오기

 

앱을 개발하면 가끔 현재 실행 중인 앱이 무엇인지 알고 싶을 때가 있다. 가령 유튜브 관련 앱을 만들어 유튜브를 실행하면 자신의 앱도 같이 실행하게 하던가, 하스스톤 전적 검색 앱을 개발하여 하스스톤을 실행하면 해당 앱이 함께 실행되거나 등과 같은 방식 말이다. 

안드로이드 5.0 (롤리팝) 이전에는 ActivityManager를 이용해 실행 중인 앱을 가져올 수 있었지만, 롤리팝 이후로는 해당 방법이 적용되지 않는다. (아니면 적용이 되는데 필자가 제대로 사용하지 못한 것일 수도 있다.) 따라서 조금은 다른 방법을 사용해야 한다.



UsageStatsManager를 사용하기 위한 기본 세팅

위에서 얘기했지만 ActivityManager는 안드로이드 5.0 (롤리팝, API Level : 21) 이전에 사용된 것이며, 그 이후 버젼부터는 UsageStatsManager를 대체해서 사용하게 된다. UsageStatsManager를 어떻게 사용해야 하는지 알아보도록 하자.


1. Min sdk Version 바꾸기

1) file - project structure를 들어간다.

2) 왼쪽 Modules를 클릭하고 Default Config에서 Min SDK Version이 api level이 21 이상인 것을 선택해주고 apply해주면 된다.


혹시 자신의 앱의 최소 타겟을 롤리팝 이전으로 설정하고자 한다면, 최소 버젼을 건들지 않고도 사용할 수 있는 방법이 있다. UsageStatsManager를 사용하는 메서드 위에 아래의 코드와 같이 RequiresApi를 설정해주면 된다.


@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public static String getPackageName(@NonNull Context context) {

    UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);

}


2. PACKAGE_USAGE_STATS 권한 설정하기

AndroidManifest.xml에 아래의 권한을 추가한다.


    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" tools:ignore="ProtectedPermissions">
        

혹시 여기서 tools 부분에 오류가 발생하는 사람이 있다면, 오류가 나는 부분을 우클릭한 후 Show Context Actions - Create namespace declaration을 누르면 아래와 같이 xmlns:tools 부분이 추가된 것을 볼 수 있을 것이다.


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.test">
    <!-- 위에서 xmlns:tools="http://schemas.android.com/tools" 부분이 추가된 것을 볼 수 있다. -->

    <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"
        tools:ignore="ProtectedPermissions"/>



    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Test">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>


여기까지 했다면 기본적인 세팅은 끝난 것이다. 이제 UsageStatsManager를 사용해보도록 하자.




UsageStatsManager를 이용하여 현재 실행 중인 앱 패키지 이름 가져오기

UsageStatsManage를 사용해 현재 실행 중인 앱의 패키지 이름을 가져오는 방법은 아래의 링크의 코드를 이용하고 분석하였다.

http://sjava.net/2019/11/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%ED%99%94%EB%A9%B4%EC%97%90-%EC%8B%A4%ED%96%89%EC%A4%91%EC%9D%B8-%EC%95%B1-%ED%99%95%EC%9D%B8%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95/ 


1. 앱 패키지 이름 가져오기


public static String getPackageName(@NonNull Context context) {

    // UsageStatsManager 선언
    UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);

    long lastRunAppTimeStamp = 0L;

    // 얼마만큼의 시간동안 수집한 앱의 이름을 가져오는지 정하기 (begin ~ end 까지의 앱 이름을 수집한다)
    final long INTERVAL = 10000;
    final long end = System.currentTimeMillis();
    // 1 minute ago
    final long begin = end - INTERVAL;

    //
    LongSparseArray packageNameMap = new LongSparseArray<>();

    // 수집한 이벤트들을 담기 위한 UsageEvents
    final UsageEvents usageEvents = usageStatsManager.queryEvents(begin, end);

    // 이벤트가 여러개 있을 경우 (최소 존재는 해야 hasNextEvent가 null이 아니니까)
    while (usageEvents.hasNextEvent()) {

        // 현재 이벤트를 가져오기
        UsageEvents.Event event = new UsageEvents.Event();
        usageEvents.getNextEvent(event);

        // 현재 이벤트가 포그라운드 상태라면 = 현재 화면에 보이는 앱이라면
        if(isForeGroundEvent(event)) {
            // 해당 앱 이름을 packageNameMap에 넣는다.
            packageNameMap.put(event.getTimeStamp(), event.getPackageName());
            // 가장 최근에 실행 된 이벤트에 대한 타임스탬프를 업데이트 해준다.
            if(event.getTimeStamp() > lastRunAppTimeStamp) {
                lastRunAppTimeStamp = event.getTimeStamp();
            }
        }
    }
    // 가장 마지막까지 있는 앱의 이름을 리턴해준다.
    return packageNameMap.get(lastRunAppTimeStamp, "").toString();
}

주석으로 대략적인 설명은 달아놨으니 전체적인 흐름을 설명하자면, UsageStatsManager의 UsageEvents를 이용해 포그라운드에서 실행되었던 앱들의 이름을 packageNameMap에 담고 lastRunAppTimeStamp를 업데이트하여 가장 마지막에 실행되었던 앱의 이름을 return 해주면 된다. 

이를 위해선 앱이 포그라운드에서 실행되는 것인지 체크를 해주는 isForeGroundEvent(event)를 구현해야 한다. 아래의 코드를 보자.


2. 앱이 포그라운드 상태인지 체크


private static boolean isForeGroundEvent(UsageEvents.Event event) {

    if(event == null) return false;

    if(BuildConfig.VERSION_CODE >= 29) 
        return event.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED;

    return event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND;
}

코드 내용이 비교적 짧으므로 주석은 굳이 달 필요 없다고 생각했고 짧게 설명해서 event가 없으면 false를 반환하고 event의 EventType이 ACTIVITY_RESUMED 또는 MOVE_TO_FOREGROUND, 즉 foreground 상태라면 true를 반환해준다.


3. 권한 체크

AppOpsManager를 이용해 USAGE_STATS의 권한 확인을 구현해주자. App-op에 대한 내용은 링크에서 확인할 수 있으며, 핵심적인 내용은 아래와 같다.


App-ops are used for two purposes: Access control and tracking.

App-ops cover a wide variety of functionality from helping with runtime permissions access control and tracking to battery consumption tracking.

https://developer.android.com/reference/android/app/AppOpsManager


권한 관련 제어 API로, 권한이 부여되지 않았다면 강제로 권한을 부여할 수 있게 해준다. 따라서 AppOpsManager를 이용해 권한이 허용되지 않았다면 이용자가 직접 권한을 체크하도록 만들어주면 된다. 권한을 체크하는 코드는 아래와 같다. 


private boolean checkPermission(){

    boolean granted = false;

    AppOpsManager appOps = (AppOpsManager) getApplicationContext()
            .getSystemService(Context.APP_OPS_SERVICE);

    int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
            android.os.Process.myUid(), getApplicationContext().getPackageName());

    if (mode == AppOpsManager.MODE_DEFAULT) {
        granted = (getApplicationContext().checkCallingOrSelfPermission(
                android.Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED);
    }
    else {
        granted = (mode == AppOpsManager.MODE_ALLOWED);
    }

    return granted;
}

마지막으로 버튼을 클릭했을 때 권한이 부여되지 않았다면 권한을 부여할 수 있는 창으로 이동하여 이용자가 직접 권한을 허용할 수 있게 만드는 기능을 구현해보자.


Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {

        if(!checkPermission())
            startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
    }
});

이제 버튼을 눌렀을 때 권한 허용을 안했다면 아래와 같이 권한을 부여하는 창으로 이동하게 된다.

 




전체 코드는 아래와 같으며, CheckPackageNameThread에서 2초마다 패키지 이름을 로그창에 출력하게 만들었다. 


package com.example.test;

import android.app.AppOpsManager;
import android.app.usage.UsageEvents;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.provider.Settings;
import android.util.LongSparseArray;
import android.view.View;
import android.widget.Button;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    CheckPackageNameThread checkPackageNameThread;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        checkPackageNameThread = new CheckPackageNameThread();
        checkPackageNameThread.start();


        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                if(!checkPermission())
                    startActivity(new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS));
            }
        });
    }

    private class CheckPackageNameThread extends Thread{

        public void run(){
            while(true){
                if(!checkPermission()) continue;
                System.out.println(getPackageName(getApplicationContext()));
                try {
                    sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private boolean checkPermission(){

        boolean granted = false;

        AppOpsManager appOps = (AppOpsManager) getApplicationContext()
                .getSystemService(Context.APP_OPS_SERVICE);

        int mode = appOps.checkOpNoThrow(AppOpsManager.OPSTR_GET_USAGE_STATS,
                android.os.Process.myUid(), getApplicationContext().getPackageName());

        if (mode == AppOpsManager.MODE_DEFAULT) {
            granted = (getApplicationContext().checkCallingOrSelfPermission(
                    android.Manifest.permission.PACKAGE_USAGE_STATS) == PackageManager.PERMISSION_GRANTED);
        }
        else {
            granted = (mode == AppOpsManager.MODE_ALLOWED);
        }

        return granted;
    }


    public static String getPackageName(@NonNull Context context) {

        // UsageStatsManager 선언
        UsageStatsManager usageStatsManager = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);

        long lastRunAppTimeStamp = 0L;

        // 얼마만큼의 시간동안 수집한 앱의 이름을 가져오는지 정하기 (begin ~ end 까지의 앱 이름을 수집한다)
        final long INTERVAL = 10000;
        final long end = System.currentTimeMillis();
        // 1 minute ago
        final long begin = end - INTERVAL;

        //
        LongSparseArray packageNameMap = new LongSparseArray<>();

        // 수집한 이벤트들을 담기 위한 UsageEvents
        final UsageEvents usageEvents = usageStatsManager.queryEvents(begin, end);

        // 이벤트가 여러개 있을 경우 (최소 존재는 해야 hasNextEvent가 null이 아니니까)
        while (usageEvents.hasNextEvent()) {

            // 현재 이벤트를 가져오기
            UsageEvents.Event event = new UsageEvents.Event();
            usageEvents.getNextEvent(event);

            // 현재 이벤트가 포그라운드 상태라면 = 현재 화면에 보이는 앱이라면
            if(isForeGroundEvent(event)) {
                // 해당 앱 이름을 packageNameMap에 넣는다.
                packageNameMap.put(event.getTimeStamp(), event.getPackageName());
                // 가장 최근에 실행 된 이벤트에 대한 타임스탬프를 업데이트 해준다.
                if(event.getTimeStamp() > lastRunAppTimeStamp) {
                    lastRunAppTimeStamp = event.getTimeStamp();
                }
            }
        }
        // 가장 마지막까지 있는 앱의 이름을 리턴해준다.
        return packageNameMap.get(lastRunAppTimeStamp, "").toString();
    }


    private static boolean isForeGroundEvent(UsageEvents.Event event) {

        if(event == null) return false;

        if(BuildConfig.VERSION_CODE >= 29)
            return event.getEventType() == UsageEvents.Event.ACTIVITY_RESUMED;

        return event.getEventType() == UsageEvents.Event.MOVE_TO_FOREGROUND;
    }

}


앱을 실행하면 사진과 같이 정상적으로 동작하는 것을 볼 수 있다. 앱을 종료하지 않고 백그라운드로 놓은 후에 여러 앱을 실행하면 해당 앱의 이름이 나오는 것을 확인할 수 있다.




마치며..

UsageStatsManager 기능은 모든 앱에서 필요한 기능이 아니며, 사용하는 경우도 많지는 않다고 생각하지만, foregorund 서비스와 연계해서 만들면 다양한 앱을 만들 수 있을 것이다. 훗날 사용하게 될 경우를 생각해서 한 번 쯤 사용해보는 것도 나쁘지 않을 것 같다.


댓글

댓글 쓰기