diff --git a/mobile/AndroidManifest.xml b/mobile/AndroidManifest.xml index 3020f113..b9019f86 100644 --- a/mobile/AndroidManifest.xml +++ b/mobile/AndroidManifest.xml @@ -80,8 +80,8 @@ - - + + @@ -92,23 +92,20 @@ - + android:pathPattern=".*\\.user.js" + android:scheme="file"/> - + android:pathPattern=".*\\.user.js" + android:scheme="content"/> - + android:pathPattern=".*\\.user.js" + android:scheme="http"/> + android:pathPattern=".*\\.user.js" + android:scheme="https"/> @@ -149,6 +146,15 @@ + + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/pager" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".ShareActivity"/> diff --git a/mobile/res/layout/dialog_progressbar.xml b/mobile/res/layout/dialog_progressbar.xml index 88191be7..6d7af384 100644 --- a/mobile/res/layout/dialog_progressbar.xml +++ b/mobile/res/layout/dialog_progressbar.xml @@ -1,6 +1,11 @@ + android:id="@+id/progressBarLoading" + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:paddingBottom="@dimen/activity_vertical_margin" + android:paddingLeft="@dimen/activity_horizontal_margin" + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingTop="@dimen/activity_vertical_margin"/> diff --git a/mobile/res/layout/preference_header_item.xml b/mobile/res/layout/preference_header_item.xml index f18136bf..19ca5127 100644 --- a/mobile/res/layout/preference_header_item.xml +++ b/mobile/res/layout/preference_header_item.xml @@ -1,5 +1,4 @@ - + android:paddingRight="?android:attr/scrollbarSize"> + android:layout_weight="1"> + android:textAppearance="?android:attr/textAppearanceMedium"/> + android:textAppearance="?android:attr/textAppearanceSmall"/> \ No newline at end of file diff --git a/mobile/res/menu/main.xml b/mobile/res/menu/main.xml index bffdd6ab..ef80fd10 100644 --- a/mobile/res/menu/main.xml +++ b/mobile/res/menu/main.xml @@ -1,3 +1,4 @@ + + - @@ -8,7 +9,10 @@ 400dip 200dip + 8dp + + 36dp \ No newline at end of file diff --git a/mobile/res/values/strings.xml b/mobile/res/values/strings.xml index f22db356..4e208cdd 100644 --- a/mobile/res/values/strings.xml +++ b/mobile/res/values/strings.xml @@ -8,6 +8,7 @@ IITC Plugins Share using… Copy to clipboard + Save to file Settings Show navigation menu Hide navigation menu @@ -161,6 +162,7 @@ Clear Cookies Search Debug + Send screenshot Add external plugins Choose account to login Login failed. @@ -184,4 +186,4 @@ Are you sure you want to proceed?]]> - + \ No newline at end of file diff --git a/mobile/res/values/styles.xml b/mobile/res/values/styles.xml index 79064a63..44fd7a3b 100644 --- a/mobile/res/values/styles.xml +++ b/mobile/res/values/styles.xml @@ -1,3 +1,4 @@ + + + + \ No newline at end of file diff --git a/mobile/res/xml/preferences.xml b/mobile/res/xml/preferences.xml index 14219efd..b1d97c5d 100644 --- a/mobile/res/xml/preferences.xml +++ b/mobile/res/xml/preferences.xml @@ -1,5 +1,6 @@ + @@ -36,8 +37,8 @@ android:fragment="com.cradle.iitc_mobile.fragments.PluginsFragment" android:key="pref_plugins" android:persistent="false" - android:title="@string/pref_plugins" - android:summary="@string/pref_plugins_sum"> + android:summary="@string/pref_plugins_sum" + android:title="@string/pref_plugins"> @@ -60,18 +61,19 @@ android:title="@string/pref_press_twice_to_exit"/> + android:key="pref_mics" + android:title="@string/pref_misc_cat"> + android:summary="@string/pref_advanced_options_sum" + android:title="@string/pref_advanced_options"> + @@ -106,4 +108,4 @@ - + \ No newline at end of file diff --git a/mobile/res/xml/searchable.xml b/mobile/res/xml/searchable.xml index 19c27279..384c4780 100644 --- a/mobile/res/xml/searchable.xml +++ b/mobile/res/xml/searchable.xml @@ -1,8 +1,7 @@ - + android:hint="@string/search_hint" + android:label="@string/app_name" + android:voiceLanguageModel="web_search" + android:voiceMaxResults="1" + android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"/> diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_FileManager.java b/mobile/src/com/cradle/iitc_mobile/IITC_FileManager.java index 21372dff..0c244cef 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_FileManager.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_FileManager.java @@ -63,8 +63,7 @@ public class IITC_FileManager { * @throws IOException */ public static void copyStream(final InputStream inStream, final OutputStream outStream, final boolean closeOutput) - throws IOException - { + throws IOException { // in case Android includes Apache commons IO in the future, this function should be replaced by IOUtils.copy final int bufferSize = 4096; final byte[] buffer = new byte[bufferSize]; @@ -94,20 +93,20 @@ public class IITC_FileManager { map.put("name", "unknown"); map.put("description", ""); map.put("category", "Misc"); - BufferedReader reader = new BufferedReader(new StringReader(header)); + final BufferedReader reader = new BufferedReader(new StringReader(header)); String headerLine; try { while ((headerLine = reader.readLine()) != null) { if (headerLine.matches("//.*@.*")) { // get start of key name (first @ in line) - String[] keyStart = headerLine.split("@", 2); + final String[] keyStart = headerLine.split("@", 2); // split key value - String[] keyValue = keyStart[1].split(" ", 2); + final String[] keyValue = keyStart[1].split(" ", 2); // remove whitespaces from string begin and end and push to map map.put(keyValue[0].trim(), keyValue[1].trim()); } } - } catch (IOException e) { + } catch (final IOException e) { Log.w(e); } return map; @@ -279,7 +278,7 @@ public class IITC_FileManager { InputStream is; String fileName; if (uri.getScheme().contains("http")) { - URLConnection conn = new URL(url).openConnection(); + final URLConnection conn = new URL(url).openConnection(); is = conn.getInputStream(); fileName = uri.getLastPathSegment(); } else { @@ -335,7 +334,7 @@ public class IITC_FileManager { // create the chooser Intent final Intent target = new Intent(Intent.ACTION_GET_CONTENT) - .setType("*/*") + .setType("file/*") .addCategory(Intent.CATEGORY_OPENABLE); final IITC_Mobile iitc = (IITC_Mobile) mActivity; diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_JSInterface.java b/mobile/src/com/cradle/iitc_mobile/IITC_JSInterface.java index d489ca6e..95556a1d 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_JSInterface.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_JSInterface.java @@ -3,7 +3,6 @@ package com.cradle.iitc_mobile; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Environment; @@ -30,22 +29,13 @@ public class IITC_JSInterface { @JavascriptInterface public void intentPosLink( final double lat, final double lng, final int zoom, final String title, final boolean isPortal) { - final Intent intent = new Intent(mIitc, ShareActivity.class); - intent.putExtra("lat", lat); - intent.putExtra("lng", lng); - intent.putExtra("zoom", zoom); - intent.putExtra("title", title); - intent.putExtra("isPortal", isPortal); - mIitc.startActivity(intent); + mIitc.startActivity(ShareActivity.forPosition(mIitc, lat, lng, zoom, title, isPortal)); } // share a string to the IITC share activity. only uses the share tab. @JavascriptInterface public void shareString(final String str) { - final Intent intent = new Intent(mIitc, ShareActivity.class); - intent.putExtra("shareString", str); - intent.putExtra("onlyShare", true); - mIitc.startActivity(intent); + mIitc.startActivity(ShareActivity.forString(mIitc, str)); } // disable javascript injection while spinner is enabled diff --git a/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java b/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java index 50511b75..075a009a 100644 --- a/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java +++ b/mobile/src/com/cradle/iitc_mobile/IITC_Mobile.java @@ -13,6 +13,8 @@ import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.Bitmap.CompressFormat; import android.net.Uri; import android.nfc.NdefMessage; import android.nfc.NdefRecord; @@ -38,11 +40,13 @@ import android.widget.TextView; import android.widget.Toast; import com.cradle.iitc_mobile.IITC_NavigationHelper.Pane; +import com.cradle.iitc_mobile.share.ShareActivity; import org.json.JSONException; import org.json.JSONObject; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.net.URISyntaxException; import java.util.Stack; @@ -571,6 +575,9 @@ public class IITC_Mobile extends Activity final CookieManager cm = CookieManager.getInstance(); cm.removeAllCookie(); return true; + case R.id.menu_send_screenshot: + sendScreenshot(); + return true; case R.id.menu_debug: mDebugging = !mDebugging; updateViews(); @@ -842,6 +849,40 @@ public class IITC_Mobile extends Activity mPermalink = href; } + private void sendScreenshot() { + Bitmap bitmap = mIitcWebView.getDrawingCache(); + if (bitmap == null) { + mIitcWebView.buildDrawingCache(); + bitmap = mIitcWebView.getDrawingCache(); + if (bitmap == null) { + Log.e("could not get bitmap!"); + return; + } + bitmap = Bitmap.createBitmap(bitmap); + if (!mIitcWebView.isDrawingCacheEnabled()) mIitcWebView.destroyDrawingCache(); + } + else { + bitmap = Bitmap.createBitmap(bitmap); + } + + try { + final File cache = getExternalCacheDir(); + final File file = File.createTempFile("IITC screenshot", ".png", cache); + if (!bitmap.compress(CompressFormat.PNG, 100, new FileOutputStream(file))) { + // quality is ignored by PNG + throw new IOException("Could not compress bitmap!"); + } + startActivityForResult(ShareActivity.forFile(this, file, "image/png"), new ResponseHandler() { + @Override + public void onActivityResult(final int resultCode, final Intent data) { + file.delete(); + } + }); + } catch (final IOException e) { + Log.e("Could not generate screenshot", e); + } + } + @Override public NdefMessage createNdefMessage(final NfcEvent event) { NdefRecord[] records; diff --git a/mobile/src/com/cradle/iitc_mobile/share/IntentGenerator.java b/mobile/src/com/cradle/iitc_mobile/share/IntentGenerator.java index b4b68893..fc4959a8 100644 --- a/mobile/src/com/cradle/iitc_mobile/share/IntentGenerator.java +++ b/mobile/src/com/cradle/iitc_mobile/share/IntentGenerator.java @@ -7,6 +7,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.os.Build; import com.cradle.iitc_mobile.Log; import com.cradle.iitc_mobile.R; @@ -136,12 +137,20 @@ public class IntentGenerator { return targets; } + /** + * get a list of intents capable of sharing a plain text string + * + * @param title + * description of the shared string + * @param text + * the string to be shared + */ public ArrayList getShareIntents(final String title, final String text) { final Intent intent = new Intent(Intent.ACTION_SEND) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) .setType("text/plain") - .putExtra(Intent.EXTRA_TEXT, text) - .putExtra(Intent.EXTRA_SUBJECT, title); + .putExtra(Intent.EXTRA_SUBJECT, title) + .putExtra(Intent.EXTRA_TEXT, text); final ArrayList targets = resolveTargets(intent); @@ -154,4 +163,30 @@ public class IntentGenerator { return targets; } + + /** + * get a list of intents capable of sharing the given content + * + * @param uri + * URI of a file to share + * @param type + * MIME type of the file + */ + public ArrayList getShareIntents(final Uri uri, final String type) { + final Intent intent = new Intent(Intent.ACTION_SEND) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) + .setType(type) + .putExtra(Intent.EXTRA_SUBJECT, uri.getLastPathSegment()) + .putExtra(Intent.EXTRA_STREAM, uri); + + final ArrayList targets = resolveTargets(intent); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + targets.add(new Intent(intent) + .setComponent(new ComponentName(mContext, SaveToFile.class)) + .putExtra(EXTRA_FLAG_TITLE, mContext.getString(R.string.activity_save_to_file))); + } + + return targets; + } } diff --git a/mobile/src/com/cradle/iitc_mobile/share/SaveToFile.java b/mobile/src/com/cradle/iitc_mobile/share/SaveToFile.java new file mode 100644 index 00000000..f01e9dc9 --- /dev/null +++ b/mobile/src/com/cradle/iitc_mobile/share/SaveToFile.java @@ -0,0 +1,97 @@ +package com.cradle.iitc_mobile.share; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.view.Window; + +import com.cradle.iitc_mobile.IITC_FileManager; +import com.cradle.iitc_mobile.Log; +import com.cradle.iitc_mobile.R; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +@TargetApi(19) +public class SaveToFile extends Activity implements Runnable { + private static final int REQUEST_SAVE_FILE = 1; + private Uri mData; + + @Override + protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { + if (requestCode == REQUEST_SAVE_FILE) { + mData = data.getData(); + if (resultCode != Activity.RESULT_OK || mData == null) { + finish(); + return; + } + + (new Thread(this)).start(); + } + + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setProgressBarIndeterminate(true); + setProgressBarIndeterminateVisibility(true); + + setContentView(R.layout.dialog_progressbar); + + final Intent intent = getIntent(); + if (!(intent.hasExtra(Intent.EXTRA_STREAM) || intent.hasExtra(Intent.EXTRA_TEXT))) { + setResult(RESULT_CANCELED); + finish(); + return; + } + + String filename = intent.getStringExtra(Intent.EXTRA_SUBJECT); + if (intent.hasExtra(Intent.EXTRA_STREAM)) { + final Uri src = intent.getParcelableExtra(Intent.EXTRA_STREAM); + if (src.getLastPathSegment() != null) filename = src.getLastPathSegment(); + } + + final Intent request = new Intent(Intent.ACTION_CREATE_DOCUMENT) + .setType(intent.getType()) + .addCategory(Intent.CATEGORY_OPENABLE) + .putExtra(Intent.EXTRA_TITLE, filename); + startActivityForResult(request, REQUEST_SAVE_FILE); + } + + @Override + public void run() { + final Intent intent = getIntent(); + if (mData == null) { + finish(); + return; + } + + try { + final ParcelFileDescriptor fdOut = getContentResolver().openFileDescriptor(mData, "w"); + final FileOutputStream os = new FileOutputStream(fdOut.getFileDescriptor()); + + if (intent.hasExtra(Intent.EXTRA_STREAM)) { + final Uri src = intent.getParcelableExtra(Intent.EXTRA_STREAM); + final ParcelFileDescriptor fdIn = getContentResolver().openFileDescriptor(src, "r"); + final FileInputStream is = new FileInputStream(fdIn.getFileDescriptor()); + IITC_FileManager.copyStream(is, os, true); + } else if (intent.hasExtra(Intent.EXTRA_TEXT)) { + os.write(intent.getStringExtra(Intent.EXTRA_TEXT).getBytes()); + os.close(); + } + fdOut.close(); + } catch (final IOException e) { + Log.w("Could not save file!", e); + } + finish(); + } + +} diff --git a/mobile/src/com/cradle/iitc_mobile/share/ShareActivity.java b/mobile/src/com/cradle/iitc_mobile/share/ShareActivity.java index 0578a7dc..0e923b53 100644 --- a/mobile/src/com/cradle/iitc_mobile/share/ShareActivity.java +++ b/mobile/src/com/cradle/iitc_mobile/share/ShareActivity.java @@ -2,8 +2,10 @@ package com.cradle.iitc_mobile.share; import android.app.ActionBar; import android.app.FragmentTransaction; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v4.app.FragmentActivity; @@ -11,20 +13,49 @@ import android.support.v4.app.NavUtils; import android.support.v4.view.ViewPager; import android.view.MenuItem; +import com.cradle.iitc_mobile.Log; import com.cradle.iitc_mobile.R; +import java.io.File; import java.util.ArrayList; public class ShareActivity extends FragmentActivity implements ActionBar.TabListener { + private static final String EXTRA_TYPE = "share-type"; + private static final int REQUEST_START_INTENT = 1; + private static final String TYPE_FILE = "file"; + private static final String TYPE_PERMALINK = "permalink"; + private static final String TYPE_PORTAL_LINK = "portal_link"; + private static final String TYPE_STRING = "string"; + + public static Intent forFile(final Context context, final File file, final String type) { + return new Intent(context, ShareActivity.class) + .putExtra(EXTRA_TYPE, TYPE_FILE) + .putExtra("uri", Uri.fromFile(file)) + .putExtra("type", type); + } + + public static Intent forPosition(final Context context, final double lat, final double lng, final int zoom, + final String title, final boolean isPortal) { + return new Intent(context, ShareActivity.class) + .putExtra(EXTRA_TYPE, isPortal ? TYPE_PORTAL_LINK : TYPE_PERMALINK) + .putExtra("lat", lat) + .putExtra("lng", lng) + .putExtra("zoom", zoom) + .putExtra("title", title) + .putExtra("isPortal", isPortal); + } + + public static Intent forString(final Context context, final String str) { + return new Intent(context, ShareActivity.class) + .putExtra(EXTRA_TYPE, TYPE_STRING) + .putExtra("shareString", str); + } + private IntentComparator mComparator; private FragmentAdapter mFragmentAdapter; private IntentGenerator mGenerator; - private boolean mIsPortal; - private String mLl; private SharedPreferences mSharedPrefs = null; - private String mTitle; private ViewPager mViewPager; - private int mZoom; private void addTab(final ArrayList intents, final int label, final int icon) { final IntentListFragment fragment = new IntentListFragment(); @@ -36,10 +67,10 @@ public class ShareActivity extends FragmentActivity implements ActionBar.TabList mFragmentAdapter.add(fragment); } - private String getIntelUrl() { - String url = "http://www.ingress.com/intel?ll=" + mLl + "&z=" + mZoom; - if (mIsPortal) { - url += "&pll=" + mLl; + private String getIntelUrl(final String ll, final int zoom, final boolean isPortal) { + String url = "http://www.ingress.com/intel?ll=" + ll + "&z=" + zoom; + if (isPortal) { + url += "&pll=" + ll; } return url; } @@ -54,6 +85,18 @@ public class ShareActivity extends FragmentActivity implements ActionBar.TabList .apply(); } + @Override + protected void onActivityResult(final int requestCode, final int resultCode, final Intent data) { + if (REQUEST_START_INTENT == requestCode) { + setResult(resultCode, data); + // parent activity can now clean up + finish(); + return; + } + + super.onActivityResult(requestCode, resultCode, data); + } + @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -68,29 +111,41 @@ public class ShareActivity extends FragmentActivity implements ActionBar.TabList actionBar.setDisplayHomeAsUpEnabled(true); final Intent intent = getIntent(); + final String type = intent.getStringExtra(EXTRA_TYPE); // from portallinks/permalinks we build 3 intents (share / geo / vanilla-intel-link) - if (!intent.getBooleanExtra("onlyShare", false)) { - mTitle = intent.getStringExtra("title"); - mLl = intent.getDoubleExtra("lat", 0) + "," + intent.getDoubleExtra("lng", 0); - mZoom = intent.getIntExtra("zoom", 0); - mIsPortal = intent.getBooleanExtra("isPortal", false); + if (TYPE_PERMALINK.equals(type) || TYPE_PORTAL_LINK.equals(type)) { + final String title = intent.getStringExtra("title"); + final String ll = intent.getDoubleExtra("lat", 0) + "," + intent.getDoubleExtra("lng", 0); + final int zoom = intent.getIntExtra("zoom", 0); + final String url = getIntelUrl(ll, zoom, TYPE_PORTAL_LINK.equals(type)); - actionBar.setTitle(mTitle); + actionBar.setTitle(title); - addTab(mGenerator.getShareIntents(mTitle, getIntelUrl()), + addTab(mGenerator.getShareIntents(title, url), R.string.tab_share, R.drawable.ic_action_share); - addTab(mGenerator.getGeoIntents(mTitle, mLl, mZoom), + addTab(mGenerator.getGeoIntents(title, ll, zoom), R.string.tab_map, R.drawable.ic_action_place); - addTab(mGenerator.getBrowserIntents(mTitle, getIntelUrl()), + addTab(mGenerator.getBrowserIntents(title, url), R.string.tab_browser, R.drawable.ic_action_web_site); - } else { - mTitle = getString(R.string.app_name); + } else if (TYPE_STRING.equals(type)) { + final String title = getString(R.string.app_name); final String shareString = intent.getStringExtra("shareString"); - addTab(mGenerator.getShareIntents(mTitle, shareString), R.string.tab_share, R.drawable.ic_action_share); + addTab(mGenerator.getShareIntents(title, shareString), R.string.tab_share, R.drawable.ic_action_share); + } else if (TYPE_FILE.equals(type)) { + + final Uri uri = intent.getParcelableExtra("uri"); + final String mime = intent.getStringExtra("type"); + + addTab(mGenerator.getShareIntents(uri, mime), R.string.tab_share, R.drawable.ic_action_share); + } else { + Log.w("Unknown sharing type: " + type); + setResult(RESULT_CANCELED); + finish(); + return; } mViewPager = (ViewPager) findViewById(R.id.pager); @@ -143,8 +198,9 @@ public class ShareActivity extends FragmentActivity implements ActionBar.TabList public void launch(final Intent intent) { mComparator.trackIntentSelection(intent); mGenerator.cleanup(intent); - startActivity(intent); - finish(); + + // we should wait for the new intent to be finished so the calling activity (IITC_Mobile) can clean up + startActivityForResult(intent, REQUEST_START_INTENT); } @Override