【问题标题】:How to create a Service which continuously monitors app usage information?如何创建一个持续监控应用使用信息的服务?
【发布时间】:2013-12-06 05:23:44
【问题描述】:

手头的问题: 我必须创建一个连续运行的Service。该服务监控 5 个应用程序,例如您手机上安装的 5 个安卓游戏。该服务需要获取以下信息: 1、游戏打开运行多少次? 2. 每场比赛的运行​​时间。

例如:假设我在我的应用程序中安装了此服务。我让它运行了一个月。我需要应用程序屏幕上的此类信息:

游戏 游戏运行次数 游戏持续时间

游戏 1 共玩了 20 次,共 15 小时

游戏 2 共 16 次玩了 25 小时

..

..

游戏 5 共 10 次玩了 12 小时

可能的方法: 当应用程序加载时,它会进入内存。在应用程序启动时注意系统时钟时间。当应用程序结束或被置于后台时,再次记录时间。

假设一个应用程序在晚上 9:00 进入内存并在晚上 9:30 退出到后台,这给了我们 30 分钟的游戏时间。下次播放应用程序时,持续时间将从存储在某种变量中的上一次播放增加到 30,依此类推。 每次将应用程序带入内存时,正在播放的应用程序的计数器应增加一。因此给了我们应用程序的播放次数。

编码: 我不知道 Android 中的 Service,因为我从来没有真正研究过它们。任何与我手头的问题相关的教程都会非常有帮助。 其次,如果有另一种方式可以做到这一点。我也想知道。我真的可以使用一些代码 sn-p 来启动这个项目。

【问题讨论】:

  • 这正是谷歌分析所做的事情。如果您需要它进行分析,您应该只使用 GA,而不是重新发明自己的版本。
  • 这5个应用和游戏是你自己的应用吗?或者他们是用户安装的任何其他应用程序?
  • @AdnanMulla 用户安装的其他应用。
  • @D'yer Mak'er 。不可能,伙计。如果您要监控的应用数据不是您开发的应用。如果他们没有公开,你就不能对他们的数据做任何事情。
  • @Javanator:如果我们监控进程名称,很有可能。一旦特定进程处于前台。您可以检查系统时间和进入后台的时间。时间差将是游戏的持续时间。

标签: android service usage-statistics


【解决方案1】:

正如您所写的,该任务是关于监视 3-rd 方应用程序的,除了定期读取进程列表并检测前台进程之外,没有其他解决方案。您需要为此提供服务。不幸的是,Android 没有提供诸如广播事件等手段来改变前台进程。

事实上,该任务需要大量代码,至少比普通答案所能包含的要多得多。我在这里发布了其中的一部分,但您应该解决幕后留下的许多细微差别,例如启动之间的同步和持久信息。这只是一个骨架。

首先,让我们编写一个应用程序对象,这是注册所有与实例相关的东西的好地方。

MonitorApp

public class MonitorApp extends Application
{
  // actual store of statistics
  private final ArrayList<HashMap<String,Object>> processList = new ArrayList<HashMap<String,Object>>();

  // fast-access index by package name (used for lookup)
  private ArrayList<String> packages = new ArrayList<String>();

  public ArrayList<HashMap<String,Object>> getProcessList()
  {
    return processList;
  }

  public ArrayList<String> getPackages()
  {
    return packages;
  }

  // TODO: you need to save and load the instance data
  // TODO: you need to address synchronization issues
}

然后让我们起草一个活动。

监控活动

import static ProcessList.COLUMN_PROCESS_NAME;
import static ProcessList.COLUMN_PROCESS_PROP;
import static ProcessList.COLUMN_PROCESS_COUNT;
import static ProcessList.COLUMN_PROCESS_TIME;

