use of com.ichi2.libanki.Media in project AnkiChinaAndroid by ankichinateam.
the class MediaSyncer method sync.
public String sync(long restSpace) throws UnknownHttpResponseException, MediaSyncException, NoEnoughServerSpaceException {
// of this class about this difference to the original.
if (mCol.getMedia().needScan()) {
mCon.publishProgress(R.string.sync_media_find);
mCol.log("findChanges");
try {
mCol.getMedia().findChanges();
} catch (SQLException ignored) {
return "corruptMediaDB";
}
}
// begin session and check if in sync
int lastUsn = mCol.getMedia().lastUsn();
JSONObject ret = mServer.begin();
int srvUsn = ret.getInt("usn");
if ((lastUsn == srvUsn) && !(mCol.getMedia().haveDirty())) {
return "noChanges";
}
// loop through and process changes from server
mCol.log("last local usn is " + lastUsn);
mDownloadCount = 0;
while (true) {
// Allow cancellation (note: media sync has no finish command, so just throw)
if (Connection.getIsCancelled()) {
Timber.i("Sync was cancelled");
throw new RuntimeException("UserAbortedSync");
}
JSONArray data = mServer.mediaChanges(lastUsn);
mCol.log("mediaChanges resp count: ", data.length());
if (data.length() == 0) {
break;
}
List<String> need = new ArrayList<>();
lastUsn = data.getJSONArray(data.length() - 1).getInt(1);
for (int i = 0; i < data.length(); i++) {
// Allow cancellation (note: media sync has no finish command, so just throw)
if (Connection.getIsCancelled()) {
Timber.i("Sync was cancelled");
throw new RuntimeException("UserAbortedSync");
}
String fname = data.getJSONArray(i).getString(0);
int rusn = data.getJSONArray(i).getInt(1);
String rsum = null;
if (!data.getJSONArray(i).isNull(2)) {
// If `rsum` is a JSON `null` value, `.optString(2)` will
// return `"null"` as a string
rsum = data.getJSONArray(i).optString(2);
}
Pair<String, Integer> info = mCol.getMedia().syncInfo(fname);
String lsum = info.first;
int ldirty = info.second;
mCol.log(String.format(Locale.US, "check: lsum=%s rsum=%s ldirty=%d rusn=%d fname=%s", TextUtils.isEmpty(lsum) ? "" : lsum.subSequence(0, 4), TextUtils.isEmpty(rsum) ? "" : rsum.subSequence(0, 4), ldirty, rusn, fname));
if (!TextUtils.isEmpty(rsum)) {
// added/changed remotely
if (TextUtils.isEmpty(lsum) || !lsum.equals(rsum)) {
mCol.log("will fetch");
need.add(fname);
} else {
mCol.log("have same already");
}
mCol.getMedia().markClean(Collections.singletonList(fname));
} else if (!TextUtils.isEmpty(lsum)) {
// deleted remotely
if (ldirty == 0) {
mCol.log("delete local");
mCol.getMedia().syncDelete(fname);
} else {
// conflict: local add overrides remote delete
mCol.log("conflict; will send");
}
} else {
// deleted both sides
mCol.log("both sides deleted");
mCol.getMedia().markClean(Collections.singletonList(fname));
}
}
_downloadFiles(need);
mCol.log("update last usn to " + lastUsn);
// commits
mCol.getMedia().setLastUsn(lastUsn);
}
// at this point, we're all up to date with the server's changes,
// and we need to send our own
boolean updateConflict = false;
int toSend = mCol.getMedia().dirtyCount();
long needSize = mCol.getMedia().getMediaSizeNeededUpload();
if (needSize >= 0 && restSpace < needSize && Consts.loginAnkiChina()) {
Timber.d("No Enough Server Space Exception,rest is:" + restSpace + ",need size:" + needSize);
throw new NoEnoughServerSpaceException(restSpace, needSize);
}
while (true) {
Pair<File, List<String>> changesZip = mCol.getMedia().mediaChangesZip();
File zip = changesZip.first;
try {
List<String> fnames = changesZip.second;
if (fnames.size() == 0) {
break;
}
mCon.publishProgress(String.format(AnkiDroidApp.getAppResources().getString(R.string.sync_media_changes_count), toSend));
JSONArray changes = mServer.uploadChanges(zip);
int processedCnt = changes.getInt(0);
int serverLastUsn = changes.getInt(1);
mCol.getMedia().markClean(fnames.subList(0, processedCnt));
mCol.log(String.format(Locale.US, "processed %d, serverUsn %d, clientUsn %d", processedCnt, serverLastUsn, lastUsn));
if (serverLastUsn - processedCnt == lastUsn) {
mCol.log("lastUsn in sync, updating local");
lastUsn = serverLastUsn;
// commits
mCol.getMedia().setLastUsn(serverLastUsn);
} else {
mCol.log("concurrent update, skipping usn update");
// commit for markClean
mCol.getMedia().getDb().commit();
updateConflict = true;
}
toSend -= processedCnt;
} finally {
zip.delete();
}
}
if (updateConflict) {
mCol.log("restart sync due to concurrent update");
return sync(restSpace);
}
int lcnt = mCol.getMedia().mediacount();
String sRet = mServer.mediaSanity(lcnt);
if ("OK".equals(sRet)) {
return "OK";
} else {
mCol.getMedia().forceResync();
return sRet;
}
}
use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class DeckPicker method onOptionsItemSelected.
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Resources res = getResources();
if (getDrawerToggle().onOptionsItemSelected(item)) {
return true;
}
int itemId = item.getItemId();
if (itemId == R.id.action_undo) {
Timber.i("DeckPicker:: Undo button pressed");
undo();
return true;
} else if (itemId == R.id.action_sync) {
Timber.i("DeckPicker:: Sync button pressed");
sync();
return true;
} else if (itemId == R.id.action_import) {
Timber.i("DeckPicker:: Import button pressed");
showDialogFragment(ImportFileSelectionFragment.createInstance(this));
return true;
} else if (itemId == R.id.action_new_filtered_deck) {
CreateDeckDialog createFilteredDeckDialog = new CreateDeckDialog(DeckPicker.this, R.string.new_deck, CreateDeckDialog.DeckDialogType.FILTERED_DECK, null);
createFilteredDeckDialog.setOnNewDeckCreated((id) -> {
// a filtered deck was created
openStudyOptions(true);
});
createFilteredDeckDialog.showFilteredDeckDialog();
return true;
} else if (itemId == R.id.action_check_database) {
Timber.i("DeckPicker:: Check database button pressed");
showDatabaseErrorDialog(DatabaseErrorDialog.DIALOG_CONFIRM_DATABASE_CHECK);
return true;
} else if (itemId == R.id.action_check_media) {
Timber.i("DeckPicker:: Check media button pressed");
showMediaCheckDialog(MediaCheckDialog.DIALOG_CONFIRM_MEDIA_CHECK);
return true;
} else if (itemId == R.id.action_empty_cards) {
Timber.i("DeckPicker:: Empty cards button pressed");
handleEmptyCards();
return true;
} else if (itemId == R.id.action_model_browser_open) {
Timber.i("DeckPicker:: Model browser button pressed");
Intent noteTypeBrowser = new Intent(this, ModelBrowser.class);
startActivityForResultWithAnimation(noteTypeBrowser, 0, START);
return true;
} else if (itemId == R.id.action_restore_backup) {
Timber.i("DeckPicker:: Restore from backup button pressed");
showDatabaseErrorDialog(DatabaseErrorDialog.DIALOG_CONFIRM_RESTORE_BACKUP);
return true;
} else if (itemId == R.id.action_export) {
Timber.i("DeckPicker:: Export collection button pressed");
String msg = getResources().getString(R.string.confirm_apkg_export);
mExportingDelegate.showExportDialog(msg);
return true;
}
return super.onOptionsItemSelected(item);
}
use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class ImportTest method testAnki2Mediadupes.
@Test
public void testAnki2Mediadupes() throws IOException, JSONException, ImportExportException {
// add a note that references a sound
Note n = mTestCol.newNote();
n.setField(0, "[sound:foo.mp3]");
long mid = n.model().getLong("id");
mTestCol.addNote(n);
// add that sound to the media directory
FileOutputStream os = new FileOutputStream(new File(mTestCol.getMedia().dir(), "foo.mp3"), false);
os.write("foo".getBytes());
os.close();
mTestCol.close();
// it should be imported correctly into an empty deck
Collection empty = getEmptyCol();
Importer imp = new Anki2Importer(empty, mTestCol.getPath());
imp.run();
List<String> expected = Collections.singletonList("foo.mp3");
List<String> actual = Arrays.asList(new File(empty.getMedia().dir()).list());
actual.retainAll(expected);
assertEquals(expected.size(), actual.size());
// and importing again will not duplicate, as the file content matches
empty.remCards(empty.getDb().queryLongList("select id from cards"));
imp = new Anki2Importer(empty, mTestCol.getPath());
imp.run();
expected = Collections.singletonList("foo.mp3");
actual = Arrays.asList(new File(empty.getMedia().dir()).list());
actual.retainAll(expected);
assertEquals(expected.size(), actual.size());
n = empty.getNote(empty.getDb().queryLongScalar("select id from notes"));
assertTrue(n.getFields()[0].contains("foo.mp3"));
// if the local file content is different, and import should trigger a rename
empty.remCards(empty.getDb().queryLongList("select id from cards"));
os = new FileOutputStream(new File(empty.getMedia().dir(), "foo.mp3"), false);
os.write("bar".getBytes());
os.close();
imp = new Anki2Importer(empty, mTestCol.getPath());
imp.run();
expected = Arrays.asList("foo.mp3", String.format("foo_%s.mp3", mid));
actual = Arrays.asList(new File(empty.getMedia().dir()).list());
actual.retainAll(expected);
assertEquals(expected.size(), actual.size());
n = empty.getNote(empty.getDb().queryLongScalar("select id from notes"));
assertTrue(n.getFields()[0].contains("_"));
// if the localized media file already exists, we rewrite the note and media
empty.remCards(empty.getDb().queryLongList("select id from cards"));
os = new FileOutputStream(new File(empty.getMedia().dir(), "foo.mp3"));
os.write("bar".getBytes());
os.close();
imp = new Anki2Importer(empty, mTestCol.getPath());
imp.run();
expected = Arrays.asList("foo.mp3", String.format("foo_%s.mp3", mid));
actual = Arrays.asList(new File(empty.getMedia().dir()).list());
actual.retainAll(expected);
assertEquals(expected.size(), actual.size());
n = empty.getNote(empty.getDb().queryLongScalar("select id from notes"));
assertTrue(n.getFields()[0].contains("_"));
empty.close();
}
use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class ImportTest method testApkg.
@Test
public void testApkg() throws IOException, ImportExportException {
String apkg = Shared.getTestFilePath(getTestContext(), "media.apkg");
Importer imp = new AnkiPackageImporter(mTestCol, apkg);
List<String> expected = Collections.emptyList();
List<String> actual = Arrays.asList(new File(mTestCol.getMedia().dir()).list());
actual.retainAll(expected);
assertEquals(actual.size(), expected.size());
imp.run();
expected = Collections.singletonList("foo.wav");
actual = Arrays.asList(new File(mTestCol.getMedia().dir()).list());
actual.retainAll(expected);
assertEquals(expected.size(), actual.size());
// import again should be idempotent in terms of media
mTestCol.remCards(mTestCol.getDb().queryLongList("select id from cards"));
imp = new AnkiPackageImporter(mTestCol, apkg);
imp.run();
expected = Collections.singletonList("foo.wav");
actual = Arrays.asList(new File(mTestCol.getMedia().dir()).list());
actual.retainAll(expected);
assertEquals(expected.size(), actual.size());
// but if the local file has different data, it will rename
mTestCol.remCards(mTestCol.getDb().queryLongList("select id from cards"));
FileOutputStream os = new FileOutputStream(new File(mTestCol.getMedia().dir(), "foo.wav"), false);
os.write("xyz".getBytes());
os.close();
imp = new AnkiPackageImporter(mTestCol, apkg);
imp.run();
assertEquals(2, new File(mTestCol.getMedia().dir()).list().length);
}
use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class CollectionHelper method initializeAnkiDroidDirectory.
/**
* 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 {@link #PREF_DECK_PATH}, 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 " + path);
}
if (!dir.canWrite()) {
throw new StorageAccessException("No write access to AnkiDroid directory " + path);
}
// 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", e);
}
}
}
Aggregations