/** Check that updating the flds column works as expected */ public void testUpdateNoteFields() { final ContentResolver cr = getContext().getContentResolver(); ContentValues cv = new ContentValues(); // Change the fields so that the first field is now "newTestValue" String[] dummyFields2 = mDummyFields.clone(); dummyFields2[0] = TEST_FIELD_VALUE; for (Uri uri : mCreatedNotes) { // Update the flds cv.put(FlashCardsContract.Note.FLDS, Utils.joinFields(dummyFields2)); cr.update(uri, cv, null, null); // Query the table again Cursor noteCursor = cr.query(uri, FlashCardsContract.Note.DEFAULT_PROJECTION, null, null, null); try { assertNotNull( "Check that there is a valid cursor for detail data after update", noteCursor); assertEquals( "Check that there is one and only one entry after update", 1, noteCursor.getCount()); assertTrue("Move to first item in cursor", noteCursor.moveToFirst()); String[] newFlds = Utils.splitFields( noteCursor.getString(noteCursor.getColumnIndex(FlashCardsContract.Note.FLDS))); assertTrue( "Check that the flds have been updated correctly", Arrays.equals(newFlds, dummyFields2)); } finally { noteCursor.close(); } } }
private String unzipSharedDeckFile(String zipFilename, String title) { ZipInputStream zipInputStream = null; Log.i(AnkiDroidApp.TAG, "unzipSharedDeckFile"); if (zipFilename.endsWith(".zip")) { Log.i(AnkiDroidApp.TAG, "zipFilename ends with .zip"); try { zipInputStream = new ZipInputStream(new FileInputStream(new File(zipFilename))); if (new File(mDestination + "/" + title + ".anki").exists()) { title += System.currentTimeMillis(); } String partialDeckPath = mDestination + "/tmp/" + title; String deckFilename = partialDeckPath + ".anki.updating"; ZipEntry zipEntry = null; while ((zipEntry = zipInputStream.getNextEntry()) != null) { Log.i(AnkiDroidApp.TAG, "zipEntry = " + zipEntry.getName()); if ("shared.anki".equalsIgnoreCase(zipEntry.getName())) { Utils.writeToFile(zipInputStream, deckFilename); } else if (zipEntry.getName().startsWith("shared.media/", 0)) { Log.i( AnkiDroidApp.TAG, "Folder created = " + new File(partialDeckPath + ".media/").mkdir()); Log.i( AnkiDroidApp.TAG, "Destination = " + AnkiDroidApp.getStorageDirectory() + "/" + title + ".media/" + zipEntry.getName().replace("shared.media/", "")); Utils.writeToFile( zipInputStream, partialDeckPath + ".media/" + zipEntry.getName().replace("shared.media/", "")); } } zipInputStream.close(); // Delete zip file new File(zipFilename).delete(); } catch (FileNotFoundException e) { Log.e(AnkiDroidApp.TAG, "FileNotFoundException = " + e.getMessage()); e.printStackTrace(); } catch (IOException e) { Log.e(AnkiDroidApp.TAG, "IOException = " + e.getMessage()); e.printStackTrace(); } } return title; }
/** * Posting feedback or error info to the server. This is called from the AsyncTask. * * @param url The url to post the feedback to. * @param type The type of the info, eg Feedback.TYPE_CRASH_STACKTRACE. * @param feedback For feedback types this is the message. For error/crash types this is the path * to the error file. * @param groupId A single time generated ID, so that errors/feedback send together can be grouped * together. * @param index The index of the error in the list * @return A Payload file showing success, response code and response message. */ public static Payload postFeedback( String url, String type, String feedback, String groupId, int index, Application app) { Payload result = new Payload(null); List<NameValuePair> pairs = null; if (!isErrorType(type)) { pairs = new ArrayList<NameValuePair>(); pairs.add(new BasicNameValuePair("type", type)); pairs.add(new BasicNameValuePair("groupid", groupId)); pairs.add(new BasicNameValuePair("index", "0")); pairs.add(new BasicNameValuePair("message", feedback)); addTimestamp(pairs); } else { pairs = Feedback.extractPairsFromError(type, feedback, groupId, index, app); if (pairs == null) { result.success = false; result.result = null; } } HttpClient httpClient = new DefaultHttpClient(); HttpPost httpPost = new HttpPost(url); httpPost.addHeader("User-Agent", "AnkiDroid"); try { httpPost.setEntity(new UrlEncodedFormEntity(pairs)); HttpResponse response = httpClient.execute(httpPost); Log.e(AnkiDroidApp.TAG, String.format("Bug report posted to %s", url)); int respCode = response.getStatusLine().getStatusCode(); switch (respCode) { case 200: result.success = true; result.returnType = respCode; result.result = Utils.convertStreamToString(response.getEntity().getContent()); Log.i(AnkiDroidApp.TAG, String.format("postFeedback OK: %s", result.result)); break; default: Log.e( AnkiDroidApp.TAG, String.format( "postFeedback failure: %d - %s", response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase())); result.success = false; result.returnType = respCode; result.result = response.getStatusLine().getReasonPhrase(); break; } } catch (ClientProtocolException ex) { Log.e(AnkiDroidApp.TAG, "ClientProtocolException: " + ex.toString()); result.success = false; result.result = ex.toString(); } catch (IOException ex) { Log.e(AnkiDroidApp.TAG, "IOException: " + ex.toString()); result.success = false; result.result = ex.toString(); } return result; }
public static int restoreBackup(String path, String backupPath) { // rename old file and move it to subdirectory if (!(new File(path)).exists() || !moveDatabaseToBrokenFolder(path, false)) { return RETURN_ERROR; } // copy backup to new position and rename it File backupFile = new File(backupPath); File colFile = new File(path); if (getFreeDiscSpace(colFile) < colFile.length() + (MIN_FREE_SPACE * 1024 * 1024)) { Log.e(AnkiDroidApp.TAG, "Not enough space on sd card to restore " + colFile.getName() + "."); return RETURN_NOT_ENOUGH_SPACE; } try { InputStream stream = new FileInputStream(backupFile); Utils.writeToFile(stream, colFile.getAbsolutePath()); stream.close(); // set timestamp of file in order to avoid creating a new backup unless its changed colFile.setLastModified(backupFile.lastModified()); } catch (IOException e) { Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e)); Log.e(AnkiDroidApp.TAG, "Restore of file " + colFile.getName() + " failed."); return RETURN_ERROR; } return RETURN_DECK_RESTORED; }
/** * Factory for AnkiFont creation. Creates a typeface wrapper from a font file representing. * * @param ctx Activity context, needed to access assets * @param path Path to typeface file, needed when this is a custom font. * @param fromAssets True if the font is to be found in assets of application * @return A new AnkiFont object or null if the file can't be interpreted as typeface. */ public static AnkiFont createAnkiFont(Context ctx, String path, boolean fromAssets) { File fontfile = new File(path); String name = Utils.splitFilename(fontfile.getName())[0]; String family = name; List<String> attributes = new ArrayList<>(); if (fromAssets) { path = fAssetPathPrefix.concat(fontfile.getName()); } Typeface tf = getTypeface(ctx, path); if (tf == null) { // unable to create typeface return null; } if (tf.isBold() || name.toLowerCase(Locale.US).contains("bold")) { attributes.add("font-weight: bolder;"); family = family.replaceFirst("(?i)-?Bold", ""); } else if (name.toLowerCase(Locale.US).contains("light")) { attributes.add("font-weight: lighter;"); family = family.replaceFirst("(?i)-?Light", ""); } else { attributes.add("font-weight: normal;"); } if (tf.isItalic() || name.toLowerCase(Locale.US).contains("italic")) { attributes.add("font-style: italic;"); family = family.replaceFirst("(?i)-?Italic", ""); } else if (name.toLowerCase(Locale.US).contains("oblique")) { attributes.add("font-style: oblique;"); family = family.replaceFirst("(?i)-?Oblique", ""); } else { attributes.add("font-style: normal;"); } if (name.toLowerCase(Locale.US).contains("condensed") || name.toLowerCase(Locale.US).contains("narrow")) { attributes.add("font-stretch: condensed;"); family = family.replaceFirst("(?i)-?Condensed", ""); family = family.replaceFirst("(?i)-?Narrow(er)?", ""); } else if (name.toLowerCase(Locale.US).contains("expanded") || name.toLowerCase(Locale.US).contains("wide")) { attributes.add("font-stretch: expanded;"); family = family.replaceFirst("(?i)-?Expanded", ""); family = family.replaceFirst("(?i)-?Wide(r)?", ""); } AnkiFont createdFont = new AnkiFont(name, family, attributes, path); // determine if override font or default font SharedPreferences preferences = AnkiDroidApp.getSharedPrefs(ctx); String defaultFont = preferences.getString("defaultFont", ""); boolean overrideFont = preferences.getString("overrideFontBehavior", "0").equals("1"); if (defaultFont.equalsIgnoreCase(name)) { if (overrideFont) { createdFont.setAsOverride(); } else { createdFont.setAsDefault(); } } return createdFont; }
/** Query .../models URI */ public void testQueryAllModels() { final ContentResolver cr = getContext().getContentResolver(); // Query all available models final Cursor allModels = cr.query(FlashCardsContract.Model.CONTENT_URI, null, null, null, null); assertNotNull(allModels); try { assertTrue("Check that there is at least one result", allModels.getCount() > 0); while (allModels.moveToNext()) { long modelId = allModels.getLong(allModels.getColumnIndex(FlashCardsContract.Model._ID)); Uri modelUri = Uri.withAppendedPath(FlashCardsContract.Model.CONTENT_URI, Long.toString(modelId)); final Cursor singleModel = cr.query(modelUri, null, null, null, null); assertNotNull(singleModel); try { assertEquals("Check that there is exactly one result", 1, singleModel.getCount()); assertTrue("Move to beginning of cursor", singleModel.moveToFirst()); String nameFromModels = allModels.getString(allModels.getColumnIndex(FlashCardsContract.Model.NAME)); String nameFromModel = singleModel.getString(allModels.getColumnIndex(FlashCardsContract.Model.NAME)); assertEquals("Check that model names are the same", nameFromModel, nameFromModels); String flds = allModels.getString(allModels.getColumnIndex(FlashCardsContract.Model.FIELD_NAMES)); assertTrue("Check that valid number of fields", Utils.splitFields(flds).length >= 1); Integer numCards = allModels.getInt(allModels.getColumnIndex(FlashCardsContract.Model.NUM_CARDS)); assertTrue("Check that valid number of cards", numCards >= 1); } finally { singleModel.close(); } } } finally { allModels.close(); } }
@SmallTest public void testGetRawQuery() { assertTrue(CardsQuery.getRawQuery(10, null).contains(Utils.join(", ", CardsQuery.PROJECTION))); assertTrue(CardsQuery.getRawQuery(10, null).contains(" LIMIT 10")); assertTrue(CardsQuery.getRawQuery(20, null).contains(" LIMIT 20")); assertFalse(CardsQuery.getRawQuery(10, null).contains(" AND cards.id > ")); assertTrue(CardsQuery.getRawQuery(10, "1234").contains(" AND cards.id > 1234 ")); }
public JSONArray meta() { JSONArray o = new JSONArray(); o.put(mCol.getMod()); o.put(mCol.getScm()); o.put(mCol.getUsnForSync()); o.put(Utils.intNow()); return o; }
public static boolean initialize(Context context) { mContext = context; SharedPreferences preferences = AnkiDroidApp.getSharedPrefs(AnkiDroidApp.getInstance().getBaseContext()); mDictionary = Integer.parseInt(preferences.getString("dictionary", Integer.toString(DICTIONARY_NONE))); switch (mDictionary) { case DICTIONARY_NONE: mIsDictionaryAvailable = false; break; case DICTIONARY_AEDICT: mDictionaryAction = "sk.baka.aedict.action.ACTION_SEARCH_EDICT"; mIsDictionaryAvailable = Utils.isIntentAvailable(mContext, mDictionaryAction); break; case DICTIONARY_LEO_WEB: case DICTIONARY_NCIKU_WEB: case DICTIONARY_EIJIRO_WEB: mDictionaryAction = "android.intent.action.VIEW"; mIsDictionaryAvailable = Utils.isIntentAvailable(mContext, mDictionaryAction); break; case DICTIONARY_LEO_APP: mDictionaryAction = "android.intent.action.SEND"; mIsDictionaryAvailable = Utils.isIntentAvailable( mContext, mDictionaryAction, new ComponentName("org.leo.android.dict", "org.leo.android.dict.LeoDict")); break; case DICTIONARY_COLORDICT: mDictionaryAction = "colordict.intent.action.SEARCH"; mIsDictionaryAvailable = Utils.isIntentAvailable(mContext, mDictionaryAction); break; case DICTIONARY_FORA: mDictionaryAction = "com.ngc.fora.action.LOOKUP"; mIsDictionaryAvailable = Utils.isIntentAvailable(mContext, mDictionaryAction); break; default: mIsDictionaryAvailable = false; break; } Log.i(AnkiDroidApp.TAG, "Is intent available = " + mIsDictionaryAvailable); return mIsDictionaryAvailable; }
private void remove(JSONObject graves) { // pretend to be the server so we don't set usn = -1 boolean wasServer = mCol.getServer(); mCol.setServer(true); try { // notes first, so we don't end up with duplicate graves mCol._remNotes(Utils.jsonArrayToLongArray(graves.getJSONArray("notes"))); // then cards mCol.remCards(Utils.jsonArrayToLongArray(graves.getJSONArray("cards")), false); // and decks JSONArray decks = graves.getJSONArray("decks"); for (int i = 0; i < decks.length(); i++) { mCol.getDecks().rem(decks.getLong(i), false, false); } } catch (JSONException e) { throw new RuntimeException(e); } mCol.setServer(wasServer); }
/** Check that inserting a new model works as expected */ public void testInsertAndUpdateModel() throws Exception { final ContentResolver cr = getContext().getContentResolver(); ContentValues cv = new ContentValues(); // Insert a new model cv.put(FlashCardsContract.Model.NAME, TEST_MODEL_NAME); cv.put(FlashCardsContract.Model.FIELD_NAMES, Utils.joinFields(TEST_MODEL_FIELDS)); cv.put(FlashCardsContract.Model.NUM_CARDS, TEST_MODEL_CARDS.length); cv.put(FlashCardsContract.Model.CSS, TEST_MODEL_CSS); Uri modelUri = cr.insert(FlashCardsContract.Model.CONTENT_URI, cv); assertNotNull("Check inserted model isn't null", modelUri); long mid = Long.parseLong(modelUri.getLastPathSegment()); final Collection col = CollectionHelper.getInstance().getCol(getContext()); try { JSONObject model = col.getModels().get(mid); assertEquals("Check model name", TEST_MODEL_NAME, model.getString("name")); assertEquals("Check css", TEST_MODEL_CSS, model.getString("css")); assertEquals( "Check templates length", TEST_MODEL_CARDS.length, model.getJSONArray("tmpls").length()); assertEquals( "Check field length", TEST_MODEL_FIELDS.length, model.getJSONArray("flds").length()); JSONArray flds = model.getJSONArray("flds"); for (int i = 0; i < flds.length(); i++) { assertEquals( "Check name of fields", flds.getJSONObject(i).getString("name"), TEST_MODEL_FIELDS[i]); } // Update each of the templates in the model for (int i = 0; i < TEST_MODEL_CARDS.length; i++) { cv = new ContentValues(); cv.put(FlashCardsContract.CardTemplate.NAME, TEST_MODEL_CARDS[i]); cv.put(FlashCardsContract.CardTemplate.QUESTION_FORMAT, TEST_MODEL_QFMT[i]); cv.put(FlashCardsContract.CardTemplate.ANSWER_FORMAT, TEST_MODEL_AFMT[i]); cv.put(FlashCardsContract.CardTemplate.BROWSER_QUESTION_FORMAT, TEST_MODEL_QFMT[i]); cv.put(FlashCardsContract.CardTemplate.BROWSER_ANSWER_FORMAT, TEST_MODEL_AFMT[i]); Uri tmplUri = Uri.withAppendedPath(Uri.withAppendedPath(modelUri, "templates"), Integer.toString(i)); assertTrue("Update rows", cr.update(tmplUri, cv, null, null) > 0); JSONObject template = col.getModels().get(mid).getJSONArray("tmpls").getJSONObject(i); assertEquals("Check template name", TEST_MODEL_CARDS[i], template.getString("name")); assertEquals("Check qfmt", TEST_MODEL_QFMT[i], template.getString("qfmt")); assertEquals("Check afmt", TEST_MODEL_AFMT[i], template.getString("afmt")); assertEquals("Check bqfmt", TEST_MODEL_QFMT[i], template.getString("bqfmt")); assertEquals("Check bafmt", TEST_MODEL_AFMT[i], template.getString("bafmt")); } } finally { // Delete the model (this will force a full-sync) try { col.modSchema(false); col.getModels().rem(col.getModels().get(mid)); } catch (ConfirmModSchemaException e) { // This will never happen throw new IllegalStateException( "Unexpected ConfirmModSchemaException trying to remove model"); } } }
private long finish(long mod) { if (mod == 0) { // server side; we decide new mod time mod = Utils.intNow(1000); } mCol.setLs(mod); mCol.setUsnAfterSync(mMaxUsn + 1); // ensure we save the mod time even if no changes made mCol.getDb().setMod(true); mCol.save(null, mod); return mod; }
public static boolean moveDatabaseToBrokenFolder(String colPath, boolean moveConnectedFilesToo) { File colFile = new File(colPath); // move file Date value = Utils.genToday(Utils.utcOffset()); String movedFilename = String.format( Utils.ENGLISH_LOCALE, colFile.getName().replace(".anki2", "") + "-corrupt-%tF.anki2", value); File movedFile = new File(getBrokenDirectory().getPath(), movedFilename); int i = 1; while (movedFile.exists()) { movedFile = new File( getBrokenDirectory().getPath(), movedFilename.replace(".anki2", "-" + Integer.toString(i) + ".anki2")); i++; } movedFilename = movedFile.getName(); if (!colFile.renameTo(movedFile)) { return false; } if (moveConnectedFilesToo) { // move all connected files (like journals, directories...) too String deckName = colFile.getName(); File directory = new File(colFile.getParent()); for (File f : directory.listFiles()) { if (f.getName().startsWith(deckName)) { if (!f.renameTo( new File( getBrokenDirectory().getPath(), f.getName().replace(deckName, movedFilename)))) { return false; } } } } return true; }
public Download(String title) { mTitle = title; this.put(title, true); mSize = -1; mDownloaded = 0; mStatus = STATUS_STARTED; // The deck file name should match the deck title, but some characters are invalid in it, // so they need to be replaced. mFilename = Utils.removeInvalidDeckNameCharacters(mTitle); if (mFilename.length() > 40) { mFilename = mFilename.substring(0, 40); } }
private String[] getCustomFonts(String defaultValue, boolean useFullPath) { List<AnkiFont> mFonts = Utils.getCustomFonts(this); int count = mFonts.size(); // Log.d(AnkiDroidApp.TAG, "There are " + count + " custom fonts"); String[] names = new String[count + 1]; names[0] = defaultValue; if (useFullPath) { for (int index = 1; index < count + 1; ++index) { names[index] = mFonts.get(index - 1).getPath(); // Log.d(AnkiDroidApp.TAG, "Adding custom font: " + names[index]); } } else { for (int index = 1; index < count + 1; ++index) { names[index] = mFonts.get(index - 1).getName(); // Log.d(AnkiDroidApp.TAG, "Adding custom font: " + names[index]); } } return names; }
public AnkiFont(Context ctx, String path, boolean fromAssets) { mPath = path; File fontfile = new File(mPath); mName = Utils.removeExtension(fontfile.getName()); mFamily = mName; if (fromAssets) { mPath = fAssetPathPrefix.concat(fontfile.getName()); } Typeface tf = getTypeface(ctx, mPath); if (tf.isBold() || mName.toLowerCase().contains("bold")) { mWeight = "font-weight: bolder;"; mFamily = mFamily.replaceFirst("(?i)-?Bold", ""); } else if (mName.toLowerCase().contains("light")) { mWeight = "font-weight: lighter;"; mFamily = mFamily.replaceFirst("(?i)-?Light", ""); } else { mWeight = "font-weight: normal;"; } if (tf.isItalic() || mName.toLowerCase().contains("italic")) { mStyle = "font-style: italic;"; mFamily = mFamily.replaceFirst("(?i)-?Italic", ""); } else if (mName.toLowerCase().contains("oblique")) { mStyle = "font-style: oblique;"; mFamily = mFamily.replaceFirst("(?i)-?Oblique", ""); } else { mStyle = "font-style: normal;"; } if (mName.toLowerCase().contains("condensed") || mName.toLowerCase().contains("narrow")) { mStretch = "font-stretch: condensed;"; mFamily = mFamily.replaceFirst("(?i)-?Condensed", ""); mFamily = mFamily.replaceFirst("(?i)-?Narrow(er)?", ""); } else if (mName.toLowerCase().contains("expanded") || mName.toLowerCase().contains("wide")) { mStretch = "font-stretch: expanded;"; mFamily = mFamily.replaceFirst("(?i)-?Expanded", ""); mFamily = mFamily.replaceFirst("(?i)-?Wide(r)?", ""); } else { mStretch = "font-stretch: normal;"; } mFamily = mFamily.replaceFirst("(?i)-?Regular", ""); }
/** * If collection has not been opened for a long time, we perform a backup here because Android * deleted sometimes corrupted decks */ public static boolean safetyBackupNeeded(String path, int days) { if (!AnkiDroidApp.getSharedPrefs(AnkiDroidApp.getInstance().getBaseContext()) .getBoolean("useBackup", true)) { return false; } File collectionFile = new File(path); File[] deckBackups = getBackups(collectionFile); int len = deckBackups.length; if (len == 0) { // no backup available return true; } else if (deckBackups[len - 1].lastModified() == collectionFile.lastModified()) { return false; } SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm"); Calendar cal = new GregorianCalendar(); cal.setTimeInMillis(System.currentTimeMillis()); Date lastBackupDate = null; while (lastBackupDate == null && len > 0) { try { len--; lastBackupDate = df.parse( deckBackups[len] .getName() .replaceAll("^.*-(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}).anki2$", "$1")); } catch (ParseException e) { lastBackupDate = null; } } if (lastBackupDate == null) { return true; } else if (lastBackupDate.getTime() + days * 24 * 3600000 < Utils.intNow(1000)) { return true; } else { return false; } }
private ArrayList<Object[]> newerRows(JSONArray data, String table, int modIdx) { long[] ids = new long[data.length()]; try { for (int i = 0; i < data.length(); i++) { ids[i] = data.getJSONArray(i).getLong(0); } HashMap<Long, Long> lmods = new HashMap<Long, Long>(); Cursor cur = null; try { cur = mCol.getDb() .getDatabase() .rawQuery( "SELECT id, mod FROM " + table + " WHERE id IN " + Utils.ids2str(ids) + " AND " + usnLim(), null); while (cur.moveToNext()) { lmods.put(cur.getLong(0), cur.getLong(1)); } } finally { if (cur != null && !cur.isClosed()) { cur.close(); } } ArrayList<Object[]> update = new ArrayList<Object[]>(); for (int i = 0; i < data.length(); i++) { JSONArray r = data.getJSONArray(i); if (!lmods.containsKey(r.getLong(0)) || lmods.get(r.getLong(0)) < r.getLong(modIdx)) { update.add(ConvUtils.jsonArray2Objects(r)); } } return update; } catch (JSONException e) { throw new RuntimeException(e); } }
public static ArrayList<HashMap<String, String>> getIntentInformation(Context context) { openDBIfClosed(context); Cursor cursor = null; ArrayList<HashMap<String, String>> list = new ArrayList<HashMap<String, String>>(); try { cursor = mMetaDb.query( "intentInformation", new String[] {"id", "fields"}, null, null, null, null, "id"); while (cursor.moveToNext()) { HashMap<String, String> item = new HashMap<String, String>(); item.put("id", Integer.toString(cursor.getInt(0))); String fields = cursor.getString(1); String[] split = Utils.splitFields(fields); String source = null; String target = null; for (int i = 0; i < split.length; i++) { if (source == null || source.length() == 0) { source = split[i]; } else if (target == null || target.length() == 0) { target = split[i]; } else { break; } } item.put("source", source); item.put("target", target); item.put("fields", fields); list.add(item); } } catch (SQLiteException e) { upgradeDB(mMetaDb, DATABASE_VERSION); Timber.e(e, "Error while querying intentInformation"); } finally { if (cursor != null && !cursor.isClosed()) { cursor.close(); } } return list; }
public static void performBackup(String path, int interval, boolean force) { SharedPreferences prefs = AnkiDroidApp.getSharedPrefs(AnkiDroidApp.getInstance().getBaseContext()); if (!prefs.getBoolean("useBackup", true) && !force) { return; } File collectionFile = new File(path); File[] deckBackups = getBackups(collectionFile); int len = deckBackups.length; if (len > 0 && deckBackups[len - 1].lastModified() == collectionFile.lastModified()) { Log.i(AnkiDroidApp.TAG, "performBackup: No backup necessary due to no collection changes"); return; } SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd-HH-mm"); Calendar cal = new GregorianCalendar(); cal.setTimeInMillis(System.currentTimeMillis()); Date lastBackupDate = null; while (lastBackupDate == null && len > 0) { try { len--; lastBackupDate = df.parse( deckBackups[len] .getName() .replaceAll("^.*-(\\d{4}-\\d{2}-\\d{2}-\\d{2}-\\d{2}).anki2$", "$1")); } catch (ParseException e) { lastBackupDate = null; } } if (lastBackupDate != null && lastBackupDate.getTime() + interval * 3600000 > Utils.intNow(1000) && !force) { Log.i(AnkiDroidApp.TAG, "performBackup: No backup created. Last backup younger than 5 hours"); return; } String backupFilename; try { backupFilename = String.format( Utils.ENGLISH_LOCALE, collectionFile.getName().replace(".anki2", "") + "-%s.anki2", df.format(cal.getTime())); } catch (UnknownFormatConversionException e) { Log.e(AnkiDroidApp.TAG, "performBackup: error on creating backup filename: " + e); return; } File backupFile = new File(getBackupDirectory().getPath(), backupFilename); if (backupFile.exists()) { Log.i(AnkiDroidApp.TAG, "performBackup: No new backup created. File already exists"); return; } if (getFreeDiscSpace(collectionFile) < collectionFile.length() + (MIN_FREE_SPACE * 1024 * 1024)) { Log.e(AnkiDroidApp.TAG, "performBackup: Not enough space on sd card to backup."); prefs.edit().putBoolean("noSpaceLeft", true).commit(); return; } try { InputStream stream = new FileInputStream(collectionFile); Utils.writeToFile(stream, backupFile.getAbsolutePath()); stream.close(); // set timestamp of file in order to avoid creating a new backup unless its changed backupFile.setLastModified(collectionFile.lastModified()); } catch (IOException e) { Log.e(AnkiDroidApp.TAG, Log.getStackTraceString(e)); Log.e(AnkiDroidApp.TAG, "performBackup: Copying of file failed."); return; } // delete old backups deleteDeckBackups(path, prefs.getInt("backupMax", 3)); }