public class MonitorActivity extends Activity implements MonitorService.ServiceCallback
{
  private ArrayList<HashMap<String,Object>> processList;
  private MonitorService backgroundService;
  private MyCustomAdapter adapter = null;
  private ListView listView = null;

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main); // TODO: provide your layout
    listView = (ListView)findViewById(R.id.id_process_listview);
    createAdapter();

    this.bindService(
      new Intent(this, MonitorService.class),
      serviceConnection,
      Context.BIND_AUTO_CREATE);
  }

  private void createAdapter()
  {
    processList = ((MonitorApp)getApplication()).getProcessList();
    adapter = new MyCustomAdapter(this, processList, R.layout.complex_list_item,
    new String[]
    {
      COLUMN_PROCESS_NAME,
      COLUMN_PROCESS_PROP, // TODO: you may calculate and pre-fill this field
                           // from COLUMN_PROCESS_COUNT and COLUMN_PROCESS_TIME
                           // so eliminating the need to use the custom adapter
    },
    new int[]
    {
      android.R.id.text1,
      android.R.id.text2
    });

    listView.setAdapter(adapter);
  }

  // callback method invoked by the service when foreground process changed
  @Override
  public void sendResults(int resultCode, Bundle b)
  {
    adapter.notifyDataSetChanged();
  }

  private class MyCustomAdapter extends SimpleAdapter
  {
    MyCustomAdapter(Context context, List<? extends Map<String, ?>> data, int resource, String[] from, int[] to)
    {
      super(context, data, resource, from, to);
    }

    @Override
    public View getView (int position, View convertView, ViewGroup parent)
    {
      View result = super.getView(position, convertView, parent);

      // TODO: customize process statistics display
      int count = (Integer)(processList.get(position).get(COLUMN_PROCESS_COUNT));
      int seconds = (Integer)(processList.get(position).get(COLUMN_PROCESS_TIME));

      return result;
    }
  }

  private ServiceConnection serviceConnection = new ServiceConnection()
  {
    @Override
    public void onServiceConnected(ComponentName className, IBinder service)
    {
      LocalBinder binder = (LocalBinder)service;
      backgroundService = binder.getService();
      backgroundService.setCallback(MonitorActivity.this);
      backgroundService.start();
    }

    @Override
    public void onServiceDisconnected(ComponentName className)
    {
      backgroundService = null;
    }
  };

  @Override
  public void onResume()
  {
    super.onResume();
    if(backgroundService != null)
    {
      backgroundService.setCallback(this);
    }
  }

  @Override
  public void onPause()
  {
    super.onPause();
    if(backgroundService != null)
    {
      backgroundService.setCallback(null);
    }
  }

}

该活动启动后台工作服务,该服务实际上监控进程。您可以将服务注册从活动移动到应用程序实例中。服务本身是这样的:

监控服务

public class MonitorService extends Service
{
  private boolean initialized = false;
  private final IBinder mBinder = new LocalBinder();
  private ServiceCallback callback = null;
  private Timer timer = null;
  private final Handler mHandler = new Handler();
  private String foreground = null;
  private ArrayList<HashMap<String,Object>> processList;
  private ArrayList<String> packages;
  private Date split = null;

  public static int SERVICE_PERIOD = 5000; // TODO: customize (this is for scan every 5 seconds)

  private final ProcessList pl = new ProcessList(this)
  {
    @Override
    protected boolean isFilteredByName(String pack)
    {
      // TODO: filter processes by names, return true to skip the process
      // always return false (by default) to monitor all processes
      return false;
    }

  };

  public interface ServiceCallback
  {
    void sendResults(int resultCode, Bundle b);
  }

  public class LocalBinder extends Binder
  {
    MonitorService getService()
    {
      // Return this instance of the service so clients can call public methods
      return MonitorService.this;
    }
  }

  @Override
  public void onCreate()
  {
    super.onCreate();
    initialized = true;
    processList = ((MonitorApp)getApplication()).getProcessList();
    packages = ((MonitorApp)getApplication()).getPackages();
  }

  @Override
  public IBinder onBind(Intent intent)
  {
    if(initialized)
    {
      return mBinder;
    }
    return null;
  }

  public void setCallback(ServiceCallback callback)
  {
    this.callback = callback;
  }

