Phone to Chrome support

This commit is contained in:
burke.davey
2010-08-31 22:48:46 +00:00
parent 328e237e3e
commit 5f0b4693f2
8 changed files with 373 additions and 185 deletions

View File

@@ -12,8 +12,9 @@
<!-- Permissions for internet access and account access -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- App must have this permission to use the library -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
@@ -32,13 +33,20 @@
android:screenOrientation="portrait">
</activity>
<!-- In order to use the c2dm library, an
application must declare a class with the name C2DMReceiver, in its
own package, extending com.google.android.c2dm.C2DMBaseReceiver
<activity android:name=".ShareLinkActivity">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>
It must also include this section in the manifest, replacing
"com.google.android.apps.chrometophone" with its package name.
-->
<!-- In order to use the c2dm library, an
application must declare a class with the name C2DMReceiver, in its
own package, extending com.google.android.c2dm.C2DMBaseReceiver
It must also include this section in the manifest, replacing
"com.google.android.apps.chrometophone" with its package name. -->
<service android:name=".C2DMReceiver" />
<!-- Only google service can send data messages for the app. If permission is not set -

View File

@@ -77,6 +77,18 @@
<!-- Notification shown when text from the desktop is copied to Android's clipboard (user can now paste the text) [CHAR LIMIT=NONE] -->
<string name="copied_desktop_clipboard">Copied desktop clipboard</string>
<!-- Brief notification that appears when user sends link from phone to desktop [CHAR LIMIT=NONE] -->
<string name="sending_link_toast">Sending link...</string>
<!-- Brief notification that appears when user has sent link from phone to desktop [CHAR LIMIT=NONE] -->
<string name="link_sent_toast">Link sent</string>
<!-- Brief notification that appears when link cannot be sent from phone to desktop [CHAR LIMIT=NONE] -->
<string name="link_not_sent_toast">Link not sent</string>
<!-- Brief notification that appears when link cannot be sent from phone to desktop (because of auth issue) [CHAR LIMIT=NONE] -->
<string name="link_not_sent_auth_toast">Link not sent - authentication required</string>
<!-- Introduction text at start of setup flow. Preserve occurrences of &lt;br>, &lt;a href="{tos_link}">, &lt;a href="{pp_link}">, and &lt;a/> [CHAR LIMIT=NONE] -->
<string name="intro_text">
Chrome to Phone lets you easily share links, maps, and currently selected phone numbers and text

View File

@@ -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<NameValuePair> 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<NameValuePair> 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<Bundle> 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);
}
}
}

View File

@@ -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)

View File

@@ -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<NameValuePair> params = new ArrayList<NameValuePair>();
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<NameValuePair> formparams = new ArrayList<NameValuePair>();
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<Bundle> 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);
}
}

View File

@@ -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;
}

View File

@@ -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<NameValuePair> params = new ArrayList<NameValuePair>();
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;
}
}

View File

@@ -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();
}
}