Project import

This commit is contained in:
daveburke
2010-05-20 14:55:05 +00:00
commit fb546ea471
44 changed files with 2575 additions and 0 deletions

8
android/.classpath Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="src" path="c2dm"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="output" path="bin"/>
</classpath>

33
android/.project Normal file
View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>chrometophone-android</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>com.android.ide.eclipse.adt.ResourceManagerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.PreCompilerBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.android.ide.eclipse.adt.ApkBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>com.android.ide.eclipse.adt.AndroidNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.apps.chrometophone"
android:versionCode="1"
android:versionName="1.0">
<!-- Only this application can receive the messages and registration result -->
<permission android:name="com.google.android.apps.chrometophone.permission.C2D_MESSAGE" android:protectionLevel="signature" />
<uses-permission android:name="com.google.android.apps.chrometophone.permission.C2D_MESSAGE" />
<!-- This app has permission to register and receive data message -->
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!-- 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.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<application android:icon="@drawable/app_icon" android:label="@string/app_name">
<activity android:name=".ActivityUI"
android:label="@string/app_name"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</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
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 -
any other app can generate it -->
<receiver android:name="com.google.android.c2dm.C2DMBroadcastReceiver"
android:permission="com.google.android.c2dm.permission.SEND">
<!-- Receive the actual message -->
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="com.google.android.apps.chrometophone" />
</intent-filter>
<!-- Receive the registration id -->
<intent-filter>
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />
<category android:name="com.google.android.apps.chrometophone" />
</intent-filter>
</receiver>
</application>
<uses-sdk android:minSdkVersion="5" />
</manifest>

View File

@@ -0,0 +1,195 @@
/*
* 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.c2dm;
import java.io.IOException;
import android.app.AlarmManager;
import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.PowerManager;
import android.util.Log;
/**
* Base class for C2D message receiver. Includes constants for the
* strings used in the protocol.
*/
public abstract class C2DMBaseReceiver extends IntentService {
private static final String C2DM_RETRY = "com.google.android.c2dm.intent.RETRY";
public static final String REGISTRATION_CALLBACK_INTENT = "com.google.android.c2dm.intent.REGISTRATION";
private static final String C2DM_INTENT = "com.google.android.c2dm.intent.RECEIVE";
// Logging tag
private static final String TAG = "C2DM";
// Extras in the registration callback intents.
public static final String EXTRA_UNREGISTERED = "unregistered";
public static final String EXTRA_ERROR = "error";
public static final String EXTRA_REGISTRATION_ID = "registration_id";
public static final String ERR_SERVICE_NOT_AVAILABLE = "SERVICE_NOT_AVAILABLE";
public static final String ERR_ACCOUNT_MISSING = "ACCOUNT_MISSING";
public static final String ERR_AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED";
public static final String ERR_TOO_MANY_REGISTRATIONS = "TOO_MANY_REGISTRATIONS";
public static final String ERR_INVALID_PARAMETERS = "INVALID_PARAMETERS";
public static final String ERR_INVALID_SENDER = "INVALID_SENDER";
public static final String ERR_PHONE_REGISTRATION_ERROR = "PHONE_REGISTRATION_ERROR";
// wakelock
private static final String WAKELOCK_KEY = "C2DM_LIB";
private static PowerManager.WakeLock mWakeLock;
private final String senderId;
/**
* The C2DMReceiver class must create a no-arg constructor and pass the
* sender id to be used for registration.
*/
public C2DMBaseReceiver(String senderId) {
// senderId is used as base name for threads, etc.
super(senderId);
this.senderId = senderId;
}
/**
* Called when a cloud message has been received.
*/
protected abstract void onMessage(Context context, Intent intent);
/**
* Called on registration error. Override to provide better
* error messages.
*
* This is called in the context of a Service - no dialog or UI.
*/
public abstract void onError(Context context, String errorId);
/**
* Called when a registration token has been received.
*/
public void onRegistrered(Context context, String registrationId) throws IOException {
// registrationId will also be saved
}
/**
* Called when the device has been unregistered.
*/
public void onUnregistered(Context context) {
}
@Override
public final void onHandleIntent(Intent intent) {
try {
Context context = getApplicationContext();
if (intent.getAction().equals(REGISTRATION_CALLBACK_INTENT)) {
handleRegistration(context, intent);
} else if (intent.getAction().equals(C2DM_INTENT)) {
onMessage(context, intent);
} else if (intent.getAction().equals(C2DM_RETRY)) {
C2DMessaging.register(context, senderId);
}
} finally {
// Release the power lock, so phone can get back to sleep.
// The lock is reference counted by default, so multiple
// messages are ok.
// If the onMessage() needs to spawn a thread or do something else,
// it should use it's own lock.
mWakeLock.release();
}
}
/**
* Called from the broadcast receiver.
* Will process the received intent, call handleMessage(), registered(), etc.
* in background threads, with a wake lock, while keeping the service
* alive.
*/
static void runIntentInService(Context context, Intent intent) {
if (mWakeLock == null) {
// This is called from BroadcastReceiver, there is no init.
PowerManager pm =
(PowerManager) context.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
WAKELOCK_KEY);
}
mWakeLock.acquire();
// Use a naming convention, similar with how permissions and intents are
// used. Alternatives are introspection or an ugly use of statics.
String receiver = context.getPackageName() + ".C2DMReceiver";
intent.setClassName(context, receiver);
context.startService(intent);
}
private void handleRegistration(final Context context, Intent intent) {
final String registrationId = intent.getStringExtra(EXTRA_REGISTRATION_ID);
String error = intent.getStringExtra(EXTRA_ERROR);
String removed = intent.getStringExtra(EXTRA_UNREGISTERED);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "dmControl: registrationId = " + registrationId +
", error = " + error + ", removed = " + removed);
}
if (removed != null) {
// Remember we are unregistered
C2DMessaging.clearRegistrationId(context);
onUnregistered(context);
return;
} else if (error != null) {
// we are not registered, can try again
C2DMessaging.clearRegistrationId(context);
// Registration failed
Log.e(TAG, "Registration error " + error);
onError(context, error);
if ("SERVICE_NOT_AVAILABLE".equals(error)) {
long backoffTimeMs = C2DMessaging.getBackoff(context);
Log.d(TAG, "Scheduling registration retry, backoff = " + backoffTimeMs);
Intent retryIntent = new Intent(C2DM_RETRY);
PendingIntent retryPIntent = PendingIntent.getBroadcast(context,
0 /*requestCode*/, retryIntent, 0 /*flags*/);
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
am.set(AlarmManager.ELAPSED_REALTIME,
backoffTimeMs, retryPIntent);
// Next retry should wait longer.
backoffTimeMs *= 2;
C2DMessaging.setBackoff(context, backoffTimeMs);
}
} else {
try {
onRegistrered(context, registrationId);
C2DMessaging.setRegistrationId(context, registrationId);
} catch (IOException ex) {
Log.e(TAG, "Registration error " + ex.getMessage());
}
}
}
}

View File

