package com.j256.ormlite.android.apptools; import java.lang.reflect.Constructor; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import android.content.Context; import android.content.res.Resources; import android.database.sqlite.SQLiteOpenHelper; import com.j256.ormlite.dao.BaseDaoImpl; import com.j256.ormlite.dao.DaoManager; import com.j256.ormlite.logger.Logger; import com.j256.ormlite.logger.LoggerFactory; /** * This helps organize and access database connections to optimize connection sharing. There are several schemes to * manage the database connections in an Android app, but as an app gets more complicated, there are many potential * places where database locks can occur. This class allows database connection sharing between multiple threads in a * single app. * * This gets injected or called with the {@link OrmLiteSqliteOpenHelper} class that is used to manage the database * connection. The helper instance will be kept in a static field and only released once its internal usage count goes * to 0. * * The {@link SQLiteOpenHelper} and database classes maintain one connection under the hood, and prevent locks in the * java code. Creating multiple connections can potentially be a source of trouble. This class shares the same * connection instance between multiple clients, which will allow multiple activities and services to run at the same * time. * * Every time you use the helper, you should call {@link #getHelper(Context)} or {@link #getHelper(Context, Class)}. * When you are done with the helper you should call {@link #releaseHelper()}. * * @author graywatson, kevingalligan */ public class OpenHelperManager { private static final String HELPER_CLASS_RESOURCE_NAME = "open_helper_classname"; private static Logger logger = LoggerFactory.getLogger(OpenHelperManager.class); private static Class<? extends OrmLiteSqliteOpenHelper> helperClass = null; private static volatile OrmLiteSqliteOpenHelper helper = null; private static boolean wasClosed = false; private static int instanceCount = 0; /** * If you are _not_ using the {@link OrmLiteBaseActivity} type classes then you will need to call this in a static * method in your code. */ public static synchronized void setOpenHelperClass(Class<? extends OrmLiteSqliteOpenHelper> openHelperClass) { if (openHelperClass == null) { helperClass = null; } else { innerSetHelperClass(openHelperClass); } } /** * Set the helper for the manager. This is most likely used for testing purposes and should only be called if you * _really_ know what you are doing. If you do use it then it should be in a static {} initializing block to make * sure you have one helper instance for your application. */ public static synchronized void setHelper(OrmLiteSqliteOpenHelper helper) { OpenHelperManager.helper = helper; } /** * Create a static instance of our open helper from the helper class. This has a usage counter on it so make sure * all calls to this method have an associated call to {@link #releaseHelper()}. This should be called during an * onCreate() type of method when the application or service is starting. The caller should then keep the helper * around until it is shutting down when {@link #releaseHelper()} should be called. */ public static synchronized <T extends OrmLiteSqliteOpenHelper> T getHelper(Context context, Class<T> openHelperClass) { if (openHelperClass == null) { throw new IllegalArgumentException("openHelperClass argument is null"); } innerSetHelperClass(openHelperClass); return loadHelper(context, openHelperClass); } /** * <p> * Similar to {@link #getHelper(Context, Class)} (which is recommended) except we have to find the helper class * through other means. This method requires that the Context be a class that extends one of ORMLite's Android base * classes such as {@link OrmLiteBaseActivity}. Either that or the helper class needs to be set in the strings.xml. * </p> * * <p> * To find the helper class, this does the following: * </p> * * <ol> * <li>If the class has been set with a call to {@link #setOpenHelperClass(Class)}, it will be used to construct a * helper.</li> * <li>If the resource class name is configured in the strings.xml file it will be used.</li> * <li>The context class hierarchy is walked looking at the generic parameters for a class extending * OrmLiteSqliteOpenHelper. This is used by the {@link OrmLiteBaseActivity} and other base classes.</li> * <li>An exception is thrown saying that it was not able to set the helper class.</li> * </ol> * * @deprecated Should use {@link #getHelper(Context, Class)} */ @Deprecated public static synchronized OrmLiteSqliteOpenHelper getHelper(Context context) { if (helperClass == null) { if (context == null) { throw new IllegalArgumentException("context argument is null"); } Context appContext = context.getApplicationContext(); innerSetHelperClass(lookupHelperClass(appContext, context.getClass())); } return loadHelper(context, helperClass); } /** * Release the helper that was previously returned by a call {@link #getHelper(Context)} or * {@link #getHelper(Context, Class)}. This will decrement the usage counter and close the helper if the counter is * 0. * * <p> * <b> WARNING: </b> This should be called in an onDestroy() type of method when your application or service is * terminating or if your code is no longer going to use the helper or derived DAOs in any way. _Don't_ call this * method if you expect to call {@link #getHelper(Context)} again before the application terminates. * </p> */ public static synchronized void releaseHelper() { instanceCount--; logger.trace("releasing helper {}, instance count = {}", helper, instanceCount); if (instanceCount <= 0) { if (helper != null) { logger.trace("zero instances, closing helper {}", helper); helper.close(); helper = null; wasClosed = true; } if (instanceCount < 0) { logger.error("too many calls to release helper, instance count = {}", instanceCount); } } } /** * Set the helper class and make sure we aren't changing it to another class. */ private static void innerSetHelperClass(Class<? extends OrmLiteSqliteOpenHelper> openHelperClass) { // make sure if that there are not 2 helper classes in an application if (openHelperClass == null) { throw new IllegalStateException("Helper class was trying to be reset to null"); } else if (helperClass == null) { helperClass = openHelperClass; } else if (helperClass != openHelperClass) { throw new IllegalStateException("Helper class was " + helperClass + " but is trying to be reset to " + openHelperClass); } } private static <T extends OrmLiteSqliteOpenHelper> T loadHelper(Context context, Class<T> openHelperClass) { if (helper == null) { if (wasClosed) { // this can happen if you are calling get/release and then get again logger.info("helper was already closed and is being re-opened"); } if (context == null) { throw new IllegalArgumentException("context argument is null"); } Context appContext = context.getApplicationContext(); helper = constructHelper(appContext, openHelperClass); logger.trace("zero instances, created helper {}", helper); /* * Filipe Leandro and I worked on this bug for like 10 hours straight. It's a doosey. * * Each ForeignCollection has internal DAO objects that are holding a ConnectionSource. Each Android * ConnectionSource is tied to a particular database connection. What Filipe was seeing was that when all of * his views we closed (onDestroy), but his application WAS NOT FULLY KILLED, the first View.onCreate() * method would open a new connection to the database. Fine. But because he application was still in memory, * the static BaseDaoImpl default cache had not been cleared and was containing cached objects with * ForeignCollections. The ForeignCollections still had references to the DAOs that had been opened with old * ConnectionSource objects and therefore the old database connection. Using those cached collections would * cause exceptions saying that you were trying to work with a database that had already been close. * * Now, whenever we create a new helper object, we must make sure that the internal object caches have been * fully cleared. This is a good lesson for anyone that is holding objects around after they have closed * connections to the database or re-created the DAOs on a different connection somehow. */ BaseDaoImpl.clearAllInternalObjectCaches(); /* * Might as well do this also since if the helper changes then the ConnectionSource will change so no one is * going to have a cache hit on the old DAOs anyway. All they are doing is holding memory. * * NOTE: we don't want to clear the config map. */ DaoManager.clearDaoCache(); instanceCount = 0; } instanceCount++; logger.trace("returning helper {}, instance count = {} ", helper, instanceCount); @SuppressWarnings("unchecked") T castHelper = (T) helper; return castHelper; } /** * Call the constructor on our helper class. */ private static OrmLiteSqliteOpenHelper constructHelper(Context context, Class<? extends OrmLiteSqliteOpenHelper> openHelperClass) { Constructor<?> constructor; try { constructor = openHelperClass.getConstructor(Context.class); } catch (Exception e) { throw new IllegalStateException( "Could not find public constructor that has a single (Context) argument for helper class " + openHelperClass, e); } try { return (OrmLiteSqliteOpenHelper) constructor.newInstance(context); } catch (Exception e) { throw new IllegalStateException("Could not construct instance of helper class " + openHelperClass, e); } } /** * Lookup the helper class either from the resource string or by looking for a generic parameter. */ private static Class<? extends OrmLiteSqliteOpenHelper> lookupHelperClass(Context context, Class<?> componentClass) { // see if we have the magic resource class name set Resources resources = context.getResources(); int resourceId = resources.getIdentifier(HELPER_CLASS_RESOURCE_NAME, "string", context.getPackageName()); if (resourceId != 0) { String className = resources.getString(resourceId); try { @SuppressWarnings("unchecked") Class<? extends OrmLiteSqliteOpenHelper> castClass = (Class<? extends OrmLiteSqliteOpenHelper>) Class.forName(className); return castClass; } catch (Exception e) { throw new IllegalStateException("Could not create helper instance for class " + className, e); } } // try walking the context class to see if we can get the OrmLiteSqliteOpenHelper from a generic parameter for (Class<?> componentClassWalk = componentClass; componentClassWalk != null; componentClassWalk = componentClassWalk.getSuperclass()) { Type superType = componentClassWalk.getGenericSuperclass(); if (superType == null || !(superType instanceof ParameterizedType)) { continue; } // get the generic type arguments Type[] types = ((ParameterizedType) superType).getActualTypeArguments(); // defense if (types == null || types.length == 0) { continue; } for (Type type : types) { // defense if (!(type instanceof Class)) { continue; } Class<?> clazz = (Class<?>) type; if (OrmLiteSqliteOpenHelper.class.isAssignableFrom(clazz)) { @SuppressWarnings("unchecked") Class<? extends OrmLiteSqliteOpenHelper> castOpenHelperClass = (Class<? extends OrmLiteSqliteOpenHelper>) clazz; return castOpenHelperClass; } } } throw new IllegalStateException( "Could not find OpenHelperClass because none of the generic parameters of class " + componentClass + " extends OrmLiteSqliteOpenHelper. You should use getHelper(Context, Class) instead."); } }