  private boolean addToStatistics(String target)
  {
    boolean changed = false;
    Date now = new Date();
    if(!TextUtils.isEmpty(target))
    {
      if(!target.equals(foreground))
      {
        int i;
        if(foreground != null && split != null)
        {
          // TODO: calculate time difference from current moment
          // to the moment when previous foreground process was activated
          i = packages.indexOf(foreground);
          long delta = (now.getTime() - split.getTime()) / 1000;
          Long time = (Long)processList.get(i).get(COLUMN_PROCESS_TIME);
          if(time != null)
          { 
            // TODO: add the delta to statistics of 'foreground' 
            time += delta;
          }
          else
          {
            time = new Long(delta);
          }
          processList.get(i).put(COLUMN_PROCESS_TIME, time);
        }

        // update count of process activation for new 'target'
        i = packages.indexOf(target);
        Integer count = (Integer)processList.get(i).get(COLUMN_PROCESS_COUNT);
        if(count != null) count++;
        else
        {
          count = new Integer(1);
        }
        processList.get(i).put(COLUMN_PROCESS_COUNT, count);

        foreground = target;
        split = now;
        changed = true;
      }
    }
    return changed; 
  }


  public void start()
  {
    if(timer == null)
    {
      timer = new Timer();
      timer.schedule(new MonitoringTimerTask(), 500, SERVICE_PERIOD);
    }

    // TODO: startForeground(srvcid, createNotification(null));
  }

  public void stop()
  {
    timer.cancel();
    timer.purge();
    timer = null;
  }

  private class MonitoringTimerTask extends TimerTask
  {
    @Override
    public void run()
    {
      fillProcessList();

      ActivityManager activityManager = (ActivityManager)MonitorService.this.getSystemService(ACTIVITY_SERVICE);
      List<RunningTaskInfo> taskInfo = activityManager.getRunningTasks(1);
      String current = taskInfo.get(0).topActivity.getPackageName();

      // check if current process changed
      if(addToStatistics(current) && callback != null)
      {
        final Bundle b = new Bundle();
        // TODO: pass necessary info to UI via bundle
        mHandler.post(new Runnable()
        {
          public void run()
          {
            callback.sendResults(1, b);
          }
        });
      }
    }
  }

  private void fillProcessList()
  {
    pl.fillProcessList(processList, packages);
  }

}

该服务使用帮助类来构建进程列表。

进程列表

public abstract class ProcessList
{
  // process package name
  public static final String COLUMN_PROCESS_NAME = "process";

  // TODO: arbitrary property (can be user-fiendly name)
  public static final String COLUMN_PROCESS_PROP = "property";

  // number of times a process has been activated
  public static final String COLUMN_PROCESS_COUNT = "count";

  // number of seconds a process was in foreground
  public static final String COLUMN_PROCESS_TIME = "time";

  private ContextWrapper context;

  ProcessList(ContextWrapper context)
  {
    this.context = context;
  }

  protected abstract boolean isFilteredByName(String pack);

  public void fillProcessList(ArrayList<HashMap<String,Object>> processList, ArrayList<String> packages)
  {
    ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
    List<RunningAppProcessInfo> procInfo = activityManager.getRunningAppProcesses();

    HashMap<String, Object> hm;
    final PackageManager pm = context.getApplicationContext().getPackageManager();

    for(int i = 0; i < procInfo.size(); i++)
    {
      String process = procInfo.get(i).processName;
      String packageList = Arrays.toString(procInfo.get(i).pkgList);
      if(!packageList.contains(process))
      {
        process = procInfo.get(i).pkgList[0];
      }

      if(!packages.contains(process) && !isFilteredByName(process))
      {
        ApplicationInfo ai;
        String applicationName = "";

        for(int k = 0; k < procInfo.get(i).pkgList.length; k++)
        {
          String thisPackage = procInfo.get(i).pkgList[k];
          try
          {
            ai = pm.getApplicationInfo(thisPackage, 0);
          }
          catch(final NameNotFoundException e)
          {
            ai = null;
          }
          if(k > 0) applicationName += " / ";
          applicationName += (String)(ai != null ? pm.getApplicationLabel(ai) : "(unknown)");
        }

        packages.add(process);
        hm = new HashMap<String, Object>();
        hm.put(COLUMN_PROCESS_NAME, process);
        hm.put(COLUMN_PROCESS_PROP, applicationName);
        processList.add(hm);
      }
    }

    // optional sorting
    Comparator<HashMap<String, Object>> comparator = new Comparator<HashMap<String, Object>>()
    {
      public int compare(HashMap<String, Object> object1, HashMap<String, Object> object2) 
      {       
        return ((String)object1.get(COLUMN_PROCESS_NAME)).compareToIgnoreCase((String)object2.get(COLUMN_PROCESS_NAME));
      }
    };
    Collections.sort(processList, comparator);

    packages.clear();
    for(HashMap<String, Object> e : processList)
    {
      packages.add((String)e.get(COLUMN_PROCESS_NAME));
    }
  }

}