@@ -0,0 +1,25 @@
/*
*/
package com.google.android.c2dm;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
/**
* Helper class to handle BroadcastReciver behavior.
* - can only run for a limited amount of time - it must start a real service
* for longer activity
* - must get the power lock, must make sure it's released when all done.
*
*/
public class C2DMBroadcastReceiver extends BroadcastReceiver {
@Override
public final void onReceive(Context context, Intent intent) {
// To keep things in one place.
C2DMBaseReceiver.runIntentInService(context, intent);
setResult(Activity.RESULT_OK, null /* data */, null /* extra */);
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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.c2dm;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
/**
* Utilities for device registration.
*
* Will keep track of the registration token in a private preference.
*/
public class C2DMessaging {
public static final String EXTRA_SENDER = "sender";
public static final String EXTRA_APPLICATION_PENDING_INTENT = "app";
public static final String REQUEST_UNREGISTRATION_INTENT = "com.google.android.c2dm.intent.UNREGISTER";
public static final String REQUEST_REGISTRATION_INTENT = "com.google.android.c2dm.intent.REGISTER";
public static final String LAST_REGISTRATION_CHANGE = "last_registration_change";
public static final String BACKOFF = "backoff";
// package
static final String PREFERENCE = "com.google.android.c2dm";
private static final long DEFAULT_BACKOFF = 30000;
/**
* Initiate c2d messaging registration for the current application
*/
public static void register(Context context,
String senderId) {
Intent registrationIntent = new Intent(REQUEST_REGISTRATION_INTENT);
registrationIntent.putExtra(EXTRA_APPLICATION_PENDING_INTENT,
PendingIntent.getBroadcast(context, 0, new Intent(), 0));
registrationIntent.putExtra(EXTRA_SENDER, senderId);
context.startService(registrationIntent);
// TODO: if intent not found, notification on need to have GSF
}
/**
* Unregister the application. New messages will be blocked by server.
*/
public static void unregister(Context context) {
Intent regIntent = new Intent(REQUEST_UNREGISTRATION_INTENT);
regIntent.putExtra(EXTRA_APPLICATION_PENDING_INTENT, PendingIntent.getBroadcast(context,
0, new Intent(), 0));
context.startService(regIntent);
}
/**
* Return the current registration id.
*
* If result is empty, the registration has failed.
*
* @return registration id, or empty string if the registration is not complete.
*/
public static String getRegistrationId(Context context) {
final SharedPreferences prefs = context.getSharedPreferences(
PREFERENCE,
Context.MODE_PRIVATE);
String registrationId = prefs.getString("dm_registration", "");
return registrationId;
}
public static long getLastRegistrationChange(Context context) {
final SharedPreferences prefs = context.getSharedPreferences(
PREFERENCE,
Context.MODE_PRIVATE);
return prefs.getLong(LAST_REGISTRATION_CHANGE, 0);
}
static long getBackoff(Context context) {
final SharedPreferences prefs = context.getSharedPreferences(
PREFERENCE,
Context.MODE_PRIVATE);
return prefs.getLong(BACKOFF, DEFAULT_BACKOFF);
}
static void setBackoff(Context context, long backoff) {
final SharedPreferences prefs = context.getSharedPreferences(
PREFERENCE,
Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putLong(BACKOFF, backoff);
editor.commit();
}
// package
static void clearRegistrationId(Context context) {
final SharedPreferences prefs = context.getSharedPreferences(
PREFERENCE,
Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString("dm_registration", "");
editor.putLong(LAST_REGISTRATION_CHANGE, System.currentTimeMillis());
editor.commit();
}
// package
static void setRegistrationId(Context context, String registrationId) {
final SharedPreferences prefs = context.getSharedPreferences(
PREFERENCE,
Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString("dm_registration", registrationId);
editor.commit();
}
}

View File

@@ -0,0 +1,13 @@
# This file is automatically generated by Android Tools.
# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
#
# This file must be checked in Version Control Systems.
#
# To customize properties used by the Ant build system use,
# "build.properties", and override values to adapt the script to your
# project structure.
# Project target.
target=android-5
# Indicates whether an apk should be generated for each density.
split.density=false

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
<TextView
style="?android:attr/listSeparatorTextViewStyle"
android:text="@string/accounts"
android:layout_marginTop="5dip" />
<Spinner
android:id="@+id/accounts"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
style="?android:attr/listSeparatorTextViewStyle"
android:text="@string/setup"
android:layout_marginTop="5dip" />
<Button android:id="@+id/register"
style="?android:attr/buttonStyleSmall"
android:text="@string/register"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="5px"
android:textColor="#FFFFFF"
/>
<TextView
style="?android:attr/listSeparatorTextViewStyle"
android:text="@string/action"
android:layout_marginTop="5dip" />
<CheckBox
android:text="@string/launch_check_box"
android:id="@+id/launch_check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="false"
android:textSize="22px"
/>
</LinearLayout>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Chrome to Phone</string>
<string name="status_registering">Status: Registering device...</string>
<string name="status_unregistering">Status: Unregistering device...</string>
<string name="status_registered">Status: Device registered</string>
<string name="status_not_registered">Status: Device not registered</string>
<string name="launch_check_box">Launch browser/maps directly</string>
<string name="accounts">Desktop Account</string>
<string name="register">Register Device</string>
<string name="unregister">Unregister Device</string>
<string name="setup">Setup</string>
<string name="action">Action</string>
</resources>

View File

@@ -0,0 +1,177 @@
/*
* 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 com.google.android.c2dm.C2DMessaging;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.CompoundButton.OnCheckedChangeListener;
/**
* Main screen - settings, registration, account selection.
*/
public class ActivityUI extends Activity {
public static final String UPDATE_UI_ACTION = "com.google.ctp.UPDATE_UI";
public static final String AUTH_PERMISSION_ACTION = "com.google.ctp.AUTH_PERMISSION";
private boolean mPendingAuth = false;
private Context mContext = null;
private TextView mTextView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = getApplicationContext();
setContentView(R.layout.main);
mTextView = (TextView) findViewById(R.id.status);
Button registerButton = (Button) findViewById(R.id.register);
registerButton.setOnClickListener(mRegisterButtonListener);
CheckBox launchCheckBox = (CheckBox) findViewById(R.id.launch_check_box);
launchCheckBox.setOnCheckedChangeListener(mLaunchCheckBoxListener);
Spinner accounts = (Spinner) findViewById(R.id.accounts);
accounts.setAdapter(getGoogleAccounts(this));
accounts.setPrompt(getString(R.string.accounts));
registerReceiver(mUpdateUIReceiver, new IntentFilter(UPDATE_UI_ACTION));
registerReceiver(mAuthPermissionReceiver, new IntentFilter(AUTH_PERMISSION_ACTION));
updateStatus();
}
@Override
public void onDestroy() {
unregisterReceiver(mUpdateUIReceiver);
unregisterReceiver(mAuthPermissionReceiver);
super.onDestroy();
}
private boolean updateStatus() {
SharedPreferences prefs = Prefs.get(this);
Button registerButton = (Button) findViewById(R.id.register);
CheckBox launchCheckBox = (CheckBox) findViewById(R.id.launch_check_box);
launchCheckBox.setChecked(prefs.getBoolean("launchBrowserOrMaps", false));
String deviceRegistrationID = prefs.getString("deviceRegistrationID", null);
if (deviceRegistrationID == null) {
mTextView.setText(R.string.status_not_registered);
registerButton.setText(R.string.register);
return false;
} else {
mTextView.setText(R.string.status_registered);
registerButton.setText(R.string.unregister);
return true;
}
}
private ArrayAdapter<String> getGoogleAccounts(Context context) {
ArrayAdapter<String> adapter =
new ArrayAdapter<String>(context, android.R.layout.simple_spinner_item);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
Account[] accounts = AccountManager.get(context).getAccounts();
for (Account account : accounts) {
if (account.type.equals("com.google")) {
adapter.add(account.name);
}
}
return adapter;
}
@Override
protected void onResume() {
super.onResume();
if (mPendingAuth) {
mPendingAuth = false;
String regId = C2DMessaging.getRegistrationId(mContext);
if (regId != null) {
DeviceRegistrar.registerWithServer(mContext, regId);
} else {
C2DMessaging.register(mContext, DeviceRegistrar.SENDER_ID);
}
mTextView.setText(R.string.status_registering);
}
}
private final OnClickListener mRegisterButtonListener = new OnClickListener() {
public void onClick(View v) {
SharedPreferences prefs = Prefs.get(mContext);
String deviceRegistrationID = prefs.getString("deviceRegistrationID", null);
if (deviceRegistrationID == null) { // register
Spinner accounts = (Spinner) findViewById(R.id.accounts);
SharedPreferences settings = Prefs.get(mContext);
SharedPreferences.Editor editor = settings.edit();
editor.putString("accountName", (String) accounts.getSelectedItem());
editor.commit();
C2DMessaging.register(mContext, DeviceRegistrar.SENDER_ID);
mTextView.setText(R.string.status_registering);
} else { // unregister
C2DMessaging.unregister(mContext);
mTextView.setText(R.string.status_unregistering);
}
}
};
private final OnCheckedChangeListener mLaunchCheckBoxListener = new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
SharedPreferences settings = Prefs.get(mContext);
SharedPreferences.Editor editor = settings.edit();
editor.putBoolean("launchBrowserOrMaps", isChecked);
editor.commit();
}
};
private final BroadcastReceiver mUpdateUIReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateStatus();
}
};
private final BroadcastReceiver mAuthPermissionReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Bundle extras = intent.getBundleExtra("AccountManagerBundle");
if (extras != null) {
Intent authIntent = (Intent) extras.get(AccountManager.KEY_INTENT);
if (authIntent != null) {
mPendingAuth = true;
startActivity(authIntent);
}
}
}
};
}

