/*
* The MIT License (MIT)
*
* Copyright (c) 2014 Pkmmte Xeleon
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.pkmmte.requestmanager;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipOutputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
public class PkRequestManager extends Static {
// General Public Constants
public static final CompressFormat PNG = CompressFormat.PNG;
public static final CompressFormat JPEG = CompressFormat.JPEG;
public static final CompressFormat WEBP = CompressFormat.WEBP;
public static final String PLAY_LINK_BASE = "https://play.google.com/store/apps/details?id=";
public static final int MAX_PROGRESS = 100;
public static final int STATUS_PRELOAD = 0;
public static final int STATUS_LOADING_INSTALLED = 1;
public static final int STATUS_LOADING_APPFILTER = 2;
public static final int STATUS_FILTERING = 3;
public static final int STATUS_LOADED = 4;
private static final String LOG_TAG = "RequestManager";
// Keep a single instance throughout the app for simplicity
private static PkRequestManager mInstance = null;
// For issue tracking purposes
private boolean debugEnabled;
// Custom request configuration data
private RequestSettings mSettings;
// Context is always useful for some reason.
private Context mContext;
// Keep an activity instance for those moments you really need it
private Activity mActivity;
// Keep track of how many requests were sent this session
private int numRequestsSent;
// List of installed app info and already defined apps as well as filtered apps
private List<AppInfo> mApps;
private List<AppInfo> mInstalledApps;
private List<String> mDefinedApps;
// Background threads
private AsyncTask<Void, Void, Void> loadTask;
private AsyncTask<Void, Void, Void> sendTask;
private AsyncTask<Void, Void, Void> autoTask;
// Listeners for various loading events
private List<InstalledAppLoadListener> mInstalledAppLoadListeners;
private List<AppFilterListener> mAppFilterListeners;
private List<AppLoadListener> mAppLoadListeners;
private List<SendRequestListener> mSendRequestListeners;
/**
* Standard RequestManager constructor.
*
* @param context
*/
public PkRequestManager(Context context) {
this.debugEnabled = false;
this.mSettings = new RequestSettings();
this.mContext = context;
this.numRequestsSent = 0;
this.mApps = new ArrayList<AppInfo>();
this.mInstalledApps = new ArrayList<AppInfo>();
this.mDefinedApps = new ArrayList<String>();
this.mInstalledAppLoadListeners = new ArrayList<InstalledAppLoadListener>();
this.mAppFilterListeners = new ArrayList<AppFilterListener>();
this.mAppLoadListeners = new ArrayList<AppLoadListener>();
this.mSendRequestListeners = new ArrayList<SendRequestListener>();
this.initLoadingTask();
this.initSendTask();
this.initAutomaticTask();
}
/**
* Creates a global RequestManager instance.
*
* @param context
*/
public static void createInstance(Context context) {
if (mInstance == null)
mInstance = new PkRequestManager(context.getApplicationContext());
}
/**
* Returns the global instance of this RequestManager.
* If you don't remember whether or not you already created
* a previous instance, add the context as a parameter.
*
* @return
*/
public static PkRequestManager getInstance() {
return mInstance;
}
/**
* Returns the global instance of this RequestManager.
*
* @param context
* @return
*/
public static PkRequestManager getInstance(Context context) {
if (mInstance == null)
mInstance = new PkRequestManager(context.getApplicationContext());
return mInstance;
}
/**
* Loads all apps installed on the device. This will also filter those
* already in the appfilter if you have that enabled in settings.
* All progress is reported via listeners.
* <p/>
* This can take a while to process so make sure you're running it on
* a background thread. You can also call the "loadAppsAsync()" method.
*/
public void loadApps() {
// Loads already apps installed on device
loadInstalledAppInfo();
// If enabled, also load apps in the appfilter and... filter them
if (mSettings.getFilterDefined()) {
loadDefinedAppInfo();
filterAppInfo();
}
}
/**
* Exactly like "loadApps()" but runs in it's own parallel background thread.
* <p/>
* Loads all apps installed on the device asynchronously. This will also filter
* those already in the appfilter if you have that enabled in settings.
* All progress is reported via listeners.
*/
public void loadAppsAsync() {
loadAppsAsync(true);
}
/**
* Exactly like "loadApps()" but runs in it's own background thread.
* <p/>
* Loads all apps installed on the device asynchronously. This will also filter
* those already in the appfilter if you have that enabled in settings.
* All progress is reported via listeners.
*
* @param parallel Boolean indicating whether to run serially or in parallel.
* True for parallel, False for serial.
*/
public void loadAppsAsync(boolean parallel) {
if (loadTask.getStatus() == AsyncTask.Status.PENDING) {
// Execute task if it's ready to go!
loadTask.executeOnExecutor(parallel ? AsyncTask.THREAD_POOL_EXECUTOR : AsyncTask.SERIAL_EXECUTOR);
} else if (loadTask.getStatus() == AsyncTask.Status.RUNNING && debugEnabled) {
// Don't execute if already running
Log.d(LOG_TAG, "Task is already running...");
} else if (loadTask.getStatus() == AsyncTask.Status.FINISHED) {
// Okay, this is not supposed to happen. Reset and recall.
if (debugEnabled)
Log.d(LOG_TAG, "Uh oh, it appears the task has finished without being reset. Resetting task...");
initLoadingTask();
loadAppsAsync(parallel);
}
}
/**
* Loads all apps if they have not yet been loaded.
* This method keeps in mind your current settings.
*/
public void loadAppsIfEmpty() {
if (!appsLoaded())
loadApps();
}
/**
* Exactly like "loadAppsIfEmpty()" but runs in it's own background thread.
* <p/>
* Loads all apps if they have not yet been loaded.
* This method keeps in mind your current settings.
*/
public void loadAppsIfEmptyAsync() {
if (!appsLoaded())
loadAppsAsync();
}
/**
* Sends a request via email. This will return prematurely if no emails have
* been set in your settings or if no apps were selected. Data sent depends on
* your settings. All progress is reported via listeners.
* <p/>
* Note: The send intent won't run immediately on some devices. As a hacky workaround,
* make sure to "setActivity()" right before running this. The listener also passes the
* send intent along with all the data attached to it. Manually launch the intent if you
* prefer not to attach the activity.
*/
public void sendRequest() {
sendRequest(false, false);
}
/**
* Sends a request via email. This will return prematurely if no emails have
* been set in your settings or if no apps were selected. Data sent depends on
* your settings. All progress is reported via listeners.
* <p/>
* Do not pass any parameters unless you know what you're doing!
* <p/>
* Note: The send intent won't run immediately on some devices. As a hacky workaround,
* make sure to "setActivity()" right before running this. The listener also passes the
* send intent along with all the data attached to it. Manually launch the intent if you
* prefer not to attach the activity.
*
* @param selectAll
* @param automatic
*/
public void sendRequest(boolean selectAll, boolean automatic) {
if (debugEnabled)
Log.d(LOG_TAG, "Sending request...");
// Retrieve proper apps
final List<AppInfo> mAppList = automatic ? getAutomaticApps() : getApps();
// Store used settings into local variables for slightly better performance
final String[] emailAddresses = mSettings.getEmailAddresses();
final String emailSubject = mSettings.getEmailSubject();
final String emailPrecontent = mSettings.getEmailPrecontent();
final String saveLoc = mSettings.getSaveLocation();
final String saveLoc2 = mSettings.getSaveLocation2();
final CompressFormat compressFormat = mSettings.getCompressFormat();
final boolean appendInformation = mSettings.getAppendInformation();
final boolean createAppfilter = mSettings.getCreateAppfilter();
final boolean createZip = mSettings.getCreateZip();
final int compressQuality = mSettings.getCompressQuality();
// Check to see if data being set is valid.
if (mAppList == null || mAppList.size() == 0) {
if (debugEnabled)
Log.d(LOG_TAG, "App List is either null or empty! Canceling send request...");
return;
}
// Check to see if email is set, return if not. No point in doing work without anywhere to send it.
if (emailAddresses == null) {
if (debugEnabled)
Log.d(LOG_TAG, "No proper email addresses were set! Canceling send request...");
return;
}
// Loop through all listeners notifying them
for (SendRequestListener mListener : mSendRequestListeners) {
mListener.onRequestStart(automatic);
}
// Create file instances based on save locations
File saveLocation = new File(saveLoc);
File saveLocation2 = new File(saveLoc2);
// Delete previous zips and files, if any.
deleteDirectory(saveLocation);
deleteDirectory(saveLocation2);
// Recreate directories
saveLocation.mkdirs();
saveLocation2.mkdirs();
// Initialize email builder and xml builder (For generating optional appfilter.xml values)
StringBuilder emailBuilder = new StringBuilder();
StringBuilder xmlBuilder = new StringBuilder();
// Append email precontent (if enabled)
if (debugEnabled)
Log.d(LOG_TAG, "Appending email precontent" + (appendInformation ? " and device information" : ""));
emailBuilder.append(emailPrecontent);
// Loop through all selected app packages
if (debugEnabled)
Log.d(LOG_TAG, "Generating data...");
final int numApps = mAppList.size();
int numSelected = 0;
int progress = 0;
for (AppInfo mAppInfo : mAppList) {
// Loop through all listeners notifying them
progress = (int) (mAppList.indexOf(mAppInfo) * MAX_PROGRESS) / numApps;
for (SendRequestListener mListener : mSendRequestListeners) {
mListener.onRequestBuild(automatic, progress);
}
// Only deal with selected apps
if (selectAll || mAppInfo.isSelected()) {
// Build email content
emailBuilder.append("Name: " + mAppInfo.getName() + "\n");
emailBuilder.append("Code: " + mAppInfo.getCode() + "\n");
emailBuilder.append("Link: " + PLAY_LINK_BASE + mAppInfo.getCode().split("/")[0] + "\n");
emailBuilder.append("\n\n");
// Generate appfilter content (if enabled)
if (createAppfilter) {
xmlBuilder.append("<!-- " + mAppInfo.getName() + " -->\n");
xmlBuilder.append("<item component=\"ComponentInfo{" + mAppInfo.getCode() + "}\" drawable=\"" + convertDrawableName(mAppInfo.getName()) + "\"/>" + "\n");
}
// Save drawable if we're going to compress into a zip later
if (createZip) {
// Attempt to use highest density drawable available
Drawable appImage = getHighResDrawable(mAppInfo);
// Fall back to regular drawable if unable to retrieve higher res
if (appImage == null)
appImage = mAppInfo.getImage();
// Convert drawable into a bitmap before saving it
Bitmap bitmap = drawableToBitmap(appImage);
// Write bitmap to storage as PNG
try {
String bmDir = saveLoc2 + "/" + mAppInfo.getCode().split("/")[0] + "_" + mAppInfo.getCode().split("/")[1] + ".png";
new File(bmDir).getParentFile().mkdirs();
FileOutputStream fOut = new FileOutputStream(bmDir);
bitmap.compress(compressFormat, compressQuality, fOut);
fOut.flush();
fOut.close();
} catch (FileNotFoundException e) {
if (debugEnabled) {
Log.e(LOG_TAG, "FileNotFoundException! Make sure the file isn't open in another app and you have writing permissions in your manifest.");
e.printStackTrace();
}
} catch (IOException e) {
if (debugEnabled) {
Log.e(LOG_TAG, "IOException! Make sure you have the appropriate permissions.");
e.printStackTrace();
}
}
}
// Increase our counter for number of apps selected
numSelected++;
}
}
// Append device information (if enabled)
if (appendInformation) {
emailBuilder.append("\nOS Version: " + System.getProperty("os.version") + "(" + Build.VERSION.INCREMENTAL + ")");
emailBuilder.append("\nOS API Level: " + Build.VERSION.SDK_INT);
emailBuilder.append("\nDevice: " + Build.DEVICE);
emailBuilder.append("\nManufacturer: " + Build.MANUFACTURER);
emailBuilder.append("\nModel (and Product): " + Build.MODEL + " (" + Build.PRODUCT + ")");
try {
PackageInfo appInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
emailBuilder.append("\nApp Version Name: " + appInfo.versionName);
emailBuilder.append("\nApp Version Code: " + appInfo.versionCode);
} catch (Exception e) {
if (debugEnabled)
Log.d(LOG_TAG, "Unable to append app version name/code...");
}
}
// Return if no apps were selected
if (numSelected == 0) {
if (debugEnabled)
Log.d(LOG_TAG, "No apps were selected. Can't send request data without any data!");
return;
} else if (debugEnabled)
Log.d(LOG_TAG, "Successfully collected data for " + numSelected + " apps!");
// Write appfilter.xml to storage, if enabled
if (createAppfilter) {
if (debugEnabled)
Log.d(LOG_TAG, "Writing appfilter.xml...");
try {
new File(saveLoc2 + "/appfilter.xml").getParentFile().mkdirs();
FileWriter fstream = new FileWriter(saveLoc2 + "/appfilter.xml");
BufferedWriter out = new BufferedWriter(fstream);
out.write(xmlBuilder.toString());
out.close();
if (debugEnabled)
Log.d(LOG_TAG, "Succesfully wrote appfilter.xml to " + saveLoc2 + "/appfilter.xml!");
} catch (Exception e) {
if (debugEnabled) {
Log.d(LOG_TAG, "Error writing generated appfilter.xml! Make sure you have writing permissions in your AndroidManifest.xml");
e.printStackTrace();
}
}
}
// Create a zip file with the current date, if enabled
String zipName = "mZip";
if (createZip) {
if (debugEnabled)
Log.d(LOG_TAG, "Zipping files...");
SimpleDateFormat date = new SimpleDateFormat("yyyyMMdd_hhmmss", Locale.getDefault());
zipName = date.format(new Date());
createZipFile(saveLoc2, true, saveLoc + "/" + zipName + ".zip");
}
// Initialize send intent with proper address, subject, and body values
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_EMAIL, emailAddresses);
intent.putExtra(Intent.EXTRA_SUBJECT, emailSubject);
intent.putExtra(Intent.EXTRA_TEXT, emailBuilder.toString());
// Attach zip/appfilter.xml if enabled
if (createZip) {
intent.setType("application/zip");
final Uri uri = Uri.parse("file://" + saveLoc + "/" + zipName + ".zip");
intent.putExtra(Intent.EXTRA_STREAM, uri);
} else if (createAppfilter) {
intent.setType("text/plain");
final Uri uri = Uri.parse("file://" + saveLoc2 + "/appfilter.xml");
intent.putExtra(Intent.EXTRA_STREAM, uri);
}
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (debugEnabled)
Log.d(LOG_TAG, "Successfully collected data!");
// Start the intent!
boolean intentSuccessful;
try {
mActivity.startActivity(Intent.createChooser(intent, "Send Email"));
intentSuccessful = true;
} catch (ActivityNotFoundException localActivityNotFoundException) {
if (debugEnabled)
Log.d(LOG_TAG, "No email app has been found!");
intentSuccessful = false;
} catch (Exception e) {
if (debugEnabled) {
Log.d(LOG_TAG, "There was an error starting the intent!");
e.printStackTrace();
}
intentSuccessful = false;
}
// Increase the number of sent requests, even if it failed
numRequestsSent++;
// Loop through all listeners notifying them
for (SendRequestListener mListener : mSendRequestListeners) {
mListener.onRequestFinished(automatic, intentSuccessful, intent);
}
}
/**
* Exactly like "sendRequest()" but runs in it's own parallel background thread.
* <p/>
* Sends a request via email. This will return prematurely if no emails have
* been set in your settings or if no apps were selected. Data sent depends on
* your settings. All progress is reported via listeners.
* <p/>
* Note: The send intent won't run immediately on some devices. As a hacky workaround,
* make sure to "setActivity()" right before running this. The listener also passes the
* send intent along with all the data attached to it. Manually launch the intent if you
* prefer not to attach the activity.
*/
public void sendRequestAsync() {
sendRequestAsync(true);
}
/**
* Exactly like "sendRequest()" but runs in it's own background thread.
* <p/>
* Sends a request via email. This will return prematurely if no emails have
* been set in your settings or if no apps were selected. Data sent depends on
* your settings. All progress is reported via listeners.
* <p/>
* Note: The send intent won't run immediately on some devices. As a hacky workaround,
* make sure to "setActivity()" right before running this. The listener also passes the
* send intent along with all the data attached to it. Manually launch the intent if you
* prefer not to attach the activity.
*
* @param parallel Boolean indicating whether to run serially or in parallel.
* True for parallel, False for serial.
*/
public void sendRequestAsync(boolean parallel) {
if (sendTask.getStatus() == AsyncTask.Status.PENDING) {
// Execute task if it's ready to go!
sendTask.executeOnExecutor(parallel ? AsyncTask.THREAD_POOL_EXECUTOR : AsyncTask.SERIAL_EXECUTOR);
} else if (sendTask.getStatus() == AsyncTask.Status.RUNNING && debugEnabled) {
// Don't execute if already running
Log.d(LOG_TAG, "Task is already running...");
} else if (sendTask.getStatus() == AsyncTask.Status.FINISHED) {
// Okay, this is not supposed to happen. Reset and recall.
if (debugEnabled)
Log.d(LOG_TAG, "Uh oh, it appears the task has finished without being reset. Resetting task...");
initSendTask();
sendRequestAsync(parallel);
}
}
/**
* Automatically loads all app information (if not already) and sends it.
* Data being sent depends on your settings.
* <p/>
* Warning: Do not run this on the main UI thread or you might get an ANR
* (Application Not Responding)! Use a background thread or call "sendAutomaticRequestAsync()" instead
*/
public void sendAutomaticRequest() {
// Load list of apps, if not already loaded
if (mInstalledApps.size() == 0 || (mSettings.getFilterAutomatic() && (mApps.size() == 0 || mDefinedApps.size() == 0))) {
loadInstalledAppInfo();
if (mSettings.getFilterAutomatic()) {
loadDefinedAppInfo();
filterAppInfo();
}
}
// Send request, marking all apps as selected
sendRequest(true, true);
}
/**
* Exactly like "sendAutomaticRequest()" but runs on it's own parallel background thread.
* <p/>
* Automatically loads all app information (if not already) and sends it.
* Data being sent depends on your settings.
*/
public void sendAutomaticRequestAsync() {
sendAutomaticRequestAsync(true);
}
/**
* Exactly like "sendAutomaticRequest()" but runs on it's own background thread.
* <p/>
* Automatically loads all app information (if not already) and sends it.
* Data being sent depends on your settings.
*
* @param parallel Boolean indicating whether to run serially or in parallel.
* True for parallel, False for serial.
*/
public void sendAutomaticRequestAsync(boolean parallel) {
if (autoTask.getStatus() == AsyncTask.Status.PENDING) {
// Execute task if it's ready to go!
autoTask.executeOnExecutor(parallel ? AsyncTask.THREAD_POOL_EXECUTOR : AsyncTask.SERIAL_EXECUTOR);
} else if (autoTask.getStatus() == AsyncTask.Status.RUNNING && debugEnabled) {
// Don't execute if already running
Log.d(LOG_TAG, "Task is already running...");
} else if (autoTask.getStatus() == AsyncTask.Status.FINISHED) {
// Okay, this is not supposed to happen. Reset and recall.
if (debugEnabled)
Log.d(LOG_TAG, "Uh oh, it appears the task has finished without being reset. Resetting task...");
initAutomaticTask();
sendAutomaticRequestAsync(parallel);
}
}
/**
* Adds an InstalledAppLoadListener to this global instance.
*
* @param listener
*/
public void addInstalledAppLoadListener(InstalledAppLoadListener listener) {
mInstalledAppLoadListeners.add(listener);
}
/**
* Removes an InstalledAppLoadListener from this global instance.
*
* @param listener
*/
public void removeInstalledAppLoadListener(InstalledAppLoadListener listener) {
mInstalledAppLoadListeners.remove(listener);
}
/**
* Removes all InstalledAppLoadListeners from this global instance.
*/
public void removeAllInstalledAppLoadListeners() {
mInstalledAppLoadListeners.clear();
}
/**
* Adds an AppFilterListener to this global instance.
*
* @param listener
*/
public void addAppFilterListener(AppFilterListener listener) {
mAppFilterListeners.add(listener);
}
/**
* Removes an AppFilterListener from this global instance.
*
* @param listener
*/
public void removeAppFilterListener(AppFilterListener listener) {
mAppFilterListeners.remove(listener);
}
/**
* Removes all AppFilterListeners from this global instance.
*/
public void removeAllAppFilterListeners() {
mAppFilterListeners.clear();
}
/**
* Adds an AppLoadListener to this global instance.
*
* @param listener
*/
public void addAppLoadListener(AppLoadListener listener) {
mAppLoadListeners.add(listener);
}
/**
* Removes an AppLoadListener from this global instance.
*
* @param listener
*/
public void removeAppLoadListener(AppLoadListener listener) {
mAppLoadListeners.remove(listener);
}
/**
* Removes all AppFilterListeners from this global instance.
*/
public void removeAllAppInfoLoadListeners() {
mAppLoadListeners.clear();
}
/**
* Adds an SendRequestListener to this global instance.
*
* @param listener
*/
public void addSendRequestListener(SendRequestListener listener) {
mSendRequestListeners.add(listener);
}
/**
* Removes an SendRequestListener from this global instance.
*
* @param listener
*/
public void removeSendRequestListener(SendRequestListener listener) {
mSendRequestListeners.remove(listener);
}
/**
* Removes all SendRequestListeners from this global instance.
*/
public void removeAllSendRequestListeners() {
mSendRequestListeners.clear();
}
/**
* Removes all listeners from this global instance.
*/
public void removeAllListeners() {
mInstalledAppLoadListeners.clear();
mAppFilterListeners.clear();
mAppLoadListeners.clear();
mSendRequestListeners.clear();
}
/**
* Deletes previous request data stored on the user's device.
* This may include zips and files.
*/
public void deleteRequestData() {
// Create file instances based on save locations
File saveLocation = new File(mSettings.getSaveLocation());
File saveLocation2 = new File(mSettings.getSaveLocation2());
// Delete previous zips and files, if any.
deleteDirectory(saveLocation);
deleteDirectory(saveLocation2);
}
/**
* Attempts to cancel execution of this task.
* This attempt will fail if the task has already completed,
* already been cancelled, or could not be cancelled for some other reason.
* If the task has already started, then the mayInterruptIfRunning parameter
* determines whether the thread executing this task should be interrupted
* in an attempt to stop the task.
* <p/>
* This applies only if you called the <b>Async</b> variant.
*
* @param mayInterruptIfRunning
*/
public void cancelLoadingTask(boolean mayInterruptIfRunning) {
this.loadTask.cancel(mayInterruptIfRunning);
this.initLoadingTask();
}
/**
* Attempts to cancel execution of this task.
* This attempt will fail if the task has already completed,
* already been cancelled, or could not be cancelled for some other reason.
* If the task has already started, then the mayInterruptIfRunning parameter
* determines whether the thread executing this task should be interrupted
* in an attempt to stop the task.
* <p/>
* This applies only if you called the <b>Async</b> variant.
*
* @param mayInterruptIfRunning
*/
public void cancelSendTask(boolean mayInterruptIfRunning) {
this.sendTask.cancel(mayInterruptIfRunning);
this.initSendTask();
}
/**
* Attempts to cancel execution of this task.
* This attempt will fail if the task has already completed,
* already been cancelled, or could not be cancelled for some other reason.
* If the task has already started, then the mayInterruptIfRunning parameter
* determines whether the thread executing this task should be interrupted
* in an attempt to stop the task.
* <p/>
* This applies only if you called the <b>Async</b> variant.
*
* @param mayInterruptIfRunning
*/
public void cancelAutomaticTask(boolean mayInterruptIfRunning) {
this.autoTask.cancel(mayInterruptIfRunning);
this.initAutomaticTask();
}
/**
* Returns a boolean letting you know if apps have been loaded.
* This keeps in mind the settings you have set.
*
* @return
*/
public boolean appsLoaded() {
return !(mInstalledApps.size() == 0 || (mSettings.getFilterDefined() && (mApps.size() == 0 || mDefinedApps.size() == 0)));
}
/**
* Returns an ArrayList of AppInfo objects.
* If apps have no been filtered, it'll return all installed apps.
* Otherwise, this will return a list of filtered apps.
*
* @return
*/
public List<AppInfo> getApps() {
if (mApps.size() == 0 || !mSettings.getFilterDefined())
return this.mInstalledApps;
else
return this.mApps;
}
/**
* Returns an ArrayList of AppInfo objects.
* If apps have no been filtered, it'll return all installed apps.
* Otherwise, this will return a list of filtered apps.
* <p/>
* Unless you're implementing your own automatic method,
* call "getApps()" instead.
*
* @return
*/
public List<AppInfo> getAutomaticApps() {
if (mApps.size() == 0 || !mSettings.getFilterAutomatic())
return this.mInstalledApps;
else
return this.mApps;
}
/**
* Returns an ArrayList of AppInfo objects.
* All apps in this list are those installed on the device.
*
* @return
*/
public List<AppInfo> getInstalledApps() {
return this.mInstalledApps;
}
/**
* Returns a String ArrayList containing component info
* gathered from the appfilter. This is used to filter
* out already-supported apps but you might find it useful
* for something else.
*
* @return
*/
public List<String> getDefinedApps() {
return this.mDefinedApps;
}
/**
* Returns the number of requests sent during this session.
* This counts canceled & failed requests.
*
* @return
*/
public int getNumRequestSent() {
return this.numRequestsSent;
}
/**
* Returns an integer count with the total number of
* apps currently selected. This keeps in mind your settings
* and will not work if you don't have the proper reference.
*
* @return int
*/
public int getNumSelected() {
int count = 0;
List<AppInfo> mAppList = getApps();
for (AppInfo mApp : mAppList) {
if (mApp.isSelected())
count++;
}
return count;
}
/**
* Selects all AppInfo Objects in the list
*/
public void selectAll() {
List<AppInfo> mAppList = getApps();
for (AppInfo mApp : mAppList) {
mApp.setSelected(true);
}
}
/**
* Deselects all AppInfo Objects in the list
*/
public void deselectAll() {
List<AppInfo> mAppList = getApps();
for (AppInfo mApp : mAppList) {
mApp.setSelected(false);
}
}
/**
* Returns a RequestSettings object with all values set.
*
* @return
*/
public RequestSettings getSettings() {
return this.mSettings;
}
/**
* Applies new settings using your own custom RequestSettings object.
*
* @param settings
*/
public void setSettings(RequestSettings settings) {
this.mSettings = settings;
}
/**
* References your current activity. This is only used as a hacky workaround
* for when the send intent doesn't automatically open on some devices.
* <p/>
* You can also use a OnRequestSendListener to catch the send intent and
* start it manually instead of using this workaround.
*
* @param activity
*/
public void setActivity(Activity activity) {
this.mActivity = activity;
}
/**
* Set the debug status for this manager.
* If true, it will periodically print logs of current
* progress so you can see what's going on.
* <p/>
* I suggest you disable this during production as it
* will consume unnecessary processing power. Besides,
* you don't want to spam your users' logs.
*
* @param debug
*/
public void setDebugging(boolean debug) {
this.debugEnabled = debug;
}
/**
* Loads and parses the appfilter.xml for already-
* defined app info. This helps filter out apps already
* supported in your theme.
*/
private void loadDefinedAppInfo() {
try {
// Initialize XmlPullParser and set the appfilter.xml (from assets) as the input
XmlPullParserFactory xmlFactoryObject = XmlPullParserFactory.newInstance();
XmlPullParser mParser = xmlFactoryObject.newPullParser();
AssetManager assManager = mContext.getAssets();
InputStream inputStream = assManager.open(mSettings.getAppfilterName());
mParser.setInput(inputStream, null);
// Loop through all listeners notifying them
for (AppFilterListener mListener : mAppFilterListeners) {
mListener.onAppPrefilter();
}
// Read number of elements for progress
int numElements = 0;
try {
InputStream testStream = assManager.open(mSettings.getAppfilterName());
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docFactory.newDocumentBuilder();
Document doc = docBuilder.parse(testStream);
NodeList list = doc.getElementsByTagName("item");
numElements = list.getLength();
} catch (Exception e) {
if (debugEnabled) {
Log.d(LOG_TAG, "Error finding the number of appfilter elements!");
e.printStackTrace();
}
}
if (debugEnabled)
Log.d(LOG_TAG, "" + numElements + " items found in appfilter.");
// Clear defined apps before adding more
mDefinedApps.clear();
// Keep track of the event type. Also, reuse one string for memory purposes
String mAppCode = null;
int eventType = mParser.getEventType();
// Loop through ALL the appfilter
int count = 0;
int progress = 0;
while (eventType != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG) {
String elementName = mParser.getName();
if (elementName.equals("item")) {
count++;
progress = (int) (count * MAX_PROGRESS) / numElements;
// Loop through all listeners notifying them
for (AppLoadListener mListener : mAppLoadListeners) {
mListener.onAppLoading(STATUS_LOADING_APPFILTER, (int) ((MAX_PROGRESS / 3) + (progress / 3)));
}
for (AppFilterListener mListener : mAppFilterListeners) {
mListener.onAppFiltering(progress, 0);
}
try {
// Read package and activity name
mAppCode = mParser.getAttributeValue(null, "component");
mAppCode = mAppCode.substring(14, mAppCode.length() - 1);
// Add new info to our ArrayList and reset the object. Log commented out to reduce logcat spam.
mDefinedApps.add(mAppCode);
//if(debugEnabled)
// Log.d(LOG_TAG, "Added appfilter app:\n" + mAppCode);
mAppCode = null;
} catch (Exception e) {
// Log and ignore whatever individual error was found.
if (debugEnabled) {
Log.e(LOG_TAG, "Error adding parsed appfilter item!");
e.printStackTrace();
}
}
}
}
eventType = mParser.next();
}
} catch (IOException eIO) {
if (debugEnabled)
Log.e(LOG_TAG, "Unable to read appfilter.xml! Are you sure you have in it your assets folder?");
} catch (XmlPullParserException eXPPE) {
if (debugEnabled) {
Log.e(LOG_TAG, "Unknown XmlPullParserException! This may be caused by a malformed appfilter.xml. Make sure it's fine.");
eXPPE.printStackTrace();
}
}
}
/**
* Loads locally installed apps. These are the ones that will
* eventually be requested.
*/
private void loadInstalledAppInfo() {
// Create package manager and sort it
PackageManager pm = mContext.getPackageManager();
List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
Collections.sort(packages, new ApplicationInfo.DisplayNameComparator(pm));
// Loop through all listeners notifying them
for (AppLoadListener mListener : mAppLoadListeners) {
mListener.onAppLoading(STATUS_PRELOAD, 0);
}
for (AppLoadListener mListener : mAppLoadListeners) {
mListener.onAppPreload();
}
// Loop through all listeners notifying them
for (InstalledAppLoadListener mListener : mInstalledAppLoadListeners) {
mListener.onInstalledAppsPreload();
}
// Clear installed apps before reloading them
mInstalledApps.clear();
// Reuse app info object for memory purposes. Same goes for other string values.
AppInfo mAppInfo = null;
Intent launchIntent = null;
String launchStr = null;
String appCode = null;
String[] splitCode = null;
// Loops through the package manager to find all installed apps
final boolean filterDefined = mSettings.getFilterDefined();
final int numPackages = packages.size();
int progress = 0;
for (ApplicationInfo packageInfo : packages) {
// Examine only valid packages
launchIntent = pm.getLaunchIntentForPackage(packageInfo.packageName);
if (launchIntent == null || launchIntent.equals("")) {
continue;
}
// Loop through all listeners notifying them
progress = (int) (packages.indexOf(packageInfo) * MAX_PROGRESS) / numPackages;
for (AppLoadListener mListener : mAppLoadListeners) {
mListener.onAppLoading(STATUS_LOADING_INSTALLED, filterDefined ? (int) (progress / 3) : progress);
}
for (InstalledAppLoadListener mListener : mInstalledAppLoadListeners) {
mListener.onInstalledAppsLoading(progress);
}
// Initialize reusable AppInfo object with default values
mAppInfo = new AppInfo();
// Set app information from the package manager
mAppInfo.setImage(packageInfo.loadIcon(pm));
mAppInfo.setName(packageInfo.loadLabel(pm).toString());
// Get app launch intent and trim it
launchStr = launchIntent.toString().split("cmp=")[1];
appCode = launchStr.substring(0, launchStr.length() - 1);
splitCode = appCode.split("/");
if (splitCode[1].startsWith("."))
appCode = splitCode[0] + "/" + splitCode[0] + splitCode[1];
appCode = appCode.trim();
// Set the trimmed app code
mAppInfo.setCode(appCode);
// Add new info to our ArrayList. Log commented out to reduce logcat spam.
mInstalledApps.add(mAppInfo);
//if(debugEnabled)
// Log.d(LOG_TAG, "Added installed app:\n" + mAppInfo.toString());
// Nullify objects to make the job easier for the GC
mAppInfo = null;
launchStr = null;
appCode = null;
splitCode = null;
}
// Loop through all listeners notifying them
for (InstalledAppLoadListener mListener : mInstalledAppLoadListeners) {
mListener.onInstalledAppsLoaded();
}
if (!filterDefined) {
for (AppLoadListener mListener : mAppLoadListeners) {
mListener.onAppLoading(STATUS_LOADED, MAX_PROGRESS);
}
}
}
/**
* Filters apps based on your appfilter components.
* All filtered apps are copied over to the "mApps" ArrayList.
*/
private void filterAppInfo() {
// Clear list of filtered apps before adding more
mApps.clear();
// Loop through all installed apps
final int numApps = mInstalledApps.size();
int progress = 0;
for (AppInfo mAppInfo : mInstalledApps) {
// Loop through all listeners notifying them
progress = (int) (mInstalledApps.indexOf(mAppInfo) * MAX_PROGRESS) / numApps;
for (AppLoadListener mListener : mAppLoadListeners) {
mListener.onAppLoading(STATUS_FILTERING, (int) ((MAX_PROGRESS / 3 * 2) + (progress / 3)));
}
for (AppFilterListener mListener : mAppFilterListeners) {
mListener.onAppFiltering(MAX_PROGRESS, progress);
}
// Check if app is already defined and filter it accordingly
if (!mDefinedApps.contains(mAppInfo.getCode())) {
mApps.add(mAppInfo);
//if(debugEnabled)
// Log.d(LOG_TAG, "Filtered IN : " + mAppInfo.getName());
}
//else if(debugEnabled)
// Log.d(LOG_TAG, "Filtered OUT : " + mAppInfo.getName());
}
if (debugEnabled)
Log.d(LOG_TAG, "Finished filtering apps!");
// Loop through all listeners notifying them
for (AppLoadListener mListener : mAppLoadListeners) {
mListener.onAppLoading(STATUS_LOADED, MAX_PROGRESS);
}
for (AppLoadListener mListener : mAppLoadListeners) {
mListener.onAppLoaded();
}
for (AppFilterListener mListener : mAppFilterListeners) {
mListener.onAppFiltered();
}
}
/**
* Created a .zip file... nuff said.
*
* @param path
* @param keepDirectoryStructure
* @param outputFile
* @return
*/
private boolean createZipFile(String path, boolean keepDirectoryStructure, String outputFile) {
File f = new File(path);
if (!f.canRead() || !f.canWrite()) {
if (debugEnabled)
Log.d(LOG_TAG, path + " cannot be compressed due to file permissions!");
return false;
}
try {
ZipOutputStream zip_out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(outputFile), mSettings.getByteBuffer()));
if (keepDirectoryStructure) {
zipFile(path, zip_out, "");
} else {
final File files[] = f.listFiles();
for (final File file : files) {
zipFolder(file, zip_out);
}
}
zip_out.close();
} catch (FileNotFoundException e) {
if (debugEnabled)
Log.e("File not found: ", e.getMessage());
return false;
} catch (IOException e) {
if (debugEnabled)
Log.e("IOException: ", e.getMessage());
return false;
}
return true;
}
/**
* Zips a file. Does this really need any explanation?
*
* @param path
* @param out
* @param relPath
* @throws java.io.IOException
*/
private void zipFile(String path, ZipOutputStream out, String relPath) throws IOException {
File file = new File(path);
if (!file.exists()) {
if (debugEnabled)
Log.d(LOG_TAG, file.getName() + " does NOT exist!");
return;
}
final byte[] buffer = new byte[mSettings.getByteBuffer()];
final String[] files = file.list();
if (file.isFile()) {
FileInputStream in = new FileInputStream(file.getAbsolutePath());
try {
out.putNextEntry(new ZipEntry(relPath + file.getName()));
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
out.closeEntry();
in.close();
} catch (ZipException zipE) {
if (debugEnabled)
Log.e(LOG_TAG, zipE.getMessage());
} finally {
if (out != null)
out.closeEntry();
if (in != null)
in.close();
}
} else if (files.length > 0) // non-empty folder
{
for (int i = 0, length = files.length; i < length; ++i) {
zipFile(path + "/" + files[i], out, relPath + file.getName() + "/");
}
}
}
/**
* Zips a folder...
*
* @param file
* @param zout
* @throws java.io.IOException
*/
private void zipFolder(File file, ZipOutputStream zout) throws IOException {
byte[] data = new byte[mSettings.getByteBuffer()];
if (file.isFile()) {
ZipEntry entry = new ZipEntry(file.getName());
zout.putNextEntry(entry);
BufferedInputStream instream = new BufferedInputStream(new FileInputStream(file));
int read;
while ((read = instream.read(data, 0, mSettings.getByteBuffer())) != -1) {
zout.write(data, 0, read);
}
zout.closeEntry();
instream.close();
} else if (file.isDirectory()) {
String[] list = file.list();
int len = list.length;
for (int i = 0; i < len; i++)
zipFolder(new File(file.getPath() + "/" + list[i]), zout);
}
}
/**
* Deletes files one by one until the entire
* directory is deleted.
*
* @param path
* @return
*/
private boolean deleteDirectory(File path) {
if (path.exists()) {
File[] files = path.listFiles();
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory())
deleteDirectory(files[i]);
else
files[i].delete();
}
}
return (path.delete());
}
/**
* Attempts to retrieve the highest resolution icon
* available. Returns the high res drawable if successfull;
* null if unsuccessful.
*
* @param app
* @return
*/
@SuppressLint("InlinedApi")
private Drawable getHighResDrawable(AppInfo app) {
PackageManager mPackageManager = mContext.getPackageManager();
ActivityInfo info;
Resources mRes;
try {
info = mPackageManager.getActivityInfo(new ComponentName(app.getCode().split("/")[0], app.getCode().split("/")[1]), PackageManager.GET_META_DATA);
mRes = mPackageManager.getResourcesForApplication(info.applicationInfo);
} catch (Exception e) {
if (debugEnabled)
Log.d(LOG_TAG, "Unable to retrieve ActivityInfo for app " + app.getName());
return null;
}
if (mRes == null)
return null;
try {
int iconId = info.getIconResource();
if (iconId == 0)
return null;
return mRes.getDrawableForDensity(iconId, DisplayMetrics.DENSITY_XXXHIGH);
} catch (Exception e) {
if (debugEnabled)
Log.d(LOG_TAG, "Unable trying to get density drawable for app" + app.getName());
return null;
}
}
/**
* Converts a Drawable object into a Bitmap object.
*
* @param drawable
* @return
*/
private Bitmap drawableToBitmap(Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable) drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
/**
* Formats drawable names for your generated appfilter.xml
*
* @param appName
* @return
*/
private String convertDrawableName(String appName) {
return (appName
.replaceAll("[^a-zA-Z0-9\\p{Z}]", "") // Remove all special characters and symbols
.replaceFirst("^[0-9]+(?!$)", "") // Remove all leading numbers unless they're all numbers
.toLowerCase(Locale.US) // Turn everything into lower case
.replaceAll("\\p{Z}", "_")); // Replace all kinds of spaces with underscores
}
/**
* Initializes our loading thread.
*/
private void initLoadingTask() {
this.loadTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
loadApps();
return null;
}
@Override
protected void onPostExecute(Void p) {
initLoadingTask();
}
};
}
/**
* Initializes our send request thread.
*/
private void initSendTask() {
this.sendTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
sendRequest();
return null;
}
@Override
protected void onPostExecute(Void p) {
initSendTask();
}
};
}
/**
* Initializes our send request thread.
*/
private void initAutomaticTask() {
this.autoTask = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
sendAutomaticRequest();
return null;
}
@Override
protected void onPostExecute(Void p) {
initAutomaticTask();
}
};
}
}