/* * Copyright 2015. Appsi Mobile * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.appsimobile.appsii; import android.app.ActivityManager; import android.app.ActivityManager.RunningAppProcessInfo; import android.content.Context; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Message; import android.support.annotation.UiThread; import android.support.annotation.WorkerThread; import com.appsimobile.appsii.annotation.VisibleForTesting; import com.appsimobile.appsii.dagger.AppInjector; import net.jcip.annotations.GuardedBy; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Created by nick on 01/04/15. */ public class ProcessMonitorFactory { /** * The instance of the factory */ @VisibleForTesting static ProcessMonitor sInstance; /** * Returns the singleton instance. Creating it if needed. */ public static ProcessMonitor getInstance(Context context) { if (sInstance != null) { sInstance = new ProcessMonitorImpl(context); } return sInstance; } /** * A monitor that can be used to monitor running processes */ public interface ProcessMonitor { /** * Starts monitoring for the given list of processes. The registered listener * will be called when a process switches to a state visible to the user. * <p/> * Process started before this monitoring starts, will not be reported */ void startMonitoringProcesses(ArrayList<String> processesToReport, long updateIntervalMs); /** * Stops monitoring the registered processes */ void stopMonitoringProcesses(); /** * Registers a listener that will be called when a monitored package moves into * the foreground. */ void setProcessMonitorListener(ProcessMonitorListener processMonitorListener); } /** * Listener that can be registered to the ProcessMonitor that will be called when a * monitored package moves into the foreground. */ interface ProcessMonitorListener { /** * Called when a monitored packages moves into the foreground. */ void onPackageRunningDetected(); } static class ProcessMonitorImpl implements ProcessMonitor { /** * A message that can be sent to the background handler to * request an update to the running packages. */ static final int MSG_UPDATE_RUNNING_PACKAGES = 1; /** * A message that can be sent to the main handler to notify * it about the set of running packages */ static final int MSG_PACKAGES_RUNNING = 2; /** * A request sent to the main handler, that can be used to * schedule an update. (Will send {@link #MSG_UPDATE_RUNNING_PACKAGES} * to the background handler). */ static final int MSG_SCHEDULE_NEXT = 3; /** * The context we are bound to */ final Context mContext; /** * The activity-manager to query the running apps */ final ActivityManager mActivityManager; /** * A simple wrapper around all objects that need to be handled with * and can be accessed across multiple threads. */ final MultiThreadState mMts; /** * The set of packages that were received during the last run. */ private final Set<String> mLastSeenPackageList = new HashSet<>(12); /** * True when the monitor is started */ boolean mStarted; long mIntervalMs; /** * The listener that is registered to receive updates. */ ProcessMonitorListener mProcessMonitorListener; /** * The handler that handles it's messages in a background thread. */ Handler mBackgroundHandler; /** * The thread used by {@link #mBackgroundHandler} */ HandlerThread mHandlerThread; @UiThread public ProcessMonitorImpl(Context context) { checkThread("ProcessMonitorImpl"); mContext = context.getApplicationContext(); mMts = new MultiThreadState(); mActivityManager = AppInjector.provideActivityManager(); } static void checkThread(String methodName) { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException( methodName + "() can only be called on the main thread"); } } @Override @UiThread public void startMonitoringProcesses(ArrayList<String> processesToReport, long updateIntervalMs) { checkThread("startMonitoringProcesses"); mMts.setProcessesToReport(processesToReport); if (!mStarted) { startHandler(); mStarted = true; postUpdateMessage(); } } @UiThread public void stopMonitoringProcesses() { checkThread("stopMonitoringProcesses"); mStarted = false; mBackgroundHandler.removeMessages(MSG_UPDATE_RUNNING_PACKAGES); quitHandler(); } @UiThread public void setProcessMonitorListener(ProcessMonitorListener processMonitorListener) { checkThread("setProcessMonitorListener"); mProcessMonitorListener = processMonitorListener; } @UiThread void quitHandler() { checkThread("quitHandler"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { mHandlerThread.quitSafely(); } else { mHandlerThread.quit(); } } @UiThread private void startHandler() { checkThread("startHandler"); mHandlerThread = new HandlerThread("Process-Watcher"); mBackgroundHandler = new Handler(mHandlerThread.getLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message msg) { ProcessMonitorImpl.this.updateRunningPackagesInBackground(); mMts.mMainHandler.sendEmptyMessage(MSG_SCHEDULE_NEXT); return true; } }); } @UiThread void postUpdateMessage() { checkThread("postUpdateMessage"); if (mBackgroundHandler != null) { mBackgroundHandler. sendEmptyMessageDelayed(MSG_UPDATE_RUNNING_PACKAGES, mIntervalMs); } } @WorkerThread void updateRunningPackagesInBackground() { List<RunningAppProcessInfo> processes = mActivityManager.getRunningAppProcesses(); if (processes == null) return; Set<String> running = null; int N = processes.size(); for (int i1 = 0; i1 < N; i1++) { RunningAppProcessInfo process = processes.get(i1); if (process.importance <= RunningAppProcessInfo.IMPORTANCE_VISIBLE) { int len = process.pkgList.length; for (int i = 0; i < len; i++) { String packageName = process.pkgList[i]; if (mMts.isMonitoringPackage(packageName)) { if (running == null) { running = new HashSet<>(1); } running.add(packageName); } } } } if (running != null) { mMts.notifyPackageNamesRunningInBackground(running); } } @UiThread void handleMessageOnMainThread(Message msg) { if (msg.what == MSG_PACKAGES_RUNNING) { // We know we get a Set<String>, so ignore the cast warning //noinspection unchecked onPackagesRunning((Set<String>) msg.obj); } else if (msg.what == MSG_UPDATE_RUNNING_PACKAGES) { postUpdateMessage(); } } @UiThread void onPackagesRunning(Set<String> packageNames) { checkThread("onPackagesRunning"); if (mProcessMonitorListener == null) return; // we only need to know new packages are now also running. // so we compose a list Set<String> temp = new HashSet<>(packageNames); temp.removeAll(mLastSeenPackageList); setLastSeenPackages(packageNames); if (!temp.isEmpty()) { mProcessMonitorListener.onPackageRunningDetected(); } } @UiThread void setLastSeenPackages(Set<String> packageNames) { checkThread("setLastSeenPackages"); mLastSeenPackageList.clear(); mLastSeenPackageList.addAll(packageNames); } class MultiThreadState { final Handler mMainHandler; @GuardedBy("this") private final ArrayList<String> mPackageNamesToReport = new ArrayList<>(8); MultiThreadState() { mMainHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() { @Override public boolean handleMessage(Message msg) { handleMessageOnMainThread(msg); return true; } }); } synchronized boolean isMonitoringPackage(String packageName) { return mPackageNamesToReport.contains(packageName); } public synchronized void setProcessesToReport(ArrayList<String> processesToReport) { mPackageNamesToReport.clear(); mPackageNamesToReport.addAll(processesToReport); } public void notifyPackageNamesRunningInBackground(Set<String> running) { Message message = mMainHandler.obtainMessage(); message.what = MSG_PACKAGES_RUNNING; message.obj = running; message.sendToTarget(); } } } }