diff options
Diffstat (limited to 'Ministro/src/org/kde/necessitas/ministro/MinistroService.java')
-rw-r--r-- | Ministro/src/org/kde/necessitas/ministro/MinistroService.java | 748 |
1 files changed, 748 insertions, 0 deletions
diff --git a/Ministro/src/org/kde/necessitas/ministro/MinistroService.java b/Ministro/src/org/kde/necessitas/ministro/MinistroService.java new file mode 100644 index 0000000..eb75801 --- /dev/null +++ b/Ministro/src/org/kde/necessitas/ministro/MinistroService.java @@ -0,0 +1,748 @@ +/* + Copyright (c) 2011, BogDan Vatra <bog_dan_ro@yahoo.com> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +package org.kde.necessitas.ministro; + +import java.io.File; +import java.io.FileInputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +public class MinistroService extends Service +{ + private static final String TAG = "MinistroService"; + + private static final String MINISTRO_CHECK_UPDATES_KEY="LASTCHECK"; + private static final String MINISTRO_CHECK_FREQUENCY_KEY="CHECKFREQUENCY"; + private static final String MINISTRO_REPOSITORY_KEY="REPOSITORY"; + private static final String MINISTRO_DEFAULT_REPOSITORY="stable"; + + /// Ministro server parameter keys + private static final String REQUIRED_MODULES_KEY="required.modules"; + private static final String APPLICATION_TITLE_KEY="application.title"; + private static final String QT_PROVIDER_KEY="qt.provider"; + private static final String MINIMUM_MINISTRO_API_KEY="minimum.ministro.api"; + private static final String MINIMUM_QT_VERSION_KEY="minimum.qt.version"; + /// Ministro server parameter keys + + /// loader parameter keys + private static final String ERROR_CODE_KEY="error.code"; + private static final String ERROR_MESSAGE_KEY="error.message"; + private static final String DEX_PATH_KEY="dex.path"; + private static final String LIB_PATH_KEY="lib.path"; + private static final String LOADER_CLASS_NAME_KEY="loader.class.name"; + + private static final String NATIVE_LIBRARIES_KEY="native.libraries"; + private static final String ENVIRONMENT_VARIABLES_KEY="environment.variables"; + private static final String APPLICATION_PARAMETERS_KEY="application.parameters"; + private static final String QT_VERSION_PARAMETER_KEY="qt.version.parameter"; + /// loader parameter keys + + /// loader error codes + private static final int EC_NO_ERROR=0; + private static final int EC_INCOMPATIBLE=1; + private static final int EC_NOT_FOUND=2; + private static final int EC_INVALID_PARAMETERS=3; + private static final int EC_INVALID_QT_VERSION=3; + /// loader error codes + + + public static String getRepository(Context c) + { + SharedPreferences preferences=c.getSharedPreferences("Ministro", MODE_PRIVATE); + return preferences.getString(MINISTRO_REPOSITORY_KEY,MINISTRO_DEFAULT_REPOSITORY); + } + + public static void setRepository(Context c, String value) + { + SharedPreferences preferences=c.getSharedPreferences("Ministro", MODE_PRIVATE); + SharedPreferences.Editor editor= preferences.edit(); + editor.putString(MINISTRO_REPOSITORY_KEY,value); + editor.putLong(MINISTRO_CHECK_UPDATES_KEY,0); + editor.commit(); + } + + public static Long getCheckFrequency(Context c) + { + SharedPreferences preferences=c.getSharedPreferences("Ministro", MODE_PRIVATE); + return preferences.getLong(MINISTRO_CHECK_FREQUENCY_KEY, 7l*24*3600*1000)/(24l*3600*1000); + } + + public static void setCheckFrequency(Context c, long value) + { + SharedPreferences preferences=c.getSharedPreferences("Ministro", MODE_PRIVATE); + SharedPreferences.Editor editor= preferences.edit(); + editor.putLong(MINISTRO_CHECK_FREQUENCY_KEY, value*24*3600*1000); + editor.putLong(MINISTRO_CHECK_UPDATES_KEY,0); + editor.commit(); + } + + // used to check Ministro Service compatibility + private static final int MINISTRO_MIN_API_LEVEL=1; + private static final int MINISTRO_MAX_API_LEVEL=2; + + // MinistroService instance, its used by MinistroActivity to directly access services data (e.g. libraries) + private static MinistroService m_instance = null; + private String m_environmentVariables = null; + private String m_applicationParams = null; + private String m_loaderClassName = null; + private String m_pathSeparator = null; + public static MinistroService instance() + { + return m_instance; + } + + public MinistroService() + { + m_instance = this; + } + + private int m_actionId=0; // last actions id + private Handler m_handler = null; + + // current downloaded libraries + private final ArrayList<Library> m_downloadedLibraries = new ArrayList<Library>(); + + ArrayList<Library> getDownloadedLibraries() + { + synchronized (this) + { + return m_downloadedLibraries; + } + } + + // current available libraries + private final ArrayList<Library> m_availableLibraries = new ArrayList<Library>(); + ArrayList<Library> getAvailableLibraries() + { + synchronized (this) + { + return m_availableLibraries; + } + } + + class CheckForUpdates extends AsyncTask<Void, Void, Void> + { + @Override + protected void onPreExecute() + { + if (m_version<MinistroActivity.downloadVersionXmlFile(MinistroService.this, true)) + { + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + + int icon = R.drawable.icon; + CharSequence tickerText = getResources().getString(R.string.new_qt_libs_msg); // ticker-text + long when = System.currentTimeMillis(); // notification time + Context context = getApplicationContext(); // application Context + CharSequence contentTitle = getResources().getString(R.string.ministro_update_msg); // expanded message title + CharSequence contentText = getResources().getString(R.string.new_qt_libs_tap_msg); // expanded message text + + Intent notificationIntent = new Intent(MinistroService.this, MinistroActivity.class); + PendingIntent contentIntent = PendingIntent.getActivity(MinistroService.this, 0, notificationIntent, 0); + + // the next two lines initialize the Notification, using the configurations above + Notification notification = new Notification(icon, tickerText, when); + notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent); + notification.defaults |= Notification.DEFAULT_SOUND; + notification.defaults |= Notification.DEFAULT_LIGHTS; + try { + nm.notify(1, notification); + } catch(Exception e) { + e.printStackTrace(); + } + } + } + + @Override + protected Void doInBackground(Void... params) { + return null; + } + } + + + // this method reload all downloaded libraries + synchronized ArrayList<Library> refreshLibraries(boolean checkCrc) + { + synchronized (this) + { + try + { + m_downloadedLibraries.clear(); + m_availableLibraries.clear(); + if (! (new File(m_versionXmlFile)).exists()) + return m_downloadedLibraries; + DocumentBuilderFactory documentFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder documentBuilder = documentFactory.newDocumentBuilder(); + Document dom = documentBuilder.parse(new FileInputStream(m_versionXmlFile)); + Element root = dom.getDocumentElement(); + m_version = Double.valueOf(root.getAttribute("version")); + m_loaderClassName=root.getAttribute("loaderClassName"); + m_applicationParams=root.getAttribute("applicationParameters"); + m_applicationParams=m_applicationParams.replaceAll("MINISTRO_PATH", getFilesDir().getAbsolutePath()); + m_environmentVariables=root.getAttribute("environmentVariables"); + m_environmentVariables=m_environmentVariables.replaceAll("MINISTRO_PATH", getFilesDir().getAbsolutePath()); + m_environmentVariables="MINISTRO_ANDROID_STYLE_PATH="+m_qtLibsRootPath+"style/\t"+m_environmentVariables; + if (root.hasAttribute("qtVersion")) + m_qtVersion = Integer.valueOf(root.getAttribute("qtVersion")); + root.normalize(); + Node node = root.getFirstChild(); + while(node != null) + { + if (node.getNodeType() == Node.ELEMENT_NODE) + { + Library lib= Library.getLibrary((Element)node, true); + File file=new File(m_qtLibsRootPath + lib.filePath); + if (file.exists()) + { + if (checkCrc && !Library.checkCRC(file.getAbsolutePath(), lib.sha1)) + file.delete(); + else + { + + boolean allOk = true; + if (lib.needs != null) + { + for(NeedsStruct needed: lib.needs)// check if its needed files are available + if (needed.type != null && needed.type.equals("jar")) + { + File f=new File(m_qtLibsRootPath + needed.filePath); + if (!f.exists()) + { + allOk = false; + break; + } + } + if (!allOk) + { + for(NeedsStruct needed: lib.needs)// remove all needed files + if (needed.type != null && needed.type.equals("jar")) + { + try { + File f=new File(m_qtLibsRootPath + needed.filePath); + if (f.exists()) + f.delete(); + } catch (Exception e) + { + e.printStackTrace(); + } + } + file.delete(); // delete the parent + } + } + if (allOk) + m_downloadedLibraries.add(lib); + } + } + m_availableLibraries.add(lib); + } + // Workaround for an unbelievable bug !!! + try { + node = node.getNextSibling(); + } catch (Exception e) { + e.printStackTrace(); + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return m_downloadedLibraries; + } + + // version xml file + private String m_versionXmlFile; + public String getVersionXmlFile() + { + return m_versionXmlFile; + } + + private String m_qtLibsRootPath; + public String getQtLibsRootPath() + { + return m_qtLibsRootPath; + } + + private double m_version = -1; + public double getVersion() + { + return m_version; + } + + private double m_qtVersion = 0x040800; + public double getQtVersion() + { + return m_qtVersion; + } + + // class used to fire an action, this class is used + // to start an activity when user needs more libraries to start its application + class ActionStruct + { + ActionStruct(IMinistroCallback cb, String[] m, ArrayList<String> notFoundMoules, String appName, Bundle p) + { + id=++m_actionId; + callback = cb; + modules = m; + parameters = p; + } + public int id; + public IMinistroCallback callback; + public String[] modules; + public Bundle parameters; + } + + // we can have more then one action + ArrayList<ActionStruct> m_actions = new ArrayList<ActionStruct>(); + + @Override + public void onCreate() + { + m_handler = new Handler(); + m_versionXmlFile = getFilesDir().getAbsolutePath()+"/version.xml"; + m_qtLibsRootPath = getFilesDir().getAbsolutePath()+"/qt/"; + m_pathSeparator = System.getProperty("path.separator", ":"); + SharedPreferences preferences=getSharedPreferences("Ministro", MODE_PRIVATE); + long lastCheck = preferences.getLong(MINISTRO_CHECK_UPDATES_KEY,0); + long checkFrequency = preferences.getLong(MINISTRO_CHECK_FREQUENCY_KEY,7l*24*3600*1000); // check once per week by default + if (MinistroActivity.isOnline(this) && System.currentTimeMillis()-lastCheck>checkFrequency) + { + refreshLibraries(true); + SharedPreferences.Editor editor= preferences.edit(); + editor.putLong(MINISTRO_CHECK_UPDATES_KEY,System.currentTimeMillis()); + editor.commit(); + new CheckForUpdates().execute((Void[])null); + } + else + refreshLibraries(false); + super.onCreate(); + } + + @Override + public void onDestroy() + { + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) + { + return new IMinistro.Stub() + { + public void requestLoader(IMinistroCallback callback, Bundle parameters) throws RemoteException + { + + checkModulesImpl(callback, parameters); + } + }; + } + + /** + * Implements the {@link IMinistro.Stub#checkModules(IMinistroCallback, String[], String, int, int)} + * service method. + * + * @param callback + * @param modules + * @param appName + * @param ministroApiLevel + * @param necessitasApiLevel + * @throws RemoteException + */ + final void checkModulesImpl(IMinistroCallback callback, Bundle parameters) throws RemoteException + { + if (!parameters.containsKey(REQUIRED_MODULES_KEY) + || !parameters.containsKey(APPLICATION_TITLE_KEY) + || !parameters.containsKey(MINIMUM_MINISTRO_API_KEY) + || !parameters.containsKey(MINIMUM_QT_VERSION_KEY)) + { + Bundle loaderParams = new Bundle(); + loaderParams.putInt(ERROR_CODE_KEY, EC_INVALID_PARAMETERS); + loaderParams.putString(ERROR_MESSAGE_KEY, getResources().getString(R.string.invalid_parameters)); + try + { + callback.loaderReady(loaderParams); + } + catch (Exception e) { + e.printStackTrace(); + } + Log.e(TAG, "Invalid parameters: " + parameters.toString()); + return; + } + int ministroApiLevel = parameters.getInt(MINIMUM_MINISTRO_API_KEY); + String[] modules = parameters.getStringArray(REQUIRED_MODULES_KEY); + String appName = parameters.getString(APPLICATION_TITLE_KEY); + + int qtApiLevel = parameters.getInt(MINIMUM_QT_VERSION_KEY); + if (qtApiLevel > m_qtVersion) // the application needs a newer qt version + { + if (parameters.getBoolean(QT_VERSION_PARAMETER_KEY, false)) + { + Bundle loaderParams = new Bundle(); + loaderParams.putInt(ERROR_CODE_KEY, EC_INVALID_QT_VERSION); + loaderParams.putString(ERROR_MESSAGE_KEY, getResources().getString(R.string.invalid_qt_version)); + try + { + callback.loaderReady(loaderParams); + } + catch (Exception e) { + e.printStackTrace(); + } + Log.e(TAG, "Invalid qt verson"); + return; + } + startRetrieval(callback, null, null, appName, parameters); + parameters.putBoolean(QT_VERSION_PARAMETER_KEY, true); + return; + } + + @SuppressWarnings("unused") + String qtProvider="necessitas"; + if (parameters.containsKey(QT_PROVIDER_KEY)) + qtProvider=parameters.getString(QT_PROVIDER_KEY); // TODO add the possibility to have more than one provider + + if (ministroApiLevel<MINISTRO_MIN_API_LEVEL || ministroApiLevel>MINISTRO_MAX_API_LEVEL) + { + // panic !!! Ministro service is not compatible, user should upgrade Ministro package + Bundle loaderParams = new Bundle(); + loaderParams.putInt(ERROR_CODE_KEY, EC_INCOMPATIBLE); + loaderParams.putString(ERROR_MESSAGE_KEY, getResources().getString(R.string.incompatible_ministo_api)); + try + { + callback.loaderReady(loaderParams); + } + catch (Exception e) { + e.printStackTrace(); + } + Log.e(TAG, "Ministro cannot satisfy API version: " + ministroApiLevel); + return; + } + + // check necessitasApiLevel !!! I'm pretty sure some people will completely ignore my warning + // and they will deploying apps to Android Market, so let's try to give them a chance. + + // this method is called by the activity client who needs modules. + ArrayList<String> notFoundModules = new ArrayList<String>(); + Bundle loaderParams = checkModules(modules, notFoundModules); + if (loaderParams.containsKey(ERROR_CODE_KEY) && EC_NO_ERROR == loaderParams.getInt(ERROR_CODE_KEY)) + { + try + { + Library.mergeBundleParameters(loaderParams, ENVIRONMENT_VARIABLES_KEY, parameters, ENVIRONMENT_VARIABLES_KEY); + Library.mergeBundleParameters(loaderParams, APPLICATION_PARAMETERS_KEY, parameters, APPLICATION_PARAMETERS_KEY); + callback.loaderReady(loaderParams); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + else + { + // Starts a retrieval of the modules which are not readily accessible. + startRetrieval(callback, modules, notFoundModules, appName, parameters); + } + } + + /** + * Creates and sets up a {@link MinistroActivity} to retrieve the modules specified in the + * <code>notFoundModules</code> argument. + * + * @param callback + * @param modules + * @param notFoundModules + * @param appName + * @throws RemoteException + */ + private void startRetrieval(IMinistroCallback callback, String[] modules + , ArrayList<String> notFoundModules, String appName, Bundle parameters) throws RemoteException + { + ActionStruct as = new ActionStruct(callback, modules, notFoundModules, appName, parameters); + m_actions.add(as); // if not, lets start an activity to do it. + + final Intent intent = new Intent(MinistroService.this, MinistroActivity.class); + intent.putExtra("id", as.id); + intent.putExtra("name", appName); + if (null != notFoundModules) + { + String[] libs = notFoundModules.toArray(new String[notFoundModules.size()]); + intent.putExtra("modules", libs); + } + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + boolean failed = false; + try + { + m_handler.postDelayed(new Runnable() { + public void run() { + MinistroService.this.startActivity(intent); + } + }, 100); + } + catch(Exception e) + { + failed = true; + throw (RemoteException) new RemoteException().initCause(e); + } + finally + { + // Removes the dead Activity from our list as it will never finish by itself. + if (failed) + m_actions.remove(as); + } + } + + /** + * Called by a finished {@link MinistroActivity} in order to let + * the service notify the application which caused the activity about + * the result of the retrieval. + * + * @param id + */ + void retrievalFinished(int id) + { + for (int i=0;i<m_actions.size();i++) + { + ActionStruct action=m_actions.get(i); + if (action.id==id) + { + postRetrieval(action); + m_actions.remove(i); + break; + } + } + if (m_actions.size() == 0) + m_actionId = 0; + } + + /** + * Helper method for the last step of the retrieval process. + * + * <p>Checks the availability of the requested modules and informs + * the requesting application about it via the {@link IMinistroCallback} + * instance.</p> + * + * @param callback + * @param modules + */ + private void postRetrieval(ActionStruct action) + { + // Does a final check whether the libraries are accessible (without caring for + // the non-accessible ones). + try + { + if (null != action.modules) + { + Bundle loaderParams = checkModules(action.modules, null); + Library.mergeBundleParameters(loaderParams, ENVIRONMENT_VARIABLES_KEY, action.parameters, ENVIRONMENT_VARIABLES_KEY); + Library.mergeBundleParameters(loaderParams, APPLICATION_PARAMETERS_KEY, action.parameters, APPLICATION_PARAMETERS_KEY); + action.callback.loaderReady(loaderParams); + } + else + checkModulesImpl(action.callback, action.parameters); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + + /** + * Checks whether a given list of libraries are readily accessible (e.g. usable by a program). + * + * <p>If the <code>notFoundModules</code> argument is given, the method fills the list with + * libraries that need to be retrieved first.</p> + * + * @param libs + * @param notFoundModules + * @return true if all modules are available + */ + Bundle checkModules(String[] modules, ArrayList<String> notFoundModules) + { + Bundle params = new Bundle(); + boolean res=true; + ArrayList<Module> libs= new ArrayList<Module>(); + Set<String> jars= new HashSet<String>(); + for (String module: modules) + res = res & addModules(module, libs, notFoundModules, jars); // don't stop on first error + + ArrayList<String> librariesArray = new ArrayList<String>(); + // sort all libraries + Collections.sort(libs, new ModuleCompare()); + for (Module lib: libs) + librariesArray.add(m_qtLibsRootPath+lib.path); + params.putStringArrayList(NATIVE_LIBRARIES_KEY, librariesArray); + + ArrayList<String> jarsArray = new ArrayList<String>(); + for (String jar: jars) + jarsArray.add(m_qtLibsRootPath+jar); + params.putString(DEX_PATH_KEY, Library.join(jarsArray, m_pathSeparator)); + + params.putString(LOADER_CLASS_NAME_KEY, m_loaderClassName); + params.putString(LIB_PATH_KEY, m_qtLibsRootPath); + params.putString(ENVIRONMENT_VARIABLES_KEY, m_environmentVariables); + params.putString(APPLICATION_PARAMETERS_KEY, m_applicationParams); + params.putInt(ERROR_CODE_KEY, res?EC_NO_ERROR:EC_NOT_FOUND); + if (!res) + params.putString(ERROR_MESSAGE_KEY, getResources().getString(R.string.dependencies_error)); + return params; + } + +/** + * Helper method for the module resolution mechanism. It deals with an individual module's + * resolution request. + * + * <p>The method checks whether a given <em>single</em> <code>module</code> is already + * accessible or needs to be retrieved first. In the latter case the method returns + * <code>false</code>.</p> + * + * <p>The method traverses a <code>module<code>'s dependencies automatically.</p> + * + * <p>In order to find out whether a <code>module</code> is accessible the method consults + * the list of downloaded libraries. If found, an entry to the <code>modules</code> list is + * added.</p> + * + * <p>In case the <code>module</code> is not immediately accessible and the <code>notFoundModules</code> + * argument exists, a list of available libraries is consulted to fill a list of modules which + * yet need to be retrieved.</p> + * + * @param module + * @param modules + * @param notFoundModules + * @param jars + * @return <code>true</code> if the given module and all its dependencies are readily available. + */ + private boolean addModules(String module, ArrayList<Module> modules + , ArrayList<String> notFoundModules, Set<String> jars) + { + // Module argument is not supposed to be null at this point. + if (modules == null) + return false; // we are in deep shit if this happens + + // Short-cut: If the module is already in our list of previously found modules then we do not + // need to consult the list of downloaded modules. + for (int i=0;i<modules.size();i++) + { + if (modules.get(i).name.equals(module)) + return true; + } + + // Consult the list of downloaded modules. If a matching entry is found, it is added to the + // list of readily accessible modules and its dependencies are checked via a recursive call. + for (Library library:m_downloadedLibraries) + { + if (library.name.equals(module)) + { + Module m = new Module(); + m.name=library.name; + m.path=library.filePath; + m.level=library.level; + if (library.needs != null) + for(NeedsStruct needed: library.needs) + if (needed.type != null && needed.type.equals("jar")) + jars.add(needed.filePath); + modules.add(m); + + boolean res = true; + if (library.depends != null) + for (String depend: library.depends) + res &= addModules(depend, modules, notFoundModules, jars); + + if (library.replaces != null) + for (String replaceLibrary: library.replaces) + for (int mIt=0; mIt<modules.size();mIt++) + if (replaceLibrary.equals(modules.get(mIt).name)) + modules.remove(mIt--); + + return res; + } + } + + // Requested module is not readily accessible. + if (notFoundModules != null) + { + // Checks list of modules which are known to not be readily accessible and returns early to + // prevent double entries. + for (int i=0;i<notFoundModules.size();i++) + { + if (notFoundModules.get(i).equals(module)) + return false; + } + + // Deal with not yet readily accessible module's dependencies. + notFoundModules.add(module); + for (int i = 0; i< m_availableLibraries.size(); i++) + { + if (m_availableLibraries.get(i).name.equals(module)) + { + if (m_availableLibraries.get(i).depends != null) + for (int depIt=0;depIt<m_availableLibraries.get(i).depends.length;depIt++) + addModules(m_availableLibraries.get(i).depends[depIt], modules, notFoundModules, jars); + break; + } + } + } + return false; + } + + /** Sorter for libraries. + * + * Hence the order in which the libraries have to be loaded is important, it is neccessary + * to sort them. + */ + static private class ModuleCompare implements Comparator<Module> + { + public int compare(Module a, Module b) + { + return a.level-b.level; + } + } + + /** Helper class which allows manipulating libraries. + * + * It is similar to the {@link Library} class but has fewer fields. + */ + static private class Module + { + String path; + String name; + int level; + } +} |