View File

@@ -0,0 +1,125 @@
/*
* 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.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import com.google.android.c2dm.C2DMBaseReceiver;
public class C2DMReceiver extends C2DMBaseReceiver {
private static final String TAG = "DataMessageReceiver";
public C2DMReceiver() {
super(DeviceRegistrar.SENDER_ID);
}
@Override
public void onRegistrered(Context context, String registration) {
DeviceRegistrar.registerWithServer(context, registration);
}
@Override
public void onUnregistered(Context context) {
SharedPreferences prefs = Prefs.get(context);
String deviceRegistrationID = prefs.getString("deviceRegistrationID", null);
DeviceRegistrar.unregisterWithServer(context, deviceRegistrationID);
}
@Override
public void onError(Context context, String errorId) {
context.sendBroadcast(new Intent("com.google.ctp.UPDATE_UI"));
}
@Override
public void onMessage(Context context, Intent intent) {
Bundle extras = intent.getExtras();
if (extras != null) {
String url = (String) extras.get("url");
String title = (String) extras.get("title");
if (url != null && title != null) {
if (url.startsWith("http")) {
SharedPreferences settings = Prefs.get(context);
if (settings.getBoolean("launchBrowserOrMaps", false)) {
launchBrowserOrMaps(context, url, title);
} else {
generateNotification(context, url, title);
}
} else {
Log.w(TAG, "Invalid URL: " + url);
}
}
}
}
private void launchBrowserOrMaps(Context context, String url, String title) {
final String GMM_PACKAGE_NAME = "com.google.android.apps.maps";
final String GMM_CLASS_NAME = "com.google.android.maps.MapsActivity";
boolean isMapsURL = url.startsWith("http://maps.google.");
RingtoneManager.getRingtone(context,
RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)).play();
try {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (isMapsURL) {
intent.setClassName(GMM_PACKAGE_NAME, GMM_CLASS_NAME);
}
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
if (isMapsURL) { // try again without GMM
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
}
}
private void generateNotification(Context context, String url, String title) {
int icon = R.drawable.status_icon;
long when = System.currentTimeMillis();
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, intent, 0);
Notification notification = new Notification(icon, title, when);
notification.setLatestEventInfo(context, title, url, contentIntent);
notification.defaults = Notification.DEFAULT_SOUND;
notification.flags |= Notification.FLAG_AUTO_CANCEL;
SharedPreferences settings = Prefs.get(context);
int notificatonID = settings.getInt("notificationID", 0); // allow multiple notifications
NotificationManager nm =
(NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(notificatonID, notification);
SharedPreferences.Editor editor = settings.edit();
editor.putInt("notificationID", ++notificatonID % 32);
editor.commit();
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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 org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
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.
*/
public class DeviceRegistrar {
private static final String TAG = "DeviceRegistrar";
static final String SENDER_ID = "stp.chrome@gmail.com";
private static final String BASE_URL = "http://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";
public static void registerWithServer(final Context context,
final String deviceRegistrationID) {
try {
HttpResponse res = makeRequest(context, deviceRegistrationID, REGISTER_URL);
if (res.getStatusLine().getStatusCode() == 200) {
SharedPreferences settings = Prefs.get(context);
SharedPreferences.Editor editor = settings.edit();
editor.putString("deviceRegistrationID", deviceRegistrationID);
editor.commit();
} else {
Log.w(TAG, "Registration error " +
String.valueOf(res.getStatusLine().getStatusCode()));
}
} catch (PendingAuthException e) {
// Ignore - we'll reregister later
} catch (Exception e) {
Log.w(TAG, "Registration error " + e.getMessage());
}
// Update dialog activity
context.sendBroadcast(new Intent("com.google.ctp.UPDATE_UI"));
}
public static void unregisterWithServer(final Context context,
final String deviceRegistrationID) {
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();
} else {
Log.w(TAG, "Unregistration error " +
String.valueOf(res.getStatusLine().getStatusCode()));
}
} catch (Exception e) {
Log.w(TAG, "Unegistration error " + e.getMessage());
}
// Update dialog activity
context.sendBroadcast(new Intent("com.google.ctp.UPDATE_UI"));
}
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
SharedPreferences settings = Prefs.get(context);
String accountName = settings.getString("accountName", null);
if (accountName == null) throw new Exception("No account");
// 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 = url + "?devregid=" +
deviceRegistrationID;
URI uri = new URI(AUTH_URL + "?continue=" +
URLEncoder.encode(continueURL, "UTF-8") +
"&auth=" + authToken);
HttpGet method = new HttpGet(uri);
HttpResponse res = client.execute(method);
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(ActivityUI.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);
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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.content.Context;
import android.content.SharedPreferences;
public final class Prefs {
public static SharedPreferences get(Context context) {
return context.getSharedPreferences("CTP_PREFS", 0);
}
}

