Search in sources :

Example 11 with Media

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;
    }
}
Also used : SQLException(android.database.SQLException) JSONArray(com.ichi2.utils.JSONArray) ArrayList(java.util.ArrayList) JSONObject(com.ichi2.utils.JSONObject) ArrayList(java.util.ArrayList) List(java.util.List) File(java.io.File) ZipFile(java.util.zip.ZipFile) NoEnoughServerSpaceException(com.ichi2.anki.exception.NoEnoughServerSpaceException)

Example 12 with Media

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);
}
Also used : DividerItemDecoration(androidx.recyclerview.widget.DividerItemDecoration) StartupFailure(com.ichi2.anki.InitialActivity.StartupFailure) Bundle(android.os.Bundle) NonNull(androidx.annotation.NonNull) Uri(android.net.Uri) DialogHandler(com.ichi2.anki.dialogs.DialogHandler) Drawable(android.graphics.drawable.Drawable) ShortcutManagerCompat(androidx.core.content.pm.ShortcutManagerCompat) Manifest(android.Manifest) Decks(com.ichi2.libanki.Decks) Fragment(androidx.fragment.app.Fragment) JSONException(com.ichi2.utils.JSONException) ContextCompat(androidx.core.content.ContextCompat) DeckPickerBackupNoSpaceLeftDialog(com.ichi2.anki.dialogs.DeckPickerBackupNoSpaceLeftDialog) IntentFilter(android.content.IntentFilter) Triple(com.ichi2.utils.Triple) SearchView(androidx.appcompat.widget.SearchView) DeckPickerContextMenu(com.ichi2.anki.dialogs.DeckPickerContextMenu) Cancellable(com.ichi2.async.Cancellable) DeckRenameException(com.ichi2.libanki.backend.exception.DeckRenameException) StringRes(androidx.annotation.StringRes) Unit(kotlin.Unit) Nullable(androidx.annotation.Nullable) Message(android.os.Message) HostNumFactory(com.ichi2.anki.web.HostNumFactory) CompatHelper(com.ichi2.compat.CompatHelper) DeckAdapter(com.ichi2.anki.widgets.DeckAdapter) LinearLayoutManager(androidx.recyclerview.widget.LinearLayoutManager) DeckPickerNoSpaceToDowngradeDialog(com.ichi2.anki.dialogs.DeckPickerNoSpaceToDowngradeDialog) DeckPickerConfirmDeleteDeckDialog(com.ichi2.anki.dialogs.DeckPickerConfirmDeleteDeckDialog) Direction(com.ichi2.anim.ActivityTransitionAnimation.Direction) FULL_DOWNLOAD(com.ichi2.async.Connection.ConflictResolution.FULL_DOWNLOAD) SdCardReceiver(com.ichi2.anki.receiver.SdCardReceiver) Editor(android.content.SharedPreferences.Editor) CustomSyncServerUrlException(com.ichi2.libanki.sync.CustomSyncServerUrlException) FileSizeFormatter(com.ichi2.anki.dialogs.DeckPickerNoSpaceToDowngradeDialog.FileSizeFormatter) DeckPickerNoSpaceLeftDialog(com.ichi2.anki.dialogs.DeckPickerNoSpaceLeftDialog) StudyOptionsListener(com.ichi2.anki.StudyOptionsFragment.StudyOptionsListener) BadgeDrawableBuilder(com.ichi2.ui.BadgeDrawableBuilder) Menu(android.view.Menu) DeckService(com.ichi2.anki.servicelayer.DeckService) Connection(com.ichi2.async.Connection) Settings(android.provider.Settings) AnkiPackageImporter(com.ichi2.libanki.importer.AnkiPackageImporter) SwipeRefreshLayout(androidx.swiperefreshlayout.widget.SwipeRefreshLayout) CollectionIntegrityStorageCheck(com.ichi2.anki.CollectionHelper.CollectionIntegrityStorageCheck) ActivityExportingDelegate(com.ichi2.anki.export.ActivityExportingDelegate) TextUtils(android.text.TextUtils) File(java.io.File) SharedPreferences(android.content.SharedPreferences) TypedValue(android.util.TypedValue) IconCompat(androidx.core.graphics.drawable.IconCompat) ImportUtils(com.ichi2.utils.ImportUtils) EditText(android.widget.EditText) SchedulerService(com.ichi2.anki.servicelayer.SchedulerService) LinearLayout(android.widget.LinearLayout) AsyncDialogFragment(com.ichi2.anki.dialogs.AsyncDialogFragment) PackageManager(android.content.pm.PackageManager) TaskManager(com.ichi2.async.TaskManager) WindowManager(android.view.WindowManager) UsageAnalytics(com.ichi2.anki.analytics.UsageAnalytics) ModelManager(com.ichi2.libanki.ModelManager) ConfirmationDialog(com.ichi2.anki.dialogs.ConfirmationDialog) AnkiStatsTaskHandler(com.ichi2.anki.stats.AnkiStatsTaskHandler) Permissions(com.ichi2.utils.Permissions) View(android.view.View) RecyclerView(androidx.recyclerview.widget.RecyclerView) SyncStatus(com.ichi2.utils.SyncStatus) FragmentTransaction(androidx.fragment.app.FragmentTransaction) BroadcastReceiver(android.content.BroadcastReceiver) DatabaseErrorDialog(com.ichi2.anki.dialogs.DatabaseErrorDialog) CustomStudyDialogFactory(com.ichi2.anki.dialogs.customstudy.CustomStudyDialogFactory) Timber(timber.log.Timber) UndoService(com.ichi2.anki.servicelayer.UndoService) List(java.util.List) TextView(android.widget.TextView) ImportFileSelectionFragment(com.ichi2.anki.dialogs.ImportFileSelectionFragment) RelativeLayout(android.widget.RelativeLayout) Filterable(android.widget.Filterable) TaskListenerWithContext(com.ichi2.async.TaskListenerWithContext) ViewPropertyAnimator(android.view.ViewPropertyAnimator) MaterialDialog(com.afollestad.materialdialogs.MaterialDialog) Snackbar(com.google.android.material.snackbar.Snackbar) Window(android.view.Window) VersionUtils(com.ichi2.utils.VersionUtils) Context(android.content.Context) TaskListener(com.ichi2.async.TaskListener) KeyEvent(android.view.KeyEvent) GravityEnum(com.afollestad.materialdialogs.GravityEnum) Pair(android.util.Pair) DeckPickerAnalyticsOptInDialog(com.ichi2.anki.dialogs.DeckPickerAnalyticsOptInDialog) ImportDialog(com.ichi2.anki.dialogs.ImportDialog) Intent(android.content.Intent) Collection(com.ichi2.libanki.Collection) StyledProgressDialog(com.ichi2.themes.StyledProgressDialog) PixelFormat(android.graphics.PixelFormat) TypedArray(android.content.res.TypedArray) MenuItem(android.view.MenuItem) WidgetStatus(com.ichi2.widget.WidgetStatus) SyncErrorDialog(com.ichi2.anki.dialogs.SyncErrorDialog) Payload(com.ichi2.async.Connection.Payload) Model(com.ichi2.libanki.Model) Build(android.os.Build) ShortcutInfoCompat(androidx.core.content.pm.ShortcutInfoCompat) Utils(com.ichi2.libanki.Utils) DialogInterface(android.content.DialogInterface) ConfirmModSchemaException(com.ichi2.anki.exception.ConfirmModSchemaException) Computation(com.ichi2.utils.Computation) AbstractDeckTreeNode(com.ichi2.libanki.sched.AbstractDeckTreeNode) ActivityCompat(androidx.core.app.ActivityCompat) CreateDeckDialog(com.ichi2.anki.dialogs.CreateDeckDialog) CollectionTask(com.ichi2.async.CollectionTask) SQLException(android.database.SQLException) CustomStudyDialog(com.ichi2.anki.dialogs.customstudy.CustomStudyDialog) Syncer(com.ichi2.libanki.sync.Syncer) MediaCheckDialog(com.ichi2.anki.dialogs.MediaCheckDialog) AdaptionUtil(com.ichi2.utils.AdaptionUtil) VisibleForTesting(androidx.annotation.VisibleForTesting) Resources(android.content.res.Resources) OnClickListener(android.view.View.OnClickListener) Intent(android.content.Intent) Resources(android.content.res.Resources) CreateDeckDialog(com.ichi2.anki.dialogs.CreateDeckDialog)