最后是清单。

AndroidManifest.xml

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

    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="18" />

    <uses-permission android:name="android.permission.GET_TASKS" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".MonitorActivity"
            android:label="@string/app_name"
            android:configChanges="orientation|keyboardHidden" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service android:name=".MonitorService" />
    </application>

</manifest>

如您所见,这已经是很多代码了。它是从一个工作应用程序中提取的部分内容,但我根据您的需要进行了快速更改,因此可能存在拼写错误、所有导入都被跳过等。不过,我希望这会有所帮助。

附录:棒棒糖+

注意:最新的 Android 版本打破了上述方法。以下是官方文档对getRunningTasks 方法和其他方法的说明:

从 LOLLIPOP 开始,此方法不再适用于第三方应用程序:引入以文档为中心的最新消息意味着它可以将个人信息泄露给调用者。为了向后兼容,它仍然会返回一小部分数据:至少是调用者自己的任务,可能还有其他一些已知不敏感的任务,例如 home。

我认为这是一种矫枉过正的做法,并且可以以更有选择性和方便的方式完成。更不用说考虑到谷歌的许多带有隐私问题的内置功能,这似乎太戏剧化了。无论如何,我们对此无能为力。

唯一的解决方法是实现 Android 无障碍服务(更多信息 herehere)并拦截所有应用程序从那里获得和失去焦点的操作。用户应手动启用该服务!您的应用程序应该以某种方式指导用户这样做。

【讨论】:

  • @Stan:对于之前没有接受答案,我深表歉意。我已经离开了。我应该奖励这个答案,这有助于弄清楚我当时面临的一些事情。我稍后会发布一些代码来展示我实际上是如何完成这项任务的。谢谢您的答复。非常感谢。
  • 我有 java.lang.RuntimeException: 无法启动活动 ComponentInfo{com.app.monitoring/com.app.monitoring.MonitorActivity}: java.lang.ClassCastException: android.app.Application 不能转换为 com.app.monitoring.MonitorApp 错误。为什么?
  • @RiefSapthana,您是否在应用清单中将您的 MonitorApp 提到为applicationandroid:name 属性?如果这没有帮助,请考虑发布一个包含更多代码和环境数据的单独问题。
【解决方案2】:

我的想法,

  1. 开发应用程序,将其部分数据公开给其他进程以访问它们。

    Google“如何使用内容提供者”。

    您的每个应用程序都需要记录他们执行的所有活动。此数据将公开以供访问。

  2. 你不需要为此一直运行服务。每当您的监控应用程序打开时。它只需要从您想要的所有应用程序中获取该数据并相应地显示它们。

  3. 对于您希望在后台定期在服务器上提交该数据的情况。 那么你需要一个服务。 但是您仍然不需要永远运行它。 做这样的事情。

    i) 从服务获取数据并上传到服务器。

    ii) 停止服务并在某个时间 T1 后安排警报以再次启动您自己的服务以再次执行步骤 1。

    iii) T1 取决于要求。您希望如何在服务器上刷新数据。

如果你想知道如何实现我上面所说的,我什至可以告诉你代码片段指针。 但我建议你先在谷歌上找到它,然后自己做。如果您遇到任何困难,请在 StackOverflow 上发布另一个具体问题。

希望这会有所帮助。 :)

编辑:

首先。只有你最终必须编码。从这里获取指针和帮助。不要指望完整的代码。请在下面查看更多详细信息。

一)

您需要将您的应用设为内容提供者。检查下面的链接。并按照其中的示例进行操作。您最终将进入一个应用程序,其数据将可供其他人使用。很容易继续。