8
appengine/.classpath Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="c2dm"/>
<classpathentry kind="con" path="com.google.appengine.eclipse.core.GAE_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
<classpathentry kind="output" path="war/WEB-INF/classes"/>
</classpath>

34
appengine/.project Normal file
View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>chrometophone-appengine</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.google.appengine.eclipse.core.enhancerbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.google.appengine.eclipse.core.projectValidator</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>com.google.gdt.eclipse.core.webAppProjectValidator</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>com.google.appengine.eclipse.core.gaeNature</nature>
<nature>com.google.gdt.eclipse.core.webAppNature</nature>
</natures>
</projectDescription>

View File

@@ -0,0 +1,4 @@
#Wed May 12 18:01:50 PDT 2010
eclipse.preferences.version=1
filesCopiedToWebInfLib=jsr107cache-1.1.jar|appengine-api-labs-1.3.2.jar|appengine-api-1.0-sdk-1.3.2.jar|geronimo-jta_1.1_spec-1.1.1.jar|datanucleus-jpa-1.1.5.jar|datanucleus-appengine-1.0.6.final.jar|jdo2-api-2.3-eb.jar|datanucleus-core-1.1.5.jar|geronimo-jpa_3.0_spec-1.1.1.jar|appengine-jsr107cache-1.3.2.jar
ormEnhancementInclusions=src/**|c2dm/**

162
appengine/COPYING Normal file
View File

@@ -0,0 +1,162 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the
copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other
entities that control, are controlled by, or are under common control with
that entity. For the purposes of this definition, "control" means (i) the
power, direct or indirect, to cause the direction or management of such
entity, whether by contract or otherwise, or (ii) ownership of fifty percent
(50%) or more of the outstanding shares, or (iii) beneficial ownership of
such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation source, and
configuration files.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled
object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that is
included in or attached to the work (an example is provided in the Appendix
below).
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial
revisions, annotations, elaborations, or other modifications represent, as a
whole, an original work of authorship. For the purposes of this License,
Derivative Works shall not include works that remain separable from, or
merely link (or bind by name) to the interfaces of, the Work and Derivative
Works thereof.
"Contribution" shall mean any work of authorship, including the original
version of the Work and any modifications or additions to that Work or
Derivative Works thereof, that is intentionally submitted to Licensor for
inclusion in the Work by the copyright owner or by an individual or Legal
Entity authorized to submit on behalf of the copyright owner. For the
purposes of this definition, "submitted" means any form of electronic,
verbal, or written communication sent to the Licensor or its
representatives, including but not limited to communication on electronic
mailing lists, source code control systems, and issue tracking systems that
are managed by, or on behalf of, the Licensor for the purpose of discussing
and improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by the copyright owner as "Not a
Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on
behalf of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide,
non-exclusive, no-charge, royalty-free, irrevocable copyright license to
reproduce, prepare Derivative Works of, publicly display, publicly perform,
sublicense, and distribute the Work and such Derivative Works in Source or
Object form.
3. Grant of Patent License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide,
non-exclusive, no-charge, royalty-free, irrevocable (except as stated in
this section) patent license to make, have made, use, offer to sell, sell,
import, and otherwise transfer the Work, where such license applies only to
those patent claims licensable by such Contributor that are necessarily
infringed by their Contribution(s) alone or by combination of their
Contribution(s) with the Work to which such Contribution(s) was submitted.
If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this
License for that Work shall terminate as of the date such litigation is
filed.
4. Redistribution. You may reproduce and distribute copies of the Work or
Derivative Works thereof in any medium, with or without modifications, and
in Source or Object form, provided that You meet the following conditions:
a. You must give any other recipients of the Work or Derivative Works a copy
of this License; and
b. You must cause any modified files to carry prominent notices stating that
You changed the files; and
c. You must retain, in the Source form of any Derivative Works that You
distribute, all copyright, patent, trademark, and attribution notices from
the Source form of the Work, excluding those notices that do not pertain to
any part of the Derivative Works; and
d. If the Work includes a "NOTICE" text file as part of its distribution,
then any Derivative Works that You distribute must include a readable copy
of the attribution notices contained within such NOTICE file, excluding
those notices that do not pertain to any part of the Derivative Works, in at
least one of the following places: within a NOTICE text file distributed as
part of the Derivative Works; within the Source form or documentation, if
provided along with the Derivative Works; or, within a display generated by
the Derivative Works, if and wherever such third-party notices normally
appear. The contents of the NOTICE file are for informational purposes only
and do not modify the License. You may add Your own attribution notices
within Derivative Works that You distribute, alongside or as an addendum to
the NOTICE text from the Work, provided that such additional attribution
notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may
provide additional or different license terms and conditions for use,
reproduction, or distribution of Your modifications, or for any such
Derivative Works as a whole, provided Your use, reproduction, and
distribution of the Work otherwise complies with the conditions stated in
this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any
Contribution intentionally submitted for inclusion in the Work by You to the
Licensor shall be under the terms and conditions of this License, without
any additional terms or conditions. Notwithstanding the above, nothing
herein shall supersede or modify the terms of any separate license agreement
you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor, except
as required for reasonable and customary use in describing the origin of the
Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in
writing, Licensor provides the Work (and each Contributor provides its
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied, including, without limitation, any
warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or
FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining
the appropriateness of using or redistributing the Work and assume any risks
associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether
in tort (including negligence), contract, or otherwise, unless required by
applicable law (such as deliberate and grossly negligent acts) or agreed to
in writing, shall any Contributor be liable to You for damages, including
any direct, indirect, special, incidental, or consequential damages of any
character arising as a result of this License or out of the use or inability
to use the Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all other
commercial damages or losses), even if such Contributor has been advised of
the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work
or Derivative Works thereof, You may choose to offer, and charge a fee for,
acceptance of support, warranty, indemnity, or other liability obligations
and/or rights consistent with this License. However, in accepting such
obligations, You may act only on Your own behalf and on Your sole
responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any
liability incurred by, or claims asserted against, such Contributor by
reason of your accepting any such warranty or additional liability.

84
appengine/build.xml Normal file
View File

@@ -0,0 +1,84 @@
<project name="Guestbook" default="datanucleusenhance">
<property name="sdk.dir" location="/home/ywen/local/appengine-java-sdk" />
<import file="${sdk.dir}/config/user/ant-macros.xml" />
<path id="project.classpath">
<pathelement path="war/WEB-INF/classes" />
<fileset dir="war/WEB-INF/lib">
<include name="**/*.jar" />
</fileset>
<fileset dir="${sdk.dir}/lib">
<include name="shared/**/*.jar" />
</fileset>
</path>
<target name="copyjars"
description="Copies the App Engine JARs to the WAR.">
<copy
todir="war/WEB-INF/lib"
flatten="true">
<fileset dir="${sdk.dir}/lib/user">
<include name="**/*.jar" />
</fileset>
</copy>
</target>
<target name="compile" depends="copyjars"
description="Compiles Java source and copies other source files to the WAR.">
<mkdir dir="war/WEB-INF/classes" />
<copy todir="war/WEB-INF/classes">
<fileset dir="src">
<exclude name="**/*.java" />
</fileset>
</copy>
<javac
srcdir="src"
destdir="war/WEB-INF/classes"
classpathref="project.classpath"
debug="on" />
<javac
srcdir="pushmessaging"
destdir="war/WEB-INF/classes"
classpathref="project.classpath"
debug="on" />
</target>
<target name="datanucleusenhance" depends="compile"
description="Performs JDO enhancement on compiled data classes.">
<enhance_war war="war" />
</target>
<target name="runserver" depends="datanucleusenhance"
description="Starts the development server.">
<dev_appserver war="war" />
</target>
<target name="update" depends="datanucleusenhance"
description="Uploads the application to App Engine.">
<appcfg action="update" war="war" />
</target>
<target name="update_indexes" depends="datanucleusenhance"
description="Uploads just the datastore index configuration to App Engine.">
<appcfg action="update_indexes" war="war" />
</target>
<target name="rollback" depends="datanucleusenhance"
description="Rolls back an interrupted application update.">
<appcfg action="rollback" war="war" />
</target>
<target name="request_logs"
description="Downloads log data from App Engine for the application.">
<appcfg action="request_logs" war="war">
<options>
<arg value="--num_days=5"/>
</options>
<args>
<arg value="logs.txt"/>
</args>
</appcfg>
</target>
</project>

