/***************************************************************************************
* Copyright (c) 2015 Timothy Rae <perceptualchaos2@gmail.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 com.ichi2.anki;
import android.Manifest;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.support.v4.content.ContextCompat;
import com.ichi2.anki.exception.StorageAccessException;
import com.ichi2.libanki.Collection;
import com.ichi2.libanki.Storage;
import java.io.File;
import java.io.IOException;
import timber.log.Timber;
/**
* Singleton which opens, stores, and closes the reference to the Collection.
*/
public class CollectionHelper {
// Collection instance belonging to sInstance
private Collection mCollection;
// Path to collection, cached for the reopenCollection() method
private String mPath;
// Name of anki2 file
public static final String COLLECTION_FILENAME = "collection.anki2";
/**
* Prevents {@link com.ichi2.async.CollectionLoader} from spuriously re-opening the {@link Collection}.
*
* <p>Accessed only from synchronized methods.
*/
private boolean mCollectionLocked;
public synchronized void lockCollection() {
mCollectionLocked = true;
}
public synchronized void unlockCollection() {
mCollectionLocked = false;
}
public synchronized boolean isCollectionLocked() {
return mCollectionLocked;
}
/**
* Lazy initialization holder class idiom. High performance and thread safe way to create singleton.
*/
private static class LazyHolder {
private static final CollectionHelper INSTANCE = new CollectionHelper();
}
/**
* @return Singleton instance of the helper class
*/
public static CollectionHelper getInstance() {
return LazyHolder.INSTANCE;
}
/**
* Get the single instance of the {@link Collection}, creating it if necessary (lazy initialization).
* @param context context which can be used to get the setting for the path to the Collection
* @return instance of the Collection
*/
public synchronized Collection getCol(Context context) {
// Open collection
String path = getCollectionPath(context);
if (!colIsOpen()) {
// Check that the directory has been created and initialized
try {
initializeAnkiDroidDirectory(getParentDirectory(path));
mPath = path;
} catch (StorageAccessException e) {
Timber.e(e, "Could not initialize AnkiDroid directory");
return null;
}
// Open the database
Timber.i("openCollection: %s", path);
mCollection = Storage.Collection(context, path, false, true);
}
return mCollection;
}
/**
* Call getCol(context) inside try / catch statement.
* Send exception report and return null if there was an exception.
* @param context
* @return
*/
public synchronized Collection getColSafe(Context context) {
try {
return getCol(context);
} catch (Exception e) {
AnkiDroidApp.sendExceptionReport(e, "CollectionHelper.getColSafe");
return null;
}
}
/**
* Checks whether or not the Android 1MB limit for the cursor size was exceeded
* @param context
* @return
*/
public synchronized boolean exceededCursorSizeLimit(Context context) {
try {
getCol(context);
} catch (IllegalStateException e) {
return true;
}
return false;
}
/**
* Close the {@link Collection}, optionally saving
* @param save whether or not save before closing
*/
public synchronized void closeCollection(boolean save) {
Timber.i("closeCollection");
if (mCollection != null) {
mCollection.close(save);
}
}
/**
* @return Whether or not {@link Collection} and its child database are open.
*/
public boolean colIsOpen() {
return mCollection != null && mCollection.getDb() != null &&
mCollection.getDb().getDatabase() != null && mCollection.getDb().getDatabase().isOpen();
}
/**
* Create the AnkiDroid directory if it doesn't exist and add a .nomedia file to it if needed.
*
* The AnkiDroid directory is a user preference stored under the "deckPath" key, and a sensible
* default is chosen if the preference hasn't been created yet (i.e., on the first run).
*
* The presence of a .nomedia file indicates to media scanners that the directory must be
* excluded from their search. We need to include this to avoid media scanners including
* media files from the collection.media directory. The .nomedia file works at the directory
* level, so placing it in the AnkiDroid directory will ensure media scanners will also exclude
* the collection.media sub-directory.
*
* @param path Directory to initialize
* @throws StorageAccessException If no write access to directory
*/
public static synchronized void initializeAnkiDroidDirectory(String path) throws StorageAccessException {
// Create specified directory if it doesn't exit
File dir = new File(path);
if (!dir.exists() && !dir.mkdirs()) {
throw new StorageAccessException("Failed to create AnkiDroid directory");
}
if (!dir.canWrite()) {
throw new StorageAccessException("No write access to AnkiDroid directory");
}
// Add a .nomedia file to it if it doesn't exist
File nomedia = new File(dir, ".nomedia");
if (!nomedia.exists()) {
try {
nomedia.createNewFile();
} catch (IOException e) {
throw new StorageAccessException("Failed to create .nomedia file");
}
}
}
/**
* Try to access the current AnkiDroid directory
* @return whether or not dir is accessible
* @param context to get directory with
*/
public static boolean isCurrentAnkiDroidDirAccessible(Context context) {
try {
initializeAnkiDroidDirectory(getCurrentAnkiDroidDirectory(context));
return true;
} catch (StorageAccessException e) {
return false;
}
}
/**
* Get the absolute path to a directory that is suitable to be the default starting location
* for the AnkiDroid folder. This is a folder named "AnkiDroid" at the top level of the
* external storage directory.
* @return the folder path
*/
public static String getDefaultAnkiDroidDirectory() {
return new File(Environment.getExternalStorageDirectory(), "AnkiDroid").getAbsolutePath();
}
/**
*
* @return the path to the actual {@link Collection} file
*/
public static String getCollectionPath(Context context) {
return new File(getCurrentAnkiDroidDirectory(context), COLLECTION_FILENAME).getAbsolutePath();
}
/**
* @return the absolute path to the AnkiDroid directory.
*/
public static String getCurrentAnkiDroidDirectory(Context context) {
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
return preferences.getString("deckPath", getDefaultAnkiDroidDirectory());
}
/**
* Get parent directory given the {@link Collection} path.
* @param path path to AnkiDroid collection
* @return path to AnkiDroid folder
*/
private static String getParentDirectory(String path) {
return new File(path).getParentFile().getAbsolutePath();
}
/**
* Check if we have permission to access the external storage
* @param context
* @return
*/
public static boolean hasStorageAccessPermission(Context context) {
return ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
}