use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class NoteEditor method populateEditFields.
private void populateEditFields(FieldChangeType type, boolean editModelMode) {
List<FieldEditLine> editLines = mFieldState.loadFieldEditLines(type);
mFieldsLayoutContainer.removeAllViews();
mCustomViewIds.clear();
mEditFields = new LinkedList<>();
// Use custom font if selected from preferences
Typeface customTypeface = null;
SharedPreferences preferences = AnkiDroidApp.getSharedPrefs(getBaseContext());
String customFont = preferences.getString("browserEditorFont", "");
if (!"".equals(customFont)) {
customTypeface = AnkiFont.getTypeface(this, customFont);
}
ClipboardManager clipboard = ContextCompat.getSystemService(this, ClipboardManager.class);
FieldEditLine previous = null;
mCustomViewIds.ensureCapacity(editLines.size());
for (int i = 0; i < editLines.size(); i++) {
FieldEditLine edit_line_view = editLines.get(i);
mCustomViewIds.add(edit_line_view.getId());
FieldEditText newTextbox = edit_line_view.getEditText();
newTextbox.setImagePasteListener(this::onImagePaste);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
if (i == 0) {
findViewById(R.id.note_deck_spinner).setNextFocusForwardId(newTextbox.getId());
}
if (previous != null) {
previous.getLastViewInTabOrder().setNextFocusForwardId(newTextbox.getId());
}
}
previous = edit_line_view;
edit_line_view.setEnableAnimation(animationEnabled());
// TODO: Remove the >= M check - one callback works on API 11.
if (CompatHelper.getSdkVersion() >= Build.VERSION_CODES.M) {
// Use custom implementation of ActionMode.Callback customize selection and insert menus
Field f = new Field(getFieldByIndex(i), getCol());
ActionModeCallback actionModeCallback = new ActionModeCallback(newTextbox, f);
edit_line_view.setActionModeCallbacks(actionModeCallback);
}
edit_line_view.setTypeface(customTypeface);
edit_line_view.setHintLocale(getHintLocaleForField(edit_line_view.getName()));
initFieldEditText(newTextbox, i, !editModelMode);
mEditFields.add(newTextbox);
SharedPreferences prefs = AnkiDroidApp.getSharedPrefs(this);
if (prefs.getInt("note_editor_font_size", -1) > 0) {
newTextbox.setTextSize(prefs.getInt("note_editor_font_size", -1));
}
newTextbox.setCapitalize(prefs.getBoolean("note_editor_capitalize", true));
ImageButton mediaButton = edit_line_view.getMediaButton();
ImageButton toggleStickyButton = edit_line_view.getToggleSticky();
// Load icons from attributes
int[] icons = Themes.getResFromAttr(this, new int[] { R.attr.attachFileImage, R.attr.upDownImage, R.attr.toggleStickyImage });
// Make the icon change between media icon and switch field icon depending on whether editing note type
if (editModelMode && allowFieldRemapping()) {
// Allow remapping if originally more than two fields
mediaButton.setBackgroundResource(icons[1]);
setRemapButtonListener(mediaButton, i);
toggleStickyButton.setBackgroundResource(0);
} else if (editModelMode && !allowFieldRemapping()) {
mediaButton.setBackgroundResource(0);
toggleStickyButton.setBackgroundResource(0);
} else {
// Use media editor button if not changing note type
mediaButton.setBackgroundResource(icons[0]);
setMMButtonListener(mediaButton, i);
// toggle sticky button
toggleStickyButton.setBackgroundResource(icons[2]);
setToggleStickyButtonListener(toggleStickyButton, i);
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O && previous != null) {
previous.getLastViewInTabOrder().setNextFocusForwardId(R.id.CardEditorTagButton);
}
mediaButton.setContentDescription(getString(R.string.multimedia_editor_attach_mm_content, edit_line_view.getName()));
toggleStickyButton.setContentDescription(getString(R.string.note_editor_toggle_sticky, edit_line_view.getName()));
mFieldsLayoutContainer.addView(edit_line_view);
}
}
use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class CardContentProvider method insert.
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
if (!hasReadWritePermission() && shouldEnforceQueryOrInsertSecurity()) {
throwSecurityException("insert", uri);
}
Collection col = CollectionHelper.getInstance().getCol(mContext);
if (col == null) {
throw new IllegalStateException(COL_NULL_ERROR_MSG);
}
col.log(getLogMessage("insert", uri));
// Find out what data the user is requesting
int match = sUriMatcher.match(uri);
switch(match) {
case NOTES:
{
/* Insert new note with specified fields and tags
*/
Long modelId = values.getAsLong(FlashCardsContract.Note.MID);
String flds = values.getAsString(FlashCardsContract.Note.FLDS);
String tags = values.getAsString(FlashCardsContract.Note.TAGS);
Models.AllowEmpty allowEmpty = Models.AllowEmpty.fromBoolean(values.getAsBoolean(FlashCardsContract.Note.ALLOW_EMPTY));
// Create empty note
com.ichi2.libanki.Note newNote = new com.ichi2.libanki.Note(col, col.getModels().get(modelId));
// Set fields
String[] fldsArray = Utils.splitFields(flds);
// Check that correct number of flds specified
if (fldsArray.length != newNote.getFields().length) {
throw new IllegalArgumentException("Incorrect flds argument : " + flds);
}
for (int idx = 0; idx < fldsArray.length; idx++) {
newNote.setField(idx, fldsArray[idx]);
}
// Set tags
if (tags != null) {
newNote.setTagsFromStr(tags);
}
// Add to collection
col.addNote(newNote, allowEmpty);
col.save();
return Uri.withAppendedPath(FlashCardsContract.Note.CONTENT_URI, Long.toString(newNote.getId()));
}
case NOTES_ID:
// Note ID is generated automatically by libanki
throw new IllegalArgumentException("Not possible to insert note with specific ID");
case NOTES_ID_CARDS:
case NOTES_ID_CARDS_ORD:
// Cards are generated automatically by libanki
throw new IllegalArgumentException("Not possible to insert cards directly (only through NOTES)");
case MODELS:
// Get input arguments
String modelName = values.getAsString(FlashCardsContract.Model.NAME);
String css = values.getAsString(FlashCardsContract.Model.CSS);
Long did = values.getAsLong(FlashCardsContract.Model.DECK_ID);
String fieldNames = values.getAsString(FlashCardsContract.Model.FIELD_NAMES);
Integer numCards = values.getAsInteger(FlashCardsContract.Model.NUM_CARDS);
Integer sortf = values.getAsInteger(FlashCardsContract.Model.SORT_FIELD_INDEX);
Integer type = values.getAsInteger(FlashCardsContract.Model.TYPE);
String latexPost = values.getAsString(FlashCardsContract.Model.LATEX_POST);
String latexPre = values.getAsString(FlashCardsContract.Model.LATEX_PRE);
// Throw exception if required fields empty
if (modelName == null || fieldNames == null || numCards == null) {
throw new IllegalArgumentException("Model name, field_names, and num_cards can't be empty");
}
if (did != null && col.getDecks().isDyn(did)) {
throw new IllegalArgumentException("Cannot set a filtered deck as default deck for a model");
}
// Create a new model
ModelManager mm = col.getModels();
Model newModel = mm.newModel(modelName);
try {
// Add the fields
String[] allFields = Utils.splitFields(fieldNames);
for (String f : allFields) {
mm.addFieldInNewModel(newModel, mm.newField(f));
}
// Add some empty card templates
for (int idx = 0; idx < numCards; idx++) {
String card_name = mContext.getResources().getString(R.string.card_n_name, idx + 1);
JSONObject t = Models.newTemplate(card_name);
t.put("qfmt", String.format("{{%s}}", allFields[0]));
String answerField = allFields[0];
if (allFields.length > 1) {
answerField = allFields[1];
}
t.put("afmt", String.format("{{FrontSide}}\\n\\n<hr id=answer>\\n\\n{{%s}}", answerField));
mm.addTemplateInNewModel(newModel, t);
}
// Add the CSS if specified
if (css != null) {
newModel.put("css", css);
}
// Add the did if specified
if (did != null) {
newModel.put("did", did);
}
if (sortf != null && sortf < allFields.length) {
newModel.put("sortf", sortf);
}
if (type != null) {
newModel.put("type", type);
}
if (latexPost != null) {
newModel.put("latexPost", latexPost);
}
if (latexPre != null) {
newModel.put("latexPre", latexPre);
}
// Add the model to collection (from this point on edits will require a full-sync)
mm.add(newModel);
col.save();
// Get the mid and return a URI
String mid = Long.toString(newModel.getLong("id"));
return Uri.withAppendedPath(FlashCardsContract.Model.CONTENT_URI, mid);
} catch (JSONException e) {
Timber.e(e, "Could not set a field of new model %s", modelName);
return null;
}
case MODELS_ID:
// Model ID is generated automatically by libanki
throw new IllegalArgumentException("Not possible to insert model with specific ID");
case MODELS_ID_TEMPLATES:
{
ModelManager models = col.getModels();
Long mid = getModelIdFromUri(uri, col);
Model existingModel = models.get(mid);
if (existingModel == null) {
throw new IllegalArgumentException("model missing: " + mid);
}
String name = values.getAsString(CardTemplate.NAME);
String qfmt = values.getAsString(CardTemplate.QUESTION_FORMAT);
String afmt = values.getAsString(CardTemplate.ANSWER_FORMAT);
String bqfmt = values.getAsString(CardTemplate.BROWSER_QUESTION_FORMAT);
String bafmt = values.getAsString(CardTemplate.BROWSER_ANSWER_FORMAT);
try {
JSONObject t = Models.newTemplate(name);
t.put("qfmt", qfmt);
t.put("afmt", afmt);
t.put("bqfmt", bqfmt);
t.put("bafmt", bafmt);
models.addTemplate(existingModel, t);
models.save(existingModel);
col.save();
return ContentUris.withAppendedId(uri, t.getInt("ord"));
} catch (ConfirmModSchemaException e) {
throw new IllegalArgumentException("Unable to add template without user requesting/accepting full-sync", e);
} catch (JSONException e) {
throw new IllegalArgumentException("Unable to get ord from new template", e);
}
}
case MODELS_ID_TEMPLATES_ID:
throw new IllegalArgumentException("Not possible to insert template with specific ORD");
case MODELS_ID_FIELDS:
{
ModelManager models = col.getModels();
long mid = getModelIdFromUri(uri, col);
Model existingModel = models.get(mid);
if (existingModel == null) {
throw new IllegalArgumentException("model missing: " + mid);
}
String name = values.getAsString(FlashCardsContract.Model.FIELD_NAME);
if (name == null) {
throw new IllegalArgumentException("field name missing for model: " + mid);
}
JSONObject field = models.newField(name);
try {
models.addField(existingModel, field);
col.save();
JSONArray flds = existingModel.getJSONArray("flds");
return ContentUris.withAppendedId(uri, flds.length() - 1);
} catch (ConfirmModSchemaException e) {
throw new IllegalArgumentException("Unable to insert field: " + name, e);
} catch (JSONException e) {
throw new IllegalArgumentException("Unable to get newly created field: " + name, e);
}
}
case SCHEDULE:
// Doesn't make sense to insert an object into the schedule table
throw new IllegalArgumentException("Not possible to perform insert operation on schedule");
case DECKS:
// Insert new deck with specified name
String deckName = values.getAsString(FlashCardsContract.Deck.DECK_NAME);
did = col.getDecks().id_for_name(deckName);
if (did != null) {
throw new IllegalArgumentException("Deck name already exists: " + deckName);
}
if (!Decks.isValidDeckName(deckName)) {
throw new IllegalArgumentException("Invalid deck name '" + deckName + "'");
}
try {
did = col.getDecks().id(deckName);
} catch (DeckRenameException filteredSubdeck) {
throw new IllegalArgumentException(filteredSubdeck.getMessage());
}
Deck deck = col.getDecks().get(did);
if (deck != null) {
try {
String deckDesc = values.getAsString(FlashCardsContract.Deck.DECK_DESC);
if (deckDesc != null) {
deck.put("desc", deckDesc);
}
} catch (JSONException e) {
Timber.e(e, "Could not set a field of new deck %s", deckName);
return null;
}
}
col.getDecks().flush();
return Uri.withAppendedPath(FlashCardsContract.Deck.CONTENT_ALL_URI, Long.toString(did));
case DECK_SELECTED:
// Can't have more than one selected deck
throw new IllegalArgumentException("Selected deck can only be queried and updated");
case DECKS_ID:
// Deck ID is generated automatically by libanki
throw new IllegalArgumentException("Not possible to insert deck with specific ID");
case MEDIA:
// contentvalue should have data and preferredFileName values
return insertMediaFile(values, col);
default:
// Unknown URI type
throw new IllegalArgumentException("uri " + uri + " is not supported");
}
}
use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class Media method addFilesFromZip.
/**
* Extract zip data; return the number of files extracted. Unlike the python version, this method consumes a
* ZipFile stored on disk instead of a String buffer. Holding the entire downloaded data in memory is not feasible
* since some devices can have very limited heap space.
*
* This method closes the file before it returns.
*/
public int addFilesFromZip(ZipFile z) throws IOException {
try {
// get meta info first
JSONObject meta = new JSONObject(Utils.convertStreamToString(z.getInputStream(z.getEntry("_meta"))));
// then loop through all files
int cnt = 0;
ArrayList<? extends ZipEntry> zipEntries = Collections.list(z.entries());
List<Object[]> media = new ArrayList<>(zipEntries.size());
for (ZipEntry i : zipEntries) {
String fileName = i.getName();
if ("_meta".equals(fileName)) {
// ignore previously-retrieved meta
continue;
}
String name = meta.getString(fileName);
// normalize name for platform
name = Utils.nfcNormalized(name);
// save file
String destPath = (dir() + File.separator) + name;
try (InputStream zipInputStream = z.getInputStream(i)) {
Utils.writeToFile(zipInputStream, destPath);
}
String csum = Utils.fileChecksum(destPath);
// update db
media.add(new Object[] { name, csum, _mtime(destPath), 0 });
cnt += 1;
}
if (!media.isEmpty()) {
mDb.executeMany("insert or replace into media values (?,?,?,?)", media);
}
return cnt;
} finally {
z.close();
}
}
use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class Media method rebuildIfInvalid.
public void rebuildIfInvalid() throws IOException {
try {
_changed();
return;
} catch (Exception e) {
if (!ExceptionUtil.containsMessage(e, "no such table: meta")) {
throw e;
}
AnkiDroidApp.sendExceptionReport(e, "media::rebuildIfInvalid");
// TODO: We don't know the root cause of the missing meta table
Timber.w(e, "Error accessing media database. Rebuilding");
// continue below
}
// Delete and recreate the file
mDb.getDatabase().close();
String path = mDb.getPath();
Timber.i("Deleted %s", path);
new File(path).delete();
mDb = new DB(path);
_initDB();
}
use of com.ichi2.libanki.Media in project Anki-Android by ankidroid.
the class Media method mediaChangesZip.
/*
* Media syncing: zips
* ***********************************************************
*/
/**
* Unlike python, our temp zip file will be on disk instead of in memory. This avoids storing
* potentially large files in memory which is not feasible with Android's limited heap space.
* <p>
* Notes:
* <p>
* - The maximum size of the changes zip is decided by the constant SYNC_ZIP_SIZE. If a media file exceeds this
* limit, only that file (in full) will be zipped to be sent to the server.
* <p>
* - This method will be repeatedly called from MediaSyncer until there are no more files (marked "dirty" in the DB)
* to send.
* <p>
* - Since AnkiDroid avoids scanning the media directory on every sync, it is possible for a file to be marked as a
* new addition but actually have been deleted (e.g., with a file manager). In this case we skip over the file
* and mark it as removed in the database. (This behaviour differs from the desktop client).
* <p>
*/
public Pair<File, List<String>> mediaChangesZip() {
File f = new File(mCol.getPath().replaceFirst("collection\\.anki2$", "tmpSyncToServer.zip"));
List<String> fnames = new ArrayList<>();
try (ZipOutputStream z = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(f)));
Cursor cur = mDb.query("select fname, csum from media where dirty=1 limit " + Consts.SYNC_MAX_FILES)) {
z.setMethod(ZipOutputStream.DEFLATED);
// meta is a list of (fname, zipname), where zipname of null is a deleted file
// NOTE: In python, meta is a list of tuples that then gets serialized into json and added
// to the zip as a string. In our version, we use JSON objects from the start to avoid the
// serialization step. Instead of a list of tuples, we use JSONArrays of JSONArrays.
JSONArray meta = new JSONArray();
int sz = 0;
byte[] buffer = new byte[2048];
for (int c = 0; cur.moveToNext(); c++) {
String fname = cur.getString(0);
String csum = cur.getString(1);
fnames.add(fname);
String normname = Utils.nfcNormalized(fname);
if (!TextUtils.isEmpty(csum)) {
try {
mCol.log("+media zip " + fname);
File file = new File(dir(), fname);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file), 2048);
z.putNextEntry(new ZipEntry(Integer.toString(c)));
int count = 0;
while ((count = bis.read(buffer, 0, 2048)) != -1) {
z.write(buffer, 0, count);
}
z.closeEntry();
bis.close();
meta.put(new JSONArray().put(normname).put(Integer.toString(c)));
sz += file.length();
} catch (FileNotFoundException e) {
Timber.w(e);
// A file has been marked as added but no longer exists in the media directory.
// Skip over it and mark it as removed in the db.
removeFile(fname);
}
} else {
mCol.log("-media zip " + fname);
meta.put(new JSONArray().put(normname).put(""));
}
if (sz >= Consts.SYNC_MAX_BYTES) {
break;
}
}
z.putNextEntry(new ZipEntry("_meta"));
z.write(Utils.jsonToString(meta).getBytes());
z.closeEntry();
// Don't leave lingering temp files if the VM terminates.
f.deleteOnExit();
return new Pair<>(f, fnames);
} catch (IOException e) {
Timber.e(e, "Failed to create media changes zip: ");
throw new RuntimeException(e);
}
}
Aggregations