View File

@@ -0,0 +1,49 @@
/*
* 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.c2dm.server;
import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;
/**
* Persistent config info for the server - authentication token
*/
@PersistenceCapable
public final class C2DMConfig {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
@SuppressWarnings("unused")
private Key key;
@Persistent
private String authToken;
public String getAuthToken() {
return (authToken == null) ? "" : authToken;
}
public void setAuthToken(String authToken) {
this.authToken = authToken;
}
public void setKey(Key key) {
this.key = key;
}
}

View File

@@ -0,0 +1,122 @@
/*
* 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.c2dm.server;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
/**
* Stores config information related to data messaging.
*
*/
class C2DMConfigLoader {
private final PersistenceManagerFactory PMF;
private static final Logger log = Logger.getLogger(C2DMConfigLoader.class.getName());
String currentToken;
C2DMConfigLoader(PersistenceManagerFactory pmf) {
this.PMF = pmf;
}
/**
* Update the token.
*
* Called on "Update-Client-Auth" or when admins set a new token.
* @param token
*/
public void updateToken(String token) {
if (token != null) {
currentToken = token;
PersistenceManager pm = PMF.getPersistenceManager();
try {
getDataMessagingConfig(pm).setAuthToken(token);
} finally {
pm.close();
}
}
}
/**
* Token expired
*/
public void invalidateCachedToken() {
currentToken = null;
}
/**
* Return the auth token.
*
* It'll first memcache, if not found will use the database.
*
* @return
*/
public String getToken() {
if (currentToken == null) {
currentToken = getDataMessagingConfig().getAuthToken();
}
return currentToken;
}
public C2DMConfig getDataMessagingConfig() {
PersistenceManager pm = PMF.getPersistenceManager();
try {
C2DMConfig dynamicConfig = getDataMessagingConfig(pm);
return dynamicConfig;
} finally {
pm.close();
}
}
private C2DMConfig getDataMessagingConfig(PersistenceManager pm) {
Key key = KeyFactory.createKey(C2DMConfig.class.getSimpleName(), 1);
C2DMConfig dmConfig = null;
try {
dmConfig = pm.getObjectById(C2DMConfig.class, key);
} catch (JDOObjectNotFoundException e) {
// Create a new JDO object
dmConfig = new C2DMConfig();
dmConfig.setKey(key);
// Must be in classpath, before sending. Do not checkin !
try {
InputStream is = this.getClass().getClassLoader().getResourceAsStream("/dataMessagingToken.txt");
if (is != null) {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String token = reader.readLine();
dmConfig.setAuthToken(token);
}
} catch (Throwable t) {
log.log(Level.SEVERE,
"Can't load initial token, use admin console", t);
}
pm.makePersistent(dmConfig);
}
return dmConfig;
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.c2dm.server;
import java.io.IOException;
import java.util.Map;
import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A task that sends tickles to device clients. This will be invoked by
* AppEngine cron to retry failed requests.
*
* You must configure war/WEB-INF/queue.xml and the web.xml entries.
*/
@SuppressWarnings("serial")
public class C2DMRetryServlet extends HttpServlet {
private static final Logger log = Logger.getLogger(C2DMRetryServlet.class.getName());
public static final String URI = "/tasks/c2dm";
public static final String RETRY_COUNT = "X-AppEngine-TaskRetryCount";
static int MAX_RETRY = 3;
/**
* Only admin can make this request.
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String registrationId = req.getParameter(C2DMessaging.PARAM_REGISTRATION_ID);
String retryCount = req.getHeader(RETRY_COUNT);
if (retryCount != null) {
int retryCnt = Integer.parseInt(retryCount);
if (retryCnt > MAX_RETRY) {
log.severe("Too many retries, drop message for :" + registrationId);
resp.setStatus(200);
return; // will not try again.
}
}
Map<String, String[]> params = req.getParameterMap();
String collapse = req.getParameter(C2DMessaging.PARAM_COLLAPSE_KEY);
boolean delayWhenIdle =
null != req.getParameter(C2DMessaging.PARAM_DELAY_WHILE_IDLE);
try {
// Send doesn't retry !!
// We use the queue exponential backoff for retries.
boolean sentOk = C2DMessaging.get(getServletContext())
.sendNoRetry(registrationId, collapse, params, delayWhenIdle);
log.info("Retry result " + sentOk + " " + registrationId);
if (sentOk) {
resp.setStatus(200);
resp.getOutputStream().write("OK".getBytes());
} else {
resp.setStatus(500); // retry this task
}
} catch (IOException ex) {
resp.setStatus(200);
resp.getOutputStream().write(("Non-retriable error:" +
ex.toString()).getBytes());
}
}
}

View File

@@ -0,0 +1,283 @@
/*
* 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.c2dm.server;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.labs.taskqueue.Queue;
import com.google.appengine.api.labs.taskqueue.QueueFactory;
import com.google.appengine.api.labs.taskqueue.TaskHandle;
import com.google.appengine.api.labs.taskqueue.TaskOptions;
/**
*/
@SuppressWarnings("serial")
public class C2DMessaging {
private static final String UPDATE_CLIENT_AUTH = "Update-Client-Auth";
public static final String DATAMESSAGING_SEND_ENDPOINT =
"https://android.clients.google.com/c2dm/send";
private static final Logger log = Logger.getLogger(C2DMessaging.class.getName());
public static final String PARAM_REGISTRATION_ID = "registration_id";
public static final String PARAM_DELAY_WHILE_IDLE = "delay_while_idle";
public static final String PARAM_COLLAPSE_KEY = "collapse_key";
private static final String UTF8 = "UTF-8";
/**
* Jitter - random interval to wait before retry.
*/
public static final int DATAMESSAGING_MAX_JITTER_MSEC = 3000;
static C2DMessaging singleton;
final C2DMConfigLoader serverConfig;
private C2DMessaging(C2DMConfigLoader serverConfig) {
this.serverConfig = serverConfig;
}
public synchronized static C2DMessaging get(ServletContext servletContext) {
if (singleton == null) {
C2DMConfigLoader serverConfig = new C2DMConfigLoader(getPMF(servletContext));
singleton = new C2DMessaging(serverConfig);
}
return singleton;
}
public synchronized static C2DMessaging get(PersistenceManagerFactory pmf) {
if (singleton == null) {
C2DMConfigLoader serverConfig = new C2DMConfigLoader(pmf);
singleton = new C2DMessaging(serverConfig);
}
return singleton;
}
C2DMConfigLoader getServerConfig() {
return serverConfig;
}
/**
* Initialize PMF - we use a context attribute, so other servlets can
* be share the same instance. This is similar with a shared static
* field, but avoids dependencies.
*/
public static PersistenceManagerFactory getPMF(ServletContext ctx) {
PersistenceManagerFactory pmfFactory =
(PersistenceManagerFactory) ctx.getAttribute(
PersistenceManagerFactory.class.getName());
if (pmfFactory == null) {
pmfFactory = JDOHelper
.getPersistenceManagerFactory("transactions-optional");
ctx.setAttribute(
PersistenceManagerFactory.class.getName(),
pmfFactory);
}
return pmfFactory;
}
public boolean sendNoRetry(String registrationId,
String collapse,
Map<String, String[]> params,
boolean delayWhileIdle)
throws IOException {
// Send a sync message to this Android device.
StringBuilder postDataBuilder = new StringBuilder();
postDataBuilder.append(PARAM_REGISTRATION_ID).
append("=").append(registrationId);
if (delayWhileIdle) {
postDataBuilder.append("&")
.append(PARAM_DELAY_WHILE_IDLE).append("=1");
}
postDataBuilder.append("&").append(PARAM_COLLAPSE_KEY).append("=").
append(collapse);
for (Object keyObj: params.keySet()) {
String key = (String) keyObj;
if (key.startsWith("data.")) {
String[] values = (String[]) params.get(key);
postDataBuilder.append("&").append(key).append("=").
append(URLEncoder.encode(values[0], UTF8));
}
}
byte[] postData = postDataBuilder.toString().getBytes(UTF8);
// Hit the dm URL.
URL url = new URL(DATAMESSAGING_SEND_ENDPOINT);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("Content-Length", Integer.toString(postData.length));
String authToken = serverConfig.getToken();
conn.setRequestProperty("Authorization", "GoogleLogin auth=" + authToken);
OutputStream out = conn.getOutputStream();
out.write(postData);
out.close();
int responseCode = conn.getResponseCode();
if (responseCode == HttpServletResponse.SC_UNAUTHORIZED ||
responseCode == HttpServletResponse.SC_FORBIDDEN) {
// The token is too old - return false to retry later, will fetch the token
// from DB. This happens if the password is changed or token expires. Either admin
// is updating the token, or Update-Client-Auth was received by another server,
// and next retry will get the good one from database.
log.warning("Unauthorized - need token");
serverConfig.invalidateCachedToken();
return false;
}
// Check for updated token header
String updatedAuthToken = conn.getHeaderField(UPDATE_CLIENT_AUTH);
if (updatedAuthToken != null && !authToken.equals(updatedAuthToken)) {
log.info("Got updated auth token from datamessaging servers: " +
updatedAuthToken);
serverConfig.updateToken(updatedAuthToken);
}
String responseLine = new BufferedReader(new InputStreamReader(conn.getInputStream()))
.readLine();
// NOTE: You *MUST* use exponential backoff if you receive a 503 response code.
// Since App Engine's task queue mechanism automatically does this for tasks that
// return non-success error codes, this is not explicitly implemented here.
// If we weren't using App Engine, we'd need to manually implement this.
log.info("Got " + responseCode + " response from Google datamessaging endpoint.");
if (responseLine == null || responseLine.equals("")) {
throw new IOException("Got empty response from Google datamessaging endpoint.");
}
String[] responseParts = responseLine.split("=", 2);
if (responseParts.length != 2) {
log.warning("Invalid message from google: " +
responseCode + " " + responseLine);
throw new IOException("Invalid response from Google " +
responseCode + " " + responseLine);
}
if (responseParts[0].equals("id")) {
log.info("Successfully sent data message to device: " + responseLine);
return true;
}
if (responseParts[0].equals("Error")) {
String err = responseParts[1];
log.warning("Got error response from Google datamessaging endpoint: " + err);
// No retry.
// TODO(costin): show a nicer error to the user.
throw new IOException("Server error: " + err);
} else {
// 500 or unparseable response - server error, needs to retry
log.warning("Invalid response from google " + responseLine + " " +
responseCode);
return false;
}
}
/**
* Helper method to send a message, with 2 parameters.
*
* Permanent errors will result in IOException.
* Retriable errors will cause the message to be scheduled for retry.
*/
public void sendWithRetry(String token, String collapseKey,
String name1, String value1, String name2, String value2)
throws IOException {
Map<String, String[]> params = new HashMap<String, String[]>();
params.put("data." + name1, new String[] {value1});
params.put("data." + name2, new String[] {value2});
boolean sentOk = sendNoRetry(token, collapseKey, params, true);
if (!sentOk) {
retry(token, collapseKey, params, true);
}
}
public boolean sendNoRetry(String token, String collapseKey,
String name1, String value1, String name2, String value2)
throws IOException {
Map<String, String[]> params = new HashMap<String, String[]>();
params.put("data." + name1, new String[] {value1});
params.put("data." + name2, new String[] {value2});
try {
return sendNoRetry(token, collapseKey, params, true);
} catch (IOException ex) {
return false;
}
}
private void retry(String token, String collapseKey,
Map<String, String[]> params, boolean delayWhileIdle) {
Queue dmQueue = QueueFactory.getQueue("c2dm");
try {
TaskOptions url =
TaskOptions.Builder.url(C2DMRetryServlet.URI)
.param(C2DMessaging.PARAM_REGISTRATION_ID, token)
.param(C2DMessaging.PARAM_COLLAPSE_KEY, collapseKey);
if (delayWhileIdle) {
url.param(PARAM_DELAY_WHILE_IDLE, "1");
}
for (String key: params.keySet()) {
String[] values = (String[]) params.get(key);
url.param(key, URLEncoder.encode(values[0], UTF8));
}
// Task queue implements the exponential backoff
long jitter = (int) Math.random() * DATAMESSAGING_MAX_JITTER_MSEC;
url.countdownMillis(jitter);
TaskHandle add = dmQueue.add(url);
} catch (UnsupportedEncodingException e) {
// Ignore - UTF8 should be supported
log.log(Level.SEVERE, "Unexpected error", e);
}
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">
<persistence-manager-factory name="transactions-optional">
<property name="javax.jdo.PersistenceManagerFactoryClass"
value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
<property name="javax.jdo.option.ConnectionURL" value="appengine"/>
<property name="javax.jdo.option.NontransactionalRead" value="true"/>
<property name="javax.jdo.option.NontransactionalWrite" value="true"/>
<property name="javax.jdo.option.RetainValues" value="true"/>
<property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
</persistence-manager-factory>
</jdoconfig>

View File

@@ -0,0 +1,55 @@
/*
* 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.chrometophone.server;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import com.google.appengine.api.datastore.Key;
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class DeviceInfo {
@PrimaryKey
@Persistent
private Key key;
@Persistent
private String deviceRegistrationID;
public DeviceInfo(Key key, String deviceRegistrationID) {
this.key = key;
this.deviceRegistrationID = deviceRegistrationID;
}
public Key getKey() {
return key;
}
public void setKey(Key key) {
this.key = key;
}
public String getDeviceRegistrationID() {
return deviceRegistrationID;
}
public void setDeviceRegistrationID(String deviceRegistrationID) {
this.deviceRegistrationID = deviceRegistrationID;
}
}

View File

@@ -0,0 +1,80 @@
/*
* 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.chrometophone.server;
import java.io.IOException;
import java.util.logging.Logger;
import javax.jdo.PersistenceManager;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
@SuppressWarnings("serial")
public class RegisterServlet extends HttpServlet {
private static final Logger log =
Logger.getLogger(RegisterServlet.class.getName());
private static final String OK_STATUS = "OK";
private static final String ERROR_STATUS = "ERROR";
@Override
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doGet(req, resp);
}
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
String deviceRegistrationID = req.getParameter("devregid");
if (deviceRegistrationID == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + "(Must specify devregid)");
return;
}
// Authorize & store device info
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
if (user != null) {
Key key = KeyFactory.createKey(DeviceInfo.class.getSimpleName(), user.getEmail());
DeviceInfo device = new DeviceInfo(key, deviceRegistrationID);
// Context-shared PMF.
PersistenceManager pm =
C2DMessaging.getPMF(getServletContext()).getPersistenceManager();
try {
pm.makePersistent(device);
resp.getWriter().println(OK_STATUS);
} catch (Exception e) {
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Error registering device)");
log.warning("Error registering device.");
} finally {
pm.close();
}
} else {
resp.getWriter().println(ERROR_STATUS + " (Not authorized)");
}
}
}

View File

@@ -0,0 +1,105 @@
/*
* 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.chrometophone.server;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
@SuppressWarnings("serial")
public class SendServlet extends HttpServlet {
private static final Logger log =
Logger.getLogger(SendServlet.class.getName());
private static final String OK_STATUS = "OK";
private static final String ERROR_STATUS = "ERROR";
public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doGet(req, resp);
}
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
String url = req.getParameter("url");
String title = req.getParameter("title");
if (url == null && title == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Must specify url and title parameters)");
return;
}
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
if (user != null) {
doSendToPhone(url, title, user.getEmail(), resp);
} else {
String followOnURL = req.getRequestURI() + "?title=" +
URLEncoder.encode(req.getParameter("title"), "UTF-8") +
"&url=" + URLEncoder.encode(req.getParameter("url"), "UTF-8");
resp.sendRedirect(userService.createLoginURL(followOnURL));
}
}
private boolean doSendToPhone(String url, String title, String userAccount,
HttpServletResponse resp) throws IOException {
// Get device info
DeviceInfo deviceInfo = null;
// Shared PMF
PersistenceManager pm =
C2DMessaging.getPMF(getServletContext()).getPersistenceManager();
try {
Key key = KeyFactory.createKey(DeviceInfo.class.getSimpleName(), userAccount);
try {
deviceInfo = pm.getObjectById(DeviceInfo.class, key);
} catch (JDOObjectNotFoundException e) {
log.warning("User unknown");
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (User unknown)");
return false;
}
} finally {
pm.close();
}
// Send push message to phone
C2DMessaging push = C2DMessaging.get(getServletContext());
if (push.sendNoRetry(deviceInfo.getDeviceRegistrationID(),
"" + url.hashCode(), "url", url, "title", title)) {
log.info("Link sent to phone!");
resp.getWriter().println(OK_STATUS);
return true;
} else {
log.warning("Error: Unable to send link to phone.");
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Unable to send link)");
return false;
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.chrometophone.server;
import java.io.IOException;
import java.util.logging.Logger;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.android.c2dm.server.C2DMessaging;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
@SuppressWarnings("serial")
public class UnregisterServlet extends HttpServlet {
private static final Logger log =
Logger.getLogger(RegisterServlet.class.getName());
private static final String OK_STATUS = "OK";
private static final String ERROR_STATUS = "ERROR";
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.setContentType("text/plain");
String deviceRegistrationID = req.getParameter("devregid");
if (deviceRegistrationID == null) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (Must specify devregid)");
return;
}
// Authorize & store device info
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
if (user != null) {
Key key = KeyFactory.createKey(DeviceInfo.class.getSimpleName(), user.getEmail());
PersistenceManager pm =
C2DMessaging.getPMF(getServletContext()).getPersistenceManager();
try {
DeviceInfo device = pm.getObjectById(DeviceInfo.class, key);
pm.deletePersistent(device);
resp.getWriter().println(OK_STATUS);
} catch (JDOObjectNotFoundException e) {
resp.setStatus(400);
resp.getWriter().println(ERROR_STATUS + " (User unknown)");
log.warning("User unknown");
} catch (Exception e) {
resp.setStatus(500);
resp.getWriter().println(ERROR_STATUS + " (Error unregistering device)");
log.warning("Error unregistering device: " + e.getMessage());
} finally {
pm.close();
}
} else {
resp.getWriter().println(ERROR_STATUS + " (Not authorized)");
}
}
}

View File

@@ -0,0 +1,24 @@
# A default log4j configuration for log4j users.
#
# To use this configuration, deploy it into your application's WEB-INF/classes
# directory. You are also encouraged to edit it as you like.
# Configure the console as our one appender
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] - %m%n
# tighten logging on the DataNucleus Categories
log4j.category.DataNucleus.JDO=WARN, A1
log4j.category.DataNucleus.Persistence=WARN, A1
log4j.category.DataNucleus.Cache=WARN, A1
log4j.category.DataNucleus.MetaData=WARN, A1
log4j.category.DataNucleus.General=WARN, A1
log4j.category.DataNucleus.Utility=WARN, A1
log4j.category.DataNucleus.Transaction=WARN, A1
log4j.category.DataNucleus.Datastore=WARN, A1
log4j.category.DataNucleus.ClassLoading=WARN, A1
log4j.category.DataNucleus.Plugin=WARN, A1
log4j.category.DataNucleus.ValueGeneration=WARN, A1
log4j.category.DataNucleus.Enhancer=WARN, A1
log4j.category.DataNucleus.SchemaTool=WARN, A1

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<application>chrometophone</application>
<version>1</version>
<system-properties>
<property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/>
</system-properties>
</appengine-web-app>

View File

@@ -0,0 +1,20 @@
# A default java.util.logging configuration.
# (All App Engine logging is through java.util.logging by default).
# Set the default logging level for all loggers to INFO
.level = INFO
# Set the default logging level for ORM, specifically, to WARNING
DataNucleus.JDO.level=WARNING
DataNucleus.Persistence.level=WARNING
DataNucleus.Cache.level=WARNING
DataNucleus.MetaData.level=WARNING
DataNucleus.General.level=WARNING
DataNucleus.Utility.level=WARNING
DataNucleus.Transaction.level=WARNING
DataNucleus.Datastore.level=WARNING
DataNucleus.ClassLoading.level=WARNING
DataNucleus.Plugin.level=WARNING
DataNucleus.ValueGeneration.level=WARNING
DataNucleus.Enhancer.level=WARNING
DataNucleus.SchemaTool.level=WARNING

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<queue-entries>
<queue>
<name>c2dm</name>
<rate>10/s</rate>
</queue>
</queue-entries>

View File

@@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<web-app xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemalocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
version="2.5">
<servlet>
<servlet-name>RegisterServlet</servlet-name>
<servlet-class>
com.google.android.chrometophone.server.RegisterServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>UnregisterServlet</servlet-name>
<servlet-class>
com.google.android.chrometophone.server.UnregisterServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>SendServlet</servlet-name>
<servlet-class>com.google.android.chrometophone.server.SendServlet
</servlet-class>
</servlet>
<servlet>
<servlet-name>dataMessagingServlet</servlet-name>
<servlet-class>
com.google.android.c2dm.server.C2DMRetryServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>RegisterServlet</servlet-name>
<url-pattern>/register</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>UnregisterServlet</servlet-name>
<url-pattern>/unregister</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>SendServlet</servlet-name>
<url-pattern>/send</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>dataMessagingServlet</servlet-name>
<url-pattern>/tasks/c2dm</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>tasks</web-resource-name>
<url-pattern>/tasks/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
</web-app>

View File

@@ -0,0 +1,61 @@
/*
* 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.
*/
{
"app_name": {
"message": "Chrome to Phone",
"description": "Name of the application"
},
"app_desc": {
"message": "This extension adds a button to let you send a link to your Android phone. Requires the Chrome to Phone app to be installed on your Android device.",
"description": "Description of the application."
},
"sending_message": {
"message": "Sending link to phone...",
"description": "Notice when link is being sent to phone."
},
"sent_message": {
"message": "Link sent to phone.",
"description": "Notice when link has been successfully sent to phone."
},
"error_sending_message": {
"message": "Error sending link: $details$",
"description": "Notice when link cannot be sent.",
"placeholders": {
"details": {
"content": "$1",
"example": "Failed to send message."
}
}
},
"login_required_message": {
"message": "Login required.",
"description": "Notice when login is required."
},
"invalid_scheme_message": {
"message": "Only http or https pages supported.",
"description": "Notice when user attempt to send a file link."
},
"about_message": {
"message": "About Chrome to Phone",
"description": "The about message."
},
"connectivity_error_message": {
"message": "Connection error.",
"description": "Error message when there is a connection error with the server."
}
}

52
extension/about.html Normal file
View File

@@ -0,0 +1,52 @@
<!--
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.
-->
<html>
<head>
<title>About Chrome to Phone</title>
<style type="text/css">
body {
min-width: 320px;
overflow-x: hidden;
font-family: verdana;
font-size: 12px;
color: black;
background-color: white;
}
</style>
</head>
<body>
<h1><img src="icon_128.png" valign="bottom">Chrome to Phone</h1>
<h2>About</h2>
<p>Chrome to Phone is an extension that enables you to send the link for the current open tab to your Android device. Simply<br>
click on the phone icon and the link will be pushed to your device. You will be required to log in with the same account used by<br>
your Android device.</p>
<h2>Setup instructions</h2>
<p>
<ol>
<li>Download the Chrome to Phone application to your Android device from <a href="http://code.google.com/p/chrometophone/downloads">http://code.google.com/p/chrometophone/downloads</a> (make sure Settings->Applications->Unknown Sources is checked). Note that this application requires Android 2.2 ("Froyo") or later.</li>
<li>Start the Chrome to Phone application on the phone. </li>
<li>Choose the account to register with (this must match the account you sign in to with Chrome to Phone on the desktop).</li>
<li>Click on "Register Device" to register your phone with the Chrome to Phone service.</li>
</ol>
</p>
<div align="center"><br>&copy;2010 Google - <a href="http://www.google.com/intl/en/about.html">About Google</a> -
<a href="http://www.google.com/privacy.html">Privacy</a> - <a href="http://chrome.google.com/extensions/intl/en/gallery_tos.html">Terms of Service</a></div>
</body>
</html>

BIN
extension/icon_128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
extension/icon_19.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 476 B

32
extension/manifest.json Normal file
View File

@@ -0,0 +1,32 @@
/*
* 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.
*/
{
"name": "__MSG_app_name__",
"description": "__MSG_app_desc__",
"version": "1.0.0",
"default_locale": "en",
"icons": { "128": "icon_128.png" },
"options_page": "about.html",
"browser_action": {
"default_title": "__MSG_app_name__",
"default_icon": "icon_19.png",
"popup": "popup.html"
},
"permissions": [
"tabs", "http://chrometophone.appspot.com/*", "https://www.google.com/accounts/ServiceLogin"
]
}

75
extension/popup.html Normal file
View File

@@ -0,0 +1,75 @@
<!--
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.
-->
<style type="text/css">
body {
min-width: 320px;
overflow-x: hidden;
font-family: verdana;
font-size: 12px;
color: black;
background-color: white;
}
</style>
<script type="text/javascript">
var baseUrl = 'http://chrometophone.appspot.com/send';
var req = new XMLHttpRequest();
function loadHandler() {
document.getElementById('msg').innerHTML = chrome.i18n.getMessage('sending_message');
document.getElementById('about').innerHTML = chrome.i18n.getMessage('about_message');
chrome.tabs.getSelected(null, function(tab) {
if (tab.url.indexOf('http:') == 0 ||
tab.url.indexOf('https:') == 0) {
sendToPhone(tab.title, tab.url);
} else {
document.getElementById('msg').innerHTML = chrome.i18n.getMessage('invalid_scheme_message');
}
});
}
function sendToPhone(title, url) {
var sendUrl = baseUrl + '?title=' + encodeURIComponent(title) + '&url=' + encodeURIComponent(url);
req.open('GET', sendUrl, true);
req.onreadystatechange = function() {
if (this.readyState == 4) {
if (req.status == 200) {
if (req.responseText.substring(0, 2) == 'OK') {
document.getElementById('msg').innerHTML = chrome.i18n.getMessage('sent_message');
} else { // most likely login, handle in new tab
document.getElementById('msg').innerHTML =
chrome.i18n.getMessage('login_required_message');
chrome.tabs.create({url: sendUrl});
}
} else {
document.getElementById('msg').innerHTML =
chrome.i18n.getMessage('error_sending_message', req.responseText);
}
}
}
req.send(null);
}
</script>
<body onload="loadHandler()">
<b><div id="msg"></div></b>
<p><a href="#" onclick="chrome.tabs.create({url: 'about.html'})"><div id="about"></div></a></p>
</body>