package com.aero.control.helpers.PerApp.AppMonitor;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.preference.PreferenceManager;
import com.aero.control.AeroActivity;
import com.aero.control.R;
import com.aero.control.helpers.FilePath;
import com.aero.control.helpers.PerApp.AppMonitor.model.AppElement;
import com.aero.control.helpers.PerApp.AppMonitor.model.AppElementDetail;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
/**
* Created by Alexander Christ on 30.04.15.
*
* Handles the main calls and passes the app context through
*/
public final class JobManager {
private AppData mAppData;
private final String mClassName = getClass().getName();
private static final String mPreferenceValue = "per_app_monitor";
private static final String FILENAME_APPMONITOR_NOTIFY = "appmonitor_notify";
private boolean mJobManagerEnable = true;
private List<AppModule> mModules;
private AppModuleData mAppModuleData;
private boolean mSleeping = false;
private boolean mPrevSleeping = false;
private Context mContext;
private boolean mNotifcationShowed = false;
private long mExportThreshold = 0;
private static JobManager mJobManager;
private JobManager(Context context) {
this.mAppData = new AppData();
this.mModules = new ArrayList<AppModule>();
this.mContext = context;
loadModules();
// We add our loaded modules;
this.mAppModuleData = new AppModuleData(getModules());
// If necessary load the saved raw data back in;
if (Configuration.THREADED_IMPORT) {
Runnable run = new Runnable() {
@Override
public void run() {
try {
importData();
} catch (OutOfMemoryError e) {
AppLogger.print(mClassName, "We tried to import to much data, deleting import file..." + e, 0);
new File(new ContextWrapper(mContext).getFilesDir() + "/" + Configuration.EMERGENCY_FILE).delete();
}
}
};
Thread worker = new Thread(run);
worker.start();
} else {
try {
importData();
} catch (OutOfMemoryError e) {
AppLogger.print(mClassName, "We tried to import to much data, deleting import file..." + e, 0);
new File(new ContextWrapper(mContext).getFilesDir() + "/" + Configuration.EMERGENCY_FILE).delete();
}
}
AppLogger.print(mClassName, "JobManager initialized, AppMonitor Version " + getVersion() + " loaded!", -1);
}
/**
* Instead of creating a new instance of the JobManager directly we call this method
* to synchronize access. It is guaranteed to return a JobManager-Object (creates a
* new one if its NULL).
*
* @param context Context, the current android context
* @return JobManager
*/
public static synchronized JobManager instance(Context context) {
if (mJobManager == null)
mJobManager = new JobManager(context);
return mJobManager;
}
/**
* Enables the JobManager
*/
public final void enable() {
this.mJobManagerEnable = true;
AppLogger.print(mClassName, "JobManager enabled!", 0);
}
/**
* Disables the JobManager
*/
public final void disable() {
this.mJobManagerEnable = false;
AppLogger.print(mClassName, "JobManager disabled!", 0);
}
/**
* Returns the current version of appmonitor.
* @return String
*/
public final String getVersion() {
return Configuration.APPMONITOR_VERSION;
}
/**
* Returns the current state of the JobManager. If true, the JobManager is enabled.
* @return boolean
*/
public final boolean getJobManagerState() {
return mJobManagerEnable;
}
/**
* Sets the current context (androids context).
* @param context
*/
public final void setContext(final Context context) {
this.mContext = context;
}
/**
* If possible, forces a cleanup of the data for a specific app
* @param appname String, the appname (e.g. com.aero.control).
*/
public final void forceCleanUp(String appname) {
AppContext context = getSimpleAppContext(appname);
if (context != null) {
AppModuleMetaData appModuleMetaData = mAppModuleData.existsAppModuleMetaData(context);
if (appModuleMetaData != null) {
appModuleMetaData.cleanUp();
}
context.cleanUp();
}
}
/**
* Main method to get the gathered data in some kind of pretty format.
* It iterates through all available modules(data) for each found app
* and adds them to a custom AppElement object.
* Also sorts the list descending for the app usage time and only adds
* apps which are at least above the minimum time threshold.
*
* @return List<AppElement>
*/
public synchronized final List<AppElement> getParentChildData(final Context context) {
final List<AppElement> data = new ArrayList<AppElement>();
final PackageManager pm = context.getPackageManager();
final List<AppModuleMetaData> appModuleMetaData = this.getModuleData().getAppModuleData();
Drawable appicon;
for (AppModuleMetaData ammd : appModuleMetaData) {
AppLogger.print(mClassName, "App Module Data found! (" + ammd.getAppContext().getAppName() + ")", 2);
AppLogger.print(mClassName, ammd.getAppContext().getAppName() + " Time used: (" + ammd.getAppContext().getTimeUsage() + "ms) " + ":", 2);
// Is this context "ready"?
if (!ammd.getAppContext().isAboveThreshold())
continue;
// Load our package image and our package label + package name here;
try {
appicon = pm.getApplicationIcon(ammd.getAppContext().getAppName());
} catch (PackageManager.NameNotFoundException e) {
appicon = null;
}
final AppElement parentData = new AppElement(ammd.getAppContext().getAppName(), appicon);
// We copy the usage time once into our parent (for sorting reasons) and once into our child;
parentData.setUsage(ammd.getAppContext().getTimeUsage());
parentData.setRealName(ammd.getAppContext().getRealAppName(context));
parentData.getChildData().add(new AppElementDetail(ammd.getAppContext().getFormatTimeUsage(), ""));
for (AppModule module : this.getModules()) {
parentData.getChildData().add(new AppElementDetail(module.getPrefix(), ammd.getAverage(module.getIdentifier()) + module.getSuffix()));
AppLogger.print(mClassName, "------ Average: " + ammd.getAverage(module.getIdentifier()), 2);
}
// Null would mean the app has been deleted or got missing
if (parentData.getRealName() != null)
data.add(parentData);
}
Collections.sort(data, new Comparator<AppElement>() {
@Override
public int compare(AppElement lhs, AppElement rhs) {
return rhs.getUsage().compareTo(lhs.getUsage());
}
});
return data;
}
/**
* Returns the raw data for an app (appname e.g. com.aero.control) for the UI.
* It contains the gathered data for one module(identifier).
* @param appname String, appname [packagename]
* @param identifier int, module identifier (see AppModule for valid identifiers)
* @return List<Integer>
*/
public final List<Integer> getRawData(final String appname, final int identifier) {
final AppContext context = getSimpleAppContext(appname);
if (context == null)
return null;
for (AppModuleMetaData ammd : this.getModuleData().getAppModuleData()) {
if (ammd.getAppContext() == context) {
// Winner!
return ammd.getRawData(identifier);
}
}
return null;
}
/**
* Saves the current available data for all apps we have collected so far
* and their module data.
* Saves the current AppContext for each app with the TimeUsed as well as
* LastChecked value. The data for each module is actually saved as raw data
* inside an array.
* This Method uses the internal API to get the data and saves it to the
* file directory of the app in a JSON-formatted string-like textfile.
*/
public void exportData() {
// Our "parent" which will contain all information;
JSONObject parentJson = new JSONObject();
long time = System.currentTimeMillis();
// Delete the file if read successfully;
new File(new ContextWrapper(mContext).getFilesDir() + "/" + Configuration.EMERGENCY_FILE).delete();
AppLogger.print(mClassName, "Starting emergency write of data...", 0);
try {
// Get all app contexts;
for (AppContext context : mAppData.getAppList()) {
// One for our current app, one for the data of the app
JSONObject currentApp = new JSONObject();
JSONObject appData = new JSONObject();
// Access our small API and get the information needed;
appData.put("TimeUsed", context.getTimeUsage());
appData.put("LastChecked", context.getLastChecked());
appData.put("AppMonitorVersion", this.getVersion());
AppLogger.print(mClassName, "Starting export for: " + context.getAppName(), 1);
List<AppModuleMetaData> moduleMetaData = Collections.synchronizedList(getModuleData().getAppModuleData());
// Get the meta data for all loaded modules;
synchronized (moduleMetaData) {
for (AppModuleMetaData ammd : moduleMetaData) {
// Find our App context;
if (ammd.getAppContext() == context) {
AppLogger.print(mClassName, "Current Context: " + context.getAppName(), 1);
// Iterate through all loaded modules;
for (AppModule module : mModules) {
// For each module we need to get the data separately;
JSONObject appModule = new JSONObject();
// Our actual values are stored in this array;
JSONArray values = new JSONArray();
AppLogger.print(mClassName, "Adding Data for module: " + module.getName(), 1);
// Add our data to our array;
List<Integer> currentValues = Collections.synchronizedList(ammd.getRawData(module.getIdentifier()));
synchronized (currentValues) {
for (Integer i : currentValues) {
values.put(i);
}
}
// Add the data to our object;
appModule.put("Values", values);
// Then add the object to our app data;
appData.put(module.getIdentifier() + "", appModule);
}
}
}
}
// Add the real app name as well as the gathered module data;
currentApp.put(context.getRealAppName(mContext), appData);
// Add everything to our parent;
parentJson.put(context.getAppName(), currentApp);
}
} catch (JSONException e) {
} catch (OutOfMemoryError e) {
AppLogger.print(mClassName, "We got OOM, forcing cleanup! Exception: " + e, 0);
for (AppModuleMetaData ammd : getModuleData().getAppModuleData()) {
forceCleanUp(ammd.getAppContext().getAppName());
}
}
AppLogger.print(mClassName, "Data gathered, writing to disk..", 1);
// Write the data to our private directory [files];
try {
FileOutputStream fos = mContext.openFileOutput(Configuration.EMERGENCY_FILE, Context.MODE_PRIVATE);
BufferedOutputStream bos = new BufferedOutputStream(fos, 8192);
try {
bos.write(parentJson.toString().getBytes());
} catch (OutOfMemoryError e) {
AppLogger.print(mClassName, "We tried to save a too large file, forcing cleanup! Exception: " + e, 0);
for (AppModuleMetaData ammd : getModuleData().getAppModuleData()) {
forceCleanUp(ammd.getAppContext().getAppName());
}
}
bos.flush();
bos.close();
AppLogger.print(mClassName, "Data successfully written to disk in (" + (System.currentTimeMillis() - time) + " ms).", 0);
} catch (IOException e) {
AppLogger.print(mClassName, "Error during data-write..." + e, 0);
}
}
/**
* If found, imports a saved file and accesses the internal APIs to load
* the data back in. Its similar to the normal initialization process
* except it clears all previous data.
*/
public void importData() {
ContextWrapper cw = new ContextWrapper(mContext);
String tmp = null;
long time = System.currentTimeMillis();
// Lock the JobManager during this operation;
this.mSleeping = true;
if (AeroActivity.genHelper.doesExist(cw.getFilesDir() + "/" + Configuration.EMERGENCY_FILE)) {
AppLogger.print(mClassName, "Emergency file detected, starting import... ", 0);
// Read our file and save it in tmp;
try {
InputStream is = mContext.openFileInput(Configuration.EMERGENCY_FILE);
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
tmp = new String(buffer, "UTF-8");
} catch (IOException e) {
AppLogger.print(mClassName, "Error during import... " + e, 0);
this.mSleeping = false;
return;
}
} else {
this.mSleeping = false;
return;
}
// Clear the existing data before;
mAppData.clearData();
mModules.clear();
mModules = new ArrayList<AppModule>();
loadModules();
// We add our loaded modules;
this.mAppModuleData = new AppModuleData(getModules());
this.mAppModuleData.setCleanupEnable(false);
// Beginning JSON parsing...
try {
JSONObject json = new JSONObject(tmp);
Iterator<?> keys = json.keys();
// First we get the actual AppName (e.g. com.aero.control);
while (keys.hasNext()) {
String tempAppName = keys.next().toString();
AppLogger.print(mClassName, tempAppName + " : ", 1);
AppContext localContext = new AppContext(tempAppName);
mAppData.addContext(localContext);
JSONObject appParent = json.getJSONObject(tempAppName);
Iterator<?> appKeys = appParent.keys();
// Next (and a little bit redundant) we get the real AppName (e.g. Aero Control);
while (appKeys.hasNext()) {
String tempApp = appKeys.next().toString();
AppLogger.print(mClassName, tempApp + ": ", 1);
JSONObject appData = appParent.getJSONObject(tempApp);
Iterator<?> dataKeys = appData.keys();
// Finally we get to the data part, first data for our AppContext
while (dataKeys.hasNext()) {
String tempData = dataKeys.next().toString();
// Find module and appcontext data;
try {
// This is our module data;
int i = Integer.parseInt(tempData);
JSONObject moduleData = appData.getJSONObject(tempData);
Iterator<?> moduleKeys = moduleData.keys();
// Get all the data stored inside the arrays of the modules;
while (moduleKeys.hasNext()) {
String tempModule = moduleKeys.next().toString();
ArrayList<Integer> values = new ArrayList<Integer>();
// Add all the data to our array list;
int length = moduleData.getJSONArray(tempModule).length();
for (int j = 0; j < length; j++) {
values.add(Integer.parseInt(moduleData.getJSONArray(tempModule).get(j).toString()));
}
// Go through our modules and read/save data;
for (AppModule module : mModules) {
try {
mAppModuleData.addData(localContext, values, Integer.parseInt(tempData));
} catch (RuntimeException e) {
AppLogger.print(mClassName, "The data for this module was not added, maybe you tried to add data for a non-existing module?", 0);
}
}
AppLogger.print(mClassName, tempModule + ": " + moduleData.getJSONArray(tempModule), 1);
}
} catch (NumberFormatException e) {
// No problem, these are our AppContext data;
AppLogger.print(mClassName, tempData + ": " + appData.get(tempData), 1);
if (tempData.equals("TimeUsed")) {
localContext.setTimeUsage(appData.getLong(tempData));
} else if(tempData.equals("LastChecked")) {
localContext.setLastChecked(appData.getLong(tempData));
}
}
}
}
}
} catch (JSONException e) {
AppLogger.print(mClassName, "Error during json-parsing: " + e, 0);
this.mSleeping = false;
}
this.mSleeping = false;
this.mAppModuleData.setCleanupEnable(true);
AppLogger.print(mClassName, "Import of data successful in (" + (System.currentTimeMillis() - time) + " ms).", 0);
}
/**
* Main method of the JobManager. Iterates through all added modules and gets the values
* for the app context. Also handles the logic if the device is sleeping.
* @param context AppContext which is passed from the calling method.
*/
public final void schedule(final AppContext context) {
// If the context is null, return early;
if (context == null) {
return;
}
if (mPrevSleeping && !mSleeping) {
// Set the last check time, so we don't count sleep-time;
context.setLastCheckedNow();
}
// Return early if we are sleeping;
if (mSleeping) {
return;
}
// Don't export data on the first load;
if (mExportThreshold == 0) {
setExportTimeNow();
}
// If we are above the threshold, export data and set a new threshold;
if (System.currentTimeMillis() > mExportThreshold ) {
exportData();
setExportTimeNow();
}
// Allow to disable (in the next cycle) the JobManager at runtime;
if (!PreferenceManager.getDefaultSharedPreferences(mContext).getBoolean(mPreferenceValue, true))
disable();
AppLogger.print(mClassName, "Calling context switch for: " + context.getAppName(), 1);
mAppData.addContext(context);
// Go through our modules and read/save data;
for (AppModule module : mModules) {
module.operate();
mAppModuleData.addData(context, module.getLastValue(), module);
}
if (!mNotifcationShowed) {
List<AppModuleMetaData> moduleMetaData = Collections.synchronizedList(this.getModuleData().getAppModuleData());
synchronized (moduleMetaData) {
for (AppModuleMetaData ammd : moduleMetaData) {
// Is this context "ready"?
if (ammd.getAppContext().isAboveThreshold()) {
if (!AeroActivity.genHelper.doesExist(mContext.getFilesDir().getAbsolutePath() + "/" + FILENAME_APPMONITOR_NOTIFY))
showNotification();
}
}
}
}
}
/**
* Returns just the AppContext from a given appname if the JobManager is not disabled
* or not sleeping.
* @param appname String, appname (e.g. com.aero.control)
* @return AppContext
*/
public final AppContext getSimpleAppContext(final String appname) {
// Since this class is used for GUI only, the sleeping part should in theory never be true;
if (!mJobManagerEnable || mSleeping) {
if (mSleeping && !mPrevSleeping)
AppLogger.print(mClassName, "JobManager is disabled", 0);
return null;
}
return mAppData.getSimpleAppContext(appname);
}
/**
* Gets the AppContext object from a lower class. If the JobManager is disabled we return
* null. Increases the timely usage and potential other usage counters.
* @param appname String, the app name (e.g. "com.aero.control")
* @return AppContext
*/
public final AppContext getAppContext(final String appname) {
if (!mJobManagerEnable || mSleeping) {
if (mSleeping && !mPrevSleeping)
AppLogger.print(mClassName, "JobManager is disabled", 0);
return null;
}
if (mPrevSleeping && !mSleeping) {
// Set the last check time, so we don't count sleep-time;
if (mAppData.getSimpleAppContext(appname) != null)
mAppData.getSimpleAppContext(appname).setLastCheckedNow();
}
return mAppData.getAppContext(appname);
}
/**
* Allows the JobManager to sleep when the device is sleeping. The actual code to check
* if the screen is turned off/on is not included here.
*
* @param sleepValue boolean, should we sleep or not?
*/
public final void setSleep(final boolean sleepValue) {
// Show debug info, but only once per sleep-cycle;
if (sleepValue && !mSleeping) {
AppLogger.print(mClassName, "JobManager is sleeping because the display is off!", 0);
}
this.mPrevSleeping = mSleeping;
this.mSleeping = sleepValue;
}
/**
* Returns the current sleeping state (true = sleeping)
* @return boolean
*/
public final boolean getSleepState() {
return mSleeping;
}
/**
* Checks if we are sleeping at the moment and wakes the JobManager up.
* Used inside the GUI.
*/
public synchronized final void wakeUp() {
if (getSleepState()) {
AppLogger.print(mClassName, "Forcing a wakeup of the JobManager...", 0);
setSleep(false);
}
}
/**
* Load all desired modules upon start which will then be periodically checked.
*/
private void loadModules() {
int counter = 0;
// Load our modules;
mModules.add(new CPUFreqModule(mContext));
if (Runtime.getRuntime().availableProcessors() > 1) {
// If we just have one core, we dont need this module;
mModules.add(new CPUNumModule(mContext));
}
mModules.add(new RAMModule(mContext));
if (AeroActivity.genHelper.doesExist(FilePath.CPU_TEMP_FILE)) {
mModules.add(new TEMPModule(mContext));
}
for (String s : FilePath.GPU_FILES_RATE) {
if (AeroActivity.genHelper.doesExist(s)) {
counter++;
}
}
if (counter > 0) {
mModules.add(new GPUFreqModule(mContext));
}
AppLogger.print(mClassName, "Modules successfully initialized!", 0);
}
/**
* Gets the complete AppModuleData which is available to the JobManager.
*
* @return AppModuleData
*/
private AppModuleData getModuleData() {
return mAppModuleData;
}
/**
* Sets the current export threshold to now + the configuration parameter (e.g. 1 minute)
*/
private void setExportTimeNow() {
mExportThreshold = System.currentTimeMillis() + Configuration.EXPORT_THRESHOLD;
}
/**
* Gets all available and loaded modules.
*
* @return List<AppModule>
*/
public final List<AppModule> getModules() {
return mModules;
}
/**
* Shows a notification which allows the user to directly enter to appmonitor fragment
*/
protected final void showNotification() {
final Intent resultIntent = new Intent(mContext, AeroActivity.class);
resultIntent.putExtra("NOTIFY_STRING", "APPMONITOR");
resultIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
final PendingIntent viewPendingIntent = PendingIntent.getActivity(mContext, 0, resultIntent, PendingIntent.FLAG_UPDATE_CURRENT);
final Notification.Builder builder = new Notification.Builder(mContext)
.setContentTitle(mContext.getText(R.string.app_name))
.setContentText(mContext.getText(R.string.notify_app_monitor_data))
.setSmallIcon(R.drawable.rocket)
.setContentIntent(viewPendingIntent)
.setAutoCancel(true);
NotificationManager notificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
notificationManager.notify(0, builder.build());
else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
notificationManager.notify(0, builder.getNotification());
try {
final FileOutputStream fos = mContext.openFileOutput(FILENAME_APPMONITOR_NOTIFY, Context.MODE_PRIVATE);
fos.write("1".getBytes());
fos.close();
}
catch (IOException e) {}
this.mNotifcationShowed = true;
}
}