Project import
8
android/.classpath
Normal 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
@@ -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>
|
||||
56
android/AndroidManifest.xml
Normal 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>
|
||||
195
android/c2dm/com/google/android/c2dm/C2DMBaseReceiver.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 */);
|
||||
}
|
||||
}
|
||||
128
android/c2dm/com/google/android/c2dm/C2DMessaging.java
Normal 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();
|
||||
|
||||
}
|
||||
}
|
||||
13
android/default.properties
Normal 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
|
||||
BIN
android/res/drawable-hdpi/app_icon.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
android/res/drawable-hdpi/status_icon.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
android/res/drawable-ldpi/app_icon.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
android/res/drawable-ldpi/status_icon.png
Normal file
|
After Width: | Height: | Size: 543 B |
BIN
android/res/drawable-mdpi/app_icon.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
android/res/drawable-mdpi/status_icon.png
Normal file
|
After Width: | Height: | Size: 756 B |
53
android/res/layout/main.xml
Normal 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>
|
||||
14
android/res/values/strings.xml
Normal 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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
android/src/com/google/android/apps/chrometophone/Prefs.java
Normal 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
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
283
appengine/c2dm/com/google/android/c2dm/server/C2DMessaging.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
15
appengine/src/META-INF/jdoconfig.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
24
appengine/src/log4j.properties
Normal 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
|
||||
23
appengine/war/WEB-INF/appengine-web.xml
Normal 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>
|
||||
20
appengine/war/WEB-INF/logging.properties
Normal 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
|
||||
22
appengine/war/WEB-INF/queue.xml
Normal 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>
|
||||
75
appengine/war/WEB-INF/web.xml
Normal 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>
|
||||
61
extension/_locales/en/messages.json
Normal 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
@@ -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>©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
|
After Width: | Height: | Size: 5.5 KiB |
BIN
extension/icon_19.png
Normal file
|
After Width: | Height: | Size: 476 B |
32
extension/manifest.json
Normal 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
@@ -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>
|
||||