package com.ichi2.anki; import android.app.Activity; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Message; import android.provider.OpenableColumns; import android.support.v4.content.IntentCompat; import com.afollestad.materialdialogs.MaterialDialog; import com.ichi2.anim.ActivityTransitionAnimation; import com.ichi2.anki.dialogs.DialogHandler; import com.ichi2.anki.services.ReminderService; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import timber.log.Timber; /** * Class which handles how the application responds to different intents, forcing it to always be single task, * but allowing custom behavior depending on the intent * * @author Tim * */ public class IntentHandler extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.progress_bar); Intent intent = getIntent(); Timber.v(intent.toString()); Intent reloadIntent = new Intent(this, DeckPicker.class); reloadIntent.setDataAndType(getIntent().getData(), getIntent().getType()); String action = intent.getAction(); if (Intent.ACTION_VIEW.equals(action)) { // This intent is used for opening apkg package files // We want to go immediately to DeckPicker, clearing any history in the process Timber.i("IntentHandler/ User requested to view a file"); boolean successful = false; String errorMessage = getResources().getString(R.string.import_error_content_provider, AnkiDroidApp.getManualUrl() + "#importing"); // If the file is being sent from a content provider we need to read the content before we can open the file if (intent.getData().getScheme().equals("content")) { // Get the original filename from the content provider URI String filename = null; Cursor cursor = null; try { cursor = this.getContentResolver().query(intent.getData(), new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null); if (cursor != null && cursor.moveToFirst()) { filename = cursor.getString(0); } } finally { if (cursor != null) cursor.close(); } // Hack to fix bug where ContentResolver not returning filename correctly if (filename == null) { if (intent.getType().equals("application/apkg") || hasValidZipFile(intent)) { // Set a dummy filename if MIME type provided or is a valid zip file filename = "unknown_filename.apkg"; Timber.w("Could not retrieve filename from ContentProvider, but was valid zip file so we try to continue"); } else { Timber.e("Could not retrieve filename from ContentProvider or read content as ZipFile"); AnkiDroidApp.sendExceptionReport(new RuntimeException("Could not import apkg from ContentProvider"), "IntentHandler.java", "apkg import failed"); } } if (filename != null && !filename.toLowerCase().endsWith(".apkg")) { // Don't import if not apkg file errorMessage = getResources().getString(R.string.import_error_not_apkg_extension, filename); } else if (filename != null) { // Copy to temporary file String tempOutDir = Uri.fromFile(new File(getCacheDir(), filename)).getEncodedPath(); successful = copyFileToCache(intent, tempOutDir); // Show import dialog if (successful) { sendShowImportFileDialogMsg(tempOutDir); } else { AnkiDroidApp.sendExceptionReport(new RuntimeException("Error importing apkg file"), "IntentHandler.java", "apkg import failed"); } } } else if (intent.getData().getScheme().equals("file")) { // When the VIEW intent is sent as a file, we can open it directly without copying from content provider String filename = intent.getData().getPath(); if (filename != null && filename.endsWith(".apkg")) { // If file has apkg extension then send message to show Import dialog sendShowImportFileDialogMsg(filename); successful = true; } else { errorMessage = getResources().getString(R.string.import_error_not_apkg_extension, filename); } } // Start DeckPicker if we correctly processed ACTION_VIEW if (successful) { reloadIntent.setAction(action); reloadIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(reloadIntent); finishWithFade(); } else { // Don't import the file if it didn't load properly or doesn't have apkg extension //Themes.showThemedToast(this, getResources().getString(R.string.import_log_no_apkg), true); String title = getResources().getString(R.string.import_log_no_apkg); new MaterialDialog.Builder(this) .title(title) .content(errorMessage) .positiveText(getResources().getString(R.string.dialog_ok)) .callback(new MaterialDialog.ButtonCallback() { @Override public void onPositive(MaterialDialog dialog) { finishWithFade(); } }) .build().show(); } } else if ("com.ichi2.anki.DO_SYNC".equals(action)) { sendDoSyncMsg(); reloadIntent.setAction(action); reloadIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(reloadIntent); finishWithFade(); } else if (intent.hasExtra(ReminderService.EXTRA_DECK_ID)) { final Intent reviewIntent = new Intent(this, Reviewer.class); CollectionHelper.getInstance().getCol(this).getDecks().select(intent.getLongExtra(ReminderService.EXTRA_DECK_ID, 0)); startActivity(reviewIntent); finishWithFade(); } else { // Launcher intents should start DeckPicker if no other task exists, // otherwise go to previous task reloadIntent.setAction(Intent.ACTION_MAIN); reloadIntent.addCategory(Intent.CATEGORY_LAUNCHER); reloadIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | IntentCompat.FLAG_ACTIVITY_CLEAR_TASK); startActivityIfNeeded(reloadIntent, 0); finishWithFade(); } } /** * Send a Message to AnkiDroidApp so that the DialogMessageHandler shows the Import apkg dialog. * @param path path to apkg file which will be imported */ private void sendShowImportFileDialogMsg(String path) { // Get the filename from the path File f = new File(path); String filename = f.getName(); // Create a new message for DialogHandler so that we see the appropriate import dialog in DeckPicker Message handlerMessage = Message.obtain(); Bundle msgData = new Bundle(); msgData.putString("importPath", path); handlerMessage.setData(msgData); if (filename.equals("collection.apkg")) { // Show confirmation dialog asking to confirm import with replace when file called "collection.apkg" handlerMessage.what = DialogHandler.MSG_SHOW_COLLECTION_IMPORT_REPLACE_DIALOG; } else { // Otherwise show confirmation dialog asking to confirm import with add handlerMessage.what = DialogHandler.MSG_SHOW_COLLECTION_IMPORT_ADD_DIALOG; } // Store the message in AnkiDroidApp message holder, which is loaded later in AnkiActivity.onResume DialogHandler.storeMessage(handlerMessage); } /** * Send a Message to AnkiDroidApp so that the DialogMessageHandler forces a sync */ private void sendDoSyncMsg() { // Create a new message for DialogHandler Message handlerMessage = Message.obtain(); handlerMessage.what = DialogHandler.MSG_DO_SYNC; // Store the message in AnkiDroidApp message holder, which is loaded later in AnkiActivity.onResume DialogHandler.storeMessage(handlerMessage); } /** Finish Activity using FADE animation **/ private void finishWithFade() { finish(); ActivityTransitionAnimation.slide(this, ActivityTransitionAnimation.UP); } /** * Check if the InputStream is to a valid non-empty zip file * @param intent intent from which to get input stream * @return whether or not valid zip file */ private boolean hasValidZipFile(Intent intent) { // Get an input stream to the data in ContentProvider InputStream in = null; try { in = getContentResolver().openInputStream(intent.getData()); } catch (FileNotFoundException e) { Timber.e(e, "Could not open input stream to intent data"); } // Make sure it's not null if (in == null) { Timber.e("Could not open input stream to intent data"); return false; } // Open zip input stream ZipInputStream zis = new ZipInputStream(in); boolean ok = false; try { try { ZipEntry ze = zis.getNextEntry(); if (ze != null) { // set ok flag to true if there are any valid entries in the zip file ok = true; } } catch (IOException e) { // don't set ok flag Timber.d(e, "Error checking if provided file has a zip entry"); } } finally { // close the input streams try { zis.close(); in.close(); } catch (Exception e) { Timber.d(e, "Error closing the InputStream"); } } return ok; } /** * Copy the data from the intent to a temporary file * @param intent intent from which to get input stream * @param tempPath temporary path to store the cached file * @return whether or not copy was successful */ private boolean copyFileToCache(Intent intent, String tempPath) { // Get an input stream to the data in ContentProvider InputStream in; try { in = getContentResolver().openInputStream(intent.getData()); } catch (FileNotFoundException e) { Timber.e(e, "Could not open input stream to intent data"); return false; } // Check non-null if (in == null) { return false; } // Create new output stream in temporary path OutputStream out; try { out = new FileOutputStream(tempPath); } catch (FileNotFoundException e) { Timber.e(e, "Could not access destination file %s", tempPath); return false; } try { // Copy the input stream to temporary file byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } in.close(); } catch (IOException e) { Timber.e(e, "Could not copy file to %s", tempPath); return false; } finally { try { out.close(); } catch (IOException e) { Timber.e(e, "Error closing tempOutDir %s", tempPath); } } return true; } }