http://developer.android.com/guide/topics/providers/content-providers.html

http://developer.android.com/guide/topics/providers/content-provider-creating.html

http://www.vogella.com/articles/AndroidSQLite/article.html

您的所有游戏应用都需要公开数据才能访问。 现在,一旦你完成了这个。

现在您只需要访问监控应用程序中的所有数据并将其显示出来。

B)

正如我所说,如果您想通过服务来做到这一点。 你不需要永远运行它。只需启动一次服务,从内容提供者加载数据,然后做任何你想做的事情。设置警报以在稍后阶段调用服务以重新执行相同的操作。

我假设您的服务是否可以每 10 分钟从内容提供商(您要监控的游戏应用)获取数据。如下所示。

public class MonitoringService extends Service {

    private static final String ACTION_FETCH = "action_fetch";
    private static final int REPEAT_DATALOAD_INTERVAL_MS = 10 * 60 * 1000; // 10 Min

    private static PendingIntent makeSelfPendingIntent(Context context) {
        PendingIntent intent = PendingIntent.getService(
                           context, 0, makeSelfIntent(context), 0);
        return intent;
    }

    private static Intent makeSelfIntent(Context context) {
        Intent intent = new Intent(context, MonitoringService.class);
        intent.setAction(ACTION_FETCH);
        return intent;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if (intent != null && intent.getAction() != null) {
            String action = intent.getAction();
            if (action.equals(ACTION_FETCH)) {
                loadDataFromContentProviderDoWhateverYouWantThen();
                setAlarmToRedoLoad();
                stopSelf();

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

    private void setAlarmToRedoLoad() {
        AlarmManager alarmManager = (AlarmManager) 
                                     getSystemService(Context.ALARM_SERVICE);
        alarmManager.set(AlarmManager.RTC_WAKEUP,
                     System.currentTimeMillis() + REPEAT_DATALOAD_INTERVAL_MS,
                     makeSelfPendingIntent(getApplicationContext()));
    }

    private void loadDataFromContentProviderDoWhateverYouWantThen(){
        check this link how to load data from content provider.
        as your games app are content providers. It should be loaded easily.
        then do whatever you want display, upload anything

        http://developer.android.com/guide/topics/providers/content-provider-basics.html
    }

    // Call this method from onCreate of your monitoring app
    public static void start(Context context) {
        Intent intent = makeSelfIntent(context);
        context.startService(intent);
    }

}

C) 确保您允许监控应用的用户随时停止此服务。在这种情况下不要忘记取消警报。所以它不会永远在后台运行。

D) 你也可以让事情基于广播。每当您的游戏应用程序保存数据时,它们应该广播此事件并让您的服务在收听该广播后被调用。仅加载它。

E) 正如 Phill 所说,您也可以使用 Google Analytics。

现在您要如何实际执行此操作取决于您的要求

希望这会有所帮助。试试看,让我知道您还面临什么问题

【讨论】:

  • 我将非常感谢一些代码。因为这对我来说很紧急。而且我所有的尝试都已经成功了。
  • 非常感谢您提供此信息。帮了大忙!
【解决方案3】:

使用此示例代码检查浏览器是否正在运行

ActivityManager activityManager = (ActivityManager) this.getSystemService( ACTIVITY_SERVICE );
    List<RunningAppProcessInfo> procInfos = actvityManager.getRunningAppProcesses();
    for(int i = 0; i < procInfos.size(); i++){
        if(procInfos.get(i).processName.equals("com.android.browser")) {
            Toast.makeText(getApplicationContext(), "Browser is running", Toast.LENGTH_LONG).show();
        }
    }

要创建服务,您必须创建一个从 Service 基类扩展的类,并在无限循环中实现该函数,例如 onStartCommand() 函数中的 while(true)。不要忘记在 &lt;service&gt;&lt;/service&gt; 标签之间的清单文件中添加您的服务,一个很好的教程在 http://www.vogella.com/articles/AndroidServices/article.html

