1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/FilePickerResultHandler.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,270 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko; 1.9 + 1.10 +import org.mozilla.gecko.mozglue.GeckoLoader; 1.11 +import org.mozilla.gecko.util.ActivityResultHandler; 1.12 +import org.mozilla.gecko.util.ThreadUtils; 1.13 + 1.14 +import android.app.Activity; 1.15 +import android.content.ContentResolver; 1.16 +import android.content.Context; 1.17 +import android.content.Intent; 1.18 +import android.database.Cursor; 1.19 +import android.net.Uri; 1.20 +import android.os.Bundle; 1.21 +import android.os.Environment; 1.22 +import android.provider.MediaStore; 1.23 +import android.provider.OpenableColumns; 1.24 +import android.support.v4.app.FragmentActivity; 1.25 +import android.support.v4.app.LoaderManager; 1.26 +import android.support.v4.app.LoaderManager.LoaderCallbacks; 1.27 +import android.support.v4.content.CursorLoader; 1.28 +import android.support.v4.content.Loader; 1.29 +import android.text.TextUtils; 1.30 +import android.text.format.Time; 1.31 +import android.util.Log; 1.32 +import android.os.Process; 1.33 + 1.34 +import java.io.File; 1.35 +import java.io.FileOutputStream; 1.36 +import java.io.InputStream; 1.37 +import java.io.IOException; 1.38 + 1.39 +class FilePickerResultHandler implements ActivityResultHandler { 1.40 + private static final String LOGTAG = "GeckoFilePickerResultHandler"; 1.41 + private static final String UPLOADS_DIR = "uploads"; 1.42 + 1.43 + protected final FilePicker.ResultHandler handler; 1.44 + private final int tabId; 1.45 + private final File cacheDir; 1.46 + 1.47 + // this code is really hacky and doesn't belong anywhere so I'm putting it here for now 1.48 + // until I can come up with a better solution. 1.49 + private String mImageName = ""; 1.50 + 1.51 + /* Use this constructor to asynchronously listen for results */ 1.52 + public FilePickerResultHandler(final FilePicker.ResultHandler handler, final Context context, final int tabId) { 1.53 + this.tabId = tabId; 1.54 + cacheDir = new File(context.getCacheDir(), UPLOADS_DIR); 1.55 + this.handler = handler; 1.56 + } 1.57 + 1.58 + private void sendResult(String res) { 1.59 + if (handler != null) { 1.60 + handler.gotFile(res); 1.61 + } 1.62 + } 1.63 + 1.64 + @Override 1.65 + public void onActivityResult(int resultCode, Intent intent) { 1.66 + if (resultCode != Activity.RESULT_OK) { 1.67 + sendResult(""); 1.68 + return; 1.69 + } 1.70 + 1.71 + // Camera results won't return an Intent. Use the file name we passed to the original intent. 1.72 + if (intent == null) { 1.73 + if (mImageName != null) { 1.74 + File file = new File(Environment.getExternalStorageDirectory(), mImageName); 1.75 + sendResult(file.getAbsolutePath()); 1.76 + } else { 1.77 + sendResult(""); 1.78 + } 1.79 + return; 1.80 + } 1.81 + 1.82 + Uri uri = intent.getData(); 1.83 + if (uri == null) { 1.84 + sendResult(""); 1.85 + return; 1.86 + } 1.87 + 1.88 + // Some file pickers may return a file uri 1.89 + if ("file".equals(uri.getScheme())) { 1.90 + String path = uri.getPath(); 1.91 + sendResult(path == null ? "" : path); 1.92 + return; 1.93 + } 1.94 + 1.95 + final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); 1.96 + final LoaderManager lm = fa.getSupportLoaderManager(); 1.97 + // Finally, Video pickers and some file pickers may return a content provider. 1.98 + Cursor cursor = null; 1.99 + try { 1.100 + // Try a query to make sure the expected columns exist 1.101 + final ContentResolver cr = fa.getContentResolver(); 1.102 + cursor = cr.query(uri, new String[] { MediaStore.Video.Media.DATA }, null, null, null); 1.103 + 1.104 + int index = cursor.getColumnIndex(MediaStore.Video.Media.DATA); 1.105 + if (index >= 0) { 1.106 + lm.initLoader(intent.hashCode(), null, new VideoLoaderCallbacks(uri)); 1.107 + return; 1.108 + } 1.109 + } catch(Exception ex) { 1.110 + // We'll try a different loader below 1.111 + } finally { 1.112 + if (cursor != null) { 1.113 + cursor.close(); 1.114 + } 1.115 + } 1.116 + 1.117 + lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri)); 1.118 + return; 1.119 + } 1.120 + 1.121 + public String generateImageName() { 1.122 + Time now = new Time(); 1.123 + now.setToNow(); 1.124 + mImageName = now.format("%Y-%m-%d %H.%M.%S") + ".jpg"; 1.125 + return mImageName; 1.126 + } 1.127 + 1.128 + private class VideoLoaderCallbacks implements LoaderCallbacks<Cursor> { 1.129 + final private Uri uri; 1.130 + public VideoLoaderCallbacks(Uri uri) { 1.131 + this.uri = uri; 1.132 + } 1.133 + 1.134 + @Override 1.135 + public Loader<Cursor> onCreateLoader(int id, Bundle args) { 1.136 + final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); 1.137 + return new CursorLoader(fa, 1.138 + uri, 1.139 + new String[] { MediaStore.Video.Media.DATA }, 1.140 + null, // selection 1.141 + null, // selectionArgs 1.142 + null); // sortOrder 1.143 + } 1.144 + 1.145 + @Override 1.146 + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 1.147 + if (cursor.moveToFirst()) { 1.148 + String res = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DATA)); 1.149 + 1.150 + // Some pickers (the KitKat Documents one for instance) won't return a temporary file here. 1.151 + // Fall back to the normal FileLoader if we didn't find anything. 1.152 + if (TextUtils.isEmpty(res)) { 1.153 + tryFileLoaderCallback(); 1.154 + return; 1.155 + } 1.156 + 1.157 + sendResult(res); 1.158 + } else { 1.159 + tryFileLoaderCallback(); 1.160 + } 1.161 + } 1.162 + 1.163 + private void tryFileLoaderCallback() { 1.164 + final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); 1.165 + final LoaderManager lm = fa.getSupportLoaderManager(); 1.166 + lm.initLoader(uri.hashCode(), null, new FileLoaderCallbacks(uri)); 1.167 + } 1.168 + 1.169 + @Override 1.170 + public void onLoaderReset(Loader<Cursor> loader) { } 1.171 + } 1.172 + 1.173 + private class FileLoaderCallbacks implements LoaderCallbacks<Cursor>, 1.174 + Tabs.OnTabsChangedListener { 1.175 + final private Uri uri; 1.176 + private String tempFile; 1.177 + 1.178 + public FileLoaderCallbacks(Uri uri) { 1.179 + this.uri = uri; 1.180 + } 1.181 + 1.182 + @Override 1.183 + public Loader<Cursor> onCreateLoader(int id, Bundle args) { 1.184 + final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); 1.185 + return new CursorLoader(fa, 1.186 + uri, 1.187 + new String[] { OpenableColumns.DISPLAY_NAME }, 1.188 + null, // selection 1.189 + null, // selectionArgs 1.190 + null); // sortOrder 1.191 + } 1.192 + 1.193 + @Override 1.194 + public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) { 1.195 + if (cursor.moveToFirst()) { 1.196 + String name = cursor.getString(0); 1.197 + // tmp filenames must be at least 3 characters long. Add a prefix to make sure that happens 1.198 + String fileName = "tmp_" + Process.myPid() + "-"; 1.199 + String fileExt = null; 1.200 + int period; 1.201 + 1.202 + final FragmentActivity fa = (FragmentActivity) GeckoAppShell.getGeckoInterface().getActivity(); 1.203 + final ContentResolver cr = fa.getContentResolver(); 1.204 + 1.205 + // Generate an extension if we don't already have one 1.206 + if (name == null || (period = name.lastIndexOf('.')) == -1) { 1.207 + String mimeType = cr.getType(uri); 1.208 + fileExt = "." + GeckoAppShell.getExtensionFromMimeType(mimeType); 1.209 + } else { 1.210 + fileExt = name.substring(period); 1.211 + fileName += name.substring(0, period); 1.212 + } 1.213 + 1.214 + // Now write the data to the temp file 1.215 + try { 1.216 + cacheDir.mkdir(); 1.217 + 1.218 + File file = File.createTempFile(fileName, fileExt, cacheDir); 1.219 + FileOutputStream fos = new FileOutputStream(file); 1.220 + InputStream is = cr.openInputStream(uri); 1.221 + byte[] buf = new byte[4096]; 1.222 + int len = is.read(buf); 1.223 + while (len != -1) { 1.224 + fos.write(buf, 0, len); 1.225 + len = is.read(buf); 1.226 + } 1.227 + fos.close(); 1.228 + 1.229 + tempFile = file.getAbsolutePath(); 1.230 + sendResult((tempFile == null) ? "" : tempFile); 1.231 + 1.232 + if (tabId > -1 && !TextUtils.isEmpty(tempFile)) { 1.233 + Tabs.registerOnTabsChangedListener(this); 1.234 + } 1.235 + } catch(IOException ex) { 1.236 + Log.i(LOGTAG, "Error writing file", ex); 1.237 + } 1.238 + } else { 1.239 + sendResult(""); 1.240 + } 1.241 + } 1.242 + 1.243 + @Override 1.244 + public void onLoaderReset(Loader<Cursor> loader) { } 1.245 + 1.246 + /*Tabs.OnTabsChangedListener*/ 1.247 + // This cleans up our temp file. If it doesn't run, we just hope that Android 1.248 + // will eventually does the cleanup for us. 1.249 + @Override 1.250 + public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { 1.251 + if (tab.getId() != tabId) { 1.252 + return; 1.253 + } 1.254 + 1.255 + if (msg == Tabs.TabEvents.LOCATION_CHANGE || 1.256 + msg == Tabs.TabEvents.CLOSED) { 1.257 + ThreadUtils.postToBackgroundThread(new Runnable() { 1.258 + @Override 1.259 + public void run() { 1.260 + File f = new File(tempFile); 1.261 + f.delete(); 1.262 + } 1.263 + }); 1.264 + 1.265 + // Tabs' listener array is safe to modify during use: its 1.266 + // iteration pattern is based on snapshots. 1.267 + Tabs.unregisterOnTabsChangedListener(this); 1.268 + } 1.269 + } 1.270 + } 1.271 + 1.272 +} 1.273 +