Search in sources :

Example 16 with Media

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);
    }
}
Also used : ClipboardManager(android.content.ClipboardManager) Typeface(android.graphics.Typeface) SharedPreferences(android.content.SharedPreferences) SuppressLint(android.annotation.SuppressLint) AudioRecordingField(com.ichi2.anki.multimediacard.fields.AudioRecordingField) IField(com.ichi2.anki.multimediacard.fields.IField) ImageField(com.ichi2.anki.multimediacard.fields.ImageField) TextField(com.ichi2.anki.multimediacard.fields.TextField) MediaClipField(com.ichi2.anki.multimediacard.fields.MediaClipField) ImageButton(android.widget.ImageButton)

Example 17 with Media

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");
    }
}
Also used : Note(com.ichi2.libanki.Note) JSONArray(com.ichi2.utils.JSONArray) JSONException(com.ichi2.utils.JSONException) Deck(com.ichi2.libanki.Deck) ModelManager(com.ichi2.libanki.ModelManager) DeckRenameException(com.ichi2.libanki.backend.exception.DeckRenameException) JSONObject(com.ichi2.utils.JSONObject) Note(com.ichi2.libanki.Note) Model(com.ichi2.libanki.Model) Collection(com.ichi2.libanki.Collection) ConfirmModSchemaException(com.ichi2.anki.exception.ConfirmModSchemaException)

Example 18 with Media

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();
    }
}
Also used : JSONObject(com.ichi2.utils.JSONObject) BufferedInputStream(java.io.BufferedInputStream) FileInputStream(java.io.FileInputStream) InputStream(java.io.InputStream) ZipEntry(java.util.zip.ZipEntry) ArrayList(java.util.ArrayList)

Example 19 with Media

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();
}
Also used : ZipFile(java.util.zip.ZipFile) File(java.io.File) EmptyMediaException(com.ichi2.libanki.exception.EmptyMediaException) IOException(java.io.IOException) SQLException(android.database.SQLException) FileNotFoundException(java.io.FileNotFoundException)

Example 20 with Media

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);
    }
}
Also used : ZipEntry(java.util.zip.ZipEntry) ArrayList(java.util.ArrayList) JSONArray(com.ichi2.utils.JSONArray) FileNotFoundException(java.io.FileNotFoundException) IOException(java.io.IOException) Cursor(android.database.Cursor) FileInputStream(java.io.FileInputStream) BufferedInputStream(java.io.BufferedInputStream) ZipOutputStream(java.util.zip.ZipOutputStream) FileOutputStream(java.io.FileOutputStream) ZipFile(java.util.zip.ZipFile) File(java.io.File) BufferedOutputStream(java.io.BufferedOutputStream) Pair(android.util.Pair)

Aggregations

File (java.io.File)43 IOException (java.io.IOException)26 Collection (com.ichi2.libanki.Collection)25 JSONObject (com.ichi2.utils.JSONObject)19 ArrayList (java.util.ArrayList)17 Test (org.junit.Test)17 ZipFile (java.util.zip.ZipFile)14 JSONException (com.ichi2.utils.JSONException)10 FileOutputStream (java.io.FileOutputStream)10 List (java.util.List)10 JSONArray (com.ichi2.utils.JSONArray)9 FileInputStream (java.io.FileInputStream)9 FileNotFoundException (java.io.FileNotFoundException)9 SharedPreferences (android.content.SharedPreferences)8 JSONObject (org.json.JSONObject)8 Resources (android.content.res.Resources)7 Uri (android.net.Uri)7 RobolectricTest (com.ichi2.anki.RobolectricTest)7 ConfirmModSchemaException (com.ichi2.anki.exception.ConfirmModSchemaException)7 AnkiPackageImporter (com.ichi2.libanki.importer.AnkiPackageImporter)7