diff --git a/AndroidManifest.xml b/AndroidManifest.xml
new file mode 100644
index 0000000..bd5480b
--- /dev/null
+++ b/AndroidManifest.xml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lib/bitlyj-2.0.0.jar b/lib/bitlyj-2.0.0.jar
new file mode 100644
index 0000000..7c6558f
Binary files /dev/null and b/lib/bitlyj-2.0.0.jar differ
diff --git a/lib/commons-codec-1.6-sources.jar b/lib/commons-codec-1.6-sources.jar
new file mode 100644
index 0000000..cc6abff
Binary files /dev/null and b/lib/commons-codec-1.6-sources.jar differ
diff --git a/lib/commons-codec-1.6.jar b/lib/commons-codec-1.6.jar
new file mode 100644
index 0000000..ee1bc49
Binary files /dev/null and b/lib/commons-codec-1.6.jar differ
diff --git a/lib/google-api-client-1.7.0-beta-sources.jar b/lib/google-api-client-1.7.0-beta-sources.jar
new file mode 100644
index 0000000..911165c
Binary files /dev/null and b/lib/google-api-client-1.7.0-beta-sources.jar differ
diff --git a/lib/google-api-client-1.7.0-beta.jar b/lib/google-api-client-1.7.0-beta.jar
new file mode 100644
index 0000000..ee8281b
Binary files /dev/null and b/lib/google-api-client-1.7.0-beta.jar differ
diff --git a/lib/google-api-client-android2-1.7.0-beta-sources.jar b/lib/google-api-client-android2-1.7.0-beta-sources.jar
new file mode 100644
index 0000000..9665f69
Binary files /dev/null and b/lib/google-api-client-android2-1.7.0-beta-sources.jar differ
diff --git a/lib/google-api-client-android2-1.7.0-beta.jar b/lib/google-api-client-android2-1.7.0-beta.jar
new file mode 100644
index 0000000..f4417ed
Binary files /dev/null and b/lib/google-api-client-android2-1.7.0-beta.jar differ
diff --git a/lib/google-api-urlshortener-v1-rev2-java-1.4.0-beta-sources.jar b/lib/google-api-urlshortener-v1-rev2-java-1.4.0-beta-sources.jar
new file mode 100644
index 0000000..ae181ad
Binary files /dev/null and b/lib/google-api-urlshortener-v1-rev2-java-1.4.0-beta-sources.jar differ
diff --git a/lib/google-api-urlshortener-v1-rev2-java-1.4.0-beta.jar b/lib/google-api-urlshortener-v1-rev2-java-1.4.0-beta.jar
new file mode 100644
index 0000000..1257ade
Binary files /dev/null and b/lib/google-api-urlshortener-v1-rev2-java-1.4.0-beta.jar differ
diff --git a/lib/google-http-client-1.7.0-beta-sources.jar b/lib/google-http-client-1.7.0-beta-sources.jar
new file mode 100644
index 0000000..def27a8
Binary files /dev/null and b/lib/google-http-client-1.7.0-beta-sources.jar differ
diff --git a/lib/google-http-client-1.7.0-beta.jar b/lib/google-http-client-1.7.0-beta.jar
new file mode 100644
index 0000000..591be28
Binary files /dev/null and b/lib/google-http-client-1.7.0-beta.jar differ
diff --git a/lib/google-http-client-android2-1.7.0-beta-sources.jar b/lib/google-http-client-android2-1.7.0-beta-sources.jar
new file mode 100644
index 0000000..c287a85
Binary files /dev/null and b/lib/google-http-client-android2-1.7.0-beta-sources.jar differ
diff --git a/lib/google-http-client-android2-1.7.0-beta.jar b/lib/google-http-client-android2-1.7.0-beta.jar
new file mode 100644
index 0000000..68d95b7
Binary files /dev/null and b/lib/google-http-client-android2-1.7.0-beta.jar differ
diff --git a/lib/google-http-client-android3-1.7.0-beta-sources.jar b/lib/google-http-client-android3-1.7.0-beta-sources.jar
new file mode 100644
index 0000000..a37d895
Binary files /dev/null and b/lib/google-http-client-android3-1.7.0-beta-sources.jar differ
diff --git a/lib/google-http-client-android3-1.7.0-beta.jar b/lib/google-http-client-android3-1.7.0-beta.jar
new file mode 100644
index 0000000..f8add82
Binary files /dev/null and b/lib/google-http-client-android3-1.7.0-beta.jar differ
diff --git a/lib/google-oauth-client-1.7.0-beta-sources.jar b/lib/google-oauth-client-1.7.0-beta-sources.jar
new file mode 100644
index 0000000..2f62105
Binary files /dev/null and b/lib/google-oauth-client-1.7.0-beta-sources.jar differ
diff --git a/lib/google-oauth-client-1.7.0-beta.jar b/lib/google-oauth-client-1.7.0-beta.jar
new file mode 100644
index 0000000..e431110
Binary files /dev/null and b/lib/google-oauth-client-1.7.0-beta.jar differ
diff --git a/lib/gson-2.1-sources.jar b/lib/gson-2.1-sources.jar
new file mode 100644
index 0000000..09396a0
Binary files /dev/null and b/lib/gson-2.1-sources.jar differ
diff --git a/lib/gson-2.1.jar b/lib/gson-2.1.jar
new file mode 100644
index 0000000..83c5c99
Binary files /dev/null and b/lib/gson-2.1.jar differ
diff --git a/lib/guava-11.0.1-sources.jar b/lib/guava-11.0.1-sources.jar
new file mode 100644
index 0000000..778c0c4
Binary files /dev/null and b/lib/guava-11.0.1-sources.jar differ
diff --git a/lib/guava-11.0.1.jar b/lib/guava-11.0.1.jar
new file mode 100644
index 0000000..af4a383
Binary files /dev/null and b/lib/guava-11.0.1.jar differ
diff --git a/lib/jackson-core-asl-1.9.4-sources.jar b/lib/jackson-core-asl-1.9.4-sources.jar
new file mode 100644
index 0000000..a9c9aae
Binary files /dev/null and b/lib/jackson-core-asl-1.9.4-sources.jar differ
diff --git a/lib/jackson-core-asl-1.9.4.jar b/lib/jackson-core-asl-1.9.4.jar
new file mode 100644
index 0000000..8ad2d81
Binary files /dev/null and b/lib/jackson-core-asl-1.9.4.jar differ
diff --git a/lib/jsr305-1.3.9.jar b/lib/jsr305-1.3.9.jar
new file mode 100644
index 0000000..a9afc66
Binary files /dev/null and b/lib/jsr305-1.3.9.jar differ
diff --git a/lib/protobuf-java-2.2.0-sources.jar b/lib/protobuf-java-2.2.0-sources.jar
new file mode 100644
index 0000000..fe5e028
Binary files /dev/null and b/lib/protobuf-java-2.2.0-sources.jar differ
diff --git a/lib/protobuf-java-2.2.0.jar b/lib/protobuf-java-2.2.0.jar
new file mode 100644
index 0000000..7a0ccde
Binary files /dev/null and b/lib/protobuf-java-2.2.0.jar differ
diff --git a/proguard.cfg b/proguard.cfg
new file mode 100644
index 0000000..1d0eecd
--- /dev/null
+++ b/proguard.cfg
@@ -0,0 +1,64 @@
+-optimizationpasses 5
+-dontusemixedcaseclassnames
+-dontskipnonpubliclibraryclasses
+-dontpreverify
+-verbose
+-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
+
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.preference.Preference
+-keep public class com.android.vending.licensing.ILicensingService
+
+-keepclasseswithmembernames class * {
+ native ;
+}
+
+-keepclasseswithmembers class * {
+ public (android.content.Context, android.util.AttributeSet);
+}
+
+-keepclasseswithmembers class * {
+ public (android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers class * extends android.app.Activity {
+ public void *(android.view.View);
+}
+
+-keepclassmembers enum * {
+ public static **[] values();
+ public static ** valueOf(java.lang.String);
+}
+
+-keep class * implements android.os.Parcelable {
+ public static final android.os.Parcelable$Creator *;
+}
+
+# Needed by google-http-client to keep generic types and @Key annotations accessed via reflection
+
+-keepclassmembers class * {
+ @com.google.api.client.util.Key ;
+}
+
+# Needed just to be safe in terms of keeping Google API service model classes
+
+-keep class com.google.api.services.*.model.*
+
+-keepattributes Signature,RuntimeVisibleAnnotations,AnnotationDefault
+
+# Needed by Guava
+
+-dontwarn sun.misc.Unsafe
+
+# See https://groups.google.com/forum/#!topic/guava-discuss/YCZzeCiIVoI
+-dontwarn com.google.common.collect.MinMaxPriorityQueue
+
+# Emaily
+-keep class com.google.api.client.googleapis.json.*
+-dontwarn org.apache.commons.codec.binary.StringUtils
+-dontwarn org.apache.commons.codec.binary.Base64
\ No newline at end of file
diff --git a/res/drawable-hdpi/icon.png b/res/drawable-hdpi/icon.png
new file mode 100644
index 0000000..960a148
Binary files /dev/null and b/res/drawable-hdpi/icon.png differ
diff --git a/res/drawable-ldpi/icon.png b/res/drawable-ldpi/icon.png
new file mode 100644
index 0000000..494c897
Binary files /dev/null and b/res/drawable-ldpi/icon.png differ
diff --git a/res/drawable-mdpi/icon.png b/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..adf62b8
Binary files /dev/null and b/res/drawable-mdpi/icon.png differ
diff --git a/res/drawable-xhdpi/icon.png b/res/drawable-xhdpi/icon.png
new file mode 100644
index 0000000..b51271b
Binary files /dev/null and b/res/drawable-xhdpi/icon.png differ
diff --git a/res/layout/bitlycreds.xml b/res/layout/bitlycreds.xml
new file mode 100644
index 0000000..a10e57a
--- /dev/null
+++ b/res/layout/bitlycreds.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values-v11/themes.xml b/res/values-v11/themes.xml
new file mode 100644
index 0000000..3b567a6
--- /dev/null
+++ b/res/values-v11/themes.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
new file mode 100644
index 0000000..ed6363f
--- /dev/null
+++ b/res/values/strings.xml
@@ -0,0 +1,46 @@
+
+
+
+ Sorry. An error was returned by %2$s while shortening the url: %1$s
+ Please provide your credentials to shorten urls.
+ Sorry. Could not connect to %1$s.
+ Sorry. No applications can perform this action.
+ Sorry. No applications can perform this action. The shortened url has been copied to the clipboard.
+ Emaily
+ Select a Google account
+ About
+ API Key
+ Cancel
+ Bit.ly API Credentials
+ Need an API key?
+ OK
+ Enter your credentials…
+ API Credentials
+ http://bitly.com/a/your_api_key/
+ Username
+ bit.ly
+ © 2012 Erik C. Thauvin
+ %1$s %2$s %3$s (%4$s %5$s, %6$s)
+ Send email, please check Help first…
+ Feedback
+ mailto:erik@thauvin.net
+ Use as shorterner?
+ goo.gl
+ Learn how to use…
+ Help
+ http://m.thauvin.net/android/Emaily/help/
+ prefs_bitly_apikey
+ prefs_bitly_creds
+ prefs_bitly_username
+ prefs_feedback
+ prefs_googl_account
+ prefs_googl_chkbox
+ prefs_google_enabled
+ prefs_googl_token
+ prefs_gool_token_expiry
+ prefs_version
+ Version
+ Shortening url…
+ Retrying…
+
+
\ No newline at end of file
diff --git a/res/values/themes.xml b/res/values/themes.xml
new file mode 100644
index 0000000..2dc10f4
--- /dev/null
+++ b/res/values/themes.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/xml/prefs.xml b/res/xml/prefs.xml
new file mode 100644
index 0000000..d65cddd
--- /dev/null
+++ b/res/xml/prefs.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/screenshots/1.png b/screenshots/1.png
new file mode 100644
index 0000000..6ca10b2
Binary files /dev/null and b/screenshots/1.png differ
diff --git a/screenshots/2.png b/screenshots/2.png
new file mode 100644
index 0000000..e15ffe5
Binary files /dev/null and b/screenshots/2.png differ
diff --git a/screenshots/3.png b/screenshots/3.png
new file mode 100644
index 0000000..eeb4365
Binary files /dev/null and b/screenshots/3.png differ
diff --git a/screenshots/4.png b/screenshots/4.png
new file mode 100644
index 0000000..fa64740
Binary files /dev/null and b/screenshots/4.png differ
diff --git a/screenshots/5.png b/screenshots/5.png
new file mode 100644
index 0000000..cca4cf8
Binary files /dev/null and b/screenshots/5.png differ
diff --git a/screenshots/api_branding.png b/screenshots/api_branding.png
new file mode 100644
index 0000000..33db0a2
Binary files /dev/null and b/screenshots/api_branding.png differ
diff --git a/screenshots/emaily.gif b/screenshots/emaily.gif
new file mode 100644
index 0000000..4a562bf
Binary files /dev/null and b/screenshots/emaily.gif differ
diff --git a/screenshots/icon-512x512.png b/screenshots/icon-512x512.png
new file mode 100644
index 0000000..7fc3339
Binary files /dev/null and b/screenshots/icon-512x512.png differ
diff --git a/screenshots/icon.png b/screenshots/icon.png
new file mode 100644
index 0000000..1b9861c
Binary files /dev/null and b/screenshots/icon.png differ
diff --git a/screenshots/icon114x114.png b/screenshots/icon114x114.png
new file mode 100644
index 0000000..93fd976
Binary files /dev/null and b/screenshots/icon114x114.png differ
diff --git a/screenshots/promo-1024x500.png b/screenshots/promo-1024x500.png
new file mode 100644
index 0000000..1bcbfb3
Binary files /dev/null and b/screenshots/promo-1024x500.png differ
diff --git a/screenshots/promo-180x120.png b/screenshots/promo-180x120.png
new file mode 100644
index 0000000..2c8e9d7
Binary files /dev/null and b/screenshots/promo-180x120.png differ
diff --git a/src/net/thauvin/erik/android/emaily/BitlyCredsDialog.java b/src/net/thauvin/erik/android/emaily/BitlyCredsDialog.java
new file mode 100644
index 0000000..71fa510
--- /dev/null
+++ b/src/net/thauvin/erik/android/emaily/BitlyCredsDialog.java
@@ -0,0 +1,109 @@
+/*
+ * @(#)Emaily.java
+ *
+ * Copyright (c) 2012 Erik C. Thauvin (http://erik.thauvin.net/)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of the authors nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * $Id$
+ *
+ */
+package net.thauvin.erik.android.emaily;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * The BitlyCredsDialog
class implements a bit.ly credential dialog.
+ *
+ * @author Erik C. Thauvin
+ * @version $Revision$
+ * @created March 28, 2012
+ * @since 1.0
+ */
+public class BitlyCredsDialog extends DialogPreference
+{
+ private final Context mContext;
+ private EditText username;
+ private EditText apikey;
+
+ public BitlyCredsDialog(Context context, AttributeSet attrs)
+ {
+ super(context, attrs);
+ mContext = context;
+ setPersistent(false);
+ }
+
+ @Override
+ protected void onBindDialogView(View view)
+ {
+ super.onBindDialogView(view);
+
+ final SharedPreferences sharedPrefs = getSharedPreferences();
+ username = (EditText) view.findViewById(R.id.bitly_username_edit);
+ apikey = (EditText) view.findViewById(R.id.bitly_apikey_edit);
+ final TextView textFld = (TextView) view.findViewById(R.id.bitly_text_fld);
+
+ username.setText(sharedPrefs.getString(mContext.getString(R.string.prefs_key_bitly_username), ""));
+ apikey.setText(sharedPrefs.getString(mContext.getString(R.string.prefs_key_bitly_apikey), ""));
+
+ textFld.setOnClickListener(new View.OnClickListener()
+ {
+ public void onClick(View v)
+ {
+ final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mContext.getString(R.string.prefs_bitly_creds_url)));
+ mContext.startActivity(intent);
+ };
+ });
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult)
+ {
+ super.onDialogClosed(positiveResult);
+
+ if (positiveResult)
+ {
+ final SharedPreferences sharedPrefs = getSharedPreferences();
+ final Editor editor = sharedPrefs.edit();
+ editor.putString(mContext.getString(R.string.prefs_key_bitly_username), username.getText().toString());
+ editor.putString(mContext.getString(R.string.prefs_key_bitly_apikey), apikey.getText().toString());
+ editor.commit();
+ }
+
+ }
+}
diff --git a/src/net/thauvin/erik/android/emaily/Emaily.java b/src/net/thauvin/erik/android/emaily/Emaily.java
new file mode 100644
index 0000000..9a72f9c
--- /dev/null
+++ b/src/net/thauvin/erik/android/emaily/Emaily.java
@@ -0,0 +1,648 @@
+/*
+ * @(#)Emaily.java
+ *
+ * Copyright (c) 2011-2012 Erik C. Thauvin (http://erik.thauvin.net/)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of the authors nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * $Id$
+ *
+ */
+package net.thauvin.erik.android.emaily;
+
+import static com.rosaloves.bitlyj.Bitly.as;
+import static com.rosaloves.bitlyj.Bitly.shorten;
+
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.Date;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.OperationCanceledException;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.text.ClipboardManager;
+import android.text.Html;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.google.api.client.googleapis.extensions.android2.auth.GoogleAccountManager;
+import com.google.api.client.googleapis.json.GoogleJsonError;
+import com.google.api.client.googleapis.json.GoogleJsonResponseException;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.http.javanet.NetHttpTransport;
+import com.google.api.client.http.json.JsonHttpRequest;
+import com.google.api.client.http.json.JsonHttpRequestInitializer;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.client.json.jackson.JacksonFactory;
+import com.google.api.services.urlshortener.Urlshortener;
+import com.google.api.services.urlshortener.UrlshortenerRequest;
+import com.google.api.services.urlshortener.model.Url;
+
+/**
+ * The Emaily
class implements a URL shortener intent.
+ *
+ * @author Erik C. Thauvin
+ * @version $Revision$
+ * @created Oct 11, 2011
+ * @since 1.0
+ */
+public class Emaily extends Activity
+{
+ private static final String ACCOUNT_TYPE = "com.google";
+ private static final String OAUTH_URL = "oauth2:https://www.googleapis.com/auth/urlshortener";
+
+ private String appName;
+ private SharedPreferences sharedPrefs;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ final Intent intent = getIntent();
+
+ appName = getApplicationContext().getResources().getString(R.string.app_name);
+ sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this);
+
+ if (Intent.ACTION_SEND.equals(intent.getAction()))
+ {
+ final boolean isGoogl = sharedPrefs.getBoolean(getString(R.string.prefs_key_googl_enabled), true);
+
+ if (isGoogl)
+ {
+ final String account = getPref(R.string.prefs_key_googl_account);
+
+ if (isValid(account))
+ {
+ startEmailyTask(intent, new Account(account, ACCOUNT_TYPE), false);
+ }
+ else
+ {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.dialog_accounts_title);
+
+ final Account[] accounts = AccountManager.get(this).getAccountsByType(ACCOUNT_TYPE);
+ final int size = accounts.length;
+ if (size > 0)
+ {
+ if (size == 1)
+ {
+ startEmailyTask(intent, accounts[0], false);
+ }
+ else
+ {
+ final CharSequence[] names = new CharSequence[size];
+ for (int i = 0; i < size; i++)
+ {
+ names[i] = accounts[i].name;
+ }
+
+ builder.setSingleChoiceItems(names, 0, new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface dialog, int which)
+ {
+ dialog.dismiss();
+
+ final Editor editor = sharedPrefs.edit();
+ editor.putString(getString(R.string.prefs_key_googl_account), names[which].toString());
+ editor.putLong(getString(R.string.prefs_key_googl_token_expiry), 0L);
+ editor.commit();
+
+ startEmailyTask(intent, accounts[which], false);
+ }
+
+ });
+
+ builder.create().show();
+ }
+ }
+ else
+ {
+ startEmailyTask(intent, isGoogl, false);
+ }
+ }
+ }
+ else
+ {
+ startEmailyTask(intent, isGoogl, false);
+ }
+ }
+ else
+ {
+ Emaily.this.finish();
+ }
+
+ }
+
+ /**
+ * Starts the task.
+ *
+ * @param intent The original intent.
+ * @param sharedPrefs The shared preference.
+ * @param isGoogl The goo.gl flag.
+ * @param isRetry The retry flag.
+ */
+ private void startEmailyTask(final Intent intent, final boolean isGoogl, final boolean isRetry)
+ {
+ final EmailyTask task;
+
+ if (isGoogl)
+ {
+ task = new EmailyTask(getPref(R.string.prefs_key_googl_account), getPref(R.string.prefs_key_googl_token), isGoogl, isRetry);
+ }
+ else
+ {
+ task = new EmailyTask(getPref(R.string.prefs_key_bitly_username), getPref(R.string.prefs_key_bitly_apikey), isGoogl, isRetry);
+ }
+
+ task.execute(intent);
+ }
+
+ /**
+ * Starts the task.
+ *
+ * @param intent The original intent.
+ * @param sharedPrefs The shared preference.
+ * @param account The account.
+ * @param isRetry The retry flag.
+ */
+ private void startEmailyTask(final Intent intent, final Account account, final boolean isRetry)
+ {
+ final GoogleAccountManager googleAccountManager = new GoogleAccountManager(Emaily.this);
+
+ final long expiry = sharedPrefs.getLong(getString(R.string.prefs_key_googl_token_expiry), 0L);
+ final long now = System.currentTimeMillis();
+ final long maxLife = (60L * 55L) * 1000L; // 55 minutes
+
+ Log.d(appName, "Token Expires: " + new Date(expiry));
+
+ if (expiry >= (now + maxLife) || expiry <= now)
+ {
+ final String token = getPref(R.string.prefs_key_googl_token);
+ if (isValid(token))
+ {
+ googleAccountManager.manager.invalidateAuthToken(ACCOUNT_TYPE, token);
+
+ Log.d(appName, "Token Invalidated: " + token);
+ }
+ }
+
+ googleAccountManager.manager.getAuthToken(account, OAUTH_URL, null, Emaily.this, new AccountManagerCallback()
+ {
+ @Override
+ public void run(AccountManagerFuture future)
+ {
+ try
+ {
+ final String token = future.getResult().getString(AccountManager.KEY_AUTHTOKEN);
+ final Editor editor = sharedPrefs.edit();
+ final long now = System.currentTimeMillis();
+
+ final long expires;
+ if (expiry < now)
+ {
+ expires = now + maxLife;
+ }
+ else
+ {
+ expires = expiry;
+ }
+
+ editor.putLong(getString(R.string.prefs_key_googl_token_expiry), expires);
+ editor.putString(getString(R.string.prefs_key_googl_token), token);
+ editor.commit();
+
+ Log.d(appName, account.toString());
+ Log.d(appName, "Token: " + token);
+ Log.d(appName, "Expires: " + new Date(expires));
+
+ startEmailyTask(intent, true, isRetry);
+
+ }
+ catch (OperationCanceledException e)
+ {
+ Log.e(appName, "Auth token request has been canceled.", e);
+ }
+ catch (Exception e)
+ {
+ Log.e(appName, "Exception while requesting the auth token.", e);
+ }
+ }
+ }, null);
+ }
+
+ /**
+ * Retries the task.
+ *
+ * @param intent The original intent.
+ */
+ public void retry(final Intent intent)
+ {
+ sharedPrefs.edit().putLong(getString(R.string.prefs_key_googl_token_expiry), 0L).commit();
+
+ startEmailyTask(intent, new Account(getPref(R.string.prefs_key_googl_account), ACCOUNT_TYPE), true);
+ }
+
+ /**
+ * Validates a string.
+ *
+ * @param s The string to validate.
+ * @return returns true
if the string is not empty or null, false
otherwise.
+ */
+ public static boolean isValid(String s)
+ {
+ return (s != null) && (!s.trim().isEmpty());
+ }
+
+ /**
+ * Returns the value of the specified shared reference based on the specified string id. The default value is empty string.
+ *
+ * @param sharedPrefs The shared preference.
+ * @param id The string id.
+ * @return The preference value.
+ */
+ public String getPref(int id)
+ {
+ return getPref(id, "");
+ }
+
+ /**
+ * Returns the value of the specified shared reference based on the specified string id.
+ *
+ * @param sharedPrefs The shared preference.
+ * @param id The string id.
+ * @param defaultValue The default value, used if the preference is empty.
+ * @return The preference value.
+ */
+ public String getPref(int id, String defaultValue)
+ {
+ return sharedPrefs.getString(getString(id), defaultValue);
+ }
+
+ /**
+ * The EmailyTask
class.
+ */
+ private class EmailyTask extends AsyncTask
+ {
+ private final ProgressDialog dialog = new ProgressDialog(Emaily.this);
+ private final String username;
+ private final String keytoken;
+ private final boolean isGoogl;
+ private final boolean isRetry;
+
+ public EmailyTask(String username, String keytoken, boolean isGoogl, boolean isRetry)
+ {
+ this.username = username;
+ this.keytoken = keytoken;
+ this.isGoogl = isGoogl;
+ this.isRetry = isRetry;
+ }
+
+ @Override
+ protected EmailyResult doInBackground(Intent... intent)
+ {
+ final EmailyResult result = new EmailyResult(intent[0]);
+
+ final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
+ emailIntent.setType("text/html");
+
+ final Bundle extras = intent[0].getExtras();
+
+ final String pageUrl = extras.getString("android.intent.extra.TEXT");
+ final String pageTitle = extras.getString("android.intent.extra.SUBJECT");
+
+ if (isValid(pageTitle))
+ {
+ emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, pageTitle);
+ }
+
+ final boolean hasCredentials = isValid(username) && isValid(keytoken);
+ final StringBuilder shortUrl = new StringBuilder();
+
+ if (isValid(pageUrl))
+ {
+ final HttpTransport transport = new NetHttpTransport();
+ final JsonFactory jsonFactory = new JacksonFactory();
+
+ String version = "";
+
+ try
+ {
+ version = '/' + getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
+ }
+ catch (NameNotFoundException ignore)
+ {
+ // Do nothing;
+ }
+
+ final Url toInsert = new Url();
+
+ for (String item : pageUrl.split("\\s"))
+ {
+ try
+ {
+ new URL(item.trim());
+
+ if (isGoogl || !hasCredentials)
+ {
+ Log.d(appName, "goo.gl -> " + item);
+
+ final Urlshortener shortener = com.google.api.services.urlshortener.Urlshortener
+ .builder(transport, jsonFactory).setApplicationName(appName + version)
+ .setJsonHttpRequestInitializer(new JsonHttpRequestInitializer()
+ {
+ @Override
+ public void initialize(JsonHttpRequest request) throws IOException
+ {
+ UrlshortenerRequest shortnerRequest = (UrlshortenerRequest) request;
+
+ shortnerRequest.setKey(getString(R.string.secret_apikey));
+
+ if (isValid(keytoken))
+ {
+ shortnerRequest.setOauthToken(keytoken);
+
+ }
+ shortnerRequest.put("client_id", getString(R.string.secret_client_id));
+ shortnerRequest.put("client_secret", getString(R.string.secret_client_secret));
+ }
+ }).build();
+
+ toInsert.setLongUrl(item.trim());
+
+ try
+ {
+ final Url shortened = shortener.url().insert(toInsert).execute();
+
+ shortUrl.append(shortened.getId());
+ }
+ catch (GoogleJsonResponseException e)
+ {
+ result.setCode(R.string.alert_error);
+
+ final GoogleJsonError err = e.getDetails();
+
+ result.setMessage(err.message);
+
+ if (err.code == 401)
+ {
+ if (!isRetry)
+ {
+ result.setRetry(true);
+ }
+ }
+
+ Log.e(appName, "Exception while shortening '" + item + "' via goo.gl.", e);
+ }
+ catch (UnknownHostException e)
+ {
+ result.setCode(R.string.alert_nohost);
+ result.setMessage(e.getMessage());
+
+ Log.e(appName, "UnknownHostException while shortening '" + item + "' via goo.gl.", e);
+ }
+ catch (IOException e)
+ {
+ result.setCode(R.string.alert_error);
+ result.setMessage(e.getMessage());
+
+ Log.e(appName, "IOException while shortening '" + item + "' via goo.gl.", e);
+ }
+ }
+ else
+ {
+ Log.d(appName, "bit.ly -> " + item);
+
+ try
+ {
+ shortUrl.append(as(username, keytoken).call(shorten(item.trim())).getShortUrl());
+ }
+ catch (Exception e)
+ {
+ final Throwable cause = e.getCause();
+
+ if (cause != null && cause instanceof UnknownHostException)
+ {
+ result.setCode(R.string.alert_nohost);
+ result.setMessage(cause.getMessage());
+ }
+ else
+ {
+ result.setCode(R.string.alert_error);
+ result.setMessage(e.getMessage());
+ }
+
+ Log.e(appName, "Exception while shortening '" + item + "' via bit.ly.", e);
+ }
+
+ break;
+ }
+
+ break;
+ }
+ catch (MalformedURLException mue)
+ {
+ Log.d(appName, "Attempted to process an invalid URL: " + item, mue);
+
+ }
+ }
+ }
+ else
+ {
+ result.setCode(R.string.alert_nocreds);
+ }
+
+ if (!result.isRetry())
+ {
+ if (shortUrl.length() > 0)
+ {
+ emailIntent.putExtra(android.content.Intent.EXTRA_TEXT,
+ Html.fromHtml("" + shortUrl + ""));
+ }
+ else
+ {
+ final CharSequence text = extras.getCharSequence("android.intent.extra.TEXT");
+
+ if (text.length() > 0)
+ {
+ emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, text);
+ }
+ else if (isValid(pageUrl))
+ {
+ emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, pageUrl);
+ }
+ }
+
+ try
+ {
+ startActivity(emailIntent);
+ }
+ catch (android.content.ActivityNotFoundException ignore)
+ {
+ if (!result.hasError() && shortUrl.length() > 0)
+ {
+
+ final ClipboardManager clip = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ clip.setText(shortUrl);
+
+ result.setCode(R.string.alert_notfound_clip);
+ }
+ else
+ {
+ result.setCode(R.string.alert_notfound);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ protected void onPostExecute(EmailyResult result)
+ {
+ if (this.dialog.isShowing())
+ {
+ this.dialog.dismiss();
+ }
+
+ if (result.isRetry())
+ {
+ Emaily.this.retry(result.getIntent());
+ }
+ else
+ {
+ if (result.hasError())
+ {
+ Toast.makeText(
+ getApplicationContext(),
+ getString(result.getCode(), result.getMessage(), isGoogl ? getString(R.string.prefs_googl_title)
+ : getString(R.string.prefs_bitly_title)), Toast.LENGTH_LONG).show();
+
+ }
+
+ Emaily.this.finish();
+ }
+ }
+
+ @Override
+ protected void onPreExecute()
+ {
+ if (isRetry)
+ {
+ this.dialog.setMessage(getString(R.string.progress_msg_retry));
+ }
+ else
+ {
+ this.dialog.setMessage(getString(R.string.progress_msg));
+ }
+
+ this.dialog.show();
+ }
+ }
+
+ /**
+ * The EmailyResult
class.
+ */
+ private class EmailyResult
+ {
+ private int code = 0;
+ private String message;
+ private boolean retry = false;
+ private final Intent intent;
+
+ public EmailyResult(Intent intent)
+ {
+ this.intent = intent;
+ }
+
+ public int getCode()
+ {
+ return code;
+ }
+
+ public String getMessage()
+ {
+ if (isValid(message))
+ {
+ return message;
+ }
+ else
+ {
+ return "";
+ }
+ }
+
+ public Intent getIntent()
+ {
+ return intent;
+ }
+
+ public boolean hasError()
+ {
+ return code != 0;
+ }
+
+ public boolean isRetry()
+ {
+ return retry;
+ }
+
+ public void setCode(int code)
+ {
+ this.code = code;
+ }
+
+ public void setMessage(String message)
+ {
+ this.message = message;
+ }
+
+ public void setRetry(boolean retry)
+ {
+ this.retry = retry;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/net/thauvin/erik/android/emaily/EmailyPrefs.java b/src/net/thauvin/erik/android/emaily/EmailyPrefs.java
new file mode 100644
index 0000000..20257e0
--- /dev/null
+++ b/src/net/thauvin/erik/android/emaily/EmailyPrefs.java
@@ -0,0 +1,170 @@
+/*
+ * @(#)EmailyPrefs.java
+ *
+ * Copyright (c) 2011-2012 Erik C. Thauvin (http://erik.thauvin.net/)
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ *
+ * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * Neither the name of the authors nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+ * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * $Id$
+ *
+ */
+package net.thauvin.erik.android.emaily;
+
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+
+/**
+ * The EmailyPrefs
class implements a preferences screen.
+ *
+ * @author Erik C. Thauvin
+ * @version $Revision$
+ * @created Oct 11, 2011
+ * @since 1.0
+ */
+public class EmailyPrefs extends PreferenceActivity implements OnSharedPreferenceChangeListener
+{
+ private SharedPreferences sharedPrefs;
+ private Editor prefsEditor;
+
+ private CheckBoxPreference mGooglBox;
+ private BitlyCredsDialog mBitlyCreds;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState)
+ {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.prefs);
+
+ sharedPrefs = getPreferenceScreen().getSharedPreferences();
+ prefsEditor = sharedPrefs.edit();
+
+ mGooglBox = (CheckBoxPreference) findPreference(getString(R.string.prefs_key_googl_chkbox));
+ mBitlyCreds = (BitlyCredsDialog) findPreference(getString(R.string.prefs_key_bitly_creds));
+
+ setSummary(mBitlyCreds, getString(R.string.prefs_key_bitly_username), getString(R.string.prefs_bitly_creds_summary));
+ setSummary(mGooglBox, getString(R.string.prefs_key_googl_account), "");
+
+ if (mGooglBox.isChecked())
+ {
+ mBitlyCreds.setEnabled(false);
+ }
+
+ final Preference version = (Preference) findPreference(getString(R.string.prefs_key_version));
+ final PreferenceScreen feedback = (PreferenceScreen) findPreference(getString(R.string.prefs_key_feedback));
+ try
+ {
+ final String vNumber = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
+
+ version.setTitle(getString(R.string.prefs_version_title) + ' ' + vNumber);
+
+ feedback.getIntent().setData(
+ Uri.parse(getString(R.string.prefs_feedback_url)
+ + "?subject="
+ + getString(R.string.prefs_feedback_subject, getString(R.string.app_name), vNumber,
+ getString(R.string.prefs_feedback_title).toLowerCase(), Build.MANUFACTURER, Build.PRODUCT,
+ Build.VERSION.RELEASE)));
+
+ }
+ catch (NameNotFoundException ignore)
+ {
+ // Do nothing.
+ }
+ }
+
+ @Override
+ protected void onResume()
+ {
+ super.onResume();
+ sharedPrefs.registerOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ protected void onPause()
+ {
+ super.onPause();
+ sharedPrefs.unregisterOnSharedPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)
+ {
+ if (key.equals(getString(R.string.prefs_key_bitly_username)))
+ {
+ setSummary(mBitlyCreds, key, getString(R.string.prefs_bitly_creds_summary));
+ }
+ else if (key.equals(getString(R.string.prefs_key_googl_chkbox)))
+ {
+ final boolean checked = mGooglBox.isChecked();
+
+ mBitlyCreds.setEnabled(!checked);
+
+ prefsEditor.putBoolean(getString(R.string.prefs_key_googl_enabled), checked);
+
+ if (!checked)
+ {
+ prefsEditor.putString(getString(R.string.prefs_key_googl_account), "");
+ prefsEditor.putLong(getString(R.string.prefs_key_googl_token_expiry), 0L);
+ mGooglBox.setSummary("");
+ }
+
+ prefsEditor.commit();
+ }
+ }
+
+ /**
+ * Sets a preference's summary.
+ *
+ * @param editPref The preference.
+ * @param key The preference key.
+ * @param defValue The default value.
+ */
+ private void setSummary(Preference editPref, String key, String defValue)
+ {
+ final String value = sharedPrefs.getString(key, defValue);
+
+ if (Emaily.isValid(value))
+ {
+ editPref.setSummary(value);
+ }
+ else
+ {
+ editPref.setSummary(defValue);
+ }
+ }
+}