【讨论】:

    【解决方案4】:

    要获取当前正在运行的应用程序,您可以使用:

    ActivityManager am = (ActivityManager) mContext.getSystemService(Activity.ACTIVITY_SERVICE);
    String packageName = am.getRunningTasks(1).get(0).topActivity.getPackageName();
    

    您可以从正在运行的服务中定期检查这一点。您还需要在清单中定义所需的权限:android.permission.GET_TASKS

    【讨论】:

    【解决方案5】:

    以下解决方案假设您拥有您尝试监控的所有五个应用程序。如果它不明智,我将为您提供另一种解决方案。

    现在让我们分解问题。

    1. 有关应用程序是否运行的信息服务。
    2. 设计服务
    3. 存储数据

    您应该使用Intent 来广播有关您的应用程序是否运行的消息。使用活动onPauseonResume 广播消息。

    现在从 onPause 和 onResume 调用这个函数,

       public void broadcastMessage(String method_name){
            Intent intent=new Intent("com.service.myservice");
            Bundle bundle=new Bundle();
            bundle.putString("app_name", this.getApplicationInfo().packageName);
            bundle.putLong("time", System.currentTimeMillis());
                //just to keep track if it is on pause or in on resume
            bundle.putString("method", method_name);
            intent.putExtras(bundle);
            this.startService(intent);
        }
    

    你可以像下面这样调用它

    public void onPause(){
            super.onPause();
            broadcastMessage(Thread.currentThread().getStackTrace()[1].getMethodName());
        }
    

    或者

    public void onPause(){
            super.onPause();
            broadcastMessage("onPause");
        }
    

    那么为什么 onPause 和 onResume 因为玩家只在这些函数调用之间玩游戏。

    设计服务

    如果您想知道原因,请使用 IntentService 并阅读 this

    @Override
        protected void onHandleIntent(Intent intent) {
            // TODO Auto-generated method stub
            Bundle bundle=intent.getExtras();
            String app_name=bundle.getString("app_name");
            long time=bundle.getLong("time");
            //then do whatever you wanna do with this data
    
        }
    

    存储

    ContentProvider是存储数据的好选择,功能也很强大;在 Android 上很好地集成和扩展,但在这种情况下,您似乎只想存储一些非常简单的信息,因此您也可以选择一些非常简单的信息。

    下面的代码使用SharedPreferences

     protected void storeData(String app_name, long time) {
            if ((app_name==null)||(time<=0)) return;
            SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext());
            Editor editor = sharedPreferences.edit();
                  //concatenating app name just to make key unique
            long start_time=sharedPreferences.getLong(app_name+"_start_time", -1);
            if (start_time==-1){
                //start time is empty means this is my start time
                // and user just started the app
                editor.putLong(app_name+"_start_time", time);
                editor.putLong(app_name+"_counter", sharedPreferences.getLong(app_name+"_counter", 0)+1);
            }else{
                //user is now closing the app. 
                long time_played=time-start_time;
                long total_time=sharedPreferences.getLong(app_name+"_total_time", 0);
                total_time=total_time+time_played;
                //saving total time
                editor.putLong(app_name+"_total_time", time);
                //removing start time for next pass
                editor.remove(app_name+"_start_time");
            }
            editor.commit();
        }
        public long getTotalTimeUserSpent(String app_name){
            SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext());
            return sharedPreferences.getLong(app_name+"_total_time", 0);
        }
        public long numberOfTimeUserPlayed(String app_name){
            SharedPreferences sharedPreferences = PreferenceManager .getDefaultSharedPreferences(getApplicationContext());
            return sharedPreferences.getLong(app_name+"_counter", 0);
        }
    

    只需从 onHandleIntent 调用这些方法。

    最后一个难题是分发。您可以将其分发到单独的应用程序或这五个应用程序的一部分。单独的应用程序是最简单的方法。但是,如果您选择将此作为所有五个应用程序的一部分,请阅读this 讨论。

    【讨论】:

      【解决方案6】:

      在您的 Android 设备上安装Covenant Eyes 将监控手机预装浏览器上的所有互联网流量,并报告设备上正在使用的其他应用程序。

      【讨论】:

        猜你喜欢
        • 2013-02-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-09-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多