Example 13 with Media

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();
}
Also used : Anki2Importer(com.ichi2.libanki.importer.Anki2Importer) Note(com.ichi2.libanki.Note) FileOutputStream(java.io.FileOutputStream) Collection(com.ichi2.libanki.Collection) File(java.io.File) Anki2Importer(com.ichi2.libanki.importer.Anki2Importer) Importer(com.ichi2.libanki.importer.Importer) AnkiPackageImporter(com.ichi2.libanki.importer.AnkiPackageImporter) NoteImporter(com.ichi2.libanki.importer.NoteImporter) TextImporter(com.ichi2.libanki.importer.TextImporter) Test(org.junit.Test) InstrumentedTest(com.ichi2.anki.tests.InstrumentedTest)

Example 14 with Media

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);
}
Also used : AnkiPackageImporter(com.ichi2.libanki.importer.AnkiPackageImporter) FileOutputStream(java.io.FileOutputStream) File(java.io.File) Anki2Importer(com.ichi2.libanki.importer.Anki2Importer) Importer(com.ichi2.libanki.importer.Importer) AnkiPackageImporter(com.ichi2.libanki.importer.AnkiPackageImporter) NoteImporter(com.ichi2.libanki.importer.NoteImporter) TextImporter(com.ichi2.libanki.importer.TextImporter) Test(org.junit.Test) InstrumentedTest(com.ichi2.anki.tests.InstrumentedTest)

Example 15 with Media

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);
        }
    }
}
Also used : StorageAccessException(com.ichi2.anki.exception.StorageAccessException) IOException(java.io.IOException) File(java.io.File)

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