diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 9a5d6a0..51a9a30 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -12,8 +12,9 @@ + - + @@ -31,14 +32,21 @@ - - + It must also include this section in the manifest, replacing + "com.google.android.apps.chrometophone" with its package name. --> Copied desktop clipboard + + Sending link... + + + Link sent + + + Link not sent + + + Link not sent - authentication required + Chrome to Phone lets you easily share links, maps, and currently selected phone numbers and text diff --git a/android/src/com/google/android/apps/chrometophone/AppEngineClient.java b/android/src/com/google/android/apps/chrometophone/AppEngineClient.java new file mode 100644 index 0000000..b7c0005 --- /dev/null +++ b/android/src/com/google/android/apps/chrometophone/AppEngineClient.java @@ -0,0 +1,155 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.apps.chrometophone; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.util.List; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; + +/** + * AppEngine client. Handles auth. + */ +public class AppEngineClient { + private static final String TAG = "AppEngineClient"; + + static final String BASE_URL = "https://chrometophone.appspot.com"; + private static final String AUTH_URL = BASE_URL + "/_ah/login"; + private static final String AUTH_TOKEN_TYPE = "ah"; + + private final Context mContext; + private final String mAccountName; + + public AppEngineClient(Context context, String accountName) { + this.mContext = context; + this.mAccountName = accountName; + } + + public HttpResponse makeRequest(String urlPath, List params) throws Exception { + HttpResponse res = makeRequestNoRetry(urlPath, params, false); + if (res.getStatusLine().getStatusCode() == 500) { + res = makeRequestNoRetry(urlPath, params, true); + } + return res; + } + + private HttpResponse makeRequestNoRetry(String urlPath, List params, boolean newToken) + throws Exception { + + // Get auth token for account + Account account = new Account(mAccountName, "com.google"); + String authToken = getAuthToken(mContext, account); + if (authToken == null) throw new PendingAuthException(mAccountName); + if (newToken) { // invalidate the cached token + AccountManager accountManager = AccountManager.get(mContext); + accountManager.invalidateAuthToken(account.type, authToken); + authToken = getAuthToken(mContext, account); + } + + // Get ACSID cookie + DefaultHttpClient client = new DefaultHttpClient(); + String continueURL = BASE_URL; + URI uri = new URI(AUTH_URL + "?continue=" + + URLEncoder.encode(continueURL, "UTF-8") + + "&auth=" + authToken); + HttpGet method = new HttpGet(uri); + final HttpParams getParams = new BasicHttpParams(); + HttpClientParams.setRedirecting(getParams, false); // continue is not used + method.setParams(getParams); + + HttpResponse res = client.execute(method); + Header[] headers = res.getHeaders("Set-Cookie"); + if (res.getStatusLine().getStatusCode() != 302 || + headers.length == 0) { + return res; + } + + String ascidCookie = null; + for (Header header: headers) { + if (header.getValue().indexOf("ACSID=") >=0) { + // let's parse it + String value = header.getValue(); + String[] pairs = value.split(";"); + ascidCookie = pairs[0]; + } + } + + // Make POST request + uri = new URI(BASE_URL + urlPath); + HttpPost post = new HttpPost(uri); + UrlEncodedFormEntity entity = + new UrlEncodedFormEntity(params, "UTF-8"); + post.setEntity(entity); + post.setHeader("Cookie", ascidCookie); + post.setHeader("X-Same-Domain", "1"); // XSRF + res = client.execute(post); + return res; + } + + private String getAuthToken(Context context, Account account) { + String authToken = null; + AccountManager accountManager = AccountManager.get(context); + try { + AccountManagerFuture future = + accountManager.getAuthToken (account, AUTH_TOKEN_TYPE, false, null, null); + Bundle bundle = future.getResult(); + authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); + // User will be asked for "App Engine" permission. + if (authToken == null) { + // No auth token - will need to ask permission from user. + Intent intent = new Intent(MainActivity.AUTH_PERMISSION_ACTION); + intent.putExtra("AccountManagerBundle", bundle); + context.sendBroadcast(intent); + } + } catch (OperationCanceledException e) { + Log.w(TAG, e.getMessage()); + } catch (AuthenticatorException e) { + Log.w(TAG, e.getMessage()); + } catch (IOException e) { + Log.w(TAG, e.getMessage()); + } + return authToken; + } + + public class PendingAuthException extends Exception { + private static final long serialVersionUID = 1L; + public PendingAuthException(String message) { + super(message); + } + } +} diff --git a/android/src/com/google/android/apps/chrometophone/C2DMReceiver.java b/android/src/com/google/android/apps/chrometophone/C2DMReceiver.java index 5b4250a..6ecc2e9 100644 --- a/android/src/com/google/android/apps/chrometophone/C2DMReceiver.java +++ b/android/src/com/google/android/apps/chrometophone/C2DMReceiver.java @@ -15,7 +15,6 @@ */ package com.google.android.apps.chrometophone; - import java.io.IOException; import org.apache.http.client.ClientProtocolException; @@ -76,7 +75,7 @@ public class C2DMReceiver extends C2DMBaseReceiver { // turn this on for a small percentage of requests or for users // who report issues. DefaultHttpClient client = new DefaultHttpClient(); - HttpGet get = new HttpGet(DeviceRegistrar.BASE_URL + "/debug?id=" + HttpGet get = new HttpGet(AppEngineClient.BASE_URL + "/debug?id=" + extras.get("collapse_key")); // No auth - the purpose is only to generate a log/confirm delivery // (to avoid overhead of getting the token) diff --git a/android/src/com/google/android/apps/chrometophone/DeviceRegistrar.java b/android/src/com/google/android/apps/chrometophone/DeviceRegistrar.java index 57da898..b049235 100644 --- a/android/src/com/google/android/apps/chrometophone/DeviceRegistrar.java +++ b/android/src/com/google/android/apps/chrometophone/DeviceRegistrar.java @@ -15,38 +15,20 @@ */ package com.google.android.apps.chrometophone; -import java.io.IOException; -import java.net.URI; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.List; -import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; -import org.apache.http.client.entity.UrlEncodedFormEntity; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.HttpParams; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.os.Bundle; import android.util.Log; /** - * Register with the chrometophone appengine server. - * Will pass the registration id and user, authenticating with app engine. + * Register/unregister with the Chrome to Phone App Engine server. */ public class DeviceRegistrar { public static final String STATUS_EXTRA = "Status"; @@ -57,14 +39,9 @@ public class DeviceRegistrar { private static final String TAG = "DeviceRegistrar"; static final String SENDER_ID = "stp.chrome@gmail.com"; - static final String BASE_URL = "https://chrometophone.appspot.com"; - // Appengine authentication - private static final String AUTH_URL = BASE_URL + "/_ah/login"; - private static final String AUTH_TOKEN_TYPE = "ah"; - - private static final String REGISTER_URL = BASE_URL + "/register"; - private static final String UNREGISTER_URL = BASE_URL + "/unregister"; + private static final String REGISTER_PATH = "/register"; + private static final String UNREGISTER_PATH = "/unregister"; public static void registerWithServer(final Context context, final String deviceRegistrationID) { @@ -72,7 +49,7 @@ public class DeviceRegistrar { public void run() { Intent updateUIIntent = new Intent("com.google.ctp.UPDATE_UI"); try { - HttpResponse res = makeRequest(context, deviceRegistrationID, REGISTER_URL); + HttpResponse res = makeRequest(context, deviceRegistrationID, REGISTER_PATH); if (res.getStatusLine().getStatusCode() == 200) { SharedPreferences settings = Prefs.get(context); SharedPreferences.Editor editor = settings.edit(); @@ -87,7 +64,7 @@ public class DeviceRegistrar { updateUIIntent.putExtra(STATUS_EXTRA, ERROR_STATUS); } context.sendBroadcast(updateUIIntent); - } catch (PendingAuthException e) { + } catch (AppEngineClient.PendingAuthException pae) { // Ignore - we'll reregister later } catch (Exception e) { Log.w(TAG, "Registration error " + e.getMessage()); @@ -100,132 +77,41 @@ public class DeviceRegistrar { public static void unregisterWithServer(final Context context, final String deviceRegistrationID) { - Intent updateUIIntent = new Intent("com.google.ctp.UPDATE_UI"); - try { - HttpResponse res = makeRequest(context, deviceRegistrationID, UNREGISTER_URL); - if (res.getStatusLine().getStatusCode() == 200) { - SharedPreferences settings = Prefs.get(context); - SharedPreferences.Editor editor = settings.edit(); - editor.remove("deviceRegistrationID"); - editor.commit(); - updateUIIntent.putExtra(STATUS_EXTRA, UNREGISTERED_STATUS); - } else { - Log.w(TAG, "Unregistration error " + - String.valueOf(res.getStatusLine().getStatusCode())); - updateUIIntent.putExtra(STATUS_EXTRA, ERROR_STATUS); - } - } catch (Exception e) { - updateUIIntent.putExtra(STATUS_EXTRA, ERROR_STATUS); - Log.w(TAG, "Unegistration error " + e.getMessage()); - } + new Thread(new Runnable() { + public void run() { + Intent updateUIIntent = new Intent("com.google.ctp.UPDATE_UI"); + try { + HttpResponse res = makeRequest(context, deviceRegistrationID, UNREGISTER_PATH); + if (res.getStatusLine().getStatusCode() == 200) { + SharedPreferences settings = Prefs.get(context); + SharedPreferences.Editor editor = settings.edit(); + editor.remove("deviceRegistrationID"); + editor.commit(); + updateUIIntent.putExtra(STATUS_EXTRA, UNREGISTERED_STATUS); + } else { + Log.w(TAG, "Unregistration error " + + String.valueOf(res.getStatusLine().getStatusCode())); + updateUIIntent.putExtra(STATUS_EXTRA, ERROR_STATUS); + } + } catch (Exception e) { + updateUIIntent.putExtra(STATUS_EXTRA, ERROR_STATUS); + Log.w(TAG, "Unegistration error " + e.getMessage()); + } - // Update dialog activity - context.sendBroadcast(updateUIIntent); + // Update dialog activity + context.sendBroadcast(updateUIIntent); + } + }).start(); } private static HttpResponse makeRequest(Context context, String deviceRegistrationID, - String url) throws Exception { - HttpResponse res = makeRequestNoRetry(context, deviceRegistrationID, url, - false); - if (res.getStatusLine().getStatusCode() == 500) { - res = makeRequestNoRetry(context, deviceRegistrationID, url, - true); - } - return res; - } - - private static HttpResponse makeRequestNoRetry(Context context, String deviceRegistrationID, - String url, boolean newToken) throws Exception { - // Get chosen user account + String urlPath) throws Exception { SharedPreferences settings = Prefs.get(context); String accountName = settings.getString("accountName", null); - if (accountName == null) throw new Exception("No account"); + List params = new ArrayList(); + params.add(new BasicNameValuePair("devregid", deviceRegistrationID)); - // Get auth token for account - Account account = new Account(accountName, "com.google"); - String authToken = getAuthToken(context, account); - if (authToken == null) { - throw new PendingAuthException(accountName); - } - if (newToken) { - // Invalidate the cached token - AccountManager accountManager = AccountManager.get(context); - accountManager.invalidateAuthToken(account.type, authToken); - authToken = getAuthToken(context, account); - } - - // Register device with server - DefaultHttpClient client = new DefaultHttpClient(); - String continueURL = BASE_URL; - - URI uri = new URI(AUTH_URL + "?continue=" + - URLEncoder.encode(continueURL, "UTF-8") + - "&auth=" + authToken); - HttpGet method = new HttpGet(uri); - // No redirect following - continue is not used - final HttpParams params = new BasicHttpParams(); - HttpClientParams.setRedirecting(params, false); - method.setParams(params); - - HttpResponse res = client.execute(method); - Header[] headers = res.getHeaders("Set-Cookie"); - if (res.getStatusLine().getStatusCode() != 302 || - headers.length == 0) { - return res; - } - String ascidCookie = null; - for (Header header: headers) { - if (header.getValue().indexOf("ACSID=") >=0) { - // let's parse it - String value = header.getValue(); - String[] pairs = value.split(";"); - ascidCookie = pairs[0]; - } - } - - uri = new URI(url); - HttpPost post = new HttpPost(uri); - List formparams = new ArrayList(); - formparams.add(new BasicNameValuePair("devregid", deviceRegistrationID)); - UrlEncodedFormEntity entity = - new UrlEncodedFormEntity(formparams, "UTF-8"); - post.setEntity(entity); - post.setHeader("Cookie", ascidCookie); - post.setHeader("X-Same-Domain", "1"); // XSRF - res = client.execute(post); - return res; - } - - private static String getAuthToken(Context context, Account account) { - String authToken = null; - AccountManager accountManager = AccountManager.get(context); - try { - AccountManagerFuture future = - accountManager.getAuthToken (account, AUTH_TOKEN_TYPE, false, null, null); - Bundle bundle = future.getResult(); - authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); - // User will be asked for "App Engine" permission. - if (authToken == null) { - // No auth token - will need to ask permission from user. - Intent intent = new Intent(MainActivity.AUTH_PERMISSION_ACTION); - intent.putExtra("AccountManagerBundle", bundle); - context.sendBroadcast(intent); - } - } catch (OperationCanceledException e) { - Log.w(TAG, e.getMessage()); - } catch (AuthenticatorException e) { - Log.w(TAG, e.getMessage()); - } catch (IOException e) { - Log.w(TAG, e.getMessage()); - } - return authToken; - } - - static class PendingAuthException extends Exception { - private static final long serialVersionUID = 1L; - - public PendingAuthException(String message) { - super(message); - } + AppEngineClient client = new AppEngineClient(context, accountName); + return client.makeRequest(urlPath, params); } } diff --git a/android/src/com/google/android/apps/chrometophone/HelpActivity.java b/android/src/com/google/android/apps/chrometophone/HelpActivity.java index 306ee88..a7e2cb6 100644 --- a/android/src/com/google/android/apps/chrometophone/HelpActivity.java +++ b/android/src/com/google/android/apps/chrometophone/HelpActivity.java @@ -23,37 +23,38 @@ public class HelpActivity extends Activity { } public static String getTOSLink() { - String link = "http://m.google.com/toscountry"; // default - - String locale = Locale.getDefault().toString(); - if (locale.equals(Locale.US.toString())) { - link = "http://m.google.com/tospage"; - } else if (locale.equals(Locale.UK.toString())) { - link = "http://m.google.co.uk/tospage"; - } else if (locale.equals(Locale.CANADA.toString())) { - link = "http://m.google.ca/tospage"; - } else if (locale.equals(Locale.CANADA_FRENCH.toString())) { - link = "http://m.google.ca/tospage?hl=fr"; - } else if (locale.equals(Locale.FRANCE.toString())) { - link = "http://m.google.fr/tospage"; - } - return link; + return constructLink(Locale.getDefault(), "/tospage", "/toscountry"); } public static String getPPLink() { - String link = "http://m.google.com/privacy"; // default + return constructLink(Locale.getDefault(), "/privacy", "/privacy"); + } - String locale = Locale.getDefault().toString(); - if (locale.toString().equals(Locale.US.toString())) { - link = "http://m.google.com/privacy"; - } else if (locale.toString().equals(Locale.UK.toString())) { - link = "http://m.google.co.uk/privacy"; - } else if (locale.toString().equals(Locale.CANADA.toString())) { - link = "http://m.google.ca/privacy"; - } else if (locale.toString().equals(Locale.CANADA_FRENCH.toString())) { - link = "http://m.google.ca/privacy?hl=fr"; - } else if (locale.toString().equals(Locale.FRANCE.toString())) { - link = "http://m.google.fr/privacy"; + private static String constructLink(Locale locale, String path, String defaultPath) { + String link = "http://m.google.com" + defaultPath; + String localeString = locale.toString(); + if (localeString.equals(Locale.CANADA.toString())) { + link = "http://m.google.ca" + path; + } else if (localeString.equals(Locale.CANADA_FRENCH.toString())) { + link = "http://m.google.ca" + path + "?hl=fr"; + } else if (localeString.equals(Locale.CHINA.toString())) { + link = "http://m.google.cn" + path; + } else if (localeString.equals(Locale.FRANCE.toString())) { + link = "http://m.google.fr" + path; + } else if (localeString.equals(Locale.GERMAN.toString())) { + link = "http://m.google.de" + path; + } else if (localeString.equals(Locale.ITALY.toString())) { + link = "http://m.google.it" + path; + } else if (localeString.equals(Locale.JAPAN.toString())) { + link = "http://m.google.co.jp" + path; + } else if (localeString.equals(Locale.KOREA.toString())) { + link = "http://m.google.co.kr" + path; + } else if (localeString.equals(Locale.TAIWAN.toString())) { + link = "http://m.google.tw" + path; + } else if (localeString.equals(Locale.UK.toString())) { + link = "http://m.google.co.uk" + path; + } else if (localeString.equals(Locale.US.toString())) { + link = "http://m.google.com" + path; } return link; } diff --git a/android/src/com/google/android/apps/chrometophone/ShareLink.java b/android/src/com/google/android/apps/chrometophone/ShareLink.java new file mode 100644 index 0000000..ab7ac99 --- /dev/null +++ b/android/src/com/google/android/apps/chrometophone/ShareLink.java @@ -0,0 +1,90 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.apps.chrometophone; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.widget.Toast; + +public class ShareLink implements Handler.Callback { + private static final String TOAST = "toast"; + private static final String BROWSER_CHANNEL_PATH = "/browserchannel"; + private static ShareLink mInstance; + private final Handler mHandler; + private final Context mContext; + + private ShareLink(Context context) { + mContext = context; + mHandler = new Handler(this); + } + + public static synchronized ShareLink getInstance(Context context) { + if (mInstance == null) { + mInstance = new ShareLink(context); + } + return mInstance; + } + + public void send(final String link) { + new Thread(new Runnable() { + public void run() { + sendToast(mContext.getString(R.string.sending_link_toast)); + try { + List params = new ArrayList(); + params.add(new BasicNameValuePair("data", link)); + SharedPreferences settings = Prefs.get(mContext); + final String accountName = settings.getString("accountName", null); + + AppEngineClient client = new AppEngineClient(mContext, accountName); + HttpResponse res = client.makeRequest(BROWSER_CHANNEL_PATH, params); + if (res.getStatusLine().getStatusCode() == 200) { + sendToast(mContext.getString(R.string.link_sent_toast)); + } else { + sendToast(mContext.getString(R.string.link_not_sent_toast)); + } + } catch (AppEngineClient.PendingAuthException e) { + sendToast(mContext.getString(R.string.link_not_sent_auth_toast)); + } catch (Exception e) { + sendToast(mContext.getString(R.string.link_not_sent_toast)); + } + } + }).start(); + } + + private void sendToast(String toastMessage) { + Message msg = new Message(); + Bundle data = new Bundle(); + data.putString(TOAST, toastMessage); + msg.setData(data); + mHandler.sendMessage(msg); + } + + public boolean handleMessage(Message msg) { + Toast.makeText(mContext, msg.getData().getString(TOAST), Toast.LENGTH_LONG).show(); + return true; + } +} diff --git a/android/src/com/google/android/apps/chrometophone/ShareLinkActivity.java b/android/src/com/google/android/apps/chrometophone/ShareLinkActivity.java new file mode 100644 index 0000000..d3195ba --- /dev/null +++ b/android/src/com/google/android/apps/chrometophone/ShareLinkActivity.java @@ -0,0 +1,37 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.android.apps.chrometophone; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; + +/** + * Invoked when user selects "Share page" in the browser. Sends link + * to AppEngine server. + */ +public class ShareLinkActivity extends Activity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (Intent.ACTION_SEND.equals(getIntent().getAction())) { + String link = getIntent().getExtras().getString(Intent.EXTRA_TEXT); + ShareLink.getInstance(this).send(link); + } + finish(); + } +}