am 4a9999f1: remove Remembering Your Users class from nav. Separate CL will redirect these pages to the Google Auth doc.
* commit '4a9999f1ad20830df4aa38d3d3a42d73cb9986f0':
remove Remembering Your Users class from nav. Separate CL will redirect these pages to the Google Auth doc.
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index bb9e19f..a25e311 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -16,6 +16,7 @@
package android.app;
+import android.R;
import com.android.internal.app.IUsageStats;
import com.android.internal.os.PkgUsageStats;
import com.android.internal.util.MemInfoReader;
@@ -369,9 +370,9 @@
// Really brain dead right now -- just take this from the configured
// vm heap size, and assume it is in megabytes and thus ends with "m".
String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
- return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1));
+ return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length() - 1));
}
-
+
/**
* Used by persistent processes to determine if they are running on a
* higher-end device so should be okay using hardware drawing acceleration
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index c9776f1..4fcb18a 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -422,6 +422,14 @@
}
}
+ /** @hide */
+ public void resetAllModes() {
+ try {
+ mService.resetAllModes();
+ } catch (RemoteException e) {
+ }
+ }
+
public void startWatchingMode(int op, String packageName, final Callback callback) {
synchronized (mModeWatchers) {
IAppOpsCallback cb = mModeWatchers.get(callback);
diff --git a/core/java/android/app/Fragment.java b/core/java/android/app/Fragment.java
index b6aeb84..422d0bc 100644
--- a/core/java/android/app/Fragment.java
+++ b/core/java/android/app/Fragment.java
@@ -580,6 +580,10 @@
if (clazz == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = context.getClassLoader().loadClass(fname);
+ if (!Fragment.class.isAssignableFrom(clazz)) {
+ throw new InstantiationException("Trying to instantiate a class " + fname
+ + " that is not a Fragment", new ClassCastException());
+ }
sClassMap.put(fname, clazz);
}
Fragment f = (Fragment)clazz.newInstance();
diff --git a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java
index 81c0a6a..0aedecb 100644
--- a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java
+++ b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java
@@ -152,6 +152,11 @@
// not implemented
}
+ @Override
+ public void captivePortalCheckCompleted(boolean isCaptivePortal) {
+ // not implemented
+ }
+
/**
* Re-enable connectivity to a network after a {@link #teardown()}.
*/
diff --git a/core/java/android/net/BaseNetworkStateTracker.java b/core/java/android/net/BaseNetworkStateTracker.java
index 1165281..e87f84c 100644
--- a/core/java/android/net/BaseNetworkStateTracker.java
+++ b/core/java/android/net/BaseNetworkStateTracker.java
@@ -102,6 +102,11 @@
}
@Override
+ public void captivePortalCheckCompleted(boolean isCaptivePortal) {
+ // not implemented
+ }
+
+ @Override
public boolean setRadio(boolean turnOn) {
// Base tracker doesn't handle radios
return true;
diff --git a/core/java/android/net/CaptivePortalTracker.java b/core/java/android/net/CaptivePortalTracker.java
index 21995c0..19c5f39 100644
--- a/core/java/android/net/CaptivePortalTracker.java
+++ b/core/java/android/net/CaptivePortalTracker.java
@@ -34,6 +34,7 @@
import android.os.RemoteException;
import android.provider.Settings;
import android.telephony.TelephonyManager;
+import android.text.TextUtils;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
@@ -42,6 +43,7 @@
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.Inet4Address;
+import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
@@ -52,17 +54,15 @@
* @hide
*/
public class CaptivePortalTracker extends StateMachine {
- private static final boolean DBG = false;
+ private static final boolean DBG = true;
private static final String TAG = "CaptivePortalTracker";
private static final String DEFAULT_SERVER = "clients3.google.com";
- private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
private static final int SOCKET_TIMEOUT_MS = 10000;
private String mServer;
private String mUrl;
- private boolean mNotificationShown = false;
private boolean mIsCaptivePortalCheckEnabled = false;
private IConnectivityManager mConnService;
private TelephonyManager mTelephonyManager;
@@ -159,12 +159,12 @@
private class DefaultState extends State {
@Override
public void enter() {
- if (DBG) log(getName() + "\n");
+ setNotificationOff();
}
@Override
public boolean processMessage(Message message) {
- if (DBG) log(getName() + message.toString() + "\n");
+ if (DBG) log(getName() + message.toString());
switch (message.what) {
case CMD_DETECT_PORTAL:
NetworkInfo info = (NetworkInfo) message.obj;
@@ -186,23 +186,24 @@
private class NoActiveNetworkState extends State {
@Override
public void enter() {
- if (DBG) log(getName() + "\n");
mNetworkInfo = null;
- /* Clear any previous notification */
- setNotificationVisible(false);
}
@Override
public boolean processMessage(Message message) {
- if (DBG) log(getName() + message.toString() + "\n");
+ if (DBG) log(getName() + message.toString());
InetAddress server;
NetworkInfo info;
switch (message.what) {
case CMD_CONNECTIVITY_CHANGE:
info = (NetworkInfo) message.obj;
- if (info.isConnected() && isActiveNetwork(info)) {
- mNetworkInfo = info;
- transitionTo(mDelayedCaptiveCheckState);
+ if (info.getType() == ConnectivityManager.TYPE_WIFI) {
+ if (info.isConnected() && isActiveNetwork(info)) {
+ mNetworkInfo = info;
+ transitionTo(mDelayedCaptiveCheckState);
+ }
+ } else {
+ log(getName() + " not a wifi connectivity change, ignore");
}
break;
default:
@@ -215,7 +216,7 @@
private class ActiveNetworkState extends State {
@Override
public void enter() {
- if (DBG) log(getName() + "\n");
+ setNotificationOff();
}
@Override
@@ -248,7 +249,6 @@
private class DelayedCaptiveCheckState extends State {
@Override
public void enter() {
- if (DBG) log(getName() + "\n");
Message message = obtainMessage(CMD_DELAYED_CAPTIVE_CHECK, ++mDelayedCheckToken, 0);
if (mDeviceProvisioned) {
sendMessageDelayed(message, DELAYED_CHECK_INTERVAL_MS);
@@ -259,7 +259,7 @@
@Override
public boolean processMessage(Message message) {
- if (DBG) log(getName() + message.toString() + "\n");
+ if (DBG) log(getName() + message.toString());
switch (message.what) {
case CMD_DELAYED_CAPTIVE_CHECK:
if (message.arg1 == mDelayedCheckToken) {
@@ -270,11 +270,17 @@
} else {
if (DBG) log("Not captive network " + mNetworkInfo);
}
+ notifyPortalCheckCompleted(mNetworkInfo, captive);
if (mDeviceProvisioned) {
if (captive) {
// Setup Wizard will assist the user in connecting to a captive
// portal, so make the notification visible unless during setup
- setNotificationVisible(true);
+ try {
+ mConnService.setProvisioningNotificationVisible(true,
+ mNetworkInfo.getType(), mNetworkInfo.getExtraInfo(), mUrl);
+ } catch(RemoteException e) {
+ e.printStackTrace();
+ }
}
} else {
Intent intent = new Intent(
@@ -300,12 +306,26 @@
return;
}
try {
+ if (DBG) log("notifyPortalCheckComplete: ni=" + info);
mConnService.captivePortalCheckComplete(info);
} catch(RemoteException e) {
e.printStackTrace();
}
}
+ private void notifyPortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
+ if (info == null) {
+ loge("notifyPortalCheckComplete on null");
+ return;
+ }
+ try {
+ if (DBG) log("notifyPortalCheckCompleted: captive=" + isCaptivePortal + " ni=" + info);
+ mConnService.captivePortalCheckCompleted(info, isCaptivePortal);
+ } catch(RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
private boolean isActiveNetwork(NetworkInfo info) {
try {
NetworkInfo active = mConnService.getActiveNetworkInfo();
@@ -318,6 +338,15 @@
return false;
}
+ private void setNotificationOff() {
+ try {
+ mConnService.setProvisioningNotificationVisible(false, ConnectivityManager.TYPE_NONE,
+ null, null);
+ } catch (RemoteException e) {
+ log("setNotificationOff: " + e);
+ }
+ }
+
/**
* Do a URL fetch on a known server to see if we get the data we expect
*/
@@ -360,58 +389,4 @@
}
return null;
}
-
- private void setNotificationVisible(boolean visible) {
- // if it should be hidden and it is already hidden, then noop
- if (!visible && !mNotificationShown) {
- return;
- }
-
- Resources r = Resources.getSystem();
- NotificationManager notificationManager = (NotificationManager) mContext
- .getSystemService(Context.NOTIFICATION_SERVICE);
-
- if (visible) {
- CharSequence title;
- CharSequence details;
- int icon;
- switch (mNetworkInfo.getType()) {
- case ConnectivityManager.TYPE_WIFI:
- title = r.getString(R.string.wifi_available_sign_in, 0);
- details = r.getString(R.string.network_available_sign_in_detailed,
- mNetworkInfo.getExtraInfo());
- icon = R.drawable.stat_notify_wifi_in_range;
- break;
- case ConnectivityManager.TYPE_MOBILE:
- title = r.getString(R.string.network_available_sign_in, 0);
- // TODO: Change this to pull from NetworkInfo once a printable
- // name has been added to it
- details = mTelephonyManager.getNetworkOperatorName();
- icon = R.drawable.stat_notify_rssi_in_range;
- break;
- default:
- title = r.getString(R.string.network_available_sign_in, 0);
- details = r.getString(R.string.network_available_sign_in_detailed,
- mNetworkInfo.getExtraInfo());
- icon = R.drawable.stat_notify_rssi_in_range;
- break;
- }
-
- Notification notification = new Notification();
- notification.when = 0;
- notification.icon = icon;
- notification.flags = Notification.FLAG_AUTO_CANCEL;
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(mUrl));
- intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
- Intent.FLAG_ACTIVITY_NEW_TASK);
- notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
- notification.tickerText = title;
- notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);
-
- notificationManager.notify(NOTIFICATION_ID, 1, notification);
- } else {
- notificationManager.cancel(NOTIFICATION_ID, 1);
- }
- mNotificationShown = visible;
- }
}
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 697bde9..02a6494 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -583,6 +583,29 @@
}
/**
+ * Returns details about the Provisioning or currently active default data network. When
+ * connected, this network is the default route for outgoing connections.
+ * You should always check {@link NetworkInfo#isConnected()} before initiating
+ * network traffic. This may return {@code null} when there is no default
+ * network.
+ *
+ * @return a {@link NetworkInfo} object for the current default network
+ * or {@code null} if no network default network is currently active
+ *
+ * <p>This method requires the call to hold the permission
+ * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}.
+ *
+ * {@hide}
+ */
+ public NetworkInfo getProvisioningOrActiveNetworkInfo() {
+ try {
+ return mService.getProvisioningOrActiveNetworkInfo();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
* Returns the IP information for the current default network.
*
* @return a {@link LinkProperties} object describing the IP info
@@ -1283,6 +1306,25 @@
}
/**
+ * Signal that the captive portal check on the indicated network
+ * is complete and whether its a captive portal or not.
+ *
+ * @param info the {@link NetworkInfo} object for the networkType
+ * in question.
+ * @param isCaptivePortal true/false.
+ *
+ * <p>This method requires the call to hold the permission
+ * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}.
+ * {@hide}
+ */
+ public void captivePortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
+ try {
+ mService.captivePortalCheckCompleted(info, isCaptivePortal);
+ } catch (RemoteException e) {
+ }
+ }
+
+ /**
* Supply the backend messenger for a network tracker
*
* @param type NetworkType to set
@@ -1297,70 +1339,26 @@
}
/**
- * The ResultReceiver resultCode for checkMobileProvisioning (CMP_RESULT_CODE)
- */
-
- /**
- * No connection was possible to the network.
- * {@hide}
- */
- public static final int CMP_RESULT_CODE_NO_CONNECTION = 0;
-
- /**
- * A connection was made to the internet, all is well.
- * {@hide}
- */
- public static final int CMP_RESULT_CODE_CONNECTABLE = 1;
-
- /**
- * A connection was made but there was a redirection, we appear to be in walled garden.
- * This is an indication of a warm sim on a mobile network.
- * {@hide}
- */
- public static final int CMP_RESULT_CODE_REDIRECTED = 2;
-
- /**
- * A connection was made but no dns server was available to resolve a name to address.
- * This is an indication of a warm sim on a mobile network.
+ * Check mobile provisioning.
*
- * {@hide}
- */
- public static final int CMP_RESULT_CODE_NO_DNS = 3;
-
- /**
- * A connection was made but could not open a TCP connection.
- * This is an indication of a warm sim on a mobile network.
- * {@hide}
- */
- public static final int CMP_RESULT_CODE_NO_TCP_CONNECTION = 4;
-
- /**
- * Check mobile provisioning. The resultCode passed to
- * onReceiveResult will be one of the CMP_RESULT_CODE_xxxx values above.
- * This may take a minute or more to complete.
- *
- * @param sendNotificaiton, when true a notification will be sent to user.
* @param suggestedTimeOutMs, timeout in milliseconds
- * @param resultReceiver needs to be supplied to receive the result
*
* @return time out that will be used, maybe less that suggestedTimeOutMs
* -1 if an error.
*
* {@hide}
*/
- public int checkMobileProvisioning(boolean sendNotification, int suggestedTimeOutMs,
- ResultReceiver resultReceiver) {
+ public int checkMobileProvisioning(int suggestedTimeOutMs) {
int timeOutMs = -1;
try {
- timeOutMs = mService.checkMobileProvisioning(sendNotification, suggestedTimeOutMs,
- resultReceiver);
+ timeOutMs = mService.checkMobileProvisioning(suggestedTimeOutMs);
} catch (RemoteException e) {
}
return timeOutMs;
}
/**
- * Get the carrier provisioning url.
+ * Get the mobile provisioning url.
* {@hide}
*/
public String getMobileProvisioningUrl() {
@@ -1370,4 +1368,32 @@
}
return null;
}
+
+ /**
+ * Get the mobile redirected provisioning url.
+ * {@hide}
+ */
+ public String getMobileRedirectedProvisioningUrl() {
+ try {
+ return mService.getMobileRedirectedProvisioningUrl();
+ } catch (RemoteException e) {
+ }
+ return null;
+ }
+
+ /**
+ * Set sign in error notification to visible or in visible
+ *
+ * @param visible
+ * @param networkType
+ *
+ * {@hide}
+ */
+ public void setProvisioningNotificationVisible(boolean visible, int networkType,
+ String extraInfo, String url) {
+ try {
+ mService.setProvisioningNotificationVisible(visible, networkType, extraInfo, url);
+ } catch (RemoteException e) {
+ }
+ }
}
diff --git a/core/java/android/net/DummyDataStateTracker.java b/core/java/android/net/DummyDataStateTracker.java
index 15a81f3..ee738fd 100644
--- a/core/java/android/net/DummyDataStateTracker.java
+++ b/core/java/android/net/DummyDataStateTracker.java
@@ -120,10 +120,16 @@
return true;
}
+ @Override
public void captivePortalCheckComplete() {
// not implemented
}
+ @Override
+ public void captivePortalCheckCompleted(boolean isCaptivePortal) {
+ // not implemented
+ }
+
/**
* Record the detailed state of a network, and if it is a
* change from the previous state, send a notification to
diff --git a/core/java/android/net/EthernetDataTracker.java b/core/java/android/net/EthernetDataTracker.java
index 27d5a58..ac2b0d9 100644
--- a/core/java/android/net/EthernetDataTracker.java
+++ b/core/java/android/net/EthernetDataTracker.java
@@ -279,6 +279,11 @@
// not implemented
}
+ @Override
+ public void captivePortalCheckCompleted(boolean isCaptivePortal) {
+ // not implemented
+ }
+
/**
* Turn the wireless radio off for a network.
* @param turnOn {@code true} to turn the radio on, {@code false}
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 4600c1a..a17b4f5 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -46,6 +46,8 @@
NetworkInfo getNetworkInfo(int networkType);
NetworkInfo[] getAllNetworkInfo();
+ NetworkInfo getProvisioningOrActiveNetworkInfo();
+
boolean isNetworkSupported(int networkType);
LinkProperties getActiveLinkProperties();
@@ -129,11 +131,17 @@
void captivePortalCheckComplete(in NetworkInfo info);
+ void captivePortalCheckCompleted(in NetworkInfo info, boolean isCaptivePortal);
+
void supplyMessenger(int networkType, in Messenger messenger);
int findConnectionTypeForIface(in String iface);
- int checkMobileProvisioning(boolean sendNotification, int suggestedTimeOutMs, in ResultReceiver resultReceiver);
+ int checkMobileProvisioning(int suggestedTimeOutMs);
String getMobileProvisioningUrl();
+
+ String getMobileRedirectedProvisioningUrl();
+
+ void setProvisioningNotificationVisible(boolean visible, int networkType, in String extraInfo, in String url);
}
diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java
index 5a1daed..b2b5314 100644
--- a/core/java/android/net/MobileDataStateTracker.java
+++ b/core/java/android/net/MobileDataStateTracker.java
@@ -40,6 +40,7 @@
import java.io.CharArrayWriter;
import java.io.PrintWriter;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Track the state of mobile data connectivity. This is done by
@@ -51,7 +52,7 @@
public class MobileDataStateTracker implements NetworkStateTracker {
private static final String TAG = "MobileDataStateTracker";
- private static final boolean DBG = false;
+ private static final boolean DBG = true;
private static final boolean VDBG = false;
private PhoneConstants.DataState mMobileDataState;
@@ -75,6 +76,8 @@
private Handler mHandler;
private AsyncChannel mDataConnectionTrackerAc;
+ private AtomicBoolean mIsCaptivePortal = new AtomicBoolean(false);
+
/**
* Create a new MobileDataStateTracker
* @param netType the ConnectivityManager network type
@@ -101,6 +104,7 @@
IntentFilter filter = new IntentFilter();
filter.addAction(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED);
+ filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN);
filter.addAction(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED);
mContext.registerReceiver(new MobileDataStateReceiver(), filter);
@@ -168,20 +172,55 @@
public void releaseWakeLock() {
}
+ private void updateLinkProperitesAndCapatilities(Intent intent) {
+ mLinkProperties = intent.getParcelableExtra(
+ PhoneConstants.DATA_LINK_PROPERTIES_KEY);
+ if (mLinkProperties == null) {
+ loge("CONNECTED event did not supply link properties.");
+ mLinkProperties = new LinkProperties();
+ }
+ mLinkCapabilities = intent.getParcelableExtra(
+ PhoneConstants.DATA_LINK_CAPABILITIES_KEY);
+ if (mLinkCapabilities == null) {
+ loge("CONNECTED event did not supply link capabilities.");
+ mLinkCapabilities = new LinkCapabilities();
+ }
+ }
+
private class MobileDataStateReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(TelephonyIntents.
+ ACTION_DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN)) {
+ String apnName = intent.getStringExtra(PhoneConstants.DATA_APN_KEY);
+ String apnType = intent.getStringExtra(PhoneConstants.DATA_APN_TYPE_KEY);
+ if (!TextUtils.equals(mApnType, apnType)) {
+ return;
+ }
+ if (DBG) {
+ log("Broadcast received: " + intent.getAction() + " apnType=" + apnType
+ + " apnName=" + apnName);
+ }
+
+ // Make us in the connecting state until we make a new TYPE_MOBILE_PROVISIONING
+ mMobileDataState = PhoneConstants.DataState.CONNECTING;
+ updateLinkProperitesAndCapatilities(intent);
+ mNetworkInfo.setIsConnectedToProvisioningNetwork(true);
+
+ // Change state to SUSPENDED so setDetailedState
+ // sends EVENT_STATE_CHANGED to connectivityService
+ setDetailedState(DetailedState.SUSPENDED, "", apnName);
+ } else if (intent.getAction().equals(TelephonyIntents.
ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) {
String apnType = intent.getStringExtra(PhoneConstants.DATA_APN_TYPE_KEY);
- if (VDBG) {
- log(String.format("Broadcast received: ACTION_ANY_DATA_CONNECTION_STATE_CHANGED"
- + "mApnType=%s %s received apnType=%s", mApnType,
- TextUtils.equals(apnType, mApnType) ? "==" : "!=", apnType));
- }
if (!TextUtils.equals(apnType, mApnType)) {
return;
}
+ // Assume this isn't a provisioning network.
+ mNetworkInfo.setIsConnectedToProvisioningNetwork(false);
+ if (DBG) {
+ log("Broadcast received: " + intent.getAction() + " apnType=" + apnType);
+ }
int oldSubtype = mNetworkInfo.getSubtype();
int newSubType = TelephonyManager.getDefault().getNetworkType();
@@ -199,7 +238,7 @@
String apnName = intent.getStringExtra(PhoneConstants.DATA_APN_KEY);
mNetworkInfo.setRoaming(intent.getBooleanExtra(
PhoneConstants.DATA_NETWORK_ROAMING_KEY, false));
- if (VDBG) {
+ if (DBG) {
log(mApnType + " setting isAvailable to " +
intent.getBooleanExtra(PhoneConstants.NETWORK_UNAVAILABLE_KEY,false));
}
@@ -233,18 +272,7 @@
setDetailedState(DetailedState.SUSPENDED, reason, apnName);
break;
case CONNECTED:
- mLinkProperties = intent.getParcelableExtra(
- PhoneConstants.DATA_LINK_PROPERTIES_KEY);
- if (mLinkProperties == null) {
- loge("CONNECTED event did not supply link properties.");
- mLinkProperties = new LinkProperties();
- }
- mLinkCapabilities = intent.getParcelableExtra(
- PhoneConstants.DATA_LINK_CAPABILITIES_KEY);
- if (mLinkCapabilities == null) {
- loge("CONNECTED event did not supply link capabilities.");
- mLinkCapabilities = new LinkCapabilities();
- }
+ updateLinkProperitesAndCapatilities(intent);
setDetailedState(DetailedState.CONNECTED, reason, apnName);
break;
}
@@ -269,18 +297,15 @@
equals(TelephonyIntents.ACTION_DATA_CONNECTION_FAILED)) {
String apnType = intent.getStringExtra(PhoneConstants.DATA_APN_TYPE_KEY);
if (!TextUtils.equals(apnType, mApnType)) {
- if (DBG) {
- log(String.format(
- "Broadcast received: ACTION_ANY_DATA_CONNECTION_FAILED ignore, " +
- "mApnType=%s != received apnType=%s", mApnType, apnType));
- }
return;
}
+ // Assume this isn't a provisioning network.
+ mNetworkInfo.setIsConnectedToProvisioningNetwork(false);
String reason = intent.getStringExtra(PhoneConstants.FAILURE_REASON_KEY);
String apnName = intent.getStringExtra(PhoneConstants.DATA_APN_KEY);
if (DBG) {
- log("Received " + intent.getAction() +
- " broadcast" + reason == null ? "" : "(" + reason + ")");
+ log("Broadcast received: " + intent.getAction() +
+ " reason=" + reason == null ? "null" : reason);
}
setDetailedState(DetailedState.FAILED, reason, apnName);
} else {
@@ -372,11 +397,27 @@
return (setEnableApn(mApnType, false) != PhoneConstants.APN_REQUEST_FAILED);
}
+ /**
+ * @return true if this is ready to operate
+ */
+ public boolean isReady() {
+ return mDataConnectionTrackerAc != null;
+ }
+
@Override
public void captivePortalCheckComplete() {
// not implemented
}
+ @Override
+ public void captivePortalCheckCompleted(boolean isCaptivePortal) {
+ if (mIsCaptivePortal.getAndSet(isCaptivePortal) != isCaptivePortal) {
+ // Captive portal change enable/disable failing fast
+ setEnableFailFastMobileData(
+ isCaptivePortal ? DctConstants.ENABLED : DctConstants.DISABLED);
+ }
+ }
+
/**
* Record the detailed state of a network, and if it is a
* change from the previous state, send a notification to
@@ -525,6 +566,40 @@
}
}
+ /**
+ * Inform DCT mobile provisioning has started, it ends when provisioning completes.
+ */
+ public void enableMobileProvisioning(String url) {
+ if (DBG) log("enableMobileProvisioning(url=" + url + ")");
+ final AsyncChannel channel = mDataConnectionTrackerAc;
+ if (channel != null) {
+ Message msg = Message.obtain();
+ msg.what = DctConstants.CMD_ENABLE_MOBILE_PROVISIONING;
+ msg.setData(Bundle.forPair(DctConstants.PROVISIONING_URL_KEY, url));
+ channel.sendMessage(msg);
+ }
+ }
+
+ /**
+ * Return if this network is the provisioning network. Valid only if connected.
+ * @param met
+ */
+ public boolean isProvisioningNetwork() {
+ boolean retVal;
+ try {
+ Message msg = Message.obtain();
+ msg.what = DctConstants.CMD_IS_PROVISIONING_APN;
+ msg.setData(Bundle.forPair(DctConstants.APN_TYPE_KEY, mApnType));
+ Message result = mDataConnectionTrackerAc.sendMessageSynchronously(msg);
+ retVal = result.arg1 == DctConstants.ENABLED;
+ } catch (NullPointerException e) {
+ loge("isProvisioningNetwork: X " + e);
+ retVal = false;
+ }
+ if (DBG) log("isProvisioningNetwork: retVal=" + retVal);
+ return retVal;
+ }
+
@Override
public void addStackedLink(LinkProperties link) {
mLinkProperties.addStackedLink(link);
diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java
index 689dae5..4d2a70d 100644
--- a/core/java/android/net/NetworkInfo.java
+++ b/core/java/android/net/NetworkInfo.java
@@ -83,7 +83,7 @@
/** Link has poor connectivity. */
VERIFYING_POOR_LINK,
/** Checking if network is a captive portal */
- CAPTIVE_PORTAL_CHECK,
+ CAPTIVE_PORTAL_CHECK
}
/**
@@ -120,6 +120,8 @@
private String mExtraInfo;
private boolean mIsFailover;
private boolean mIsRoaming;
+ private boolean mIsConnectedToProvisioningNetwork;
+
/**
* Indicates whether network connectivity is possible:
*/
@@ -148,6 +150,7 @@
mState = State.UNKNOWN;
mIsAvailable = false; // until we're told otherwise, assume unavailable
mIsRoaming = false;
+ mIsConnectedToProvisioningNetwork = false;
}
/** {@hide} */
@@ -164,6 +167,7 @@
mIsFailover = source.mIsFailover;
mIsRoaming = source.mIsRoaming;
mIsAvailable = source.mIsAvailable;
+ mIsConnectedToProvisioningNetwork = source.mIsConnectedToProvisioningNetwork;
}
}
@@ -322,6 +326,22 @@
}
}
+ /** {@hide} */
+ @VisibleForTesting
+ public boolean isConnectedToProvisioningNetwork() {
+ synchronized (this) {
+ return mIsConnectedToProvisioningNetwork;
+ }
+ }
+
+ /** {@hide} */
+ @VisibleForTesting
+ public void setIsConnectedToProvisioningNetwork(boolean val) {
+ synchronized (this) {
+ mIsConnectedToProvisioningNetwork = val;
+ }
+ }
+
/**
* Reports the current coarse-grained state of the network.
* @return the coarse-grained state
@@ -405,7 +425,9 @@
append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo).
append(", roaming: ").append(mIsRoaming).
append(", failover: ").append(mIsFailover).
- append(", isAvailable: ").append(mIsAvailable);
+ append(", isAvailable: ").append(mIsAvailable).
+ append(", isConnectedToProvisioningNetwork: ").
+ append(mIsConnectedToProvisioningNetwork);
return builder.toString();
}
}
@@ -433,6 +455,7 @@
dest.writeInt(mIsFailover ? 1 : 0);
dest.writeInt(mIsAvailable ? 1 : 0);
dest.writeInt(mIsRoaming ? 1 : 0);
+ dest.writeInt(mIsConnectedToProvisioningNetwork ? 1 : 0);
dest.writeString(mReason);
dest.writeString(mExtraInfo);
}
@@ -455,6 +478,7 @@
netInfo.mIsFailover = in.readInt() != 0;
netInfo.mIsAvailable = in.readInt() != 0;
netInfo.mIsRoaming = in.readInt() != 0;
+ netInfo.mIsConnectedToProvisioningNetwork = in.readInt() != 0;
netInfo.mReason = in.readString();
netInfo.mExtraInfo = in.readString();
return netInfo;
diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java
index cf77a1c..9ed7533 100644
--- a/core/java/android/net/NetworkStateTracker.java
+++ b/core/java/android/net/NetworkStateTracker.java
@@ -144,6 +144,11 @@
public void captivePortalCheckComplete();
/**
+ * Captive portal check has completed
+ */
+ public void captivePortalCheckCompleted(boolean isCaptive);
+
+ /**
* Turn the wireless radio off for a network.
* @param turnOn {@code true} to turn the radio on, {@code false}
*/
diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java
index 2a2f7cf..0fe5cc4 100644
--- a/core/java/android/net/SSLCertificateSocketFactory.java
+++ b/core/java/android/net/SSLCertificateSocketFactory.java
@@ -81,9 +81,6 @@
}
};
- private static final HostnameVerifier HOSTNAME_VERIFIER =
- HttpsURLConnection.getDefaultHostnameVerifier();
-
private SSLSocketFactory mInsecureFactory = null;
private SSLSocketFactory mSecureFactory = null;
private TrustManager[] mTrustManagers = null;
@@ -195,7 +192,7 @@
if (session == null) {
throw new SSLException("Cannot verify SSL socket without session");
}
- if (!HOSTNAME_VERIFIER.verify(hostname, session)) {
+ if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) {
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + hostname);
}
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 3c8187e..8b28e21 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4937,6 +4937,13 @@
"data_stall_alarm_aggressive_delay_in_ms";
/**
+ * The number of milliseconds to allow the provisioning apn to remain active
+ * @hide
+ */
+ public static final String PROVISIONING_APN_ALARM_DELAY_IN_MS =
+ "provisioning_apn_alarm_delay_in_ms";
+
+ /**
* The interval in milliseconds at which to check gprs registration
* after the first registration mismatch of gprs and voice service,
* to detect possible data network registration problems.
diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl
index a9da863..cfd9cc7 100644
--- a/core/java/com/android/internal/app/IAppOpsService.aidl
+++ b/core/java/com/android/internal/app/IAppOpsService.aidl
@@ -33,4 +33,5 @@
List<AppOpsManager.PackageOps> getPackagesForOps(in int[] ops);
List<AppOpsManager.PackageOps> getOpsForPackage(int uid, String packageName, in int[] ops);
void setMode(int code, int uid, String packageName, int mode);
+ void resetAllModes();
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 4bff536..c23012a 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -151,6 +151,7 @@
<protected-broadcast android:name="android.intent.action.DREAMING_STARTED" />
<protected-broadcast android:name="android.intent.action.DREAMING_STOPPED" />
<protected-broadcast android:name="android.intent.action.ANY_DATA_STATE" />
+ <protected-broadcast android:name="android.intent.action.DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN" />
<protected-broadcast android:name="com.android.server.WifiManager.action.START_SCAN" />
<protected-broadcast android:name="com.android.server.WifiManager.action.DELAYED_DRIVER_STOP" />
@@ -181,6 +182,9 @@
<protected-broadcast android:name="android.intent.action.ACTION_IDLE_MAINTENANCE_START" />
<protected-broadcast android:name="android.intent.action.ACTION_IDLE_MAINTENANCE_END" />
+ <protected-broadcast
+ android:name="com.android.server.connectivityservice.CONNECTED_TO_PROVISIONING_NETWORK_ACTION" />
+
<!-- ====================================== -->
<!-- Permissions for things that cost money -->
<!-- ====================================== -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 4572f5b..707590b 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -772,6 +772,10 @@
<!-- IP address of the dns server to use if nobody else suggests one -->
<string name="config_default_dns_server" translatable="false">8.8.8.8</string>
+ <!-- The default mobile provisioning apn. Empty by default, maybe overridden by
+ an mcc/mnc specific config.xml -->
+ <string name="mobile_provisioning_apn" translatable="false"></string>
+
<!-- The default mobile provisioning url. Empty by default, maybe overridden by
an mcc/mnc specific config.xml -->
<string name="mobile_provisioning_url" translatable="false"></string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index b885d56..a67c2af 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -703,6 +703,7 @@
<java-symbol type="string" name="preposition_for_time" />
<java-symbol type="string" name="progress_erasing" />
<java-symbol type="string" name="progress_unmounting" />
+ <java-symbol type="string" name="mobile_provisioning_apn" />
<java-symbol type="string" name="mobile_provisioning_url" />
<java-symbol type="string" name="mobile_redirected_provisioning_url" />
<java-symbol type="string" name="reboot_safemode_confirm" />
diff --git a/docs/html/google/gcm/adv.jd b/docs/html/google/gcm/adv.jd
index 567b12c..1360624 100644
--- a/docs/html/google/gcm/adv.jd
+++ b/docs/html/google/gcm/adv.jd
@@ -22,12 +22,7 @@
</ol>
</li>
<li><a href="#retry">Automatic Retry Using Exponential Back-Off</a></li>
-<li><a href="#unreg">Unregistration</a>
- <ol>
- <li><a href="#unreg-why">Why you should rarely unregister</a></li>
- <li><a href="#unreg-how">How unregistration works</a></li>
- </ol>
-</li>
+<li><a href="#unreg">How Unregistration Works</a></li>
<li><a href="#collapsible">Send-to-Sync vs. Messages with Payload</a>
<ol>
<li><a href="#s2s">Send-to-sync messages</a></li>
@@ -36,8 +31,7 @@
</ol>
</li>
<li><a href="#ttl">Setting an Expiration Date for a Message</a> </li>
-<li><a href="#throttling"></a><a href="#multi-senders">Receiving Messages from
-Multiple Senders</a></li>
+<li><a href="#throttling"></a><a href="#multi-senders">Receiving Messages from Multiple Senders</a></li>
</ol>
</div>
@@ -48,56 +42,17 @@
<h2 id="msg-lifetime">Lifetime of a Message</h2>
-<p>When a 3rd-party server posts a message to GCM and receives a message ID back,
-it does not mean that the message was already delivered to the device. Rather, it
-means that it was accepted for delivery. What happens to the message after it is
-accepted depends on many factors.</p>
-
-<p>In the best-case scenario, if the device is connected to GCM, the screen is on,
-and there are no throttling restrictions (see <a href="#throttling">Throttling</a>),
-the message will be delivered right away.</p>
-
+<p>When a 3rd-party server posts a message to GCM and receives a message ID back, it does not mean that the message was already delivered to the device. Rather, it means that it was accepted for delivery. What happens to the message after it is accepted depends on many factors.</p>
+<p>In the best-case scenario, if the device is connected to GCM, the screen is on, and there are no throttling restrictions (see <a href="#throttling">Throttling</a>), the message will be delivered right away.</p>
<p>If the device is connected but idle, the message will still be
-delivered right away unless the <code>delay_while_idle</code> flag is set to true.
-Otherwise, it will be stored in the GCM servers until the device is awake. And
-that's where the <code>collapse_key</code> flag plays a role: if there is already
-a message with the same collapse key (and registration ID) stored and waiting for
-delivery, the old message will be discarded and the new message will take its place
-(that is, the old message will be collapsed by the new one). However, if the collapse
-key is not set, both the new and old messages are stored for future delivery.
-Collapsible messages are also called <a href="#s2s">send-to-sync messages</a>.</p>
+delivered right away unless the <code>delay_while_idle</code> flag is set to true. Otherwise, it will be stored in the GCM servers until the device is awake. And that's where the <code>collapse_key</code> flag plays a role: if there is already a message with the same collapse key (and registration ID) stored and waiting for delivery, the old message will be discarded and the new message will take its place (that is, the old message will be collapsed by the new one). However, if the collapse key is not set, both the new and old messages are stored for future delivery.</p>
-<p class="note"><strong>Note:</strong> There is a limit on how many messages can
-be stored without collapsing. That limit is currently 100. If the limit is reached,
-all stored messages are discarded. Then when the device is back online, it receives
-a special message indicating that the limit was reached. The application can then
-handle the situation properly, typically by requesting a full sync.
-<br><br>
-Likewise, there is a limit on how many <code>collapse_key</code>s you can have for
-a particular device. GCM allows a maximum of 4 different collapse keys to be used
-by the GCM server per device
-any given time. In other words, the GCM server can simultaneously store 4 different
-send-to-sync messages, each with a different collapse key. If you exceed this number
-GCM will only keep 4 collapse keys, with no guarantees about which ones they will be.
-See <a href="#s2s">Send-to-sync messages</a> for more information.
-</p>
+<p class="note"><strong>Note:</strong> There is a limit on how many messages can be stored without collapsing. That limit is currently 100. If the limit is reached, all stored messages are discarded. Then when the device is back online, it receives a special message indicating that the limit was reached. The application can then handle the situation properly, typically by requesting a full sync.</p>
-<p>If the device is not connected to GCM, the message will be stored until a
-connection is established (again respecting the collapse key rules). When a connection
-is established, GCM will deliver all pending messages to the device, regardless of
-the <code>delay_while_idle</code> flag. If the device never gets connected again
-(for instance, if it was factory reset), the message will eventually time out and
-be discarded from GCM storage. The default timeout is 4 weeks, unless the
-<code>time_to_live</code> flag is set.</p>
+<p>If the device is not connected to GCM, the message will be stored until a connection is established (again respecting the collapse key rules). When a connection is established, GCM will deliver all pending messages to the device, regardless of the <code>delay_while_idle</code> flag. If the device never gets connected again (for instance, if it was factory reset), the message will eventually time out and be discarded from GCM storage. The default timeout is 4 weeks, unless the <code>time_to_live</code> flag is set.</p>
-<p>Finally, when GCM attempts to deliver a message to the device and the
-application was uninstalled, GCM will discard that message right away and
-invalidate the registration ID. Future attempts to send a message to that device
-will get a <code>NotRegistered</code> error. See <a href="#unreg">
-How Unregistration Works</a> for more information.</p>
-<p>Although is not possible to track the status of each individual message, the
-Google APIs Console stats are broken down by messages sent to device, messages
-collapsed, and messages waiting for delivery.</p>
+<p>Finally, when GCM attempts to deliver a message to the device and the application was uninstalled, GCM will discard that message right away and invalidate the registration ID. Future attempts to send a message to that device will get a <code>NotRegistered</code> error. See <a href="#unreg">How Unregistration Works</a> for more information.</p>
+<p>Although is not possible to track the status of each individual message, the Google APIs Console stats are broken down by messages sent to device, messages collapsed, and messages waiting for delivery.</p>
<h2 id="throttling">Throttling</h2>
<p>To prevent abuse (such as sending a flood of messages to a device) and
@@ -119,112 +74,107 @@
efficiency reasons.</p>
<h2 id="reg-state">Keeping the Registration State in Sync</h2>
-<p>Whenever the application registers as described in
-<a href="{@docRoot}google/gcm/client.html">Implementing GCM Client</a>,
-it should save the registration ID for future use, pass it to the
-3rd-party server to complete the registration, and keep track of
-whether the server completed the registration. If the server fails
-to complete the registration, it should try again or unregister from GCM.</p>
-
+<p>Whenever the application receives a <code>com.google.android.c2dm.intent.REGISTRATION</code> intent with a <code>registration_id</code> extra, it should save the ID for future use, pass it to the 3rd-party server to complete the registration, and keep track of whether the server completed the registration. If the server fails to complete the registration, it should try again or unregister from GCM.</p>
<p>There are also two other scenarios that require special care:</p>
<ul>
<li>Application update</li>
<li>Backup and restore
</li>
</ul>
-<p>When an application is updated, it should invalidate its existing registration
-ID, as it is not guaranteed to work with the new version. Because there is no
-lifecycle method called when the application is updated, the best way to achieve
-this validation is by storing the current application version when a registration
-ID is stored. Then when the application is started, compare the stored value with
-the current application version. If they do not match, invalidate the stored data
-and start the registration process again.</p>
+<p>When an application is updated, it should invalidate its existing registration ID, as it is not guaranteed to work with the new version. Because there is no lifecycle method called when the application is updated, the best way to achieve this validation is by storing the current application version when a registration ID is stored. Then when the application is started, compare the stored value with the current application version. If they do not match, invalidate the stored data and start the registration process again.</p>
-<p>Similarly, you should not save the registration ID when an application is
-backed up. This is because the registration ID could become invalid by the time
-the application is restored, which would put the application in an invalid state
-(that is, the application thinks it is registered, but the server and GCM do not
-store that registration ID anymore—thus the application will not get more
-messages).</p>
+<p>Similarly, you should not save the registration ID when an application is backed up. This is because the registration ID could become invalid by the time the application is restored, which would put the application in an invalid state (that is, the application thinks it is registered, but the server and GCM do not store that registration ID anymore—thus the application will not get more messages).</p>
<h3 id="canonical">Canonical IDs</h3>
-<p>On the server side, as long as the application is behaving well, everything
-should work normally. However, if a bug in the application triggers multiple
-registrations for the same device, it can be hard to reconcile state and you might
-end up with duplicate messages.</p>
-<p>GCM provides a facility called "canonical registration IDs" to easily
-recover from these situations. A canonical registration ID is defined to be the ID
-of the last registration requested by your application. This is the ID that the
-server should use when sending messages to the device.</p>
-<p>If later on you try to send a message using a different registration ID, GCM
-will process the request as usual, but it will include the canonical registration
-ID in the <code>registration_id</code> field of the response. Make sure to replace
-the registration ID stored in your server with this canonical ID, as eventually
-the ID you're using will stop working.</p>
+<p>On the server side, as long as the application is behaving well, everything should work normally. However, if a bug in the application triggers multiple registrations for the same device, it can be hard to reconcile state and you might end up with duplicate messages.</p>
+<p>GCM provides a facility called "canonical registration IDs" to easily recover from these situations. A canonical registration ID is defined to be the ID of the last registration requested by your application. This is the ID that the server should use when sending messages to the device.</p>
+<p>If later on you try to send a message using a different registration ID, GCM will process the request as usual, but it will include the canonical registration ID in the <code>registration_id</code> field of the response. Make sure to replace the registration ID stored in your server with this canonical ID, as eventually the ID you're using will stop working.</p>
<h2 id="retry">Automatic Retry Using Exponential Back-Off</h2>
-<p>When registration or unregistration fails, the app should retry the failed operation.</p>
-<p>In the simplest case, if your application attempts to register and GCM is not a
-fundamental part of the application, the application could simply ignore the error
-and try to register again the next time it starts. Otherwise, it should retry the
-previous operation using exponential back-off. In exponential back-off, each time
-there is a failure, it should wait twice the previous amount of time before trying
-again. If the register (or unregister) operation was synchronous, it could be retried
-in a simple loop. However, since it is asynchronous, the best approach is to schedule
-a {@link android.app.PendingIntent} to retry the operation.
+<p>When the application receives a <code>com.google.android.c2dm.intent.REGISTRATION</code> intent with the <code>error</code> extra set as <code>SERVICE_NOT_AVAILABLE</code>, it should retry the failed operation (register or unregister).</p>
+<p>In the simplest case, if your application just calls <code>register</code> and GCM is not a fundamental part of the application, the application could simply ignore the error and try to register again the next time it starts. Otherwise, it should retry the previous operation using exponential back-off. In exponential back-off, each time there is a failure, it should wait twice the previous amount of time before trying again. If the register (or unregister) operation was synchronous, it could be retried in a simple loop. However, since it is asynchronous, the best approach is to schedule a pending intent to retry the operation. The following steps describe how to implement this in the <code>MyIntentService</code> example used above:</p>
+<ol>
+ <li> Create a random token to verify the origin of the retry intent:
-<h2 id="unreg">Unregistration</h2>
+<pre class="prettyprint pretty-java">private static final String TOKEN =
+ Long.toBinaryString(new Random().nextLong());
+</pre>
-<p>This section explains when you should unregister in GCM and what happens
-when you do.</p>
+ <li> Change the <code>handleRegistration()</code> method so it creates the pending intent when appropriate:</li>
-<h3 id="unreg-why">Why you should rarely unregister</h3>
+<pre class="prettyprint pretty-java">...
+if (error != null) {
+ if ("SERVICE_NOT_AVAILABLE".equals(error)) {
+ long backoffTimeMs = // get back-off time from shared preferences
+ long nextAttempt = SystemClock.elapsedRealtime() + backoffTimeMs;
+ Intent retryIntent = new Intent("com.example.gcm.intent.RETRY");
+ retryIntent.putExtra("token", TOKEN);
+ PendingIntent retryPendingIntent =
+ PendingIntent.getBroadcast(context, 0, retryIntent, 0);
+ AlarmManager am = (AlarmManager)
+ context.getSystemService(Context.ALARM_SERVICE);
+ am.set(AlarmManager.ELAPSED_REALTIME, nextAttempt, retryPendingIntent);
+ backoffTimeMs *= 2; // Next retry should wait longer.
+ // update back-off time on shared preferences
+ } else {
+ // Unrecoverable error, log it
+ Log.i(TAG, "Received error: " + error);
+}
+...</pre>
+<p> The back-off time is stored in a shared preference. This ensures that it is persistent across multiple activity launches. The name of the intent does not matter, as long as the same intent is used in the following steps.</p></li>
-<p>A registration ID (regID) represents a particular Android application running
-on a particular device. You should only need to unregister in rare cases, such as
-if you want an app to stop receiving messages, or if you suspect that the regID has
-been compromised. In general, though, once an app has a regID, you shouldn't need
-to change it.</p>
+ <li> Change the <code>onHandleIntent()</code> method adding an <code>else if</code> case for the retry intent:</li>
-<p>In particular, you should never unregister your app as a mechanism for
-logout or for switching between users, for the following reasons:</p>
+<pre class="prettyprint pretty-java">...
+} else if (action.equals("com.example.gcm.intent.RETRY")) {
+ String token = intent.getStringExtra("token");
+ // make sure intent was generated by this class, not by a malicious app
+ if (TOKEN.equals(token)) {
+ String registrationId = // get from shared properties
+ if (registrationId != null) {
+ // last operation was attempt to unregister; send UNREGISTER intent again
+ } else {
+ // last operation was attempt to register; send REGISTER intent again
+ }
+}
+...</pre>
-<ul>
- <li>A regID maps an app to a device. It isn't associated with a particular
- logged in user. If you unregister and then re-register, GCM may return the same
- ID or a different ID—there's no guarantee either way.</li>
+ <li> Create a new instance of <code>MyReceiver</code> in your activity:</li>
- <li>Unregistration may take up to 5 minutes to propagate.</li>
- <li>After unregistration, re-registration may again take up to 5 minutes to
-propagate. During this time messages may be rejected due to the state of being
-unregistered, and after all this, messages may still go to the wrong user.</li>
-</ul>
+<pre class="prettyprint pretty-java">private final MyBroadcastReceiver mRetryReceiver = new MyBroadcastReceiver();
+</pre>
+ <li>In the activity's <code>onCreate()</code> method, register the new instance to receive the <code>com.example.gcm.intent.RETRY</code> intent:
+ <pre class="prettyprint pretty-java">...
+IntentFilter filter = new IntentFilter("com.example.gcm.intent.RETRY");
+filter.addCategory(getPackageName());
+registerReceiver(mRetryReceiver, filter);
+...</pre>
-<p>The solution is to manage your own mapping between users, the regID, and
-individual messages:</p>
+<p class="note"><strong>Note:</strong> You must dynamically create a new instance of the broadcast receiver since the one defined by the manifest can only receive intents with the <code>com.google.android.c2dm.permission.SEND</code> permission. The permission <code>com.google.android.c2dm.permission.SEND</code> is a system permission and as such it cannot be granted to a regular application.</p>
-<ul>
- <li>Your app server should maintain a mapping between the current user
-and the regID. This should include information about which user is supposed to
-receive a particular message.</li>
- <li>The app running on the device should check to ensure that messages it
-receives match the logged in user.</li>
-</ul>
+</li>
+ <li>In the activity's <code>onDestroy()</code> method, unregister the broadcast receiver:</li>
-<h3 id="unreg-how">How unregistration works</h3>
-
-<p>An application can be automatically unregistered after it is uninstalled from
-the device. However, this process does not happens right away, as Android does not
-provide an uninstall callback. What happens in this scenario is as follows:</p>
+<pre class="prettyprint pretty-java">unregisterReceiver(mRetryReceiver);</pre>
+</ol>
+<h2 id="unreg">How Unregistration Works</h2>
+<p>There are two ways to unregister a device from GCM: manually and automatically.</p>
+<p>An Android application can manually unregister itself by issuing a <code>com.google.android.c2dm.intent.UNREGISTER</code> intent, which is useful when the application offers a logoff feature (so it can unregister on logoff and register again on logon). See the <a href="gcm.html#unregistering">Architectural Overview</a> for more discussion of this topic. This is the sequence of events when an application unregisters itself:</p>
+<ol>
+ <li> The application issues a <code>com.google.android.c2dm.intent.UNREGISTER</code> intent, passing the package name as an extra.</li>
+ <li>When the GCM server is done with the unregistration, it sends a <code>com.google.android.c2dm.intent.REGISTRATION</code> intent with the <code>unregistered</code> extra set.</li>
+ <li>The application then must contact the 3rd-party server so it can remove the registration ID.</li>
+ <li>The application should also clear its registration ID.
+ </li>
+</ol>
+<p>An application can be automatically unregistered after it is uninstalled from the device. However, this process does not happens right away, as Android does not provide an uninstall callback. What happens in this scenario is as follows:</p>
<ol>
<li>The end user uninstalls the application.</li>
<li>The 3rd-party server sends a message to GCM server.</li>
<li>The GCM server sends the message to the device.</li>
- <li>The GCM client receives the message and queries Package Manager about
-whether there are broadcast receivers configured to receive it, which returns
-<code>false</code>.
+ <li>The GCM client receives the message and queries Package Manager about whether there are broadcast receivers configured to receive it, which returns <code>false</code>.
</li>
<li>The GCM client informs the GCM server that the application was uninstalled.</li>
<li>The GCM server marks the registration ID for deletion.</li>
@@ -234,16 +184,9 @@
</li>
</ol>
-<p class ="note"><strong>Note:</strong> The GCM client is the Google Cloud
-Messaging framework present on the device.</p>
+<p class ="note"><strong>Note:</strong> The GCM client is the Google Cloud Messaging framework present on the device.</p>
-<p>Note that it might take a while for the registration ID be completely removed
-from GCM. Thus it is possible that messages sent during step 7 above gets a valid
-message ID as response, even though the message will not be delivered to the device.
-Eventually, the registration ID will be removed and the server will get a
-<code>NotRegistered</code> error, without any further action being required from
-the 3rd-party server (this scenario happens frequently while an application is
-being developed and tested).</p>
+<p>Note that it might take a while for the registration ID be completely removed from GCM. Thus it is possible that messages sent during step 7 above gets a valid message ID as response, even though the message will not be delivered to the device. Eventually, the registration ID will be removed and the server will get a <code>NotRegistered</code> error, without any further action being required from the 3rd-party server (this scenario happens frequently while an application is being developed and tested).</p>
<h2 id="collapsible">Send-to-Sync vs. Messages with Payload</h2>
@@ -253,45 +196,17 @@
<li>By default, it is stored by GCM for 4 weeks.</li>
</ul>
-<p>But despite these similarities, messages can behave very differently depending
-on their particular settings. One major distinction between messages is whether
-they are collapsed (where each new message replaces the preceding message) or not
-collapsed (where each individual message is delivered). Every message sent in GCM
-is either a "send-to-sync" (collapsible) message or a "message with
-payload" (non-collapsible message). These concepts are described in more
-detail in the following sections.</p>
+<p>But despite these similarities, messages can behave very differently depending on their particular settings. One major distinction between messages is whether they are collapsed (where each new message replaces the preceding message) or not collapsed (where each individual message is delivered). Every message sent in GCM is either a "send-to-sync" (collapsible) message or a "message with payload" (non-collapsible message). These concepts are described in more detail in the following sections.</p>
<h3 id="s2s"><strong>Send-to-sync messages</strong></h3>
-<p>A send-to-sync (collapsible) message is often a "tickle" that tells
-a mobile application to sync data from the server. For example, suppose you have
-an email application. When a user receives new email on the server, the server
-pings the mobile application with a "New mail" message. This tells the
-application to sync to the server to pick up the new email. The server might send
-this message multiple times as new mail continues to accumulate, before the application
-has had a chance to sync. But if the user has received 25 new emails, there's no
-need to preserve every "New mail" message. One is sufficient. Another
-example would be a sports application that updates users with the latest score.
-Only the most recent message is relevant, so it makes sense to have each new
-message replace the preceding message. </p>
+<p>A send-to-sync (collapsible) message is often a "tickle" that tells a mobile application to sync data from the server. For example, suppose you have an email application. When a user receives new email on the server, the server pings the mobile application with a "New mail" message. This tells the application to sync to the server to pick up the new email. The server might send this message multiple times as new mail continues to accumulate, before the application has had a chance to sync. But if the user has received 25 new emails, there's no need to preserve every "New mail" message. One is sufficient. Another example would be a sports application that updates users with the latest score. Only the most recent message is relevant, so it makes sense to have each new message replace the preceding message. </p>
-<p>The email and sports applications are cases where you would probably use the
-GCM <code>collapse_key</code> parameter. A <em>collapse key</em> is an arbitrary
-string that is used to collapse a group of like messages when the device is offline,
-so that only the most recent message gets sent to the client. For example,
-"New mail," "Updates available," and so on</p>
-<p>GCM allows a maximum of 4 different collapse keys to be used by the GCM server
-at any given time. In other words, the GCM server can simultaneously store 4
-different send-to-sync messages per device, each with a different collapse key.
-For example, Device A can have A1, A2, A3, and A4. Device B can have B1, B2, B3,
-and B4, and so on. If you exceed this number GCM will only keep 4 collapse keys, with no
-guarantees about which ones they will be.</p>
+<p>The email and sports applications are cases where you would probably use the GCM <code>collapse_key</code> parameter. A <em>collapse key</em> is an arbitrary string that is used to collapse a group of like messages when the device is offline, so that only the most recent message gets sent to the client. For example, "New mail," "Updates available," and so on</p>
+<p>GCM allows a maximum of 4 different collapse keys to be used by the GCM server at any given time. In other words, the GCM server can simultaneously store 4 different send-to-sync messages, each with a different collapse key. If you exceed this number GCM will only keep 4 collapse keys, with no guarantees about which ones they will be.</p>
<h3 id="payload">Messages with payload</h3>
-<p>Unlike a send-to-sync message, every "message with payload"
-(non-collapsible message) is delivered. The payload the message contains can be
-up to 4kb. For example, here is a JSON-formatted message in an IM application in
-which spectators are discussing a sporting event:</p>
+<p>Unlike a send-to-sync message, every "message with payload" (non-collapsible message) is delivered. The payload the message contains can be up to 4kb. For example, here is a JSON-formatted message in an IM application in which spectators are discussing a sporting event:</p>
<pre class="prettyprint pretty-json">{
"registration_id" : "APA91bHun4MxP5egoKMwt2KZFBaFUH-1RYqx...",
@@ -302,42 +217,19 @@
},
}</pre>
-<p>A "message with payload" is not simply a "ping" to the
-mobile application to contact the server to fetch data. In the aforementioned IM
-application, for example, you would want to deliver every message, because every
-message has different content. To specify a non-collapsible message, you simply
-omit the <code>collapse_key</code> parameter. Thus GCM will send each message
-individually. Note that the order of delivery is not guaranteed.</p>
-
-<p>GCM will store up to 100 non-collapsible messages. After that, all messages
-are discarded from GCM, and a new message is created that tells the client how
-far behind it is. The message is delivered through a regular
-<code>com.google.android.c2dm.intent.RECEIVE</code> intent, with the following
-extras:</p>
+<p>A "message with payload" is not simply a "ping" to the mobile application to contact the server to fetch data. In the aforementioned IM application, for example, you would want to deliver every message, because every message has different content. To specify a non-collapsible message, you simply omit the <code>collapse_key</code> parameter. Thus GCM will send each message individually. Note that the order of delivery is not guaranteed.</p>
+<p>GCM will store up to 100 non-collapsible messages. After that, all messages are discarded from GCM, and a new message is created that tells the client how far behind it is. The message is delivered through a regular <code>com.google.android.c2dm.intent.RECEIVE</code> intent, with the following extras:</p>
<ul>
- <li> <code>message_type</code>—The value is always the string
-"deleted_messages".</li>
- <li><code>total_deleted</code>—The value is a string with the number of
-deleted messages.</li>
+ <li> <code>message_type</code>—The value is always the string "deleted_messages".</li>
+ <li><code>total_deleted</code>—The value is a string with the number of deleted messages.</li>
</ul>
-<p>The application should respond by syncing with the server to recover the
-discarded messages. </p>
+<p>The application should respond by syncing with the server to recover the discarded messages. </p>
<h3 id="which">Which should I use?</h3>
- <p>If your application does not need to use non-collapsible messages, collapsible
-messages are a better choice from a performance standpoint, because they put less
-of a burden on the device battery. However, if you use collapsible messages, remember that
-<strong>GCM only allows a maximum of 4 different collapse keys to be used by the GCM server
-per device at any given time</strong>. You must not exceed this number, or it could cause
-unpredictable consequences.</p>
+ <p>If your application does not need to use non-collapsible messages, collapsible messages are a better choice from a performance standpoint, because they put less of a burden on the device battery.</p>
<h2 dir="ltr" id="ttl">Setting an Expiration Date for a Message</h2>
-<p>The Time to Live (TTL) feature lets the sender specify the maximum lifespan
-of a message using the <code>time_to_live</code> parameter in the send request.
-The value of this parameter must be a duration from 0 to 2,419,200 seconds, and
-it corresponds to the maximum period of time for which GCM will store and try to
-deliver the message. Requests that don't contain this field default to the maximum
-period of 4 weeks.</p>
+<p>The Time to Live (TTL) feature lets the sender specify the maximum lifespan of a message using the <code>time_to_live</code> parameter in the send request. The value of this parameter must be a duration from 0 to 2,419,200 seconds, and it corresponds to the maximum period of time for which GCM will store and try to deliver the message. Requests that don't contain this field default to the maximum period of 4 weeks.</p>
<p>Here are some possible uses for this feature:</p>
<ul>
<li>Video chat incoming calls</li>
@@ -345,29 +237,9 @@
<li>Calendar events</li>
</ul>
<h3 id="bg">Background </h3>
-<p>GCM will usually deliver messages immediately after they are sent. However,
-this might not always be possible. For example, the device could be turned off,
-offline, or otherwise unavailable. In other cases, the sender itself might request
-that messages not be delivered until the device becomes active by using the
-<code>delay_while_idle</code> flag. Finally, GCM might intentionally delay messages
-to prevent an application from consuming excessive resources and negatively
-impacting battery life.</p>
-
-<p>When this happens, GCM will store the message and deliver it as soon as it's
-feasible. While this is fine in most cases, there are some applications for which
-a late message might as well never be delivered. For example, if the message is
-an incoming call or video chat notification, it will only be meaningful for a
-small period of time before the call is terminated. Or if the message is an
-invitation to an event, it will be useless if received after the event has ended.</p>
-
-<p>Another advantage of specifying the expiration date for a message is that GCM
-will never throttle messages with a <code>time_to_live</code> value of 0 seconds.
-In other words, GCM will guarantee best effort for messages that must be delivered
-"now or never." Keep in mind that a <code>time_to_live</code> value of
-0 means messages that can't be delivered immediately will be discarded. However,
-because such messages are never stored, this provides the best latency for
-sending notifications.</p>
-
+<p>GCM will usually deliver messages immediately after they are sent. However, this might not always be possible. For example, the device could be turned off, offline, or otherwise unavailable. In other cases, the sender itself might request that messages not be delivered until the device becomes active by using the <code>delay_while_idle</code> flag. Finally, GCM might intentionally delay messages to prevent an application from consuming excessive resources and negatively impacting battery life.</p>
+<p>When this happens, GCM will store the message and deliver it as soon as it's feasible. While this is fine in most cases, there are some applications for which a late message might as well never be delivered. For example, if the message is an incoming call or video chat notification, it will only be meaningful for a small period of time before the call is terminated. Or if the message is an invitation to an event, it will be useless if received after the event has ended.</p>
+<p>Another advantage of specifying the expiration date for a message is that GCM will never throttle messages with a <code>time_to_live</code> value of 0 seconds. In other words, GCM will guarantee best effort for messages that must be delivered "now or never." Keep in mind that a <code>time_to_live</code> value of 0 means messages that can't be delivered immediately will be discarded. However, because such messages are never stored, this provides the best latency for sending notifications.</p>
<p>Here is an example of a JSON-formatted request that includes TTL:</p>
<pre class="prettyprint pretty-json">
{
@@ -384,23 +256,9 @@
<h2 id="multi-senders">Receiving Messages from Multiple Senders</h2>
-
-<p>GCM allows multiple parties to send messages to the same application. For
-example, suppose your application is an articles aggregator with multiple
-contributors, and you want each of them to be able to send a message when they
-publish a new article. This message might contain a URL so that the application
-can download the article. Instead of having to centralize all sending activity in
-one location, GCM gives you the ability to let each of these contributors send
-its own messages.</p>
-
-<p>To make this possible, all you need to do is have each sender generate its own
-project number. Then include those IDs in the sender field, separated by commas,
-when requesting a registration. Finally, share the registration ID with your
-partners, and they'll be able to send messages to your application using their
-own authentication keys.</p>
-<p>This code snippet illustrates this feature. Senders are passed as an intent
-extra in a comma-separated list:</p>
-
+<p>GCM allows multiple parties to send messages to the same application. For example, suppose your application is an articles aggregator with multiple contributors, and you want each of them to be able to send a message when they publish a new article. This message might contain a URL so that the application can download the article. Instead of having to centralize all sending activity in one location, GCM gives you the ability to let each of these contributors send its own messages.</p>
+<p>To make this possible, all you need to do is have each sender generate its own project number. Then include those IDs in the sender field, separated by commas, when requesting a registration. Finally, share the registration ID with your partners, and they'll be able to send messages to your application using their own authentication keys.</p>
+<p>This code snippet illustrates this feature. Senders are passed as an intent extra in a comma-separated list:</p>
<pre class="prettyprint pretty-java">Intent intent = new Intent(GCMConstants.INTENT_TO_GCM_REGISTRATION);
intent.setPackage(GSF_PACKAGE);
intent.putExtra(GCMConstants.EXTRA_APPLICATION_PENDING_INTENT,
@@ -411,3 +269,4 @@
</pre>
<p>Note that there is limit of 100 multiple senders.</p>
+
diff --git a/docs/html/google/gcm/ccs.jd b/docs/html/google/gcm/ccs.jd
index 244278e..0cadbd2 100644
--- a/docs/html/google/gcm/ccs.jd
+++ b/docs/html/google/gcm/ccs.jd
@@ -1,96 +1,93 @@
-page.title=GCM Cloud Connection Server (XMPP)
+page.title=GCM Cloud Connection Server
@jd:body
<div id="qv-wrapper">
<div id="qv">
+<h2>Quickview</h2>
+
+<ul>
+<li>Get an introduction to key CCS terms and concepts.</li>
+<li>Learn how to send and receive both upstream and downstream messages in CCS.</li>
+</ul>
+
<h2>In this document</h2>
<ol class="toc">
+ <li><a href="#gcm">CCS vs. GCM HTTP</a> </li>
<li><a href="#usage">How to Use CCS</a>
- <ol class="toc">
- <li><a href="#auth">Authentication</a></li>
- </ol>
- </li>
- <li><a href="#format">Message Format</a>
- <ol class="toc">
- <li><a href="#request">Request format</a></li>
- <li><a href="#response">Response format</a></li>
- </ol>
- </li>
- <li><a href="#upstream">Upstream Messages</a> </li>
- <li><a href="#flow">Flow Control</a> </li>
- <li><a href="#implement">Implementing an XMPP-based App Server</a>
- <ol class="toc">
- <li><a href="#smack">Java sample using the Smack library</a></li>
- <li><a href="#python">Python sample</a></li>
+ <ol>
+ <li><a href="#send_msg">Sending Messages</a></li>
+ <li><a href="#format">Message Format</a></li>
+ <li><a href="#msg_examples">Message Examples</a></li>
</ol>
</li>
+ <li><a href="#flow">Flow Control</a> </li>
</ol>
<h2>See Also</h2>
<ol class="toc">
-<li><a href="{@docRoot}google/play-services/gcm/http.html">HTTP</a></li>
<li><a href="{@docRoot}google/play-services/gcm/gs.html">Getting Started</a></li>
-<li><a href="{@docRoot}google/play-services/gcm/server.html">Implementing GCM Server</a></li>
-<li><a href="{@docRoot}google/play-services/gcm/client.html">Implementing GCM Client</a></li>
-<li><a href="https://services.google.com/fb/forms/gcm/" class="external-link"
-target="_android">CCS and User Notifications Signup Form</a></li>
+<li><a href="https://services.google.com/fb/forms/gcm/" class="external-link" target="_android">CCS and User Notifications Signup Form</a></li>
</ol>
</div>
</div>
-<p class="note"><strong>Note:</strong> To try out this feature, sign up using
-<a href="https://services.google.com/fb/forms/gcm/">this form</a>.</p>
+<p class="note"><strong>Note:</strong> To try out this feature, sign up using <a href="https://services.google.com/fb/forms/gcm/">this form</a>.</p>
-<p>The GCM Cloud Connection Server (CCS) is a connection server based on XMPP.
-CCS allows 3rd-party app servers (which you're
-responsible for implementing) to communicate
-with Android devices by establishing a persistent TCP connection with Google
-servers using the XMPP protocol. This communication is asynchronous and bidirectional.</p>
-<p>You can continue to use the HTTP request mechanism to send messages to GCM
-servers, side-by-side with CCS which uses XMPP. Some of the benefits of CCS include:</p>
+<p>The GCM Cloud Connection Server (CCS) allows third party servers to communicate with Android devices by establishing a persistent TCP connection with Google servers using the XMPP protocol. This communication is asynchronous and bidirectional.</p>
+<p>You can continue to use the HTTP request mechanism to send messages to GCM servers, side-by-side with CCS which uses XMPP. Some of the benefits of CCS include:</p>
<ul>
- <li>The asynchronous nature of XMPP allows you to send more messages with fewer
-resources.</li>
- <li>Communication is bidirectional—not only can the server send messages
-to the device, but the device can send messages back to the server.</li>
-<li>You can send messages back using the same connection used for receiving,
-thereby improving battery life.</li>
+ <li>The asynchronous nature of XMPP allows you to send more messages with fewer resources.</li>
+ <li>Communication is bidirectional—not only can the server send messages to the device, but the device can send messages back to the server.</li>
+<li>You can send messages back using the same connection used for receiving, thereby improving battery life.</li>
</ul>
-<p>The upstream messaging (device-to-cloud) feature of CCS is part of the Google
-Play services platform. Upstream messaging is available through the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a>
-APIs. For examples, see
-<a href="#implement">Implementing an XMPP-based App Server</a>.</p>
+<p>The upstream messaging (device-to-cloud) feature of CCS is part of the Google Play services platform. Upstream messaging is available through the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> APIs. To use upstream messaging and the new streamlined registration process, you must <a href="{@docRoot}google/play-services/setup.html">set up</a> the Google Play services SDK.</p>
-<p class="note"><strong>Note:</strong> See
-<a href="server.html#params">Implementing GCM Server</a> for a list of all the message
-parameters and which connection server(s) supports them.</p>
+<p class="note"><strong>Note:</strong> For an example of an XMPP server, see <a href="server.html#xmpp">GCM Server</a>.
+<h2 id="gcm">CCS vs. GCM HTTP</h2>
+
+<p>CCS messaging differs from GCM HTTP messaging in the following ways:</p>
+<ul>
+ <li>Upstream/Downstream messages
+ <ul>
+ <li>GCM HTTP: Downstream only: cloud-to-device. </li>
+ <li>CCS: Upstream and downstream (device-to-cloud, cloud-to-device). </li>
+ </ul>
+ </li>
+ <li>Asynchronous messaging
+ <ul>
+ <li>GCM HTTP: 3rd-party servers send messages as HTTP POST requests and wait for a response. This mechanism is synchronous and causes the sender to block before sending another message.</li>
+ <li>CCS: 3rd-party servers connect to Google infrastructure using a persistent XMPP connection and send/receive messages to/from all their devices at full line speed. CCS sends acknowledgements or failure notifications (in the form of special ACK and NACK JSON-encoded XMPP messages) asynchronously.</li>
+ </ul>
+ </li>
+
+ <li>JSON
+ <ul>
+ <li>GCM HTTP: JSON messages sent as HTTP POST.</li>
+ <li>CCS: JSON messages encapsulated in XMPP messages.</li>
+ </ul>
+ </li>
+</ul>
+<p>This document describes how to use CCS. For general concepts and information on how to use GCM HTTP, see the <a href="gcm.html">GCM Architectural Overview</a>.</p>
<h2 id="usage">How to Use CCS</h2>
-<p>GCM Cloud Connection Server (CCS) is an XMPP endpoint, running on
-{@code http://gcm.googleapis.com} port 5235.</p>
+<p>GCM Cloud Connection Server (CCS) is an XMPP endpoint, running on {@code http://gcm.googleapis.com} port 5235.</p>
-<p>CCS requires a Transport Layer Security (TLS) connection. That means the XMPP
-client must initiate a TLS connection.
-For example in Java, you would call {@code setSocketFactory(SSLSocketFactory)}.</p>
+<p>CCS requires a Transport Layer Security (TLS) connection. That means the XMPP client must initiate a TLS connection.
+For example in smack, you would call {@code setSocketFactory(SSLSocketFactory)}, similar to “old style SSL” XMPP connections and https.</p>
-<p>CCS requires a SASL PLAIN authentication mechanism using
-{@code <your_GCM_Sender_Id>@gcm.googleapis.com} (GCM sender ID) and the
-API key as the password, where the sender ID and API key are the same as described
-in <a href="gs.html">Getting Started</a>.</p>
+<p>CCS requires a SASL PLAIN authentication mechanism using {@code <your_GCM_Sender_Id>@gcm.googleapis.com} (GCM sender ID) and the API key as the password, where the sender ID and API key are the same as described in <a href="gs.html">Getting Started</a>.</p>
<p> You can use most XMPP libraries to interact with CCS.</p>
-<h3 id="auth">Authentication</h3>
+<h3 id="send_msg">Sending messages</h3>
<p>The following snippets illustrate how to perform authentication in CCS.</p>
<h4>Client</h4>
@@ -111,13 +108,13 @@
<h4>Client</h4>
<pre><auth mechanism="PLAIN"
xmlns="urn:ietf:params:xml:ns:xmpp-sasl">MTI2MjAwMzQ3OTMzQHByb2plY3RzLmdjbS5hb
+mRyb2lkLmNvbQAxMjYyMDAzNDc5FzNAcHJvamVjdHMtZ2EtLmFuZHJvaWQuY29tAEFJe
mFTeUIzcmNaTmtmbnFLZEZiOW1oekNCaVlwT1JEQTJKV1d0dw==</auth>
</pre>
-
<h4>Server</h4>
<pre><success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/></pre>
-<h2 id="format">Message Format</h2>
+<h3 id="format">Message Format</h3>
<p>CCS uses normal XMPP <code><message></code> stanzas. The body of the message must be:
</p>
<pre>
@@ -126,42 +123,25 @@
</gcm>
</pre>
-<p>The JSON payload for server-to-device is similar to what the GCM http endpoint
-uses, with these exceptions:</p>
+<p>The JSON payload for server-to-device is similar to what the GCM http endpoint uses, with these exceptions:</p>
<ul>
<li>There is no support for multiple recipients.</li>
<li>{@code to} is used instead of {@code registration_ids}.</li>
- <li>CCS adds the field {@code message_id}, which is required. This ID uniquely
-identifies the message in an XMPP connection. The ACK or NACK from CCS uses the
-{@code message_id} to identify a message sent from 3rd-party app servers to CCS.
-Therefore, it's important that this {@code message_id} not only be unique, but
-always present.</li>
+ <li>CCS adds the field {@code message_id}, which is required. This ID uniquely identifies the message in an XMPP connection. The ACK or NACK from CCS uses the {@code message_id} to identify a message sent from 3rd-party servers to CCS. Therefore, it's important that this {@code message_id} not only be unique, but always present.</li>
-<li>For ACK/NACK messages that are special control messages, you also need to
-include a {@code message_type} field in the JSON message. The value can be either
-'ack' or 'nack'. For example:
+ <li>For ACK/NACK messages that are special control messages, you also need to include a {@code message_type} field in the JSON message. For example:
-<pre>message_type = ('ack');</pre>
+<pre>message_type = ('ack' OR 'nack');</pre>
</li>
</ul>
-<p>For each device message your app server receives from CCS, it needs to send
-an ACK message.
-It never needs to send a NACK message. If you don't send an ACK for a message,
-CCS will just resend it.
+<p>For each message a device sends to the server, you need to send an ACK message. You never need to send a NACK message. If you don't send an ACK for a message, CCS will just resend it.
</p>
-<p>CCS also sends an ACK or NACK for each server-to-device message. If you do not
-receive either, it means that the TCP connection was closed in the middle of the
-operation and your server needs to resend the messages. See
-<a href="#flow">Flow Control</a> for details.
+<p>CCS also sends an ACK or NACK for each server-to-device message. If you do not receive either, it means that the TCP connection was closed in the middle of the operation and your server needs to resend the messages.
</p>
-<p class="note"><strong>Note:</strong> See
-<a href="server.html#params">Implementing GCM Server</a> for a list of all the message
-parameters and which connection server(s) supports them.</p>
+<h3 id="msg_examples">Message Examples</h3>
-<h3 id="request">Request format</h3>
-
-<p>Here is an XMPP stanza containing the JSON message from a 3rd-party app server to CCS:
+<p>Here is an XMPP stanza containing the JSON message from a 3rd-party server to CCS:
</p>
<pre><message id="">
@@ -180,15 +160,7 @@
</message>
</pre>
-<h3 id="response">Response format</h3>
-
-<p>A CCS response can have 3 possible forms. The first one is a regular 'ack'
-message. But when the response contains an error, there are 2
-different forms the message can take, described below.</p>
-
-<h4 id="ack">ACK message</h4>
-
-<p>Here is an XMPP stanza containing the ACK/NACK message from CCS to 3rd-party app server:
+<p>Here is an XMPP stanza containing the ACK/NACK message from CCS to 3rd-party server:
</p>
<pre><message id="">
<gcm xmlns="google:mobile:data">
@@ -199,138 +171,24 @@
}
</gcm>
</message>
-</pre>
-<h4 id="nack">NACK message</h4>
-
-<p>A NACK error is a regular XMPP message in which the {@code message_type} status
-message is "nack". A NACK message contains:</p>
-<ul>
-<li>Nack error code.</li>
-<li>Nack error description.</li>
-</ul>
-
-<p>Below are some examples.</p>
-
-<p>Bad registration:</p>
-<pre><message>
- <data:gcm xmlns:data="google:mobile:data">
- {
- "error":"BAD_REGISTRATION", // error code
- "message_id":"msgId1",
- "from":"PA91bHFOtaQGSwupt5l1og",
- "message_type":"nack"
- }
- </data:gcm>
-</message></pre>
-
-<p>Invalid "time to live":</p>
-
-<pre><message>
- <data:gcm xmlns:data="google:mobile:data">
- {
- "error":"InvalidJson : INVALID_TTL : Invalid value (-1) for \"time_to_live\": must be between 0 and \"2419200\"\n",
- "message_id":"msgId1",
- "from":"APA91bHFOtaQGSwupt5l1og",
- "message_type":"nack"
- }
- </data:gcm>
-</message></pre>
-
-<p>JSON type error:</p>
-
-<pre><message>
- <data:gcm xmlns:data="google:mobile:data">
- {
- "error":"InvalidJson : JSON_TYPE_ERROR : Field \"delay_while_idle\" must be a JSON java.lang.Boolean: not-boolean-user-supplied-value\n",
- "message_id":"msgId1",
- "from":"APA91bHFOtaQGSwupt5l1og",
- "message_type":"nack"
- }
- </data:gcm>
-</message></pre>
-
-
-<p>The following table lists some of the more common NACK error codes.</p>
-
-<p class="table-caption" id="table1">
- <strong>Table 1.</strong> NACK error codes.</p>
-
-<table border="1">
-<tr>
-<th>Error Code</th>
-<th>Description</th>
-</tr>
-<tr>
-<td>{@code BAD_REGISTRATION}</td>
-<td>The device has a registration ID, but it's invalid.</td>
-</tr>
-<tr>
-<td>{@code DEVICE_UNREGISTERED}</td>
-<td>The device is not registered.</td>
-</tr>
-<tr>
-<td>{@code INTERNAL_SERVER_ERROR}</td>
-<td>The server encountered an error while trying to process the request.</td>
-</tr>
-<tr>
-<td>{@code SERVICE_UNAVAILABLE}</td>
-<td>The CCS connection server is temporarily unavailable, try again later
-(using exponential backoff, etc.).</td>
-</tr>
-<tr>
-<td>{@code BAD_ACK}</td>
-<td>The ACK message is improperly formed.</td>
-</tr>
-<tr>
-<td>{@code AUTHENTICATION_FAILED}</td>
-<td>This is a 401 error indicating that there was an error authenticating the sender account.</td>
-</tr>
-<tr>
-<td>{@code INVALID_TTL}</td>
-<td>There was an error in the supplied "time to live" value.</td>
-</tr>
-<tr>
-<td>{@code JSON_TYPE_ERROR}</td>
-<td>There was an error in the supplied JSON data type.</td>
-</tr>
-</table>
-
-<h4 id="stanza">Stanza error</h4>
-
-<p>You can also get a stanza error in certain cases.
-A stanza error contains:</p>
-<ul>
-<li>Stanza error code.</li>
-<li>Stanza error description (free text).</li>
-</ul>
-<p>For example:</p>
-
-<pre><message id="3" type="error" to="123456789@gcm.googleapis.com/ABC">
+<message id="">
<gcm xmlns="google:mobile:data">
- {"random": "text"}
+ {
+ "from":"REGID",
+ "message_id":"m-1366082849205"
+ "error": ERROR_CODE,
+ "message_type":"nack"
+ }
</gcm>
- <error code="400" type="modify">
- <bad-request xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
- <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
- InvalidJson: JSON_PARSING_ERROR : Missing Required Field: message_id\n
- </text>
- </error>
</message>
</pre>
+<h4>Upstream Messages</h4>
-<h2 id="upstream">Upstream Messages</h2>
+<p>Using CCS and the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">GoogleCloudMessaging</a> API, you can send messages from a user's device to the cloud.</p>
-<p>Using CCS and the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a>
-API, you can send messages from a user's device to the cloud.</p>
-
-<p>Here is how you send an upstream message using the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a>
-API. For a complete example, see <a href="client.html">Implementing GCM Client</a>:</p>
+<p>Here is how you send an upstream message using the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">GoogleCloudMessaging</a> API. For a complete example, see <a href="gs.html#gs_example">Getting Started</a>:</p>
<pre>GoogleCloudMessaging gcm = GoogleCloudMessaging.get(context);
String GCM_SENDER_ID = "Your-Sender-ID";
@@ -340,15 +198,12 @@
// Bundle data consists of a key-value pair
data.putString("hello", "world");
// "time to live" parameter
-// This is optional. It specifies a value in seconds up to 4 weeks.
int ttl = [0 seconds, 4 weeks]
gcm.send(GCM_SENDER_ID + "@gcm.googleapis.com", id, ttl, data);
</pre>
-<p>This call generates the necessary XMPP stanza for sending the upstream message.
-The message goes from the app on the device to CCS to the 3rd-party app server.
-The stanza has the following format:</p>
+<p>This call generates the necessary XMPP stanza for sending the upstream message. The message goes from the app on the device to CCS to the 3rd-party server. The stanza has the following format:</p>
<pre><message id="">
<gcm xmlns="google:mobile:data">
@@ -364,8 +219,7 @@
</gcm>
</message></pre>
-<p>Here is the format of the ACK expected by CCS from 3rd-party app servers in
-response to the above message:</p>
+<p>Here is the format of the ACK expected by CCS from 3rd-party servers in response to the above message:</p>
<pre><message id="">
<gcm xmlns="google:mobile:data">
@@ -377,478 +231,13 @@
</gcm>
</message></pre>
+
<h2 id="flow">Flow Control</h2>
-<p>Every message sent to CCS receives either an ACK or a NACK response. Messages
-that haven't received one of these responses are considered pending. If the pending
-message count reaches 1000, the 3rd-party app server should stop sending new messages
-and wait for CCS to acknowledge some of the existing pending messages as illustrated in
-figure 1:</p>
+<p>Every message sent to CCS receives either an ACK or a NACK response. Messages that haven't received one of these responses are considered pending. If the pending message count reaches 1000, the 3rd-party server should stop sending new messages and wait for CCS to acknowledge some of the existing pending messages.</p>
-<img src="{@docRoot}images/gcm/CCS-ack.png">
+<p>Conversely, to avoid overloading the 3rd-party server, CCS will stop sending if there are too many unacknowledged messages. Therefore, the 3rd-party server should "ACK" received messages as soon as possible to maintain a constant flow of incoming messages. The aforementioned pending message limit doesn't apply to these ACKs. Even if the pending message count reaches 1000, the 3rd-party server should continue sending ACKs to avoid blocking delivery of new messages.</p>
-<p class="img-caption">
- <strong>Figure 1.</strong> Message/ack flow.
+<p>ACKs are only valid within the context of one connection. If the connection is closed before a message can be ACKed, the 3rd-party server should wait for CCS to resend the message before ACKing it again.
</p>
-<p>Conversely, to avoid overloading the 3rd-party app server, CCS will stop sending
-if there are too many unacknowledged messages. Therefore, the 3rd-party app server
-should "ACK" upstream messages, received from the client application via CCS, as soon as possible
-to maintain a constant flow of incoming messages. The aforementioned pending message limit doesn't
-apply to these ACKs. Even if the pending message count reaches 1000, the 3rd-party app server
-should continue sending ACKs for messages received from CCS to avoid blocking delivery of new
-upstream messages.</p>
-
-<p>ACKs are only valid within the context of one connection. If the connection is
-closed before a message can be ACKed, the 3rd-party app server should wait for CCS
-to resend the upstream message before ACKing it again. Similarly, all pending messages for which an
-ACK/NACK was not received from CCS before the connection was closed should be sent again.
-</p>
-
-<h2 id="implement">Implementing an XMPP-based App Server</h2>
-
-<p>This section gives examples of implementing an app server that works with CCS.
-Note that a full GCM implementation requires a client-side implementation, in
-addition to the server. For more information, see <a href="client.html">
-Implementing GCM Client</a>.</a>
-
-<h3 id="smack">Java sample using the Smack library</h3>
-
-<p>Here is a sample app server written in Java, using the
-<a href="http://www.igniterealtime.org/projects/smack/">Smack</a> library.</p>
-
-<pre>import org.jivesoftware.smack.ConnectionConfiguration;
-import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
-import org.jivesoftware.smack.ConnectionListener;
-import org.jivesoftware.smack.PacketInterceptor;
-import org.jivesoftware.smack.PacketListener;
-import org.jivesoftware.smack.XMPPConnection;
-import org.jivesoftware.smack.XMPPException;
-import org.jivesoftware.smack.filter.PacketTypeFilter;
-import org.jivesoftware.smack.packet.DefaultPacketExtension;
-import org.jivesoftware.smack.packet.Message;
-import org.jivesoftware.smack.packet.Packet;
-import org.jivesoftware.smack.packet.PacketExtension;
-import org.jivesoftware.smack.provider.PacketExtensionProvider;
-import org.jivesoftware.smack.provider.ProviderManager;
-import org.jivesoftware.smack.util.StringUtils;
-import org.json.simple.JSONValue;
-import org.json.simple.parser.ParseException;
-import org.xmlpull.v1.XmlPullParser;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Random;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.net.ssl.SSLSocketFactory;
-/**
- * Sample Smack implementation of a client for GCM Cloud Connection Server.
- *
- * <p>For illustration purposes only.
- */
-public class SmackCcsClient {
-
- Logger logger = Logger.getLogger("SmackCcsClient");
-
- public static final String GCM_SERVER = "gcm.googleapis.com";
- public static final int GCM_PORT = 5235;
-
- public static final String GCM_ELEMENT_NAME = "gcm";
- public static final String GCM_NAMESPACE = "google:mobile:data";
-
- static Random random = new Random();
- XMPPConnection connection;
- ConnectionConfiguration config;
-
- /**
- * XMPP Packet Extension for GCM Cloud Connection Server.
- */
- class GcmPacketExtension extends DefaultPacketExtension {
- String json;
-
- public GcmPacketExtension(String json) {
- super(GCM_ELEMENT_NAME, GCM_NAMESPACE);
- this.json = json;
- }
-
- public String getJson() {
- return json;
- }
-
- @Override
- public String toXML() {
- return String.format("<%s xmlns=\"%s\">%s</%s>", GCM_ELEMENT_NAME,
- GCM_NAMESPACE, json, GCM_ELEMENT_NAME);
- }
-
- @SuppressWarnings("unused")
- public Packet toPacket() {
- return new Message() {
- // Must override toXML() because it includes a <body>
- @Override
- public String toXML() {
-
- StringBuilder buf = new StringBuilder();
- buf.append("<message");
- if (getXmlns() != null) {
- buf.append(" xmlns=\"").append(getXmlns()).append("\"");
- }
- if (getLanguage() != null) {
- buf.append(" xml:lang=\"").append(getLanguage()).append("\"");
- }
- if (getPacketID() != null) {
- buf.append(" id=\"").append(getPacketID()).append("\"");
- }
- if (getTo() != null) {
- buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\"");
- }
- if (getFrom() != null) {
- buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\"");
- }
- buf.append(">");
- buf.append(GcmPacketExtension.this.toXML());
- buf.append("</message>");
- return buf.toString();
- }
- };
- }
- }
-
- public SmackCcsClient() {
- // Add GcmPacketExtension
- ProviderManager.getInstance().addExtensionProvider(GCM_ELEMENT_NAME,
- GCM_NAMESPACE, new PacketExtensionProvider() {
-
- @Override
- public PacketExtension parseExtension(XmlPullParser parser)
- throws Exception {
- String json = parser.nextText();
- GcmPacketExtension packet = new GcmPacketExtension(json);
- return packet;
- }
- });
- }
-
- /**
- * Returns a random message id to uniquely identify a message.
- *
- * <p>Note:
- * This is generated by a pseudo random number generator for illustration purpose,
- * and is not guaranteed to be unique.
- *
- */
- public String getRandomMessageId() {
- return "m-" + Long.toString(random.nextLong());
- }
-
- /**
- * Sends a downstream GCM message.
- */
- public void send(String jsonRequest) {
- Packet request = new GcmPacketExtension(jsonRequest).toPacket();
- connection.sendPacket(request);
- }
-
- /**
- * Handles an upstream data message from a device application.
- *
- * <p>This sample echo server sends an echo message back to the device.
- * Subclasses should override this method to process an upstream message.
- */
- public void handleIncomingDataMessage(Map<String, Object> jsonObject) {
- String from = jsonObject.get("from").toString();
-
- // PackageName of the application that sent this message.
- String category = jsonObject.get("category").toString();
-
- // Use the packageName as the collapseKey in the echo packet
- String collapseKey = "echo:CollapseKey";
- @SuppressWarnings("unchecked")
- Map<String, String> payload = (Map<String, String>) jsonObject.get("data");
- payload.put("ECHO", "Application: " + category);
-
- // Send an ECHO response back
- String echo = createJsonMessage(from, getRandomMessageId(), payload, collapseKey, null, false);
- send(echo);
- }
-
- /**
- * Handles an ACK.
- *
- * <p>By default, it only logs a {@code INFO} message, but subclasses could override it to
- * properly handle ACKS.
- */
- public void handleAckReceipt(Map<String, Object> jsonObject) {
- String messageId = jsonObject.get("message_id").toString();
- String from = jsonObject.get("from").toString();
- logger.log(Level.INFO, "handleAckReceipt() from: " + from + ", messageId: " + messageId);
- }
-
- /**
- * Handles a NACK.
- *
- * <p>By default, it only logs a {@code INFO} message, but subclasses could override it to
- * properly handle NACKS.
- */
- public void handleNackReceipt(Map<String, Object> jsonObject) {
- String messageId = jsonObject.get("message_id").toString();
- String from = jsonObject.get("from").toString();
- logger.log(Level.INFO, "handleNackReceipt() from: " + from + ", messageId: " + messageId);
- }
-
- /**
- * Creates a JSON encoded GCM message.
- *
- * @param to RegistrationId of the target device (Required).
- * @param messageId Unique messageId for which CCS will send an "ack/nack" (Required).
- * @param payload Message content intended for the application. (Optional).
- * @param collapseKey GCM collapse_key parameter (Optional).
- * @param timeToLive GCM time_to_live parameter (Optional).
- * @param delayWhileIdle GCM delay_while_idle parameter (Optional).
- * @return JSON encoded GCM message.
- */
- public static String createJsonMessage(String to, String messageId, Map<String, String> payload,
- String collapseKey, Long timeToLive, Boolean delayWhileIdle) {
- Map<String, Object> message = new HashMap<String, Object>();
- message.put("to", to);
- if (collapseKey != null) {
- message.put("collapse_key", collapseKey);
- }
- if (timeToLive != null) {
- message.put("time_to_live", timeToLive);
- }
- if (delayWhileIdle != null && delayWhileIdle) {
- message.put("delay_while_idle", true);
- }
- message.put("message_id", messageId);
- message.put("data", payload);
- return JSONValue.toJSONString(message);
- }
-
- /**
- * Creates a JSON encoded ACK message for an upstream message received from an application.
- *
- * @param to RegistrationId of the device who sent the upstream message.
- * @param messageId messageId of the upstream message to be acknowledged to CCS.
- * @return JSON encoded ack.
- */
- public static String createJsonAck(String to, String messageId) {
- Map<String, Object> message = new HashMap<String, Object>();
- message.put("message_type", "ack");
- message.put("to", to);
- message.put("message_id", messageId);
- return JSONValue.toJSONString(message);
- }
-
- /**
- * Connects to GCM Cloud Connection Server using the supplied credentials.
- *
- * @param username GCM_SENDER_ID@gcm.googleapis.com
- * @param password API Key
- * @throws XMPPException
- */
- public void connect(String username, String password) throws XMPPException {
- config = new ConnectionConfiguration(GCM_SERVER, GCM_PORT);
- config.setSecurityMode(SecurityMode.enabled);
- config.setReconnectionAllowed(true);
- config.setRosterLoadedAtLogin(false);
- config.setSendPresence(false);
- config.setSocketFactory(SSLSocketFactory.getDefault());
-
- // NOTE: Set to true to launch a window with information about packets sent and received
- config.setDebuggerEnabled(true);
-
- // -Dsmack.debugEnabled=true
- XMPPConnection.DEBUG_ENABLED = true;
-
- connection = new XMPPConnection(config);
- connection.connect();
-
- connection.addConnectionListener(new ConnectionListener() {
-
- @Override
- public void reconnectionSuccessful() {
- logger.info("Reconnecting..");
- }
-
- @Override
- public void reconnectionFailed(Exception e) {
- logger.log(Level.INFO, "Reconnection failed.. ", e);
- }
-
- @Override
- public void reconnectingIn(int seconds) {
- logger.log(Level.INFO, "Reconnecting in %d secs", seconds);
- }
-
- @Override
- public void connectionClosedOnError(Exception e) {
- logger.log(Level.INFO, "Connection closed on error.");
- }
-
- @Override
- public void connectionClosed() {
- logger.info("Connection closed.");
- }
- });
-
- // Handle incoming packets
- connection.addPacketListener(new PacketListener() {
-
- @Override
- public void processPacket(Packet packet) {
- logger.log(Level.INFO, "Received: " + packet.toXML());
- Message incomingMessage = (Message) packet;
- GcmPacketExtension gcmPacket =
- (GcmPacketExtension) incomingMessage.getExtension(GCM_NAMESPACE);
- String json = gcmPacket.getJson();
- try {
- @SuppressWarnings("unchecked")
- Map<String, Object> jsonObject =
- (Map<String, Object>) JSONValue.parseWithException(json);
-
- // present for "ack"/"nack", null otherwise
- Object messageType = jsonObject.get("message_type");
-
- if (messageType == null) {
- // Normal upstream data message
- handleIncomingDataMessage(jsonObject);
-
- // Send ACK to CCS
- String messageId = jsonObject.get("message_id").toString();
- String from = jsonObject.get("from").toString();
- String ack = createJsonAck(from, messageId);
- send(ack);
- } else if ("ack".equals(messageType.toString())) {
- // Process Ack
- handleAckReceipt(jsonObject);
- } else if ("nack".equals(messageType.toString())) {
- // Process Nack
- handleNackReceipt(jsonObject);
- } else {
- logger.log(Level.WARNING, "Unrecognized message type (%s)",
- messageType.toString());
- }
- } catch (ParseException e) {
- logger.log(Level.SEVERE, "Error parsing JSON " + json, e);
- } catch (Exception e) {
- logger.log(Level.SEVERE, "Couldn't send echo.", e);
- }
- }
- }, new PacketTypeFilter(Message.class));
-
-
- // Log all outgoing packets
- connection.addPacketInterceptor(new PacketInterceptor() {
- @Override
- public void interceptPacket(Packet packet) {
- logger.log(Level.INFO, "Sent: {0}", packet.toXML());
- }
- }, new PacketTypeFilter(Message.class));
-
- connection.login(username, password);
- }
-
- public static void main(String [] args) {
- final String userName = "Your GCM Sender Id" + "@gcm.googleapis.com";
- final String password = "API Key";
-
- SmackCcsClient ccsClient = new SmackCcsClient();
-
- try {
- ccsClient.connect(userName, password);
- } catch (XMPPException e) {
- e.printStackTrace();
- }
-
- // Send a sample hello downstream message to a device.
- String toRegId = "RegistrationIdOfTheTargetDevice";
- String messageId = ccsClient.getRandomMessageId();
- Map<String, String> payload = new HashMap<String, String>();
- payload.put("Hello", "World");
- payload.put("CCS", "Dummy Message");
- payload.put("EmbeddedMessageId", messageId);
- String collapseKey = "sample";
- Long timeToLive = 10000L;
- Boolean delayWhileIdle = true;
- ccsClient.send(createJsonMessage(toRegId, messageId, payload, collapseKey,
- timeToLive, delayWhileIdle));
- }
-}</pre>
-<h3 id="python">Python sample</h3>
-
-<p>Here is an example of a CCS app server written in Python. This sample echo
-server sends an initial message, and for every upstream message received, it sends
-a dummy response back to the application that sent the upstream message. This
-example illustrates how to connect, send, and receive GCM messages using XMPP. It
-shouldn't be used as-is on a production deployment.</p>
-
-<pre>
-#!/usr/bin/python
-import sys, json, xmpp, random, string
-
-SERVER = 'gcm.googleapis.com'
-PORT = 5235
-USERNAME = "Your GCM Sender Id"
-PASSWORD = "API Key"
-REGISTRATION_ID = "Registration Id of the target device"
-
-unacked_messages_quota = 1000
-send_queue = []
-
-# Return a random alphanumerical id
-def random_id():
- rid = ''
- for x in range(8): rid += random.choice(string.ascii_letters + string.digits)
- return rid
-
-def message_callback(session, message):
- global unacked_messages_quota
- gcm = message.getTags('gcm')
- if gcm:
- gcm_json = gcm[0].getData()
- msg = json.loads(gcm_json)
- if not msg.has_key('message_type'):
- # Acknowledge the incoming message immediately.
- send({'to': msg['from'],
- 'message_type': 'ack',
- 'message_id': msg['message_id']})
- # Queue a response back to the server.
- if msg.has_key('from'):
- # Send a dummy echo response back to the app that sent the upstream message.
- send_queue.append({'to': msg['from'],
- 'message_id': random_id(),
- 'data': {'pong': 1}})
- elif msg['message_type'] == 'ack' or msg['message_type'] == 'nack':
- unacked_messages_quota += 1
-
-def send(json_dict):
- template = ("<message><gcm xmlns='google:mobile:data'>{1}</gcm></message>")
- client.send(xmpp.protocol.Message(
- node=template.format(client.Bind.bound[0], json.dumps(json_dict))))
-
-def flush_queued_messages():
- global unacked_messages_quota
- while len(send_queue) and unacked_messages_quota > 0:
- send(send_queue.pop(0))
- unacked_messages_quota -= 1
-
-client = xmpp.Client('gcm.googleapis.com', debug=['socket'])
-client.connect(server=(SERVER,PORT), secure=1, use_srv=False)
-auth = client.auth(USERNAME, PASSWORD)
-if not auth:
- print 'Authentication failed!'
- sys.exit(1)
-
-client.RegisterHandler('message', message_callback)
-
-send_queue.append({'to': REGISTRATION_ID,
- 'message_id': 'reg_id',
- 'data': {'message_destination': 'RegId',
- 'message_id': random_id()}})
-
-while True:
- client.Process(1)
- flush_queued_messages()</pre>
diff --git a/docs/html/google/gcm/client.jd b/docs/html/google/gcm/client.jd
index df357a2..7604932 100644
--- a/docs/html/google/gcm/client.jd
+++ b/docs/html/google/gcm/client.jd
@@ -1,663 +1,24 @@
-page.title=Implementing GCM Client
+page.title=GCM Client
page.tags="cloud","push","messaging"
@jd:body
<div id="qv-wrapper">
<div id="qv">
-
-<h2>In this document</h2>
-
-<ol class="toc">
-<li><a href="#play-services">Set Up Google Play Services</a></li>
-<li><a href="#manifest">Edit Your Application's Manifest</a></li>
-<li><a href="#app">Write Your Application</a>
- <ol class="toc">
- <li><a href="#sample-play">Check for Google Play Services APK</a></li>
- <li><a href="#sample-register">Register for GCM</a></li>
- <li><a href="#sample-send">Send a message</a></li>
- <li><a href="#sample-receive">Receive a message</a></li>
- </ol>
- <li><a href="#run">Running the Sample</a></li>
- <li><a href="#stats">Viewing Statistics</a></li>
-</li>
-
-</ol>
-
<h2>See Also</h2>
<ol class="toc">
<li><a href="gs.html">Getting Started</a></li>
-<li><a href="server.html">Implementing GCM Server</a></li>
+<li><a href="server.html">GCM Server</a></li>
</ol>
</div>
</div>
-<p>A GCM client is a GCM-enabled app that runs on an Android device. To write your
-client code, we recommend that you use the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a> APIs.
-The client helper library that was offered in previous versions of GCM still works,
-but it has been superseded by the more efficient
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a> APIs.</p>
+<p>A GCM client is a GCM-enabled app that runs on an Android device. To write your client code, we recommend that you use the new <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> APIs. The client helper library that was offered in previous versions of GCM still works, but it has been superseded by the more efficient <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> APIs.</p>
-<p>A full GCM implementation requires both a client implementation and a server
-implementation. For more
-information about implementing the server side, see <a href="server.html">
-Implementing GCM Server</a>.</p>
+<p>A full GCM implementation requires both a client implementation and a server-side implementation. For a step-by-step guide to creating a complete sample implementation that includes both client and server, see <a href="gs.html">Getting Started</a>. </p>
-<p>The following sections walk you through the steps involved in writing a GCM
-client-side application. Your client app can be arbitrarily complex, but at bare
-minimum, a GCM client app must include code to register (and thereby get a
-registration ID), and a broadcast receiver to receive messages sent by GCM.
-</p>
-
-<h2 id="play-services">Step 1: Set Up Google Play Services</h2>
-
-<p>To write your client application, use the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a> API.
-To use this API, you must set up your project to use the Google Play services SDK,
-as described in <a href="/google/play-services/setup.html">Setup Google Play
-Services SDK</a>.</p>
-
-<p class="note"><strong>Caution:</strong> When you add the Play Services library to
-your project, be sure to add it <em>with resources</em>, as described in
-<a href="{@docRoot}google/play-services/setup.html#Setup">
-Setup Google Play Services SDK</a>. The key point is that you must
-<em>reference</em> the library—simply adding a {@code .jar} file to
-your Eclipse project will not work. You must follow the directions
-for referencing a library, or your app won't be able to access
-the library's resources, and it won't run properly.
-If you're using Android Studio, this is the string to add to the
-{@code dependency} section of your application's {@code build.gradle} file:</p>
-
-<pre>dependencies {
- compile: "com.google.android.gms:play-services:3.1.+"
-}
-</pre>
-
-
-<h2 id="manifest">Step 2: Edit Your Application's Manifest</h2>
-
-<p>Add the following to your application's manifest:</p>
-<ul>
- <li>The <code>com.google.android.c2dm.permission.RECEIVE</code> permission so
-the Android application can register and receive messages.</li>
- <li>The <code>android.permission.INTERNET</code> permission so the Android
-application can send the registration ID to the 3rd party server.</li>
- <li>The <code>android.permission.GET_ACCOUNTS</code> permission as GCM requires
-a Google account (necessary only if if the device is running a version lower than
-Android 4.0.4)</li>
- <li>The <code>android.permission.WAKE_LOCK</code> permission so the application
-can keep the processor from sleeping when a message is received. Optional—use
-only if the app wants to keep the device from sleeping.</li>
- <li>An <code>applicationPackage + ".permission.C2D_MESSAGE"</code>
-permission to prevent other Android applications from registering and receiving
-the Android application's messages. The permission name must exactly match this
-pattern—otherwise the Android application will not receive the messages.</li>
- <li>A receiver for <code>com.google.android.c2dm.intent.RECEIVE</code>, with
-the category set
-as <code>applicationPackage</code>. The receiver should require the
-<code>com.google.android.c2dm.SEND</code> permission, so that only the GCM
-Framework can send a message to it. If your app uses an {@link android.app.IntentService}
-(not required, but a common pattern), this receiver should be an instance of
-{@link android.support.v4.content.WakefulBroadcastReceiver}.
-A {@link android.support.v4.content.WakefulBroadcastReceiver} takes care of
-creating and managing a
-<a href="{@docRoot}reference/android/os/PowerManager.html#PARTIAL_WAKE_LOCK">
-partial wake lock</a> for your app.</li>
-
-<li>A {@link android.app.Service} (typically an {@link android.app.IntentService})
-to which the {@link android.support.v4.content.WakefulBroadcastReceiver} passes off
-the work of handling the GCM message, while ensuring that the device does not
-go back to sleep in the process. Including an {@link android.app.IntentService} is
-optional—you could choose to process your messages in a regular
-{@link android.content.BroadcastReceiver} instead, but realistically, most apps will
-use a {@link android.app.IntentService}.
-</li>
- <li>If the GCM feature is critical to the Android application's function, be sure to
-set <code>android:minSdkVersion="8"</code> or higher in the manifest. This
-ensures that the Android application cannot be installed in an environment in which it
-could not run properly. </li>
-</ul>
-
-<p>Here are excerpts from a sample manifest that supports GCM:</p>
-
-<pre class="prettyprint pretty-xml">
-<manifest package="com.example.gcm" ...>
-
- <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/>
- <uses-permission android:name="android.permission.INTERNET" />
- <uses-permission android:name="android.permission.GET_ACCOUNTS" />
- <uses-permission android:name="android.permission.WAKE_LOCK" />
- <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
-
- <permission android:name="com.example.gcm.permission.C2D_MESSAGE"
- android:protectionLevel="signature" />
- <uses-permission android:name="com.example.gcm.permission.C2D_MESSAGE" />
-
- <application ...>
- <receiver
- android:name=".GcmBroadcastReceiver"
- android:permission="com.google.android.c2dm.permission.SEND" >
- <intent-filter>
- <action android:name="com.google.android.c2dm.intent.RECEIVE" />
- <category android:name="com.example.gcm" />
- </intent-filter>
- </receiver>
- <service android:name=".GcmIntentService" />
- </application>
-
-</manifest>
-</pre>
-
-<h2 id="app"> Step 3: Write Your Application</h2>
-
-<p>Finally, write your application. This section features a sample client
-application that illustrates how to use the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a> APIs. The sample consists of a main activity
-({@code DemoActivity}), a {@link android.support.v4.content.WakefulBroadcastReceiver}
-({@code GcmBroadcastReceiver}), and an {@link android.app.IntentService}
-({@code GcmIntentService}). You can find the complete source code for this sample at the
-<a href="http://code.google.com/p/gcm">open source site</a>.</p>
-
-<p>Note the following:</p>
-
-<ul>
- <li>Among other things, the sample illustrates registration and upstream
-(device-to-cloud) messaging. Upstream messaging only applies to apps that are running against a
-<a href="ccs.html">CCS</a> (XMPP) server; HTTP-based servers don't support upstream messaging.</li>
- <li>The <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
- {@code GoogleCloudMessaging}</a>
-registration APIs replace the old registration process, which was based on the
-now-obsolete client helper library. While the old registration process still works,
-we encourage you to use the newer
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a>
-registration APIs, regardless of your underlying server.</li>
-</ul>
-
-<h3 id="sample-play">Check for Google Play Services APK</h3>
-
-<p>As described in <a href="{@docRoot}google/play-services/setup.html">
-Setup Google Play Services SDK</a>, apps that rely on the Play Services SDK
-should always check the device for a compatible Google Play services APK before
-accessing Google Play services features. In the sample app this check is done in
-two places: in the main activity's {@code onCreate()} method, and in its
-{@code onResume()} method. The check in {@code onCreate()} ensures that the app
-can't be used without a successful check. The check in {@code onResume()} ensures
-that if the user returns to the running app through some other means, such as
-through the back button, the check is still performed. If the
-device doesn't have a compatible Google Play services APK, your app can call
-{@code GooglePlayServicesUtil.getErrorDialog()} to allow users to download the
-APK from the Google Play Store or enable it in the device's system settings.
-For example:</p>
-
-<pre>private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
-...
-@Override
-public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.main);
- mDisplay = (TextView) findViewById(R.id.display);
-
- context = getApplicationContext();
-
- // Check device for Play Services APK.
- if (checkPlayServices()) {
- // If this check succeeds, proceed with normal processing.
- // Otherwise, prompt user to get valid Play Services APK.
- ...
- }
-}
-
-// You need to do the Play Services APK check here too.
-@Override
-protected void onResume() {
- super.onResume();
- checkPlayServices();
-}
-
-/**
- * Check the device to make sure it has the Google Play Services APK. If
- * it doesn't, display a dialog that allows users to download the APK from
- * the Google Play Store or enable it in the device's system settings.
- */
-private boolean checkPlayServices() {
- int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
- if (resultCode != ConnectionResult.SUCCESS) {
- if (GooglePlayServicesUtil.isUserRecoverableError(resultCode)) {
- GooglePlayServicesUtil.getErrorDialog(resultCode, this,
- PLAY_SERVICES_RESOLUTION_REQUEST).show();
- } else {
- Log.i(TAG, "This device is not supported.");
- finish();
- }
- return false;
- }
- return true;
-}</pre>
-
-<h3 id="sample-register">Register for GCM</h3>
-<p>An Android application needs to register with GCM servers before it can receive
-messages. When an app registers, it receives a registration ID, which it can then
-store for future use. In the following snippet the {@code onCreate()} method in the sample app's
-main activity checks to see if the app is already registered with GCM and with
-the server:</p>
-
-<pre>/**
- * Main UI for the demo app.
- */
-public class DemoActivity extends Activity {
-
- public static final String EXTRA_MESSAGE = "message";
- public static final String PROPERTY_REG_ID = "registration_id";
- private static final String PROPERTY_APP_VERSION = "appVersion";
- private final static int PLAY_SERVICES_RESOLUTION_REQUEST = 9000;
-
- /**
- * Substitute you own sender ID here. This is the project number you got
- * from the API Console, as described in "Getting Started."
- */
- String SENDER_ID = "Your-Sender-ID";
-
- /**
- * Tag used on log messages.
- */
- static final String TAG = "GCMDemo";
-
- TextView mDisplay;
- GoogleCloudMessaging gcm;
- AtomicInteger msgId = new AtomicInteger();
- SharedPreferences prefs;
- Context context;
-
- String regid;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.main);
- mDisplay = (TextView) findViewById(R.id.display);
-
- context = getApplicationContext();
-
- // Check device for Play Services APK. If check succeeds, proceed with
- // GCM registration.
- if (checkPlayServices()) {
- gcm = GoogleCloudMessaging.getInstance(this);
- regid = getRegistrationId(context);
-
- if (regid.isEmpty()) {
- registerInBackground();
- }
- } else {
- Log.i(TAG, "No valid Google Play Services APK found.");
- }
- }
-...
-}</pre>
-
-<p>The app calls {@code getRegistrationId()} to see whether there is an existing
-registration ID stored in shared preferences:</p>
-
-<pre>/**
- * Gets the current registration ID for application on GCM service.
- * <p>
- * If result is empty, the app needs to register.
- *
- * @return registration ID, or empty string if there is no existing
- * registration ID.
- */
-private String getRegistrationId(Context context) {
- final SharedPreferences prefs = getGCMPreferences(context);
- String registrationId = prefs.getString(PROPERTY_REG_ID, "");
- if (registrationId.isEmpty()) {
- Log.i(TAG, "Registration not found.");
- return "";
- }
- // Check if app was updated; if so, it must clear the registration ID
- // since the existing regID is not guaranteed to work with the new
- // app version.
- int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
- int currentVersion = getAppVersion(context);
- if (registeredVersion != currentVersion) {
- Log.i(TAG, "App version changed.");
- return "";
- }
- return registrationId;
-}
-...
-/**
- * @return Application's {@code SharedPreferences}.
- */
-private SharedPreferences getGCMPreferences(Context context) {
- // This sample app persists the registration ID in shared preferences, but
- // how you store the regID in your app is up to you.
- return getSharedPreferences(DemoActivity.class.getSimpleName(),
- Context.MODE_PRIVATE);
-}</pre>
-
-<p>If the registration ID doesn't exist or the app was updated,
-{@code getRegistrationId()} returns an empty string
-to indicate that the app needs to get a new regID. {@code getRegistrationId()} calls
-the following method to check the app version:</p>
-
-<pre>/**
- * @return Application's version code from the {@code PackageManager}.
- */
-private static int getAppVersion(Context context) {
- try {
- PackageInfo packageInfo = context.getPackageManager()
- .getPackageInfo(context.getPackageName(), 0);
- return packageInfo.versionCode;
- } catch (NameNotFoundException e) {
- // should never happen
- throw new RuntimeException("Could not get package name: " + e);
- }
-}</pre>
-
-
-<p>If there isn't a valid existing registration ID, {@code DemoActivity} calls the
-following {@code registerInBackground()} method to register. Note that because the GCM
-methods {@code register()} and {@code unregister()} are blocking, this has to
-take place on a background thread. This sample uses {@link android.os.AsyncTask}
-to accomplish this:</p>
-
-<pre>
-/**
- * Registers the application with GCM servers asynchronously.
- * <p>
- * Stores the registration ID and app versionCode in the application's
- * shared preferences.
- */
-private void registerInBackground() {
- new AsyncTask<Void, Void, String>() {
- @Override
- protected String doInBackground(Void... params) {
- String msg = "";
- try {
- if (gcm == null) {
- gcm = GoogleCloudMessaging.getInstance(context);
- }
- regid = gcm.register(SENDER_ID);
- msg = "Device registered, registration ID=" + regid;
-
- // You should send the registration ID to your server over HTTP,
- // so it can use GCM/HTTP or CCS to send messages to your app.
- // The request to your server should be authenticated if your app
- // is using accounts.
- sendRegistrationIdToBackend();
-
- // For this demo: we don't need to send it because the device
- // will send upstream messages to a server that echo back the
- // message using the 'from' address in the message.
-
- // Persist the regID - no need to register again.
- storeRegistrationId(context, regid);
- } catch (IOException ex) {
- msg = "Error :" + ex.getMessage();
- // If there is an error, don't just keep trying to register.
- // Require the user to click a button again, or perform
- // exponential back-off.
- }
- return msg;
- }
-
- @Override
- protected void onPostExecute(String msg) {
- mDisplay.append(msg + "\n");
- }
- }.execute(null, null, null);
- ...
- /**
- * Sends the registration ID to your server over HTTP, so it can use GCM/HTTP
- * or CCS to send messages to your app. Not needed for this demo since the
- * device sends upstream messages to a server that echoes back the message
- * using the 'from' address in the message.
- */
- private void sendRegistrationIdToBackend() {
- // Your implementation here.
- }
-}</pre>
-
-<p>After registering, the app calls {@code storeRegistrationId()} to store the
-registration ID in shared preferences for future use. This is just one way of
-persisting a regID. You might choose to use a different approach in your app:</p>
-
-<pre>/**
- * Stores the registration ID and app versionCode in the application's
- * {@code SharedPreferences}.
- *
- * @param context application's context.
- * @param regId registration ID
- */
-private void storeRegistrationId(Context context, String regId) {
- final SharedPreferences prefs = getGCMPreferences(context);
- int appVersion = getAppVersion(context);
- Log.i(TAG, "Saving regId on app version " + appVersion);
- SharedPreferences.Editor editor = prefs.edit();
- editor.putString(PROPERTY_REG_ID, regId);
- editor.putInt(PROPERTY_APP_VERSION, appVersion);
- editor.commit();
-}</pre>
-
-<h3 id="sample-send">Send a message</h3>
-<p>When the user clicks the app's <strong>Send</strong> button, the app sends an
-upstream message using the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a> APIs. In order to receive the upstream message,
-your server should be connected to CCS. You can use one of the demo servers in
-<a href="ccs.html#implement">Implementing an XMPP-based App Server</a> to run the sample and connect
-to CCS.</p>
-
-<pre>public void onClick(final View view) {
- if (view == findViewById(R.id.send)) {
- new AsyncTask<Void, Void, String>() {
- @Override
- protected String doInBackground(Void... params) {
- String msg = "";
- try {
- Bundle data = new Bundle();
- data.putString("my_message", "Hello World");
- data.putString("my_action",
- "com.google.android.gcm.demo.app.ECHO_NOW");
- String id = Integer.toString(msgId.incrementAndGet());
- gcm.send(SENDER_ID + "@gcm.googleapis.com", id, data);
- msg = "Sent message";
- } catch (IOException ex) {
- msg = "Error :" + ex.getMessage();
- }
- return msg;
- }
-
- @Override
- protected void onPostExecute(String msg) {
- mDisplay.append(msg + "\n");
- }
- }.execute(null, null, null);
- } else if (view == findViewById(R.id.clear)) {
- mDisplay.setText("");
- }
-}</pre>
-
-<h3 id="sample-receive">Receive a message</h3>
-
-<p>As described above in <a href="#manifest">Step 2</a>, the app includes a
-{@link android.support.v4.content.WakefulBroadcastReceiver} for the <code>com.google.android.c2dm.intent.RECEIVE</code>
-intent. A broadcast receiver is the mechanism GCM uses to deliver messages. When {@code onClick()}
-calls {@code gcm.send()}, it triggers the broadcast receiver's {@code onReceive()}
-method, which has the responsibility of making sure that the GCM message gets handled.</p>
-<p>A {@link android.support.v4.content.WakefulBroadcastReceiver} is a special type of
-broadcast receiver that takes care of
-creating and managing a
-<a href="{@docRoot}reference/android/os/PowerManager.html#PARTIAL_WAKE_LOCK">
-partial wake lock</a> for your app.
-It passes off the work of processing the GCM message to a
-{@link android.app.Service} (typically an
-{@link android.app.IntentService}), while ensuring that the device does not
-go back to sleep in the transition. If you don't hold a wake lock while transitioning
-the work to a service, you are effectively allowing the device to go back to sleep before
-the work completes. The net result is that the app might not finish processing
-the GCM message until some arbitrary point in the future, which is not what you want.</p>
-
-<p class="note"><strong>Note:</strong> Using {@link android.support.v4.content.WakefulBroadcastReceiver}
-is not a requirement. If you have a relatively simple app that doesn't require
-a service, you can intercept the GCM message in a regular {@link android.content.BroadcastReceiver}
-and do your processing there. Once you get the intent that GCM passes into
-your broadcast receiver's {@code onReceive()} method, what you do with it
-is up to you.</p>
-
-<p>This snippet starts {@code GcmIntentService} with the method
-{@link android.support.v4.content.WakefulBroadcastReceiver#startWakefulService startWakefulService()}.
-This method is comparable to {@link android.content.Context#startService startService()}, except that
-the {@link android.support.v4.content.WakefulBroadcastReceiver} is holding a
-wake lock when the service starts. The intent that is passed with
-{@link android.support.v4.content.WakefulBroadcastReceiver#startWakefulService startWakefulService()}
-holds an extra identifying the wake lock:</p>
-
-
-<pre>public class GcmBroadcastReceiver extends WakefulBroadcastReceiver {
- @Override
- public void onReceive(Context context, Intent intent) {
- // Explicitly specify that GcmIntentService will handle the intent.
- ComponentName comp = new ComponentName(context.getPackageName(),
- GcmIntentService.class.getName());
- // Start the service, keeping the device awake while it is launching.
- startWakefulService(context, (intent.setComponent(comp)));
- setResultCode(Activity.RESULT_OK);
- }
-}</pre>
-
-<p>The intent service shown below does the actual work of handling the GCM
-message. When the service is finished, it calls
-{@link android.support.v4.content.WakefulBroadcastReceiver#completeWakefulIntent GcmBroadcastReceiver.completeWakefulIntent()}
-to release the wake lock. The
-{@link android.support.v4.content.WakefulBroadcastReceiver#completeWakefulIntent completeWakefulIntent()}
-method has as its parameter the same intent that was
-passed in from the {@link android.support.v4.content.WakefulBroadcastReceiver}.
-</p>
-
-<p>This snippet processes the GCM message based on message type, and posts the
-result in a notification. But what you do with GCM messages in your app is up to
-you—the possibilities are endless. For example, the message might be a ping,
-telling the app to sync to a server to retrieve new content, or it might be a
-chat message that you display in the UI.</p>
-
-<pre>
-public class GcmIntentService extends IntentService {
- public static final int NOTIFICATION_ID = 1;
- private NotificationManager mNotificationManager;
- NotificationCompat.Builder builder;
-
- public GcmIntentService() {
- super("GcmIntentService");
- }
-
- @Override
- protected void onHandleIntent(Intent intent) {
- Bundle extras = intent.getExtras();
- GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(this);
- // The getMessageType() intent parameter must be the intent you received
- // in your BroadcastReceiver.
- String messageType = gcm.getMessageType(intent);
-
- if (!extras.isEmpty()) { // has effect of unparcelling Bundle
- /*
- * Filter messages based on message type. Since it is likely that GCM
- * will be extended in the future with new message types, just ignore
- * any message types you're not interested in, or that you don't
- * recognize.
- */
- if (GoogleCloudMessaging.
- MESSAGE_TYPE_SEND_ERROR.equals(messageType)) {
- sendNotification("Send error: " + extras.toString());
- } else if (GoogleCloudMessaging.
- MESSAGE_TYPE_DELETED.equals(messageType)) {
- sendNotification("Deleted messages on server: " +
- extras.toString());
- // If it's a regular GCM message, do some work.
- } else if (GoogleCloudMessaging.
- MESSAGE_TYPE_MESSAGE.equals(messageType)) {
- // This loop represents the service doing some work.
- for (int i=0; i<5; i++) {
- Log.i(TAG, "Working... " + (i+1)
- + "/5 @ " + SystemClock.elapsedRealtime());
- try {
- Thread.sleep(5000);
- } catch (InterruptedException e) {
- }
- }
- Log.i(TAG, "Completed work @ " + SystemClock.elapsedRealtime());
- // Post notification of received message.
- sendNotification("Received: " + extras.toString());
- Log.i(TAG, "Received: " + extras.toString());
- }
- }
- // Release the wake lock provided by the WakefulBroadcastReceiver.
- GcmBroadcastReceiver.completeWakefulIntent(intent);
- }
-
- // Put the message into a notification and post it.
- // This is just one simple example of what you might choose to do with
- // a GCM message.
- private void sendNotification(String msg) {
- mNotificationManager = (NotificationManager)
- this.getSystemService(Context.NOTIFICATION_SERVICE);
-
- PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
- new Intent(this, DemoActivity.class), 0);
-
- NotificationCompat.Builder mBuilder =
- new NotificationCompat.Builder(this)
- .setSmallIcon(R.drawable.ic_stat_gcm)
- .setContentTitle("GCM Notification")
- .setStyle(new NotificationCompat.BigTextStyle()
- .bigText(msg))
- .setContentText(msg);
-
- mBuilder.setContentIntent(contentIntent);
- mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
- }
-}</pre>
-
-<h2 id="run">Running the Sample</h2>
-
-<p>To run the sample:</p>
-
-<ol>
- <li>Follow the instructions in <a href="gs.html">Getting Started</a> to get your sender ID and
- API key.</li>
- <li>Implement your client app, as described in this document. You can find the complete source
- code for the client app at the <a href="http://code.google.com/p/gcm">open source site</a>.</li>
- <li>Run one of the demo servers (Java or Python) provided in
-<a href="ccs.html#implement">Implementing an XMPP-based App Server</a>. Whichever demo server you
- choose, don't forget to edit its code before running it to supply
-your sender ID and API key.
-</li>
-
-</ol>
-
-<h2 id="stats">Viewing Statistics</h2>
-
-<p>To view statistics and any error messages for your GCM applications:</p>
-<ol>
- <li> Go to the <code><a href="http://play.google.com/apps/publish">Developer Console</a></code>.</li>
- <li>Login with your developer account.
- <p>You will see a page that has a list of all of your apps.</p></li>
- <li> Click on the "statistics" link next to the app for which you
-want to view GCM stats.
- <p>Now you are on the statistics page.</p> </li>
- <li>Go to the drop-down menu and select the GCM metric you want to view.
- </li>
-</ol>
-<p class="note"><strong>Note:</strong> Stats on the Google API Console are not
-enabled for GCM. You must use the <a href="http://play.google.com/apps/publish">Developer Console</a>.</p>
+<p>
diff --git a/docs/html/google/gcm/gcm.jd b/docs/html/google/gcm/gcm.jd
index 3c80b5f..ceb82b0 100644
--- a/docs/html/google/gcm/gcm.jd
+++ b/docs/html/google/gcm/gcm.jd
@@ -1,23 +1,48 @@
-page.title=Overview
+page.title=GCM Architectural Overview
@jd:body
<div id="qv-wrapper">
<div id="qv">
+<h2>Quickview</h2>
+
+<ul>
+<li>Get an introduction to key GCM terms and concepts.</li>
+<li>Learn the basic features of a GCM application.</li>
+<li>Understand the role of the 3rd-party application server, and how to send messages and process results.</li>
+</ul>
+
+
<h2>In this document</h2>
<ol class="toc">
- <li><a href="#key">Key Concepts</a></li>
- <li><a href="#arch">Architectural Overview</a></li>
- <li><a href="#lifecycle">Lifecycle Flow</a>
- <ol class="toc">
- <li><a href="#register">Enable GCM</a></li>
- <li><a href="#push-process">Send a message</a></li>
- <li><a href="#receiving">Receive a message</a></li>
+ <li><a href="#intro">Introduction</a> </li>
+ <li><a href="#arch">Architectural Overview</a>
+ <ol>
+ <li><a href="#lifecycle">Lifecycle Flow</a></li>
+ <ol>
+ <li><a href="#register">Enable GCM</a></li>
+ <li><a href="#push-process">Send a message</a></li>
+ <li><a href="#receiving">Receive a message</a></li>
+ </ol>
+ <li><a href="#user">What Does the User See?</a></li>
</ol>
</li>
+ <li><a href="#server">Role of the 3rd-party Application Server</a>
+ <ol class="toc">
+ <li><a href="#send-msg">Sending Messages</a>
+ <ol>
+ <li><a href="#request">Request format</a></li>
+ <li><a href="#response">Response format</a></li>
+ </ol>
+ </li>
+ </ol>
+ <li><a href="#stats">Viewing Statistics</a>
+ </li>
</ol>
+
+
</div>
</div>
@@ -25,15 +50,24 @@
developers send data from servers to their Android applications on Android
devices, and upstream messages from the user's device back to the cloud.
This could be a lightweight message telling the Android application
-that there is new data to be fetched from the server (for instance, a "new email"
-notification informing the application that it is out of sync with the back end),
-or it could be a message containing up to 4kb of payload
+that there is new data to be fetched from the server (for instance, a movie
+uploaded by a friend), or it could be a message containing up to 4kb of payload
data (so apps like instant messaging can consume the message directly). The GCM
service handles all aspects of queueing of messages and delivery to the target
Android application running on the target device.</p>
+
+<p>GCM introduces GCM Cloud Connection Server (CCS), which you can use
+in tandem with GCM HTTP service/endpoint/APIs.
+CCS uses XMPP, and it offers asynchronous, bidirectional
+messaging. For more information, see
+<a href="ccs.html">GCM Cloud Connection Server</a>.
+
<p class="note"> To jump right into using GCM with your Android
- applications, see <a href="gs.html">Getting Started</a>.</p>
+ applications, see the instructions in <a href="gs.html">Getting Started</a>.</p>
+
+
+<h2 id="intro">Introduction</h2>
<p>Here are the primary characteristics of Google Cloud
Messaging (GCM):</p>
@@ -41,11 +75,9 @@
<ul>
<li>It allows 3rd-party application servers to send messages to
their Android applications.</li>
- <li>Using the <a href="ccs.html">GCM Cloud Connection Server</a>, you can receive
-upstream messages from the user's device.</li>
+ <li>Using the <a href="ccs.html">GCM Cloud Connection Server</a>, you can receive upstream messages from the user's device.</li>
<li>An Android application on an Android device doesn't need to be running to receive
-messages. The system will wake up the Android application via Intent broadcast
-when the message arrives, as long as the application is set up with the proper
+messages. The system will wake up the Android application via Intent broadcast when the message arrives, as long as the application is set up with the proper
broadcast receiver and permissions.</li>
<li>It does not provide any built-in user interface or other handling for
message data. GCM simply passes raw message data received straight to the
@@ -53,67 +85,57 @@
application might post a notification, display a custom user interface, or
silently sync data.</li>
<li>It requires devices running Android 2.2 or higher that also have the
-Google Play Store application installed, or or an emulator running Android 2.2
-with Google APIs. However, you are not limited to deploying your
+Google Play Store application installed, or or an emulator running Android 2.2 with Google APIs. However, you are not limited to deploying your
Android applications through Google Play Store.</li>
- <li>It uses an existing connection for Google services. For pre-3.0 devices,
-this requires users to
-set up their Google account on their mobile devices. A Google account is not a
-requirement on devices running Android 4.0.4 or higher.</li>
+ <li>It uses an existing connection for Google services. For pre-3.0 devices, this requires users to
+set up their Google account on their mobile devices. A Google account is not a requirement on devices running Android 4.0.4 or higher.</li>
</ul>
-
-<h2 id="key">Key Concepts</h2>
-
+<h2 id="arch">Architectural Overview</h2>
+<p>This section gives an overview of how GCM works. </p>
<p>This table summarizes the key terms and concepts involved in GCM. It is
divided into these categories:</p>
<ul>
- <li><strong>Components</strong> — The entities that play a primary role in
+ <li><strong>Components</strong> — The physical entities that play a role in
GCM.</li>
<li><strong>Credentials</strong> — The IDs and tokens that are used in
different stages of GCM to ensure that all parties have been authenticated, and
that the message is going to the correct place.</li>
</ul>
-<p class="table-caption" id="table1">
- <strong>Table 1.</strong> GCM components and credentials.</p>
-
<table>
<tr>
<th colspan="2">Components</th>
</tr>
<tr>
- <td width="165"><strong>Client App</strong></td>
- <td width="1176">The GCM-enabled Android application that is running on a
- device. This must be a 2.2 Android device that has Google Play Store installed, and it must
-have at least one logged in Google account if the device is running a version
-lower than Android 4.0.4. Alternatively, for testing you can use an emulator
-running Android 2.2 with Google APIs.</td>
+ <td width="165"><strong>Mobile Device</strong></td>
+ <td width="1176">The device that is running an Android application that uses
+GCM. This must be a 2.2 Android device that has Google Play Store installed, and it must
+have at least one logged in Google account if the device is running a version lower than Android 4.0.4. Alternatively, for testing you can use an emulator running Android 2.2 with Google APIs.</td>
</tr>
<tr>
<td><strong>3rd-party Application Server</strong></td>
- <td>An application server that you write as part of implementing
-GCM. The 3rd-party application server sends data to an
-Android application on the device via the GCM connection server.</td>
+ <td>An application server that developers set up as part of implementing
+GCM in their applications. The 3rd-party application server sends data to an
+Android application on the device via the GCM server.</td>
</tr>
<tr>
- <td><strong>GCM Connection Servers</strong></td>
- <td>The Google-provided servers involved in taking messages from the 3rd-party
+ <td><strong>GCM Servers</strong></td>
+ <td>The Google servers involved in taking messages from the 3rd-party
application server and sending them to the device. </td>
</tr>
<tr>
- <th colspan="2">Credentials</th>
+ <th colspan="2"><strong>Credentials</strong></th>
</tr>
<tr>
<td><strong>Sender ID</strong></td>
- <td>A project number you acquire from the API console, as described in
-<a href="gs.html#create-proj">Getting Started</a>. The sender
-ID is used in the <a href="#register">registration process</a> to identify a
-3rd-party application server that is permitted to send messages to the device.</td>
+ <td>A project number you acquire from the API console, as described in <a href="gs.html#create-proj">Getting Started</a>. The sender
+ID is used in the <a href="#registering">registration process</a> to identify an
+Android application that is permitted to send messages to the device.</td>
</tr>
<tr>
<td><strong>Application ID</strong></td>
<td>The Android application that is registering to receive messages. The Android application
-is identified by the package name from the <a href="client.html#manifest">manifest</a>.
+is identified by the package name from the <a href="#manifest">manifest</a>.
This ensures that the messages are targeted to the correct Android application.</td>
</tr>
<tr>
@@ -136,8 +158,7 @@
</tr>
<tr>
<td><strong>Google User Account</strong></td>
- <td>For GCM to work, the mobile device must include at least one Google
-account if the device is running a version lower than Android 4.0.4.</td>
+ <td>For GCM to work, the mobile device must include at least one Google account if the device is running a version lower than Android 4.0.4.</td>
</tr>
<tr>
<td><strong>Sender Auth Token</strong></td>
@@ -146,46 +167,25 @@
The API key is included in the header of POST requests that send messages.</td>
</tr>
+ <tr>
+ <td><strong>Notification Key</strong></td>
+ <td>Part of the user notifications feature, which provides a mapping between a user and instances of an app running on multiple devices owned by the user. The {@code notification_key} is the token that GCM uses to fan out notifications to all devices whose registration IDs are associated with the key. For more discussion of this topic, see <a href="notifications.html">User Notifications</a>.</td>
+ </tr>
+
+<tr>
+ <td><strong>Notification Key Name</strong></td>
+ <td>Part of the user notifications feature. The {@code notification_key_name} is a name or identifier (can be a username for a 3rd-party app) that is unique to a given user. It is used by third parties to group together registration IDs for a single user. For more discussion of this topic, see <a href="notifications.html">User Notifications</a>.</td>
+ </tr>
+
</table>
-<h2 id="arch">Architectural Overview</h2>
-
-<p>A GCM implementation includes a Google-provided
-connection server, a 3rd-party app server that interacts with the connection
-server, and a GCM-enabled client app running on an Android device:</p>
-
-<img src="{@docRoot}images/gcm/GCM-arch.png">
-
-<p class="img-caption">
- <strong>Figure 1.</strong> GCM Architecture.
-</p>
-
-<p>This is how these components interact:</p>
-<ul>
- <li>Google-provided <strong>GCM Connection Servers</strong> take messages from
-a 3rd-party application server and send these messages to a
-GCM-enabled Android application (the "client app") running on a device.
-Currently Google provides connection servers for <a href="http.html">HTTP</a>
-and <a href="ccs.html">XMPP</a>.</li>
- <li>The <strong>3rd-Party Application Server</strong> is a component that you
-implement to work with your chosen GCM connection server(s). App servers send
-messages to a GCM connection server; the connection server enqueues and stores the
-message, and then sends it to the device when the device is online.
-For more information, see <a href="server.html">Implementing GCM Server</a>.</li>
- <li>The <strong>Client App</strong> is a GCM-enabled Android application running
-on a device. To receive GCM messages, this app must register with GCM and get a
-registration ID. If you are using the <a href="ccs.html">XMPP</a> (CCS) connection
-server, the client app can send "upstream" messages back to the connection server.
-For more information on how to implement the client app, see
-<a href="client.html">Implementing GCM Client</a>.</li>
-</ul>
-
-<h2 id="lifecycle">Lifecycle Flow</h2>
+<h3 id="lifecycle">Lifecycle Flow</h3>
<ul>
<li><a href="#register">Enable GCM</a>. An Android application running on a
mobile device registers to receive messages.</li>
-
+ <li><a href="notifications.html">User Notifications</a>. A 3rd-party server can optionally group multiple registration IDs
+in a {@code notification_key} to send messages to multiple devices owned by a single user.</li>
<li><a href="#push-process">Send a message</a>. A 3rd-party application
server sends messages to the device.</li>
<li><a href="#receiving">Receive a message</a>. An Android application
@@ -194,18 +194,62 @@
<p>These processes are described in more detail below.</p>
-<h3 id="register">Enable GCM</h3>
+<h4 id="register">Enable GCM</h4>
-<p>The first time the Android application needs to use the messaging service, it
-calls the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a> method {@code register()}, as discussed in
-<a href="client.html">Implementing GCM Client</a>.
-The {@code register()} method returns a registration ID. The Android
-application should store this ID for later use (for instance,
-to check in <code>onCreate()</code> if it is already registered).
+<p>This is the sequence of events that occurs when an Android application
+running on a mobile device registers to receive messages:<span
+class="red-text"></span></p>
+
+<ol>
+ <li>The first time the Android application needs to use the messaging service, it
+fires off a registration Intent to a GCM server.
+ <p>This registration Intent
+(<code>com.google.android.c2dm.intent.REGISTER</code>) includes the sender ID, and the Android application ID.</p>
+<p class="note"><strong>Note:</strong> Because there is no lifecycle method that is called when the application is run for
+the first time, the registration intent should be sent on <code>onCreate()</code>, but only if the application is not registered yet.
</p>
+ </li>
+ <li>If the registration is successful, the GCM server broadcasts a <code>com.google.android.c2dm.intent.REGISTRATION</code> intent which gives the Android application a registration
+ID.
+ <p>The Android application should store this ID for later use (for instance, to check on <code>onCreate()</code> if it is already registered).
+Note that Google may periodically refresh the registration ID, so you should design your Android application
+with the understanding that the <code>com.google.android.c2dm.intent.REGISTRATION</code> intent may be called
+multiple times. Your Android application needs to be able to respond
+accordingly.</p></li>
+ <li>To complete the registration, the Android application sends the registration ID to
+the application server. The application server typically stores the registration
+ID in a database. </li>
+</ol>
-<h3 id="push-process">Send a message</h3>
+<p>The registration ID lasts until the Android application explicitly unregisters
+itself, or until Google refreshes the registration ID for your Android application.</p>
+
+<p class="note"><strong>Note:</strong> When users uninstall an application, it is not automatically unregistered on GCM. It is only unregistered when the GCM server tries to send a message to the device and the device answers that the application is uninstalled or it does not have a broadcast receiver configured to receive <code>com.google.android.c2dm.intent.RECEIVE</code> intents. At that point, your server should mark the device as unregistered (the server will receive a <code><a href="#unreg_device">NotRegistered</a></code> error).</p>
+ <p>
+Note that it might take a few minutes for the registration ID to be completely removed from the GCM server. So if the 3rd-party server sends a message during this time, it will get a valid message ID, even though the message will not be delivered to the device.</p>
+
+
+
+
+<h4 id="push-process">Send a Message</h4>
+
+<p>For an application server to send a message to an Android application, the following things must be in
+place:</p>
+
+<ul>
+ <li>The Android application has stored a target that it can specify as the recipient of a message. This can be one of the following:
+ <ul>
+ <li>A single registration ID (or an array of registration IDs) that allows the app to receive messages
+for a particular device.</li>
+ <li>A {@code notification_key} and corresponding {@code notification_key_name}, used to map a single user to multiple registration IDs. For more discussion of this topic, see <a href="notifications.html">User Notifications</a>.</li>
+ </ul>
+ </li>
+
+<li>An API key. This is something that the developer must have already
+set up on the application server for the Android application (for more discussion, see
+<a href="#server">Role of the 3rd-party Application Server</a>). Now it will
+get used to send messages to the device. </li>
+</ul>
<p>Here is the sequence of events that occurs when the application server sends a
message:</p>
@@ -220,14 +264,13 @@
targeted Android application gets the message. This wakes the Android application up. The
Android application does not need to be running beforehand to receive the message.</li>
<li>The Android application processes the message. If the Android application is doing
-non-trivial processing, you may want to grab a
-{@link android.os.PowerManager.WakeLock} and do any processing in a service.</li>
+non-trivial processing, you may want to grab a {@link android.os.PowerManager.WakeLock} and do any processing in a Service.</li>
</ol>
<p> An Android application can unregister GCM if it no longer wants to receive
messages.</p>
-<h3 id="receiving">Receive a message</h3>
+<h4 id="receiving">Receive a Message</h4>
<p>This is the sequence of events that occurs when an Android application
installed on a mobile device receives a message:</p>
@@ -239,8 +282,482 @@
in a <code>com.google.android.c2dm.intent.RECEIVE</code> Intent as a set of
extras.</li>
<li>The Android application extracts the raw data
-from the <code>com.google.android.c2dm.intent.RECEIVE</code><code> </code>Intent
-by key and processes the data.</li>
+from the <code>com.google.android.c2dm.intent.RECEIVE</code><code> </code>Intent by key and processes the data.</li>
</ol>
+<h3 id="user">What Does the User See?</h3>
+
+<p>When mobile device users install Android applications that include GCM, the Google Play Store will inform them that the Android application
+includes GCM. They must approve the use of this feature to install the
+Android application. </p>
+
+
+<h2 id="server">Role of the 3rd-party Application Server</h2>
+
+<p>Before you can write client Android applications that use the GCM feature, you must
+have an application server that meets the following criteria:</p>
+
+<ul>
+ <li>Able to communicate with your client.</li>
+ <li>Able to fire off HTTPS requests to the GCM server.</li>
+ <li>Able to handle requests and resend them as needed, using <a href="http://en.wikipedia.org/wiki/Exponential_backoff">exponential back-off.</a></li>
+ <li>Able to store the API key and client registration IDs. The
+API key is included in the header of POST requests that send
+messages.</li>
+</ul>
+
+<h3 id="send-msg">Sending Messages</h3>
+<p>This section describes how the 3rd-party application server sends messages to one or more mobile devices. Note the following:</p>
+<ul>
+ <li>A 3rd-party application server can either send messages to a single device or to multiple devices. A message sent to multiple devices simultaneously is called a <em>multicast message</em>.</li>
+ <li>To send a single message to multiple devices owned by a single user, you can use a {@code notification_key}, as described in <a href="notifications.html">User Notifications</a>.
+
+ <li>You have 2 choices in how you construct requests and responses: plain text or JSON.</li>
+ <li>However, to send multicast messages, you must use JSON. Plain text will not work.</li>
+</ul>
+<p>Before the 3rd-party application server can send a message to an
+ Android application, it must have received a registration ID from it.</p>
+<h4 id="request">Request format</h4>
+<p>To send a message, the application server issues a POST request to <code>https://android.googleapis.com/gcm/send</code>.</p>
+<p>A message request is made of 2 parts: HTTP header and HTTP body.</p>
+
+<p>The HTTP header must contain the following headers:</p>
+<ul>
+ <li><code>Authorization</code>: key=YOUR_API_KEY</li>
+ <li><code>Content-Type</code>: <code>application/json</code> for JSON; <code>application/x-www-form-urlencoded;charset=UTF-8</code> for plain text.
+ </li>
+</ul>
+
+<p>For example:
+</p>
+<pre>Content-Type:application/json
+Authorization:key=AIzaSyB-1uEai2WiUapxCs2Q0GZYzPu7Udno5aA
+
+{
+ "registration_ids" : ["APA91bHun4MxP5egoKMwt2KZFBaFUH-1RYqx..."],
+ "data" : {
+ ...
+ },
+}</pre>
+<p class="note">
+ <p><strong>Note:</strong> If <code>Content-Type</code> is omitted, the format is assumed to be plain text.</p>
+</p>
+
+<p>The HTTP body content depends on whether you're using JSON or plain text. For JSON, it must contain a string representing a JSON object with the following fields:</p>
+<table>
+ <tr>
+ <th>Field</th>
+ <th>Description</th>
+ </tr>
+ <tr>
+ <td><code>registration_ids</code></td>
+ <td>A string array with the list of devices (registration IDs) receiving the message. It must contain at least 1 and at most 1000 registration IDs. To send a multicast message, you must use JSON. For sending a single message to a single device, you could use a JSON object with just 1 registration id, or plain text (see below). A request must include a recipient—this can be either a registration ID, an array of registration IDs, or a {@code notification_key}.</td>
+ </tr>
+ <tr>
+ <td><code>notification_key</code></td>
+ <td>A string that maps a single user to multiple registration IDs associated with that user. This
+allows a 3rd-party server to send a single message to multiple app instances (typically on multiple devices) owned by a single user. A 3rd-party server can use {@code notification_key} as the target for a message instead of an individual registration ID (or array of registration IDs). The maximum number of members allowed for a {@code notification_key} is 10. For more discussion of this topic, see <a href="notifications.html">User Notifications</a>. Optional.</td>
+ </tr>
+
+<tr>
+ <td><code>notification_key_name</code></td>
+ <td>A name or identifier (can be a username for a 3rd-party app) that is unique to a given user. It is used by 3rd parties to group together registration IDs for a single user. The <code>notification_key_name</code> should be uniquely named per app in case you have multiple apps for the same project ID. This ensures that notifications only go to the intended target app. For more discussion of this topic, see <a href="notifications.html">User Notifications</a>.</td>
+ </tr>
+
+ <tr>
+ <td><code>collapse_key</code></td>
+ <td>An arbitrary string (such as "Updates Available") that is used to collapse a group of like messages
+when the device is offline, so that only the last message gets sent to the
+client. This is intended to avoid sending too many messages to the phone when it
+comes back online. Note that since there is no guarantee of the order in which
+messages get sent, the "last" message may not actually be the last
+message sent by the application server. See <a href="adv.html#collapsible">Advanced Topics</a> for more discussion of this topic. Optional.</td>
+ </tr>
+ <tr>
+ <td><code>data</code></td>
+ <td>A JSON object whose fields represents the key-value pairs of the message's payload data. If present, the payload data it will be
+included in the Intent as application data, with the key being the extra's name. For instance, <code>"data":{"score":"3x1"}</code> would result in an intent extra named <code>score</code> whose value is the string <code>3x1</code>.
+
+There is no limit on the number of key/value pairs, though there is a limit on the total size of the message (4kb). The values could be any JSON object, but we recommend using strings, since the values will be converted to strings in the GCM server anyway. If you want to include objects or other non-string data types (such as integers or booleans), you have to do the conversion to string yourself. Also note that the key cannot be a reserved word (<code>from</code> or any word starting with <code>google.</code>). To complicate things slightly, there are some reserved words (such as <code>collapse_key</code>) that are technically allowed in payload data. However, if the request also contains the word, the value in the request will overwrite the value in the payload data. Hence using words that are defined as field names in this table is not recommended, even in cases where they are technically allowed. Optional.</td>
+
+
+ </tr>
+ <tr>
+ <td><code>delay_while_idle</code></td>
+ <td>If included, indicates that the message should not be sent immediately
+if the device is idle. The server will wait for the device to become active, and
+then only the last message for each <code>collapse_key</code> value will be
+sent. Optional. The default value is <code>false</code>, and must be a JSON boolean.</td>
+ </tr>
+ <tr>
+ <td><code>time_to_live</code></td>
+ <td>How long (in seconds) the message should be kept on GCM storage if the device is offline. Optional (default time-to-live is 4 weeks, and must be set as a JSON number).</td>
+ </tr>
+<tr>
+ <td><code>restricted_package_name</code></td>
+ <td>A string containing the package name of your application. When set, messages will only be sent to registration IDs that match the package name. Optional.
+ </td>
+</tr>
+<tr>
+ <td><code>dry_run</code></td>
+ <td>If included, allows developers to test their request without actually sending a message. Optional. The default value is <code>false</code>, and must be a JSON boolean.
+ </td>
+</tr>
+</table>
+
+<p>If you are using plain text instead of JSON, the message fields must be set as HTTP parameters sent in the body, and their syntax is slightly different, as described below:
+<table>
+ <tr>
+ <th>Field</th>
+ <th>Description</th>
+ </tr>
+ <tr>
+ <td><code>registration_id</code></td>
+ <td>Must contain the registration ID of the single device receiving the message. Required.</td>
+ </tr>
+ <tr>
+ <td><code>collapse_key</code></td>
+ <td>Same as JSON (see previous table). Optional.</td>
+ </tr>
+ <tr>
+ <td><code>data.<key></code></td>
+
+ <td>Payload data, expressed as parameters prefixed with <code>data.</code> and suffixed as the key. For instance, a parameter of <code>data.score=3x1</code> would result in an intent extra named <code>score</code> whose value is the string <code>3x1</code>. There is no limit on the number of key/value parameters, though there is a limit on the total size of the message. Also note that the key cannot be a reserved word (<code>from</code> or any word starting with
+<code>google.</code>). To complicate things slightly, there are some reserved words (such as <code>collapse_key</code>) that are technically allowed in payload data. However, if the request also contains the word, the value in the request will overwrite the value in the payload data. Hence using words that are defined as field names in this table is not recommended, even in cases where they are technically allowed. Optional.</td>
+
+ </tr>
+ <tr>
+ <td><code>delay_while_idle</code></td>
+ <td>Should be represented as <code>1</code> or <code>true</code> for <code>true</code>, anything else for <code>false</code>. Optional. The default value is <code>false</code>.</td>
+ </tr>
+ <tr>
+ <td><code>time_to_live</code></td>
+ <td>Same as JSON (see previous table). Optional.</td>
+ </tr>
+<tr>
+ <td><code>restricted_package_name</code></td>
+ <td>Same as JSON (see previous table). Optional.
+ </td>
+</tr>
+<tr>
+ <td><code>dry_run</code></td>
+ <td>Same as JSON (see previous table). Optional.
+ </td>
+</tr>
+</table>
+
+<p>If you want to test your request (either JSON or plain text) without delivering the message to the devices, you can set an optional HTTP or JSON parameter called <code>dry_run</code> with the value <code>true</code>. The result will be almost identical to running the request without this parameter, except that the message will not be delivered to the devices. Consequently, the response will contain fake IDs for the message and multicast fields (see <a href="#response">Response format</a>).</p>
+
+ <h4 id="example-requests">Example requests</h4>
+ <p>Here is the smallest possible request (a message without any parameters and just one recipient) using JSON:</p>
+ <pre class="prettyprint pretty-json">{ "registration_ids": [ "42" ] }</pre>
+
+ <p>And here the same example using plain text:</p>
+ <pre class="prettyprint">registration_id=42</pre>
+
+ <p> Here is a message with a payload and 6 recipients:</p>
+ <pre class="prettyprint pretty-HTML">{ "data": {
+ "score": "5x1",
+ "time": "15:10"
+ },
+ "registration_ids": ["4", "8", "15", "16", "23", "42"]
+}</pre>
+ <p>Here is a message with all optional fields set and 6 recipients:</p>
+ <pre class="prettyprint pretty-json">{ "collapse_key": "score_update",
+ "time_to_live": 108,
+ "delay_while_idle": true,
+ "data": {
+ "score": "4x8",
+ "time": "15:16.2342"
+ },
+ "registration_ids":["4", "8", "15", "16", "23", "42"]
+}</pre>
+ <p>And here is the same message using plain-text format (but just 1 recipient): </p>
+
+ <pre class="prettyprint">collapse_key=score_update&time_to_live=108&delay_while_idle=1&data.score=4x8&data.time=15:16.2342&registration_id=42
+ </pre>
+
+<p class="note"><strong>Note:</strong> If your organization has a firewall
+that restricts the traffic to or
+from the Internet, you need to configure it to allow connectivity with GCM in order for
+your Android devices to receive messages.
+The ports to open are: 5228, 5229, and 5230. GCM typically only uses 5228, but
+it sometimes uses 5229 and 5230. GCM doesn't provide specific IPs, so you should allow
+your firewall to accept outgoing connections to all IP addresses
+contained in the IP blocks listed in Google's ASN of 15169.</p>
+
+
+<h4 id="response">Response format</h4>
+
+<p>There are two possible outcomes when trying to send a message:</p>
+<ul>
+ <li>The message is processed successfully.</li>
+ <li>The GCM server rejects the request.</li>
+</ul>
+
+<p>When the message is processed successfully, the HTTP response has a 200 status and the body contains more information about the status of the message (including possible errors). When the request is rejected,
+the HTTP response contains a non-200 status code (such as 400, 401, or 503).</p>
+
+<p>The following table summarizes the statuses that the HTTP response header might contain. Click the troubleshoot link for advice on how to deal with each type of error.</p>
+<table border=1>
+ <tr>
+ <th>Response</th>
+ <th>Description</th>
+ </tr>
+ <tr>
+ <td>200</td>
+ <td>Message was processed successfully. The response body will contain more details about the message status, but its format will depend whether the request was JSON or plain text. See <a href="#success">Interpreting a success response</a> for more details.</td>
+ </tr>
+ <tr>
+ <td>400</td>
+ <td><span id="internal-source-marker_0.2">Only applies for JSON requests. Indicates that the request could not be parsed as JSON, or it contained invalid fields (for instance, passing a string where a number was expected). The exact failure reason is described in the response and the problem should be addressed before the request can be retried.</td>
+ </tr>
+ <tr>
+ <td>401</td>
+ <td>There was an error authenticating the sender account. <a href="#auth_error">Troubleshoot</a></td>
+ </tr>
+ <tr>
+ <td>5xx</td>
+ <td>Errors in the 500-599 range (such as 500 or 503) indicate that there was an internal error in the GCM server while trying to process the request, or that the server is temporarily unavailable (for example, because of timeouts). Sender must retry later, honoring any <code>Retry-After</code> header included in the response. Application servers must implement exponential back-off. <a href="#internal_error">Troubleshoot</a></td>
+ </tr>
+</table>
+
+<h4 id="success">Interpreting a success response</h4>
+<p>When a JSON request is successful (HTTP status code 200), the response body contains a JSON object with the following fields:</p>
+<table>
+ <tr>
+ <th>Field</th>
+ <th>Description</th>
+ </tr>
+ <tr>
+ <td><code>multicast_id</code></td>
+ <td>Unique ID (number) identifying the multicast message.</td>
+ </tr>
+ <tr>
+ <td><code>success</code></td>
+ <td>Number of messages that were processed without an error.</td>
+ </tr>
+ <tr>
+ <td><code>failure</code></td>
+ <td>Number of messages that could not be processed.</td>
+ </tr>
+ <tr>
+ <td><code>canonical_ids</code></td>
+ <td>Number of results that contain a canonical registration ID. See <a href="adv.html#canonical">Advanced Topics</a> for more discussion of this topic.</td>
+ </tr>
+ <tr>
+ <td><code>results</code></td>
+ <td>Array of objects representing the status of the messages processed. The objects are listed in the same order as the request (i.e., for each registration ID in the request, its result is listed in the same index in the response) and they can have these fields:<br>
+ <ul>
+ <li><code>message_id</code>: String representing the message when it was successfully processed.</li>
+ <li><code>registration_id</code>: If set, means that GCM processed the message but it has another canonical registration ID for that device, so sender should replace the IDs on future requests (otherwise they might be rejected). This field is never set if there is an error in the request.
+ </li>
+ <li><code>error</code>: String describing an error that occurred while processing the message for that recipient. The possible values are the same as documented in the above table, plus "Unavailable" (meaning GCM servers were busy and could not process the message for that particular recipient, so it could be retried).</li>
+ </ul></td>
+ </tr>
+</table>
+<p>If the value of <code>failure</code> and <code>canonical_ids</code> is 0, it's not necessary to parse the remainder of the response. Otherwise, we recommend that you iterate through the results field and do the following for each object in that list:</p>
+<ul>
+ <li>If <code>message_id</code> is set, check for <code>registration_id</code>:
+ <ul>
+ <li>If <code>registration_id</code> is set, replace the original ID with the new value (canonical ID) in your server database. Note that the original ID is not part of the result, so you need to obtain it from the list of <code>registration_ids</code> passed in the request (using the same index).</li>
+ </ul>
+ </li>
+ <li>Otherwise, get the value of <code>error</code>:
+ <ul>
+ <li>If it is <code>Unavailable</code>, you could retry to send it in another request.</li>
+ <li>If it is <code>NotRegistered</code>, you should remove the registration ID from your server database because the application was uninstalled from the device or it does not have a broadcast receiver configured to receive <code>com.google.android.c2dm.intent.RECEIVE</code> intents.</li>
+ <li>Otherwise, there is something wrong in the registration ID passed in the request; it is probably a non-recoverable error that will also require removing the registration from the server database. See <a href="#error_codes">Interpreting an error response</a> for all possible error values.</li>
+ </ul>
+ </li>
+</ul>
+
+<p>When a plain-text request is successful (HTTP status code 200), the response body contains 1 or 2 lines in the form of key/value pairs.
+The first line is always available and its content is either <code>id=<em>ID of sent message</em></code> or <code>Error=<em>GCM error code</em></code>. The second line, if available,
+has the format of <code>registration_id=<em>canonical ID</em></code>. The second line is optional, and it can only be sent if the first line is not an error. We recommend handling the plain-text response in a similar way as handling the JSON response:</p>
+<ul>
+ <li>If first line starts with <code>id</code>, check second line:
+ <ul>
+ <li>If second line starts with <code>registration_id</code>, gets its value and replace the registration IDs in your server database.</li>
+ </ul>
+ </li>
+ <li>Otherwise, get the value of <code>Error</code>:
+ <ul>
+ <li>If it is <code>NotRegistered</code>, remove the registration ID from your server database.</li>
+ <li>Otherwise, there is probably a non-recoverable error (<strong>Note: </strong>Plain-text requests will never return <code>Unavailable</code> as the error code, they would have returned a 500 HTTP status instead).</li>
+ </ul>
+ </li>
+</ul>
+
+<h4 id="error_codes">Interpreting an error response</h4>
+<p>Here are the recommendations for handling the different types of error that might occur when trying to send a message to a device:</p>
+
+<dl>
+<dt id="missing_reg"><strong>Missing Registration ID</strong></dt>
+<dd>Check that the request contains a registration ID (either in the <code>registration_id</code> parameter in a plain text message, or in the <code>registration_ids</code> field in JSON).
+<br/>Happens when error code is <code>MissingRegistration</code>.</dd>
+
+<dt id="invalid_reg"><strong>Invalid Registration ID</strong></dt>
+<dd>Check the formatting of the registration ID that you pass to the server. Make sure it matches the registration ID the phone receives in the <code>com.google.android.c2dm.intent.REGISTRATION</code> intent and that you're not truncating it or adding additional characters.
+<br/>Happens when error code is <code>InvalidRegistration</code>.</dd>
+
+<dt id="mismatched_sender"><strong>Mismatched Sender</strong></dt>
+<dd>A registration ID is tied to a certain group of senders. When an application registers for GCM usage, it must specify which senders are allowed to send messages. Make sure you're using one of those when trying to send messages to the device. If you switch to a different sender, the existing registration IDs won't work.
+Happens when error code is <code>MismatchSenderId</code>.</dd>
+
+<dt id="unreg_device"><strong>Unregistered Device</strong></dt>
+<dd>An existing registration ID may cease to be valid in a number of scenarios, including:
+<ul>
+ <li>If the application manually unregisters by issuing a <span class="prettyprint pretty-java"><code>com.google.android.c2dm.intent.UNREGISTER</code></span><code> </code>intent.</li>
+ <li>If the application is automatically unregistered, which can happen (but is not guaranteed) if the user uninstalls the application.</li>
+ <li>If the registration ID expires. Google might decide to refresh registration IDs. </li>
+ <li>If the application is updated but the new version does not have a broadcast receiver configured to receive <code>com.google.android.c2dm.intent.RECEIVE</code> intents.</li>
+</ul>
+For all these cases, you should remove this registration ID from the 3rd-party server and stop using it to send
+messages.
+<br/>Happens when error code is <code>NotRegistered</code>.</dd>
+
+<dt id="big_msg"><strong>Message Too Big</strong></dt>
+ <dd>The total size of the payload data that is included in a message can't exceed 4096 bytes. Note that this includes both the size of the keys as well as the values.
+<br/>Happens when error code is <code>MessageTooBig</code>.</dd>
+
+<dt id="invalid_datakey"><strong>Invalid Data Key</strong></dt>
+<dd>The payload data contains a key (such as <code>from</code> or any value prefixed by <code>google.</code>) that is used internally by GCM in the <code>com.google.android.c2dm.intent.RECEIVE</code> Intent and cannot be used. Note that some words (such as <code>collapse_key</code>) are also used by GCM but are allowed in the payload, in which case the payload value will be overridden by the GCM value.
+<br />
+Happens when the error code is <code>InvalidDataKey</code>.</dd>
+
+<dt id="ttl_error"><strong>Invalid Time To Live</strong></dt>
+ <dd>The value for the Time to Live field must be an integer representing a duration in seconds between 0 and 2,419,200 (4 weeks). Happens when error code is <code>InvalidTtl</code>.
+</dd>
+
+ <dt id="auth_error"><strong>Authentication Error</strong></dt>
+ <dd>The sender account that you're trying to use to send a message couldn't be authenticated. Possible causes are: <ul>
+<li>Authorization header missing or with invalid syntax.</li>
+<li>Invalid project number sent as key.</li>
+<li>Key valid but with GCM service disabled.</li>
+<li>Request originated from a server not whitelisted in the Server Key IPs.</li>
+
+</ul>
+Check that the token you're sending inside the <code>Authorization</code> header is the correct API key associated with your project. You can check the validity of your API key by running the following command:<br/>
+
+<pre># api_key=YOUR_API_KEY
+
+# curl --header "Authorization: key=$api_key" --header Content-Type:"application/json" https://android.googleapis.com/gcm/send -d "{\"registration_ids\":[\"ABC\"]}"</pre>
+
+
+
+If you receive a 401 HTTP status code, your API key is not valid. Otherwise you should see something like this:<br/>
+
+<pre>
+{"multicast_id":6782339717028231855,"success":0,"failure":1,"canonical_ids":0,"results":[{"error":"InvalidRegistration"}]}
+</pre>
+If you want to confirm the validity of a registration ID, you can do so by replacing "ABC" with the registration ID.
+<br/>
+Happens when the HTTP status code is 401.
+
+ <dt id="timeout"><strong>Timeout</strong></dt>
+
+<dd>The server couldn't process the request in time. You should retry the
+same request, but you MUST obey the following requirements:
+
+<ul>
+
+<li>Honor the <code>Retry-After</code> header if it's included in the response from the GCM server.</li>
+
+
+<li>Implement exponential back-off in your retry mechanism. This means an
+exponentially increasing delay after each failed retry (e.g. if you waited one
+second before the first retry, wait at least two second before the next one,
+then 4 seconds and so on). If you're sending multiple messages, delay each one
+independently by an additional random amount to avoid issuing a new request for
+all messages at the same time.</li>
+
+
+Senders that cause problems risk being blacklisted.
+<br />
+Happens when the HTTP status code is between 501 and 599, or when the <code>error</code> field of a JSON object in the results array is <code>Unavailable</code>.
+</dd>
+
+<dt id="internal_error"><strong>Internal Server Error</strong></dt>
+
+<dd>
+The server encountered an error while trying to process the request. You
+could retry the same request (obeying the requirements listed in the <a href="#timeout">Timeout</a>
+section), but if the error persists, please report the problem in the <a href="https://groups.google.com/forum/?fromgroups#!forum/android-gcm">android-gcm group</a>.
+<br />
+Happens when the HTTP status code is 500, or when the <code>error</code> field of a JSON
+object in the results array is <code>InternalServerError</code>.
+</dd>
+
+<dt id="restricted_package_name"><strong>Invalid Package Name</strong></dt>
+
+<dd>
+A message was addressed to a registration ID whose package name did not match the value passed in the request. Happens when error code is
+<code>InvalidPackageName</code>.
+</dd>
+
+
+</dl>
+<h4>Example responses</h4>
+<p>This section shows a few examples of responses indicating messages that were processed successfully. See <a href="#example-requests">Example requests</a> for the requests these responses are based on.</p>
+<p> Here is a simple case of a JSON message successfully sent to one recipient without canonical IDs in the response:</p>
+<pre class="prettyprint pretty-json">{ "multicast_id": 108,
+ "success": 1,
+ "failure": 0,
+ "canonical_ids": 0,
+ "results": [
+ { "message_id": "1:08" }
+ ]
+}</pre>
+
+<p>Or if the request was in plain-text format:</p>
+<pre class="prettyprint">id=1:08
+</pre>
+
+<p>Here are JSON results for 6 recipients (IDs 4, 8, 15, 16, 23, and 42 respectively) with 3 messages successfully processed, 1 canonical registration ID returned, and 3 errors:</p>
+<pre class="prettyprint pretty-json">{ "multicast_id": 216,
+ "success": 3,
+ "failure": 3,
+ "canonical_ids": 1,
+ "results": [
+ { "message_id": "1:0408" },
+ { "error": "Unavailable" },
+ { "error": "InvalidRegistration" },
+ { "message_id": "1:1516" },
+ { "message_id": "1:2342", "registration_id": "32" },
+ { "error": "NotRegistered"}
+ ]
+}
+</pre>
+<p> In this example:</p>
+<ul>
+ <li>First message: success, not required.</li>
+ <li>Second message: should be resent (to registration ID 8).</li>
+ <li>Third message: had an unrecoverable error (maybe the value got corrupted in the database).</li>
+ <li>Fourth message: success, nothing required.</li>
+ <li>Fifth message: success, but the registration ID should be updated in the server database (from 23 to 32).</li>
+ <li>Sixth message: registration ID (42) should be removed from the server database because the application was uninstalled from the device.</li>
+</ul>
+<p>Or if just the 4th message above was sent using plain-text format:</p>
+<pre class="prettyprint">Error=InvalidRegistration
+</pre>
+<p>If the 5th message above was also sent using plain-text format:</p>
+<pre class="prettyprint">id=1:2342
+registration_id=32
+</pre>
+
+<h3 id="stats">Viewing Statistics</h3>
+
+<p>To view statistics and any error messages for your GCM applications:</p>
+<ol>
+ <li> Go to the <code><a href="http://play.google.com/apps/publish">Developer Console</a></code>.</li>
+ <li>Login with your developer account.
+ <p>You will see a page that has a list of all of your apps.</p></li>
+ <li> Click on the "statistics" link next to the app for which you want to view GCM stats.
+ <p>Now you are on the statistics page.</p> </li>
+ <li>Go to the drop-down menu and select the GCM metric you want to view.
+ </li>
+</ol>
+<p class="note"><strong>Note:</strong> Stats on the Google API Console are not enabled for GCM. You must use the <a href="http://play.google.com/apps/publish">Developer Console</a>.</p>
+
diff --git a/docs/html/google/gcm/gs.jd b/docs/html/google/gcm/gs.jd
index f6b7ebe..8ceea0cc 100644
--- a/docs/html/google/gcm/gs.jd
+++ b/docs/html/google/gcm/gs.jd
@@ -1,4 +1,4 @@
-page.title=Getting Started
+page.title=Getting Started with GCM
page.tags="cloud","push","messaging"
@jd:body
@@ -12,7 +12,8 @@
<li><a href="#create-proj">Creating a Google API Project</a></li>
<li><a href="#gcm-service">Enabling the GCM Service</a></li>
<li><a href="#access-key">Obtaining an API Key</a></li>
-<li><a href="#next">Next Steps</a></li>
+<li><a href="#client">Writing a Client App</a></li>
+<li><a href="#server">Writing the Server Code</a></li>
</ol>
<h2>See Also</h2>
@@ -25,12 +26,12 @@
</div>
</div>
-<p>This document tells you how to get started setting up a GCM
+<p>The sections below guide you through the process of setting up a GCM
implementation.
-Before you begin, make sure to <a href="/google/play-services/setup.html">set up
-the Google Play Services SDK</a>. You need this SDK to use the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a> methods.</p>
+Before you start, make sure to <a href="/google/play-services/setup.html">set up
+the Google Play Services SDK</a>. You need this SDK to use the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> methods.</p>
+
+<p>Note that a full GCM implementation requires a server-side implementation, in addition to the client implementation in your app. This document offers a complete example that includes both the client and server.</p>
<h2 id="create-proj">Creating a Google API project</h2>
@@ -40,17 +41,13 @@
</li>
<li>If you haven't created an API project yet, this page will prompt you to do so:
<p><img src="{@docRoot}images/gcm/gcm-create-api-proj.png" class="screenshot" /></p>
-<p class="note"><strong>Note:</strong> If you already have existing projects,
-the first page you see will be the <strong>Dashboard</strong> page. From there
-you can create a new project by opening the project drop-down menu (upper left corner)
-and choosing <strong>Other projects > Create</strong>.</p></li>
+<p class="note"><strong>Note:</strong> If you already have existing projects, the first page you see will be the <strong>Dashboard</strong> page. From there you can create a new project by opening the project drop-down menu (upper left corner) and choosing <strong>Other projects > Create</strong>.</p></li>
<li> Click <strong>Create project</strong>.
Your browser URL will change to something like:</li>
<pre> https://code.google.com/apis/console/#project:<strong>4815162342</strong></pre>
- <li> Take note of the value after <code>#project:</code> (4815162342 in this
-example). This is your project number, and it will be used later on as the GCM sender ID.</li>
+ <li> Take note of the value after <code>#project:</code> (4815162342 in this example). This is your project number, and it will be used later on as the GCM sender ID.</li>
</ol>
<h2 id="gcm-service">Enabling the GCM Service</h2>
@@ -64,53 +61,463 @@
<h2 id="access-key">Obtaining an API Key</h2>
<p>To obtain an API key:</p>
<ol>
- <li> In the main Google APIs Console page, select <strong>API Access</strong>.
-You will see a screen that resembles the following:</li>
+ <li> In the main Google APIs Console page, select <strong>API Access</strong>. You will see a screen that resembles the following:</li><br />
-<img src="{@docRoot}images/gcm/gcm-api-access.png" style="width:400px;padding:4px;">
-
- <li>Click <strong>Create new Server key</strong>. Either a server key or a
-browser key should work. The advantage to using a server key is that it allows
-you to whitelist IP addresses. The following screen appears:</li>
+<img src="{@docRoot}images/gcm/gcm-api-access.png" style="width:400px;padding:4px;margin-bottom:0em;">
-<img src="{@docRoot}images/gcm/gcm-config-server-key.png" style="width:400px;padding:4px;">
+ <li>Click <strong>Create new Server key</strong>. Either a server key or a browser key should work. The advantage to using a server key is that it allows you to whitelist IP addresses. The following screen appears:</li><br />
+
+
+<img src="{@docRoot}images/gcm/gcm-config-server-key.png" style="width:400px;padding:4px;margin-bottom:0em;">
- <li>Click <strong>Create</strong>:</li>
+ <li>Click <strong>Create</strong>:</li><br />
-<img src="{@docRoot}images/gcm/gcm-api-key.png" style="width:400px;padding:4px;">
+<img src="{@docRoot}images/gcm/gcm-api-key.png" style="width:400px;padding:4px;margin-bottom:0em;">
</ol>
-<p> Take note of the <strong>API key</strong> value (<code>YourKeyWillBeShownHere</code>)
-in this example, as it will be used later on.</p>
-<p class="note"><strong>Note:</strong> If you need to rotate the key, click
-<strong>Generate new key</strong>. A new key will be created while the old one
-will still be active for up to 24 hours. If you want to get rid of the old key
-immediately (for example, if you feel it was compromised), click <strong>Delete key</strong>.</p>
+<p> Take note of the <strong>API key</strong> value (<code>YourKeyWillBeShownHere</code>) in this example, as it will be used later on.</p>
+<p class="note"><strong>Note:</strong> If you need to rotate the key, click <strong>Generate new key</strong>. A new key will be created while the old one will still be active for up to 24 hours. If you want to get rid of the old key immediately (for example, if you feel it was compromised), click <strong>Delete key</strong>.</p>
-<h2 id="next">Next Steps</h2>
+<p>The following sections walk you through the steps of creating client and server-side code.</p>
-<p>Once you've finished the tasks listed above, you're ready to start
-implementing GCM. Here is an overview of the basic steps:</p>
+<h2 id="client">Writing a Client App</h2>
-<ol>
- <li>Decide which Google-provided GCM connection server you want to use—
- <a href="http.html">HTTP</a> or <a href="ccs.html">XMPP</a> (CCS). GCM connection servers
-take messages from a 3rd-party application
-server (written by you) and send them to a GCM-enabled Android application (the
-"client app," also written by you) running on a device. </li>
- <li>Implement an application server (the "3rd-party application server") to interact
-with your chosen GCM connection server. The app server sends data to a
-GCM-enabled Android client application via the GCM connection server. For more
-information about implementing the server side, see <a href="server.html">
-Implementing GCM Server</a>.</li>
-<li>Write your client app. This is the GCM-enabled Android application that runs
-on a device. See <a href="client.html">Implementing GCM Client</a> for more information.</li>
-</ol>
+<p>This section walks you through the steps involved in writing a client-side application—that is, the GCM-enabled application that runs on an Android device. This client sample is designed to work in conjunction with the server code shown in <a href="#server">Writing the Server Code</a>, below.</p>
+
+
+
+<h3 id="manifest">Step 1: Edit Your App's Manifest</h3>
+<ul>
+ <li>The <code>com.google.android.c2dm.permission.RECEIVE</code> permission so the Android application can register and receive messages.</li>
+ <li>The <code>android.permission.INTERNET</code> permission so the Android application can send the registration ID to the 3rd party server.</li>
+ <li>The <code>android.permission.GET_ACCOUNTS</code> permission as GCM requires a Google account (necessary only if if the device is running a version lower than Android 4.0.4)</li>
+ <li>The <code>android.permission.WAKE_LOCK</code> permission so the application can keep the processor from sleeping when a message is received. Optional—use only if the app wants to keep the device from sleeping.</li>
+ <li>An <code>applicationPackage + ".permission.C2D_MESSAGE"</code> permission to prevent other Android applications from registering and receiving the Android application's
+messages. The permission name must exactly match this pattern—otherwise the Android application will not receive the messages.</li>
+ <li>A receiver for <code>com.google.android.c2dm.intent.RECEIVE</code>, with the category set
+as <code>applicationPackage</code>. The receiver should require the <code>com.google.android.c2dm.SEND</code> permission, so that only the GCM
+Framework can send a message to it. Note that the receiving
+of messages is implemented as an <a href="{@docRoot}guide/components/intents-filters.html">intent</a>.</li>
+ <li>An intent service to handle the intents received by the broadcast receiver. Optional.</li>
+ <li>If the GCM feature is critical to the Android application's function, be sure to
+set <code>android:minSdkVersion="8"</code> in the manifest. This
+ensures that the Android application cannot be installed in an environment in which it
+could not run properly. </li>
+</ul>
+
+<p>Here are excerpts from a manifest that supports GCM:</p>
+
+<pre class="prettyprint pretty-xml">
+<manifest package="com.example.gcm" ...>
+
+ <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/>
+ <uses-permission android:name="android.permission.INTERNET" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.WAKE_LOCK" />
+ <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
+
+ <permission android:name="com.example.gcm.permission.C2D_MESSAGE"
+ android:protectionLevel="signature" />
+ <uses-permission android:name="com.example.gcm.permission.C2D_MESSAGE" />
+
+ <application ...>
+ <receiver
+ android:name=".MyBroadcastReceiver"
+ android:permission="com.google.android.c2dm.permission.SEND" >
+ <intent-filter>
+ <action android:name="com.google.android.c2dm.intent.RECEIVE" />
+ <category android:name="com.example.gcm" />
+ </intent-filter>
+ </receiver>
+ <service android:name=".MyIntentService" />
+ </application>
+
+</manifest>
+</pre>
+
+
+<h3 id="register">Step 2: Register for GCM</h3>
+
+<p>An Android application running on a mobile device registers to receive messages by calling
+the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> method
+<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html#register">{@code register(senderID...)}</a>.
+This method registers the application for GCM and returns the registration ID. This streamlined approach replaces the previous
+GCM registration process. See the example below for details.</p>
+
+<h3 id="app"> Step 3: Write Your Application</h3>
+
+<p>Finally, write your application. GCM offers a variety of ways to get the job done:</p>
+
+<ul>
+ <li>For your messaging server, you can either use the new <a href="ccs.html">GCM Cloud Connection Server</a> (CCS), the older <a href="gcm.html">GCM HTTP server</a>, or both in tandem. For more discussion, see see <a href="server.html">GCM Server</a>.</li>
+ <li>To write your client application (that is, the GCM-enabled app that runs on an Android device), use the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> APIs as shown below. Don't forget to set up your project to use the Google Play services SDK as described in <a href="/google/play-services/setup.html">Setup Google Play Services SDK</a>.</li>
+</ul>
+</li>
+
+</ul>
+
+<h4 id="example">Example</h4>
+
+<p>Here is a sample client application that illustrates how to use the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> APIs. The sample consists of a main activity ({@code DemoActivity}) and a broadcast receiver ({@code GcmBroadcastReceiver}). You can use this client sample code in conjunction with the server code shown in <a href="#server">Writing the Server Code</a>.</p>
+
+<p>Note the following:</p>
+
+<ul>
+ <li>The sample primarily illustrates two things: registration, and upstream messaging. Upstream messaging only applies to apps that are running against a <a href="ccs.html">CCS</a> server; HTTP-based servers don't support upstream messaging.</li>
+ <li>The <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> registration APIs replace the old registration process, which was based on the now-obsolete client helper library. While the old registration process still works, we encourage you to use the newer <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> registration APIs, regardless of your underlying server.</li>
+</ul>
+
+<h5>Registering</h5>
+<p>An Android application needs to register with GCM servers before it can receive messages. So in its {@code onCreate()} method, {@code DemoActivity} checks to see whether the app is registered with GCM and with the server:</p>
+
+<pre>/**
+ * Main UI for the demo app.
+ */
+public class DemoActivity extends Activity {
+
+ public static final String EXTRA_MESSAGE = "message";
+ public static final String PROPERTY_REG_ID = "registration_id";
+ private static final String PROPERTY_APP_VERSION = "appVersion";
+ private static final String PROPERTY_ON_SERVER_EXPIRATION_TIME =
+ "onServerExpirationTimeMs";
+ /**
+ * Default lifespan (7 days) of a reservation until it is considered expired.
+ */
+ public static final long REGISTRATION_EXPIRY_TIME_MS = 1000 * 3600 * 24 * 7;
+
+ /**
+ * Substitute you own sender ID here.
+ */
+ String SENDER_ID = "Your-Sender-ID";
+
+ /**
+ * Tag used on log messages.
+ */
+ static final String TAG = "GCMDemo";
+
+ TextView mDisplay;
+ GoogleCloudMessaging gcm;
+ AtomicInteger msgId = new AtomicInteger();
+ SharedPreferences prefs;
+ Context context;
+
+ String regid;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.main);
+ mDisplay = (TextView) findViewById(R.id.display);
+
+ context = getApplicationContext();
+ regid = getRegistrationId(context);
+
+ if (regid.length() == 0) {
+ registerBackground();
+ }
+ gcm = GoogleCloudMessaging.getInstance(this);
+ }
+...
+}</pre>
+
+<p>The app calls {@code getRegistrationId()} to see whether there is an existing registration ID stored in shared preferences:</p>
+
+<pre>/**
+ * Gets the current registration id for application on GCM service.
+ * <p>
+ * If result is empty, the registration has failed.
+ *
+ * @return registration id, or empty string if the registration is not
+ * complete.
+ */
+private String getRegistrationId(Context context) {
+ final SharedPreferences prefs = getGCMPreferences(context);
+ String registrationId = prefs.getString(PROPERTY_REG_ID, "");
+ if (registrationId.length() == 0) {
+ Log.v(TAG, "Registration not found.");
+ return "";
+ }
+ // check if app was updated; if so, it must clear registration id to
+ // avoid a race condition if GCM sends a message
+ int registeredVersion = prefs.getInt(PROPERTY_APP_VERSION, Integer.MIN_VALUE);
+ int currentVersion = getAppVersion(context);
+ if (registeredVersion != currentVersion || isRegistrationExpired()) {
+ Log.v(TAG, "App version changed or registration expired.");
+ return "";
+ }
+ return registrationId;
+}
+
+...
+
+/**
+ * @return Application's {@code SharedPreferences}.
+ */
+private SharedPreferences getGCMPreferences(Context context) {
+ return getSharedPreferences(DemoActivity.class.getSimpleName(),
+ Context.MODE_PRIVATE);
+}</pre>
+
+<p>If the registration ID doesn't exist, or the app was updated, or the registration ID has expired, {@code getRegistrationId()} returns an empty string to indicate that the app needs to get a new regID. {@code getRegistrationId()} calls the following methods to check the app version and whether the regID has expired:</p>
+
+<pre>/**
+ * @return Application's version code from the {@code PackageManager}.
+ */
+private static int getAppVersion(Context context) {
+ try {
+ PackageInfo packageInfo = context.getPackageManager()
+ .getPackageInfo(context.getPackageName(), 0);
+ return packageInfo.versionCode;
+ } catch (NameNotFoundException e) {
+ // should never happen
+ throw new RuntimeException("Could not get package name: " + e);
+ }
+}
+
+/**
+ * Checks if the registration has expired.
+ *
+ * <p>To avoid the scenario where the device sends the registration to the
+ * server but the server loses it, the app developer may choose to re-register
+ * after REGISTRATION_EXPIRY_TIME_MS.
+ *
+ * @return true if the registration has expired.
+ */
+private boolean isRegistrationExpired() {
+ final SharedPreferences prefs = getGCMPreferences(context);
+ // checks if the information is not stale
+ long expirationTime =
+ prefs.getLong(PROPERTY_ON_SERVER_EXPIRATION_TIME, -1);
+ return System.currentTimeMillis() > expirationTime;
+}</pre>
+
+
+<p>If there isn't a valid existing registration ID, {@code DemoActivity} calls the following {@code registerBackground()} method to register. Note that because GCM methods are blocking, this has to take place on a background thread. This sample uses {@link android.os.AsyncTask} to accomplish this:</p>
+
+<pre>
+/**
+ * Registers the application with GCM servers asynchronously.
+ * <p>
+ * Stores the registration id, app versionCode, and expiration time in the
+ * application's shared preferences.
+ */
+private void registerBackground() {
+ new AsyncTask<Void, Void, String>() {
+ @Override
+ protected String doInBackground(Void... params) {
+ String msg = "";
+ try {
+ if (gcm == null) {
+ gcm = GoogleCloudMessaging.getInstance(context);
+ }
+ regid = gcm.register(SENDER_ID);
+ msg = "Device registered, registration id=" + regid;
+
+ // You should send the registration ID to your server over HTTP,
+ // so it can use GCM/HTTP or CCS to send messages to your app.
+
+ // For this demo: we don't need to send it because the device
+ // will send upstream messages to a server that echo back the message
+ // using the 'from' address in the message.
+
+ // Save the regid - no need to register again.
+ setRegistrationId(context, regid);
+ } catch (IOException ex) {
+ msg = "Error :" + ex.getMessage();
+ }
+ return msg;
+ }
+
+ @Override
+ protected void onPostExecute(String msg) {
+ mDisplay.append(msg + "\n");
+ }
+ }.execute(null, null, null);
+}</pre>
+
+<p>After registering, the app calls {@code setRegistrationId()} to store the registration ID in shared preferences for future use:</p>
+
+<pre>/**
+ * Stores the registration id, app versionCode, and expiration time in the
+ * application's {@code SharedPreferences}.
+ *
+ * @param context application's context.
+ * @param regId registration id
+ */
+private void setRegistrationId(Context context, String regId) {
+ final SharedPreferences prefs = getGCMPreferences(context);
+ int appVersion = getAppVersion(context);
+ Log.v(TAG, "Saving regId on app version " + appVersion);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString(PROPERTY_REG_ID, regId);
+ editor.putInt(PROPERTY_APP_VERSION, appVersion);
+ long expirationTime = System.currentTimeMillis() + REGISTRATION_EXPIRY_TIME_MS;
+
+ Log.v(TAG, "Setting registration expiry time to " +
+ new Timestamp(expirationTime));
+ editor.putLong(PROPERTY_ON_SERVER_EXPIRATION_TIME, expirationTime);
+ editor.commit();
+}</pre>
+
+<h5>Sending a message</h5>
+<p>When the user clicks the app's <strong>Send</strong> button, the app sends an upstream message using the new <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">{@code GoogleCloudMessaging}</a> APIs. In order to receive the upstream message, your server should be connected to CCS. You can use the code shown in <a href="#server">Writing the Server Code</a> as a sample XMPP client to connect to CCS.</p>
+
+<pre>public void onClick(final View view) {
+ if (view == findViewById(R.id.send)) {
+ new AsyncTask<Void, Void, String>() {
+ @Override
+ protected String doInBackground(Void... params) {
+ String msg = "";
+ try {
+ Bundle data = new Bundle();
+ data.putString("hello", "World");
+ String id = Integer.toString(msgId.incrementAndGet());
+ gcm.send(SENDER_ID + "@gcm.googleapis.com", id, data);
+ msg = "Sent message";
+ } catch (IOException ex) {
+ msg = "Error :" + ex.getMessage();
+ }
+ return msg;
+ }
+
+ @Override
+ protected void onPostExecute(String msg) {
+ mDisplay.append(msg + "\n");
+ }
+ }.execute(null, null, null);
+ } else if (view == findViewById(R.id.clear)) {
+ mDisplay.setText("");
+ }
+}</pre>
+
+<p>As described above in <a href="#manifest">Step 1</a>, the app includes a broadcast receiver for the <code>com.google.android.c2dm.intent.RECEIVE</code> intent. This is the mechanism GCM uses to deliver messages. When {@code onClick()} calls {@code gcm.send()}, it triggers the broadcast receiver's {@code onReceive()} method, which has the responsibility of handling the GCM message. In this sample the receiver's {@code onReceive()} method calls {@code sendNotification()} to put the message into a notification:</p>
+
+<pre>/**
+ * Handling of GCM messages.
+ */
+public class GcmBroadcastReceiver extends BroadcastReceiver {
+ static final String TAG = "GCMDemo";
+ public static final int NOTIFICATION_ID = 1;
+ private NotificationManager mNotificationManager;
+ NotificationCompat.Builder builder;
+ Context ctx;
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ GoogleCloudMessaging gcm = GoogleCloudMessaging.getInstance(context);
+ ctx = context;
+ String messageType = gcm.getMessageType(intent);
+ if (GoogleCloudMessaging.MESSAGE_TYPE_SEND_ERROR.equals(messageType)) {
+ sendNotification("Send error: " + intent.getExtras().toString());
+ } else if (GoogleCloudMessaging.MESSAGE_TYPE_DELETED.equals(messageType)) {
+ sendNotification("Deleted messages on server: " +
+ intent.getExtras().toString());
+ } else {
+ sendNotification("Received: " + intent.getExtras().toString());
+ }
+ setResultCode(Activity.RESULT_OK);
+ }
+
+ // Put the GCM message into a notification and post it.
+ private void sendNotification(String msg) {
+ mNotificationManager = (NotificationManager)
+ ctx.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ PendingIntent contentIntent = PendingIntent.getActivity(ctx, 0,
+ new Intent(ctx, DemoActivity.class), 0);
+
+ NotificationCompat.Builder mBuilder =
+ new NotificationCompat.Builder(ctx)
+ .setSmallIcon(R.drawable.ic_stat_gcm)
+ .setContentTitle("GCM Notification")
+ .setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(msg))
+ .setContentText(msg);
+
+ mBuilder.setContentIntent(contentIntent);
+ mNotificationManager.notify(NOTIFICATION_ID, mBuilder.build());
+ }
+}</pre>
+
+<h2 id="server">Writing the Server Code</h2>
+
+<p>Here is an example of a CCS server written in Python. You can use this in conjunction with the sample client code shown above. This sample echo server sends an initial message, and for every upstream message received, it sends a dummy response back to the application that sent the upstream message. This example illustrates how to connect,
+send, and receive GCM messages using XMPP. It shouldn't be used as-is
+on a production deployment. For examples of HTTP-based servers, see <a href="server.html">GCM Server</a>.</p>
+
+<pre>
+#!/usr/bin/python
+import sys, json, xmpp, random, string
+
+SERVER = 'gcm.googleapis.com'
+PORT = 5235
+USERNAME = ''
+PASSWORD = ''
+REGISTRATION_ID = ''
+
+unacked_messages_quota = 1000
+send_queue = []
+
+# Return a random alphanumerical id
+def random_id():
+ rid = ''
+ for x in range(8): rid += random.choice(string.ascii_letters + string.digits)
+ return rid
+
+def message_callback(session, message):
+ global unacked_messages_quota
+ gcm = message.getTags('gcm')
+ if gcm:
+ gcm_json = gcm[0].getData()
+ msg = json.loads(gcm_json)
+ if not msg.has_key('message_type'):
+ # Acknowledge the incoming message immediately.
+ send({'to': msg['from'],
+ 'message_type': 'ack',
+ 'message_id': msg['message_id']})
+ # Queue a response back to the server.
+ if msg.has_key('from'):
+ # Send a dummy echo response back to the app that sent the upstream message.
+ send_queue.append({'to': msg['from'],
+ 'message_id': random_id(),
+ 'data': {'pong': 1}})
+ elif msg['message_type'] == 'ack' or msg['message_type'] == 'nack':
+ unacked_messages_quota += 1
+
+def send(json_dict):
+ template = ("<message><gcm xmlns='google:mobile:data'>{1}</gcm></message>")
+ client.send(xmpp.protocol.Message(
+ node=template.format(client.Bind.bound[0], json.dumps(json_dict))))
+
+def flush_queued_messages():
+ global unacked_messages_quota
+ while len(send_queue) and unacked_messages_quota > 0:
+ send(send_queue.pop(0))
+ unacked_messages_quota -= 1
+
+client = xmpp.Client('gcm.googleapis.com', debug=['socket'])
+client.connect(server=(SERVER,PORT), secure=1, use_srv=False)
+auth = client.auth(USERNAME, PASSWORD)
+if not auth:
+ print 'Authentication failed!'
+ sys.exit(1)
+
+client.RegisterHandler('message', message_callback)
+
+send_queue.append({'to': REGISTRATION_ID,
+ 'message_id': 'reg_id',
+ 'data': {'message_destination': 'RegId',
+ 'message_id': random_id()}})
+
+while True:
+ client.Process(1)
+ flush_queued_messages()</pre>
+
diff --git a/docs/html/google/gcm/http.jd b/docs/html/google/gcm/http.jd
deleted file mode 100644
index b8d8659..0000000
--- a/docs/html/google/gcm/http.jd
+++ /dev/null
@@ -1,618 +0,0 @@
-page.title=GCM HTTP Connection Server
-@jd:body
-
-<div id="qv-wrapper">
-<div id="qv">
-
-
-<h2>In this document</h2>
-
-<ol class="toc">
- <li><a href="#auth">Authentication</a> </li>
- <li><a href="#request">Request Format</a> </li>
- <li><a href="#response">Response Format</a>
- <ol class="toc">
- <li><a href="#success">Interpreting a success response</a>
- <li><a href="#error_codes">Interpreting an error response</a>
- <li><a href="#example-responses">Example responses</a>
- </ol>
- </li>
- <li><a href="#app-server">Implementing an HTTP-Based App Server</a>
-</ol>
-
-<h2>See Also</h2>
-
-<ol class="toc">
-<li><a href="gs.html">Getting Started</a></li>
-<li><a href="client.html">Implementing GCM Client</a></li>
-<li><a href="ccs.html">Cloud Connection Server</a></li>
-
-
-</ol>
-
-</div>
-</div>
-
-<p>This document describes the GCM HTTP connection server. Connection servers
-are the Google-provided servers that take messages from the 3rd-party
-application server and sending them to the device.</p>
-
-
-
-<p class="note"><strong>Note:</strong> See
-<a href="server.html#params">Implementing GCM Server</a> for a list of all the message
-parameters and which connection server(s) supports them.</p>
-
-
-<h2 id="auth">Authentication</h2>
-
-<p>To send a message, the application server issues a POST request to
-<code>https://android.googleapis.com/gcm/send</code>.</p>
-<p>A message request is made of 2 parts: HTTP header and HTTP body.</p>
-
-<p>The HTTP header must contain the following headers:</p>
-<ul>
- <li><code>Authorization</code>: key=YOUR_API_KEY</li>
- <li><code>Content-Type</code>: <code>application/json</code> for JSON; <code>application/x-www-form-urlencoded;charset=UTF-8</code> for plain text.
- </li>
-</ul>
-
-<p>For example:
-</p>
-<pre>Content-Type:application/json
-Authorization:key=AIzaSyB-1uEai2WiUapxCs2Q0GZYzPu7Udno5aA
-
-{
- "registration_ids" : ["APA91bHun4MxP5egoKMwt2KZFBaFUH-1RYqx..."],
- "data" : {
- ...
- },
-}</pre>
-<p class="note">
- <p><strong>Note:</strong> If <code>Content-Type</code> is omitted, the format
-is assumed to be plain text.</p>
-</p>
-
-<p>The HTTP body content depends on whether you're using JSON or plain text.
-See
-<a href="server.html#params">Implementing GCM Server</a> for a list of all the
-parameters your JSON or plain text message can contain.</p>
-
-
- <h2 id="request">Request Format</h2>
- <p>Here is the smallest possible request (a message without any parameters and
-just one recipient) using JSON:</p>
- <pre class="prettyprint pretty-json">{ "registration_ids": [ "42" ] }</pre>
-
- <p>And here the same example using plain text:</p>
- <pre class="prettyprint">registration_id=42</pre>
-
- <p> Here is a message with a payload and 6 recipients:</p>
- <pre class="prettyprint pretty-HTML">{ "data": {
- "score": "5x1",
- "time": "15:10"
- },
- "registration_ids": ["4", "8", "15", "16", "23", "42"]
-}</pre>
- <p>Here is a message with all optional fields set and 6 recipients:</p>
- <pre class="prettyprint pretty-json">{ "collapse_key": "score_update",
- "time_to_live": 108,
- "delay_while_idle": true,
- "data": {
- "score": "4x8",
- "time": "15:16.2342"
- },
- "registration_ids":["4", "8", "15", "16", "23", "42"]
-}</pre>
- <p>And here is the same message using plain-text format (but just 1 recipient): </p>
-
- <pre class="prettyprint">collapse_key=score_update&time_to_live=108&delay_while_idle=1&data.score=4x8&data.time=15:16.2342&registration_id=42
- </pre>
-
-<p class="note"><strong>Note:</strong> If your organization has a firewall
-that restricts the traffic to or
-from the Internet, you need to configure it to allow connectivity with GCM in order for
-your Android devices to receive messages.
-The ports to open are: 5228, 5229, and 5230. GCM typically only uses 5228, but
-it sometimes uses 5229 and 5230. GCM doesn't provide specific IPs, so you should allow
-your firewall to accept outgoing connections to all IP addresses
-contained in the IP blocks listed in Google's ASN of 15169.</p>
-
-
-
-<h2 id="response">Response format</h2>
-
-<p>There are two possible outcomes when trying to send a message:</p>
-<ul>
- <li>The message is processed successfully.</li>
- <li>The GCM server rejects the request.</li>
-</ul>
-
-<p>When the message is processed successfully, the HTTP response has a 200 status
-and the body contains more information about the status of the message
-(including possible errors). When the request is rejected,
-the HTTP response contains a non-200 status code (such as 400, 401, or 503).</p>
-
-<p>The following table summarizes the statuses that the HTTP response header might
-contain. Click the troubleshoot link for advice on how to deal with each type of
-error.</p>
-<table border=1>
- <tr>
- <th>Response</th>
- <th>Description</th>
- </tr>
- <tr>
- <td>200</td>
- <td>Message was processed successfully. The response body will contain more
-details about the message status, but its format will depend whether the request
-was JSON or plain text. See <a href="#success">Interpreting a success response</a>
-for more details.</td>
- </tr>
- <tr>
- <td>400</td>
- <td><span id="internal-source-marker_0.2">Only applies for JSON requests.
-Indicates that the request could not be parsed as JSON, or it contained invalid
-fields (for instance, passing a string where a number was expected). The exact
-failure reason is described in the response and the problem should be addressed
-before the request can be retried.</td>
- </tr>
- <tr>
- <td>401</td>
- <td>There was an error authenticating the sender account.
-<a href="#auth_error">Troubleshoot</a></td>
- </tr>
- <tr>
- <td>5xx</td>
- <td>Errors in the 500-599 range (such as 500 or 503) indicate that there wa
-an internal error in the GCM server while trying to process the request, or that
-the server is temporarily unavailable (for example, because of timeouts). Sender
-must retry later, honoring any <code>Retry-After</code> header included in the
-response. Application servers must implement exponential back-off.
-<a href="#internal_error">Troubleshoot</a></td>
- </tr>
-</table>
-
-<h3 id="success">Interpreting a success response</h3>
-<p>When a JSON request is successful (HTTP status code 200), the response body
-contains a JSON object with the following fields:</p>
-<table>
- <tr>
- <th>Field</th>
- <th>Description</th>
- </tr>
- <tr>
- <td><code>multicast_id</code></td>
- <td>Unique ID (number) identifying the multicast message.</td>
- </tr>
- <tr>
- <td><code>success</code></td>
- <td>Number of messages that were processed without an error.</td>
- </tr>
- <tr>
- <td><code>failure</code></td>
- <td>Number of messages that could not be processed.</td>
- </tr>
- <tr>
- <td><code>canonical_ids</code></td>
- <td>Number of results that contain a canonical registration ID. See
-<a href="adv.html#canonical">Advanced Topics</a> for more discussion of this topic.</td>
- </tr>
- <tr>
- <td><code>results</code></td>
- <td>Array of objects representing the status of the messages processed. The
-objects are listed in the same order as the request (i.e., for each registration
-ID in the request, its result is listed in the same index in the response) and
-they can have these fields:<br>
- <ul>
- <li><code>message_id</code>: String representing the message when it was
-successfully processed.</li>
- <li><code>registration_id</code>: If set, means that GCM processed the
-message but it has another canonical registration ID for that device, so sender
-should replace the IDs on future requests (otherwise they might be rejected).
-This field is never set if there is an error in the request.
- </li>
- <li><code>error</code>: String describing an error that occurred while
-processing the message for that recipient. The possible values are the same as
-documented in the above table, plus "Unavailable" (meaning GCM servers
-were busy and could not process the message for that particular recipient, so
-it could be retried).</li>
- </ul></td>
- </tr>
-</table>
-<p>If the value of <code>failure</code> and <code>canonical_ids</code> is 0, it's
-not necessary to parse the remainder of the response. Otherwise, we recommend
-that you iterate through the results field and do the following for each object
-in that list:</p>
-<ul>
- <li>If <code>message_id</code> is set, check for <code>registration_id</code>:
- <ul>
- <li>If <code>registration_id</code> is set, replace the original ID with
-the new value (canonical ID) in your server database. Note that the original ID
-is not part of the result, so you need to obtain it from the list of
-code>registration_ids</code> passed in the request (using the same index).</li>
- </ul>
- </li>
- <li>Otherwise, get the value of <code>error</code>:
- <ul>
- <li>If it is <code>Unavailable</code>, you could retry to send it in another
-request.</li>
- <li>If it is <code>NotRegistered</code>, you should remove the registration
-ID from your server database because the application was uninstalled from the
-device or it does not have a broadcast receiver configured to receive
-<code>com.google.android.c2dm.intent.RECEIVE</code> intents.</li>
- <li>Otherwise, there is something wrong in the registration ID passed in
-the request; it is probably a non-recoverable error that will also require removing
-the registration from the server database. See <a href="#error_codes">Interpreting
-an error response</a> for all possible error values.</li>
- </ul>
- </li>
-</ul>
-
-<p>When a plain-text request is successful (HTTP status code 200), the response
-body contains 1 or 2 lines in the form of key/value pairs.
-The first line is always available and its content is either <code>id=<em>ID of
-sent message</em></code> or <code>Error=<em>GCM error code</em></code>. The second
-line, if available,
-has the format of <code>registration_id=<em>canonical ID</em></code>. The second
-line is optional, and it can only be sent if the first line is not an error. We
-recommend handling the plain-text response in a similar way as handling the
-JSON response:</p>
-<ul>
- <li>If first line starts with <code>id</code>, check second line:
- <ul>
- <li>If second line starts with <code>registration_id</code>, gets its value
-and replace the registration IDs in your server database.</li>
- </ul>
- </li>
- <li>Otherwise, get the value of <code>Error</code>:
- <ul>
- <li>If it is <code>NotRegistered</code>, remove the registration ID from
-your server database.</li>
- <li>Otherwise, there is probably a non-recoverable error (<strong>Note:
-</strong>Plain-text requests will never return <code>Unavailable</code> as the
-error code, they would have returned a 500 HTTP status instead).</li>
- </ul>
- </li>
-</ul>
-
-<h3 id="error_codes">Interpreting an error response</h3>
-<p>Here are the recommendations for handling the different types of error that
-might occur when trying to send a message to a device:</p>
-
-<dl>
-<dt id="missing_reg"><strong>Missing Registration ID</strong></dt>
-<dd>Check that the request contains a registration ID (either in the
-<code>registration_id</code> parameter in a plain text message, or in the
-<code>registration_ids</code> field in JSON).
-<br/>Happens when error code is <code>MissingRegistration</code>.</dd>
-
-<dt id="invalid_reg"><strong>Invalid Registration ID</strong></dt>
-<dd>Check the formatting of the registration ID that you pass to the server. Make
-sure it matches the registration ID the phone receives in the
-<code>com.google.android.c2dm.intent.REGISTRATION</code> intent and that you're
-not truncating it or adding additional characters.
-<br/>Happens when error code is <code>InvalidRegistration</code>.</dd>
-
-<dt id="mismatched_sender"><strong>Mismatched Sender</strong></dt>
-<dd>A registration ID is tied to a certain group of senders. When an application
-registers for GCM usage, it must specify which senders are allowed to send messages.
-Make sure you're using one of those when trying to send messages to the device.
-If you switch to a different sender, the existing registration IDs won't work.
-Happens when error code is <code>MismatchSenderId</code>.</dd>
-
-<dt id="unreg_device"><strong>Unregistered Device</strong></dt>
-<dd>An existing registration ID may cease to be valid in a number of scenarios, including:
-<ul>
- <li>If the application manually unregisters by issuing a
-<span class="prettyprint pretty-java">
-<code>com.google.android.c2dm.intent.UNREGISTER</code></span><code>
-</code>intent.</li>
- <li>If the application is automatically unregistered, which can happen
-(but is not guaranteed) if the user uninstalls the application.</li>
- <li>If the registration ID expires. Google might decide to refresh registration
-IDs. </li>
- <li>If the application is updated but the new version does not have a broadcast
-receiver configured to receive <code>com.google.android.c2dm.intent.RECEIVE</code>
-intents.</li>
-</ul>
-For all these cases, you should remove this registration ID from the 3rd-party
-server and stop using it to send
-messages.
-<br/>Happens when error code is <code>NotRegistered</code>.</dd>
-
-<dt id="big_msg"><strong>Message Too Big</strong></dt>
- <dd>The total size of the payload data that is included in a message can't
-exceed 4096 bytes. Note that this includes both the size of the keys as well
-as the values.
-<br/>Happens when error code is <code>MessageTooBig</code>.</dd>
-
-<dt id="invalid_datakey"><strong>Invalid Data Key</strong></dt>
-<dd>The payload data contains a key (such as <code>from</code> or any value
-prefixed by <code>google.</code>) that is used internally by GCM in the
-<code>com.google.android.c2dm.intent.RECEIVE</code> Intent and cannot be used.
-Note that some words (such as <code>collapse_key</code>) are also used by GCM
-but are allowed in the payload, in which case the payload value will be
-overridden by the GCM value.
-<br />
-Happens when the error code is <code>InvalidDataKey</code>.</dd>
-
-<dt id="ttl_error"><strong>Invalid Time To Live</strong></dt>
- <dd>The value for the Time to Live field must be an integer representing
-a duration in seconds between 0 and 2,419,200 (4 weeks). Happens when error code
-is <code>InvalidTtl</code>.
-</dd>
-
- <dt id="auth_error"><strong>Authentication Error</strong></dt>
- <dd>The sender account that you're trying to use to send a message couldn't be
-authenticated. Possible causes are: <ul>
-<li>Authorization header missing or with invalid syntax.</li>
-<li>Invalid project number sent as key.</li>
-<li>Key valid but with GCM service disabled.</li>
-<li>Request originated from a server not whitelisted in the Server Key IPs.</li>
-
-</ul>
-Check that the token you're sending inside the <code>Authorization</code> header
-is the correct API key associated with your project. You can check the validity
-of your API key by running the following command:<br/>
-
-<pre># api_key=YOUR_API_KEY
-
-# curl --header "Authorization: key=$api_key" --header Content-Type:"application/json" https://android.googleapis.com/gcm/send -d "{\"registration_ids\":[\"ABC\"]}"</pre>
-
-
-
-If you receive a 401 HTTP status code, your API key is not valid. Otherwise you
-should see something like this:<br/>
-
-<pre>
-{"multicast_id":6782339717028231855,"success":0,"failure":1,"canonical_ids":0,"results":[{"error":"InvalidRegistration"}]}
-</pre>
-If you want to confirm the validity of a registration ID, you can do so by
-replacing "ABC" with the registration ID.
-<br/>
-Happens when the HTTP status code is 401.
-
- <dt id="timeout"><strong>Timeout</strong></dt>
-
-<dd>The server couldn't process the request in time. You should retry the
-same request, but you MUST obey the following requirements:
-
-<ul>
-
-<li>Honor the <code>Retry-After</code> header if it's included in the response
-from the GCM server.</li>
-
-
-<li>Implement exponential back-off in your retry mechanism. This means an
-exponentially increasing delay after each failed retry (e.g. if you waited one
-second before the first retry, wait at least two second before the next one,
-then 4 seconds and so on). If you're sending multiple messages, delay each one
-independently by an additional random amount to avoid issuing a new request for
-all messages at the same time.</li>
-
-
-Senders that cause problems risk being blacklisted.
-<br />
-Happens when the HTTP status code is between 501 and 599, or when the
-<code>error</code> field of a JSON object in the results array is <code>Unavailable</code>.
-</dd>
-
-<dt id="internal_error"><strong>Internal Server Error</strong></dt>
-
-<dd>
-The server encountered an error while trying to process the request. You
-could retry the same request (obeying the requirements listed in the <a href="#timeout">Timeout</a>
-section), but if the error persists, please report the problem in the
-<a href="https://groups.google.com/forum/?fromgroups#!forum/android-gcm">android-gcm group</a>.
-<br />
-Happens when the HTTP status code is 500, or when the <code>error</code> field of a JSON
-object in the results array is <code>InternalServerError</code>.
-</dd>
-
-<dt id="restricted_package_name"><strong>Invalid Package Name</strong></dt>
-
-<dd>
-A message was addressed to a registration ID whose package name did not match
-the value passed in the request. Happens when error code is
-<code>InvalidPackageName</code>.
-</dd>
-</dl>
-
-<h3 id="example-responses">Example responses</h3>
-<p>This section shows a few examples of responses indicating messages that were
-processed successfully. See <a href="#request">Request Format</a> for
-the requests these responses are based on.</p>
-<p> Here is a simple case of a JSON message successfully sent to one recipient
-without canonical IDs in the response:</p>
-<pre class="prettyprint pretty-json">{ "multicast_id": 108,
- "success": 1,
- "failure": 0,
- "canonical_ids": 0,
- "results": [
- { "message_id": "1:08" }
- ]
-}</pre>
-
-<p>Or if the request was in plain-text format:</p>
-<pre class="prettyprint">id=1:08
-</pre>
-
-<p>Here are JSON results for 6 recipients (IDs 4, 8, 15, 16, 23, and 42 respectively)
-with 3 messages successfully processed, 1 canonical registration ID returned,
-and 3 errors:</p>
-<pre class="prettyprint pretty-json">{ "multicast_id": 216,
- "success": 3,
- "failure": 3,
- "canonical_ids": 1,
- "results": [
- { "message_id": "1:0408" },
- { "error": "Unavailable" },
- { "error": "InvalidRegistration" },
- { "message_id": "1:1516" },
- { "message_id": "1:2342", "registration_id": "32" },
- { "error": "NotRegistered"}
- ]
-}
-</pre>
-<p> In this example:</p>
-<ul>
- <li>First message: success, not required.</li>
- <li>Second message: should be resent (to registration ID 8).</li>
- <li>Third message: had an unrecoverable error (maybe the value got corrupted
-in the database).</li>
- <li>Fourth message: success, nothing required.</li>
- <li>Fifth message: success, but the registration ID should be updated in the
-server database (from 23 to 32).</li>
- <li>Sixth message: registration ID (42) should be removed from the server database
-because the application was uninstalled from the device.</li>
-</ul>
-<p>Or if just the 4th message above was sent using plain-text format:</p>
-<pre class="prettyprint">Error=InvalidRegistration
-</pre>
-<p>If the 5th message above was also sent using plain-text format:</p>
-<pre class="prettyprint">id=1:2342
-registration_id=32
-</pre>
-
-
-<h2 id="app-server">Implementing an HTTP-Based App Server</h2>
-
-<p>This section gives examples of implementing an app server that works with the
-GCM HTTP connection server. Note that a full GCM implementation requires a
-client-side implementation, in addition to the server.</a>
-
-
-<p>Requirements</p>
-<p>For the web server:</p>
-<ul>
- <li> <a href="http://ant.apache.org/">Ant 1.8</a> (it might work with earlier versions, but it's not guaranteed).</li>
- <li>One of the following:
- <ul>
- <li>A running web server compatible with Servlets API version 2.5, such as
-<a href="http://tomcat.apache.org/">Tomcat 6</a> or <a href="http://jetty.codehaus.org/">Jetty</a>, or</li>
- <li><a href="http://code.google.com/appengine/">Java App Engine SDK</a>
-version 1.6 or later.</li>
- </ul>
- </li>
- <li>A Google account registered to use GCM.</li>
- <li>The API key for that account.</li>
-</ul>
-<p>For the Android application:</p>
-<ul>
- <li>Emulator (or device) running Android 2.2 with Google APIs.</li>
- <li>The Google API project number of the account registered to use GCM.</li>
-</ul>
-
-<h3 id="gcm-setup">Setting Up GCM</h3>
-<p>Before proceeding with the server and client setup, it's necessary to register
-a Google account with the Google API Console, enable Google Cloud Messaging in GCM,
-and obtain an API key from the <a href="https://code.google.com/apis/console">
-Google API Console</a>.</p>
-<p>For instructions on how to set up GCM, see <a href="gs.html">Getting Started</a>.</p>
-
-
-<h3 id="server-setup">Setting Up an HTTP Server</h3>
-<p>This section describes the different options for setting up an HTTP server.</p>
-
-<h4 id="webserver-setup">Using a standard web server</h4>
-<p>To set up the server using a standard, servlet-compliant web server:</p>
-<ol>
- <li>From the <a href="http://code.google.com/p/gcm">open source site</a>,
-download the following directories: <code>gcm-server</code>,
-<code>samples/gcm-demo-server</code>, and <code>samples/gcm-demo-appengine</code>.</p>
-
-
- <li>In a text editor, edit the <code>samples/gcm-demo-server/WebContent/WEB-INF/classes/api.key</code> and replace the existing text with the API key obtained above.</li>
- <li>In a shell window, go to the <code>samples/gcm-demo-server</code> directory.</li>
- <li>Generate the server's WAR file by running <code>ant war</code>:</li>
-
- <pre class="prettyprint">$ ant war
-
-Buildfile:build.xml
-
-init:
- [mkdir] Created dir: build/classes
- [mkdir] Created dir: dist
-
-compile:
- [javac] Compiling 6 source files to build/classes
-
-war:
- [war] Building war: <strong>dist/gcm-demo.war</strong>
-
-BUILD SUCCESSFUL
-Total time: 0 seconds
-</pre>
-
- <li>Deploy the <code>dist/gcm-demo.war</code> to your running server. For instance, if you're using Jetty, copy <code>gcm-demo.war</code> to the <code>webapps</code> directory of the Jetty installation.</li>
- <li>Open the server's main page in a browser. The URL depends on the server you're using and your machine's IP address, but it will be something like <code>http://192.168.1.10:8080/gcm-demo/home</code>, where <code>gcm-demo</code> is the application context and <code>/home</code> is the path of the main servlet.
-
- </li>
-</ol>
-<p class="note"><strong>Note:</strong> You can get the IP by running <code>ifconfig</code> on Linux or MacOS, or <code>ipconfig</code> on Windows. </p>
-
-<p> You server is now ready.</p>
-
-<h4 id="appengine-setup">Using App Engine for Java</h4>
-
-<p>To set up the server using a standard App Engine for Java:</p>
-<ol>
- <li>Get the files from the <a href="http://code.google.com/p/gcm">open source
-site</a>, as described above.</p>
- </li>
- <li>In a text editor, edit
-<code>samples/gcm-demo-appengine/src/com/google/android/gcm/demo/server/ApiKeyInitializer.java</code>
-and replace the existing text with the API key obtained above.
-
- <p class="note"><strong>Note:</strong> The API key value set in that class will
-be used just once to create a persistent entity on App Engine. If you deploy
-the application, you can use App Engine's <code>Datastore Viewer</code> to change
-it later.</p>
-
- </li>
- <li>In a shell window, go to the <code>samples/gcm-demo-appengine</code> directory.</li>
- <li>Start the development App Engine server by <code>ant runserver</code>,
-using the <code>-Dsdk.dir</code> to indicate the location of the App Engine SDK
-and <code>-Dserver.host</code> to set your server's hostname or IP address:</li>
-
-<pre class="prettyprint">
-$ ant -Dsdk.dir=/opt/google/appengine-java-sdk runserver -Dserver.host=192.168.1.10
-Buildfile: gcm-demo-appengine/build.xml
-
-init:
- [mkdir] Created dir: gcm-demo-appengine/dist
-
-copyjars:
-
-compile:
-
-datanucleusenhance:
- [enhance] DataNucleus Enhancer (version 1.1.4) : Enhancement of classes
- [enhance] DataNucleus Enhancer completed with success for 0 classes. Timings : input=28 ms, enhance=0 ms, total=28 ms. Consult the log for full details
- [enhance] DataNucleus Enhancer completed and no classes were enhanced. Consult the log for full details
-
-runserver:
- [java] Jun 15, 2012 8:46:06 PM com.google.apphosting.utils.jetty.JettyLogger info
- [java] INFO: Logging to JettyLogger(null) via com.google.apphosting.utils.jetty.JettyLogger
- [java] Jun 15, 2012 8:46:06 PM com.google.apphosting.utils.config.AppEngineWebXmlReader readAppEngineWebXml
- [java] INFO: Successfully processed gcm-demo-appengine/WebContent/WEB-INF/appengine-web.xml
- [java] Jun 15, 2012 8:46:06 PM com.google.apphosting.utils.config.AbstractConfigXmlReader readConfigXml
- [java] INFO: Successfully processed gcm-demo-appengine/WebContent/WEB-INF/web.xml
- [java] Jun 15, 2012 8:46:09 PM com.google.android.gcm.demo.server.ApiKeyInitializer contextInitialized
- [java] SEVERE: Created fake key. Please go to App Engine admin console, change its value to your API Key (the entity type is 'Settings' and its field to be changed is 'ApiKey'), then restart the server!
- [java] Jun 15, 2012 8:46:09 PM com.google.appengine.tools.development.DevAppServerImpl start
- [java] INFO: The server is running at http://192.168.1.10:8080/
- [java] Jun 15, 2012 8:46:09 PM com.google.appengine.tools.development.DevAppServerImpl start
- [java] INFO: The admin console is running at http://192.168.1.10:8080/_ah/admin
-</pre>
-
- <li>Open the server's main page in a browser. The URL depends on the server
-you're using and your machine's IP address, but it will be something like
-<code>http://192.168.1.10:8080/home</code>, where <code>/home</code>
-is the path of the main servlet.</li>
-
- <p class="note"><strong>Note:</strong> You can get the IP by running <code>ifconfig</code>
-on Linux or MacOS, or <code>ipconfig</code> on Windows.</p>
-
-</ol>
-<p> You server is now ready.</p>
diff --git a/docs/html/google/gcm/notifications.jd b/docs/html/google/gcm/notifications.jd
index 43a7368..5171850 100644
--- a/docs/html/google/gcm/notifications.jd
+++ b/docs/html/google/gcm/notifications.jd
@@ -14,15 +14,14 @@
<h2>In this document</h2>
<ol class="toc">
- <li><a href="#request">Request Format</a></li>
- <li><a href="#create">Generate a Notification Key</a></li>
- <li><a href="#add">Add Registration IDs</a></li>
- <li><a href="#remove">Remove Registration IDs</a></li>
- <li><a href="#upstream">Send Upstream Messages</a></li>
- <li><a href="#response">Response Formats</a>
- <ol class="toc">
- <li><a href="#response-create">Create/add/remove operations</a>
- <li><a href="#response-send">Send operations</a>
+ <li><a href="#what">What are User Notifications?</a> </li>
+ <li><a href="#examples">Examples</a>
+ <ol>
+ <li><a href="#create">Generate a notification key</a></li>
+ <li><a href="#add">Add registration IDs</a></li>
+ <li><a href="#remove">Remove registration IDs</a></li>
+ <li><a href="#upstream">Send upstream messages</a></li>
+ <li><a href="#response">Response formats</a></li>
</ol>
</li>
</ol>
@@ -39,51 +38,32 @@
<p class="note"><strong>Note:</strong> To try out this feature, sign up using <a href="https://services.google.com/fb/forms/gcm/">this form</a>.</p>
+<p>The upstream messaging (device-to-cloud) feature described in this document is part of the Google Play services platform. Upstream messaging is available through the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">GoogleCloudMessaging</a> APIs. To use upstream messaging and the new streamlined registration process, you must <a href="{@docRoot}google/play-services/setup.html">set up</a> the Google Play services SDK.</p>
-<p>With user notifications, 3rd-party app servers can send a single message to
-multiple instance of an app running on devices owned by a single user. This feature
-is called <em>user notifications</em>. User notifications make it possible for every
-app instance that a user owns to reflect the latest messaging state. For example:</p>
+<h2 id="what">What are User Notifications?</h2>
+
+<p>Third party servers can send a single message to multiple instance of an app running on devices owned by a single user. This feature is called <em>user notifications</em>. User notifications make it possible for every app instance that a user owns to reflect the latest messaging state. For example:</p>
<ul>
- <li>If a message has been handled on one device, the GCM message on the other
-devices are dismissed. For example, if a user has handled a calendar notification
-on one device, the notification will go away on the user's other devices.</li>
-
- <li>If a message has not been delivered yet to a device and but it has been handled,
-the GCM server removes it from the unsent queue for the other devices.</li>
-
- <li>Likewise, a device can send messages to the {@code notification_key}, which
-is the token that GCM uses to fan out notifications to all devices whose
-registration IDs are associated with the key.</li>
+ <li>If a message has been handled on one device, the GCM message on the other devices are dismissed. For example, if a user has handled a calendar notification on one device, the notification will go away on the user's other devices.</li>
+ <li>If a message has not been delivered yet to a device and but it has been handled, the GCM server removes it from the unsent queue for the other devices.</li>
+ <li>Likewise, a device can send messages to the {@code notification_key}, which is the token that GCM uses to fan out notifications to all devices whose registration IDs are associated with the key.</li>
</ul>
-<p>The way this works is that during registration, the 3rd-party server requests
-a {@code notification_key}. The {@code notification_key} maps a particular user
-to all of the user's associated registration IDs (a regID represents a particular
-Android application running on a particular device). Then instead of sending one
-message to one regID at a time, the 3rd-party server can send a message to to the
-{@code notification_key}, which then sends the message to all of the user's regIDs.</p>
+<p>The way this works is that during registration, the 3rd-party server requests a {@code notification_key}. The {@code notification_key} maps a particular user to all of the user's associated registration IDs (a regID represents a particular Android application running on a particular device). Then instead of sending one message to one regID at a time, the 3rd-party server can send a message to to the {@code notification_key}, which then sends the message to all of the user's regIDs.</p>
-<p class="note"><strong>Note:</strong> A notification dismissal message is like any
-other upstream message, meaning that it will be delivered to the other devices that
-belong to the specified {@code notification_key}. You should design your app to
-handle cases where the app receives a dismissal message, but has not yet displayed
-the notification that is being dismissed. You can solve this by caching the dismissal
-and then reconciling it with the corresponding notification.
+<p class="note"><strong>Note:</strong> A notification dismissal message is like any other upstream message, meaning that it will be delivered to the other devices that belong to the specified {@code notification_key}. You should design your app to handle cases where the app receives a dismissal message, but has not yet displayed the notification that is being dismissed. You can solve this by caching the dismissal and then reconciling it with the corresponding notification.
</p>
-<p>You can use this feature with either the <a href="ccs.html">XMPP</a> (CCS) or
-<a href="http.html">HTTP</a> connection server.</p>
+<p>You can use this feature with either the new <a href="ccs.html">GCM Cloud Connection Server</a> (CCS), or the older <a href="gcm.html">GCM HTTP server</a>.</p>
-<p>The examples below show you how to perform generate/add/remove operations,
-and how to send upstream messages. For generate/add/remove operations, the
-message body is JSON.</p>
+<h3 id="examples">Examples</h3>
-<h2 id="request">Request Format</h2>
-<p>To send a message, the application server issues a POST request to
-<code>https://android.googleapis.com/gcm/notification</code>.</p>
+<p>The examples in this section show you how to perform generate/add/remove operations, and how to send upstream messages. For generate/add/remove operations, the message body is JSON.</p>
+
+<h4 id="request">Request format</h4>
+<p>To send a message, the application server issues a POST request to <code>https://android.googleapis.com/gcm/notification</code>.</p>
<p>Here is the HTTP request header you should use for all create/add/remove operations:</p>
@@ -92,22 +72,12 @@
Header: "Authorization", "key=API_KEY"
</pre>
-<h2 id="create">Generate a Notification Key</h2>
+<h4 id="create">Generate a notification key</h4>
-<p>This example shows how to create a new <code>notification_key</code> for a
-<code>notification_key_name</code> called <code>appUser-Chris</code>.
-The {@code notification_key_name} is a name or identifier (can be a username for
-a 3rd-party app) that is unique to a given user. It is used by third parties to
-group together registration IDs for a single user. Note that <code>notification_key_name</code>
-and <code>notification_key</code> are unique to a group of registration IDs. It is also
-important that <code>notification_key_name</code> be uniquely named per app in case
-you have multiple apps for the same project ID. This ensures that notifications
-only go to the intended target app.</p>
+<p>This example shows how to create a new <code>notification_key</code> for a <code>notification_key_name</code> called <code>appUser-Chris</code>. The {@code notification_key_name} is a name or identifier (can be a username for a 3rd-party app) that is unique to a given user. It is used by third parties to group together registration IDs for a single user. Note that <code>notification_key_name</code> and <code>notification_key</code> are unique to a group of registration IDs. It is also important that <code>notification_key_name</code> be uniquely named per app in case you have multiple apps for the same project ID. This ensures that notifications only go to the intended target app.</p>
-<p>A create operation returns a token (<code>notification_key</code>). Third parties
-must save this token (as well as its mapping to the <code>notification_key_name</code>)
-to use in subsequent operations:</p>
+<p>A create operation returns a token (<code>notification_key</code>). Third parties must save this token (as well as its mapping to the <code>notification_key_name</code>) to use in subsequent operations:</p>
<pre>request:
{
@@ -116,14 +86,11 @@
"registration_ids": ["4", "8", "15", "16", "23", "42"]
}</pre>
-<h2 id="add">Add Registration IDs</h2>
+<h4 id="add">Add registration IDs</h4>
-<p>This example shows how to add registration IDs for a given notification key.
-The maximum number of members allowed for a {@code notification_key} is 10.</p>
+<p>This example shows how to add registration IDs for a given notification key. The maximum number of members allowed for a {@code notification_key} is 10.</p>
-<p>Note that the <code>notification_key_name</code> is not strictly required for
-adding/removing regIDs. But including it protects you against accidentally using
-the incorrect <code>notification_key</code>.</p>
+<p>Note that the <code>notification_key_name</code> is not strictly required for adding/removing regIDs. But including it protects you against accidentally using the incorrect <code>notification_key</code>.</p>
<pre>request:
{
@@ -133,7 +100,7 @@
"registration_ids": ["4", "8", "15", "16", "23", "42"]
}</pre>
-<h2 id="remove">Remove Registration IDs</h2>
+<h4 id="remove">Remove registration IDs</h4>
<p>This example shows how to remove registration IDs for a given notification key:</p>
<pre>request:
@@ -144,14 +111,9 @@
"registration_ids": ["4", "8", "15", "16", "23", "42"]
}</pre>
-<h2 id="upstream">Send Upstream Messages</h2>
+<h4 id="upstream">Send upstream messages</h4>
-<p>To send an upstream (device-to-cloud) message, you must use the
-<a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">
-{@code GoogleCloudMessaging}</a> API. Specifying a {@code notification_key} as the target
-for an upstream message allows a user on one device to send a message to other
-devices in the notification group—for example, to dismiss a notification.
-Here is an example that shows targeting a {@code notification_key}:</p>
+<p>To send an upstream (device-to-cloud) message, you must use the <a href="{@docRoot}reference/com/google/android/gms/gcm/GoogleCloudMessaging.html">GoogleCloudMessaging</a> API. Specifying a {@code notification_key} as the target for an upstream message allows a user on one device to send a message to other devices in the notification group—for example, to dismiss a notification. Here is an example that shows targeting a {@code notification_key}:</p>
<pre>GoogleCloudMessaging gcm = GoogleCloudMessaging.get(context);
String to = NOTIFICATION_KEY;
@@ -163,21 +125,17 @@
gcm.send(to, id, data);
</pre>
-<p>This call generates the necessary XMPP stanza for sending the message. The
-Bundle data consists of a key-value pair.</p>
+<p>This call generates the necessary XMPP stanza for sending the message. The Bundle data consists of a key-value pair.</p>
-<p>For a complete example, see <a href="client.html">Implementing GCM Client</a>.
+<p>For a complete example, see <a href="gs.html#gs_example">Getting Started</a>.
-<h2 id="response">Response Formats</h2>
+<h4 id="response">Response formats</h4>
-<p>This section shows examples of the responses that can be returned for
-notification key operations.</p>
+<p>This section shows examples of the responses that can be returned for notification key operations.</p>
-<h3 id="response-create">Create/add/remove operations</h3>
+<h5>Response for create/add/remove operations</h5>
-<p>When you make a request to create a {@code notification_key} or to add/remove its
-regIDs, a successful response always returns the <code>notification_key</code>.
-his is the {@code notification_key} you will use for sending messages:</p>
+<p>When you make a request to create a {@code notification_key} or to add/remove its the wayregIDs, a successful response always returns the <code>notification_key</code>. This is the {@code notification_key} you will use for sending messages:</p>
<pre>HTTP status: 200
{
@@ -185,23 +143,18 @@
}</pre>
-<h3 id="response-send">Send operations</h3>
+<h5>Response for send operations</h5>
-<p>For a send operation that has a {@code notification_key} as its target, the
-possible responses are success, partial success, and failure.</p>
+<p>For a send operation that has a {@code notification_key} as its target, the possible responses are success, partial success, and failure.</p>
-<p>Here is an example of "success"—the {@code notification_key} has 2 regIDs
-associated with it, and the message was successfully sent to both of them:</p>
+<p>Here is an example of "success"—the {@code notification_key} has 2 regIDs associated with it, and the message was successfully sent to both of them:</p>
<pre>{
"success": 2,
"failure": 0
}</pre>
-<p>Here is an example of "partial success"—the {@code notification_key} has
-3 regIDs associated with it. The message was successfully send to 1 of the regIDs,
-but not to the other 2. The response message lists the regIDs that failed to
-receive the message:</p>
+<p>Here is an example of "partial success"—the {@code notification_key} has 3 regIDs associated with it. The message was successfully send to 1 of the regIDs, but not to the other 2. The response message lists the regIDs that failed to receive the message:</p>
<pre>{
"success":1,
@@ -212,9 +165,7 @@
]
}</pre>
-<p>In the case of failure, the response has HTTP code 503 and no JSON. When a message
-fails to be delivered to one or more of the regIDs associated with a {@code notification_key},
-the 3rd-party server should retry.</p>
+<p>In the case of failure, the response has HTTP code 503 and no JSON. When a message fails to be delivered to one or more of the regIDs associated with a {@code notification_key}, the 3rd-party server should retry.</p>
diff --git a/docs/html/google/gcm/server.jd b/docs/html/google/gcm/server.jd
index b5e6b48..92a1531 100644
--- a/docs/html/google/gcm/server.jd
+++ b/docs/html/google/gcm/server.jd
@@ -1,34 +1,36 @@
-page.title=Implementing GCM Server
+page.title=GCM Server
@jd:body
<div id="qv-wrapper">
<div id="qv">
+<h2>Quickview</h2>
+
+<ul>
+<li>Understand how to set up the server side of a GCM app.</li>
+<li>Become familiar with the <a href="{@docRoot}reference/com/google/android/gcm/server/package-summary.html">GCM server helper library</a>.</li>
+</ul>
+
+
<h2>In this document</h2>
-<ol class="toc">
- <li><a href="#choose">Choosing a GCM Connection Server</a></li>
- <li><a href="#role">Role of the 3rd-party Application Server</a></li>
- <li><a href="#send-msg">Sending Messages</a>
- <ol class="toc">
-
- <li><a href="#target">Target</a></li>
- <li><a href="#payload">Payload</a></li>
- <li><a href="#params">Message parameters</a>
+<ol>
+ <li><a href="#requirements">Requirements</a> </li>
+ <li><a href="#gcm-setup">Setting Up GCM</a></li>
+ <li><a href="#server-setup">Setting Up an HTTP Server</a>
+ <ol>
+ <li><a href="#webserver-setup">Using a standard web server</a></li>
+ <li><a href="#appengine-setup">Using App Engine for Java</a></li>
</ol>
- </li>
- <li><a href="#receive">Receiving Messages</a> </li>
</li>
-
</ol>
<h2>See Also</h2>
<ol class="toc">
<li><a href="gs.html">Getting Started</a></li>
-<li><a href="client.html">Implementing GCM Client</a></li>
-<li><a href="ccs.html">Cloud Connection Server (XMPP)</a></li>
-<li><a href="http.html">HTTP Connection Server</a></li>
+<li><a href="client.html">GCM Client</a></li>
+<li><a href="ccs.html">Cloud Connection Server</a></li>
</ol>
@@ -37,342 +39,122 @@
</div>
-<p>The server side of GCM consists of 2 components:</p>
+
+
+<p>This document gives examples of GCM server-side code for HTTP. For an example of an XMPP server (<a href="ccs.html">Cloud Connection Server</a>), see <a href="gs.html#server">Getting Started</a>. Note that a full GCM implementation requires a client-side implementation, in addition to the server. For a complete working example that includes client and server-side code, see <a href="gs.html">Getting Started</a>.</a>
+
+<h2 id="requirements">Requirements</h2>
+<p>For the web server:</p>
<ul>
-<li>Google-provided <strong>GCM Connection Servers</strong>
-take messages from a 3rd-party application server and send them to a GCM-enabled
-Android application (the "client app") running on a device. For example,
-Google provides connection servers for <a href="{@docRoot}google/gcm/http.html">
-HTTP</a> and <a href="{@docRoot}google/gcm/ccs.html">CCS</a> (XMPP).</li>
-<li>A <strong>3rd-party application server</strong> that you must implement. This application
-server sends data to a GCM-enabled Android application via the chosen GCM connection server.</li>
-</ul>
-</p>
-
-<p>Here are the basic steps you follow to implement your 3rd-party app server:</p>
-
-<ul>
- <li>Decide which GCM connection server(s) you want to use. Note that if you want to use
- upstream messaging from your client applications, you must use CCS. For a more detailed
- discussion of this, see <a href="#choose">
- Choosing a GCM Connection Server</a>.</li>
- <li>Decide how you want to implement your app server. For example:
- <ul>
- <li>If you decide to use the HTTP connection server, you can use the
-GCM server helper library and demo app to help in implementing your app server.</li>
- <li>If you decide to use the XMPP connection server, you can use
-the provided Python or Java <a href="http://www.igniterealtime.org/projects/smack/">
-Smack</a> demo apps as a starting point.</li>
- <li>Note that Google AppEngine does not support connections to CCS.</li>
- </ul>
- </li>
- </ul>
- </li>
-</ul>
-
-<p>A full GCM implementation requires both a client implementation and a server
-implementation. For more
-information about implementing the client side, see <a href="client.html">
-Implementing GCM Client</a>.</p>
-
-<h2 id="choose">Choosing a GCM Connection Server</h2>
-
-<p>Currently GCM provides two connection servers: <a href="{@docRoot}google/gcm/http.html">
-HTTP</a> and <a href="{@docRoot}google/gcm/ccs.html">CCS</a> (XMPP). You can use them
-separately or in tandem. CCS messaging differs from GCM HTTP messaging in the following ways:</p>
-<ul>
- <li>Upstream/Downstream messages
+ <li> <a href="http://ant.apache.org/">Ant 1.8</a> (it might work with earlier versions, but it's not guaranteed).</li>
+ <li>One of the following:
<ul>
- <li>GCM HTTP: Downstream only: cloud-to-device. </li>
- <li>CCS: Upstream and downstream (device-to-cloud, cloud-to-device). </li>
+ <li>A running web server compatible with Servlets API version 2.5, such as <a href="http://tomcat.apache.org/">Tomcat 6</a> or <a href="http://jetty.codehaus.org/">Jetty</a>, or</li>
+ <li><a href="http://code.google.com/appengine/">Java App Engine SDK</a> version 1.6 or later.</li>
</ul>
</li>
- <li>Asynchronous messaging
- <ul>
- <li>GCM HTTP: 3rd-party app servers send messages as HTTP POST requests and
-wait for a response. This mechanism is synchronous and causes the sender to block
-before sending another message.</li>
- <li>CCS: 3rd-party app servers connect to Google infrastructure using a
-persistent XMPP connection and send/receive messages to/from all their devices
-at full line speed. CCS sends acknowledgment or failure notifications (in the
-form of special ACK and NACK JSON-encoded XMPP messages) asynchronously.</li>
- </ul>
- </li>
-
- <li>JSON
- <ul>
- <li>GCM HTTP: JSON messages sent as HTTP POST.</li>
- <li>CCS: JSON messages encapsulated in XMPP messages.</li>
- </ul>
- </li>
+ <li>A Google account registered to use GCM.</li>
+ <li>The API key for that account.</li>
</ul>
-
-<h2 id="role">Role of the 3rd-party Application Server</h2>
-
-<p>Before you can write client Android applications that use the GCM feature, you must
-have an application server that meets the following criteria:</p>
-
+<p>For the Android application:</p>
<ul>
- <li>Able to communicate with your client.</li>
- <li>Able to fire off properly formatted requests to the GCM server.</li>
- <li>Able to handle requests and resend them as needed, using
-<a href="http://en.wikipedia.org/wiki/Exponential_backoff">exponential back-off.</a></li>
- <li>Able to store the API key and client registration IDs. The
-API key is included in the header of POST requests that send
-messages.</li>
- <li>Able to store the API key and client registration IDs.</li>
- <li>Able to generate message IDs to uniquely identify each message it sends.</li>
+ <li>Emulator (or device) running Android 2.2 with Google APIs.</li>
+ <li>The Google API project number of the account registered to use GCM.</li>
</ul>
+<h2 id="gcm-setup">Setting Up GCM</h2>
+<p>Before proceeding with the server and client setup, it's necessary to register a Google account with the Google API Console, enable Google Cloud Messaging in GCM, and obtain an API key from the <a href="https://code.google.com/apis/console">Google API Console</a>.</p>
+<p>For instructions on how to set up GCM, see <a href="gs.html">Getting Started</a>.</p>
-<h2 id="send-msg">Sending Messages</h2>
-<p>Here is the general sequence of events that occurs when a 3rd-party application
-server sends a message:</p>
+<h2 id="server-setup">Setting Up an HTTP Server</h2>
+<p>This section describes the different options for setting up an HTTP server.</p>
+<h3 id="webserver-setup">Using a standard web server</h3>
+<p>To set up the server using a standard, servlet-compliant web server:</p>
<ol>
- <li>The application server sends a message to GCM servers.</li>
- <li>Google enqueues and stores the message in case the device is offline.</li>
- <li>When the device is online, Google sends the message to the device.</li>
- <li>On the device, the system broadcasts the message to the specified Android
-application via Intent broadcast with proper permissions, so that only the targeted
-ndroid application gets the message. This wakes the Android application up.
-The Android application does not need to be running beforehand to receive the message.</li>
- <li>The Android application processes the message. </li>
+ <li>From the <a href="http://code.google.com/p/gcm">open source site</a>, download the following directories: <code>gcm-server</code>, <code>samples/gcm-demo-server</code>, and <code>samples/gcm-demo-appengine</code>.</p>
+
+
+ <li>In a text editor, edit the <code>samples/gcm-demo-server/WebContent/WEB-INF/classes/api.key</code> and replace the existing text with the API key obtained above.</li>
+ <li>In a shell window, go to the <code>samples/gcm-demo-server</code> directory.</li>
+ <li>Generate the server's WAR file by running <code>ant war</code>:</li>
+
+ <pre class="prettyprint">$ ant war
+
+Buildfile:build.xml
+
+init:
+ [mkdir] Created dir: build/classes
+ [mkdir] Created dir: dist
+
+compile:
+ [javac] Compiling 6 source files to build/classes
+
+war:
+ [war] Building war: <strong>dist/gcm-demo.war</strong>
+
+BUILD SUCCESSFUL
+Total time: 0 seconds
+</pre>
+
+ <li>Deploy the <code>dist/gcm-demo.war</code> to your running server. For instance, if you're using Jetty, copy <code>gcm-demo.war</code> to the <code>webapps</code> directory of the Jetty installation.</li>
+ <li>Open the server's main page in a browser. The URL depends on the server you're using and your machine's IP address, but it will be something like <code>http://192.168.1.10:8080/gcm-demo/home</code>, where <code>gcm-demo</code> is the application context and <code>/home</code> is the path of the main servlet.
+
+ </li>
</ol>
+<p class="note"><strong>Note:</strong> You can get the IP by running <code>ifconfig</code> on Linux or MacOS, or <code>ipconfig</code> on Windows. </p>
-<p>The following sections describe the basic requirements for
-sending messages.</p>
+<p> You server is now ready.</p>
-<h3 id="target">Target</h3>
-<p>Required. When your app server sends a message in GCM, it must specify a target.</p>
-<p>For HTTP you must specify the target as one of:</p>
-<ul>
-<li><code>registration_ids</code>: For sending to 1 more more devices (up to 1000).
-When you send a message to multiple registration IDs, that is called a multicast message.</li>
-<li><code>notification_key</code>: For sending to multiple devices owned by a single user.</li>
-</ul>
-<p>For CCS (XMPP):</p>
-<ul>
-<li>You must specify the target as the "to" field, where the "to"
-field may contain a single registration ID or a notification key.
-CCS does not support multicast messaging.</li>
-</ul>
-<h3 id="payload">Payload</h3>
-<p>Optional. If you are including a payload in the message, you use the <code>data</code>
-parameter to include the payload. This applies for both HTTP and CCS.</p>
+<h3 id="appengine-setup">Using App Engine for Java</h3>
-<h3 id="params">Message parameters</h3>
-
-<p>The following table lists the parameters that a 3rd-party app server might
-include in the JSON messages it sends to a connection server. See the "Where Supported"
-column for information about which connection servers support that particular
-parameter.</p>
-
-<p class="table-caption" id="table1">
- <strong>Table 1.</strong> Message parameters.</p>
-
-<table>
- <tr>
- <th>Field</th>
- <th>Description</th>
-<th>Where Supported</th>
-</tr>
- <td><code>to</code></td>
-<td>In CCS, used in place of <code>registration_ids</code> to specify the
-recipient of a message. Its value must be a registration ID.
-The value is a string. Required.</td>
-<td>CCS</td>
-</tr>
-<tr>
-<td><code>message_id</code></td>
-<td>In CCS, uniquely identifies a message in an XMPP connection. The value is a
-string that uniquely identifies the associated message. The value is a string. Required.</td>
-<td>CCS</td>
-</tr>
-<tr>
-<td><code>message_type</code></td>
-<td>In CCS, indicates a special status message, typically sent by the system.
-However, your app server also uses this parameter to send an 'ack' or 'nack'
-message back to the CCS connection server. For more discussion of this topic, see
-<a href="ccs.html">Cloud Connection Server</a>. The value is a string. Optional.</td>
-<td>CCS</td>
-<tr>
- <td><code>registration_ids</code></td>
- <td>A string array with the list of devices (registration IDs) receiving the
-message. It must contain at least 1 and at most 1000 registration IDs. To send a
-multicast message, you must use JSON. For sending a single message to a single
-device, you could use a JSON object with just 1 registration id, or plain text
-(see below). A request must include a recipient—this can be either a
-registration ID, an array of registration IDs, or a {@code notification_key}.
-Required.</td>
-<td>HTTP</td>
-</tr>
- <tr>
- <td><code>notification_key</code></td>
- <td>A string that maps a single user to multiple registration IDs associated
-with that user. This allows a 3rd-party server to send a single message to
-multiple app instances (typically on multiple devices) owned by a single user.
-A 3rd-party server can use {@code notification_key} as the target for a message
-instead of an individual registration ID (or array of registration IDs). The maximum
-number of members allowed for a {@code notification_key} is 10. For more discussion
-of this topic, see <a href="notifications.html">User Notifications</a>. Optional.
-</td>
-<td style="width:100px">HTTP. This feature is supported in CCS, but you use it by
-specifying a notification key in the "to" field.</td>
-</tr>
- <tr>
- <td><code>collapse_key</code></td>
- <td>An arbitrary string (such as "Updates Available") that is used
-to collapse a group of like messages
-when the device is offline, so that only the last message gets sent to the
-client. This is intended to avoid sending too many messages to the phone when it
-comes back online. Note that since there is no guarantee of the order in which
-messages get sent, the "last" message may not actually be the last
-message sent by the application server. Collapse keys are also called
-<a href="#s2s">send-to-sync messages</a>.
-<br>
-<strong>Note:</strong> GCM allows a maximum of 4 different collapse keys to be
-used by the GCM server
-at any given time. In other words, the GCM server can simultaneously store 4
-different send-to-sync messages per device, each with a different collapse key.
-If you exceed
-this number GCM will only keep 4 collapse keys, with no guarantees about which
-ones they will be. See <a href="adv.html#collapsible">Advanced Topics</a> for more
-discussion of this topic. Optional.</td>
-<td>CCS, HTTP</td>
-</tr>
- <tr>
- <td><code>data</code></td>
- <td>A JSON object whose fields represents the key-value pairs of the message's
-payload data. If present, the payload data it will be
-included in the Intent as application data, with the key being the extra's name.
-For instance, <code>"data":{"score":"3x1"}</code> would result in an intent extra
-named <code>score</code> whose value is the string <code>3x1</code>.
-There is no limit on the number of key/value pairs, though there is a limit on
-the total size of the message (4kb). The values could be any JSON object, but we
-recommend using strings, since the values will be converted to strings in the GCM
-server anyway. If you want to include objects or other non-string data types
-(such as integers or booleans), you have to do the conversion to string yourself.
-Also note that the key cannot be a reserved word (<code>from</code> or any word
-starting with <code>google.</code>). To complicate things slightly, there are
-some reserved words (such as <code>collapse_key</code>) that are technically
-allowed in payload data. However, if the request also contains the word, the
-value in the request will overwrite the value in the payload data. Hence using
-words that are defined as field names in this table is not recommended, even in
-cases where they are technically allowed. Optional.</td>
-<td>CCS, HTTP</td>
-</tr>
- <tr>
- <td><code>delay_while_idle</code></td>
- <td>If included, indicates that the message should not be sent immediately
-if the device is idle. The server will wait for the device to become active, and
-then only the last message for each <code>collapse_key</code> value will be
-sent. The default value is <code>false</code>, and must be a JSON boolean. Optional.</td>
-<td>CCS, HTTP</td>
-</tr>
- <tr>
- <td><code>time_to_live</code></td>
- <td>How long (in seconds) the message should be kept on GCM storage if the
-device is offline. Optional (default time-to-live is 4 weeks, and must be set as
-a JSON number).</td>
-<td>CCS, HTTP</td>
-</tr>
-<tr>
- <td><code>restricted_package_name</code></td>
- <td>A string containing the package name of your application. When set, messages
-will only be sent to registration IDs that match the package name. Optional.
- </td>
-<td>HTTP</td>
-</tr>
-<tr>
- <td><code>dry_run</code></td>
- <td>If included, allows developers to test their request without actually
-sending a message. Optional. The default value is <code>false</code>, and must
-be a JSON boolean.
- </td>
-<td>HTTP</td>
-</tr>
-</table>
-
-<p>If you want to test your request (either JSON or plain text) without delivering
-the message to the devices, you can set an optional HTTP or JSON parameter called
-<code>dry_run</code> with the value <code>true</code>. The result will be almost
-identical to running the request without this parameter, except that the message
-will not be delivered to the devices. Consequently, the response will contain fake
-IDs for the message and multicast fields.</p>
-
-<h3 id="plain-text">Plain text (HTTP only)</h3>
-
-<p>If you are using plain text instead of JSON, the message fields must be set as
-HTTP parameters sent in the body, and their syntax is slightly different, as
-described below:
-<table>
- <tr>
- <th>Field</th>
- <th>Description</th>
- </tr>
- <tr>
- <td><code>registration_id</code></td>
- <td>Must contain the registration ID of the single device receiving the message.
-Required.</td>
- </tr>
- <tr>
- <td><code>collapse_key</code></td>
- <td>Same as JSON (see previous table). Optional.</td>
- </tr>
- <tr>
- <td><code>data.<key></code></td>
-
- <td>Payload data, expressed as parameters prefixed with <code>data.</code> and
-suffixed as the key. For instance, a parameter of <code>data.score=3x1</code> would
-result in an intent extra named <code>score</code> whose value is the string
-<code>3x1</code>. There is no limit on the number of key/value parameters, though
-there is a limit on the total size of the message. Also note that the key cannot
-be a reserved word (<code>from</code> or any word starting with
-<code>google.</code>). To complicate things slightly, there are some reserved words
-(such as <code>collapse_key</code>) that are technically allowed in payload data.
-However, if the request also contains the word, the value in the request will
-overwrite the value in the payload data. Hence using words that are defined as
-field names in this table is not recommended, even in cases where they are
-technically allowed. Optional.</td>
-
- </tr>
- <tr>
- <td><code>delay_while_idle</code></td>
- <td>Should be represented as <code>1</code> or <code>true</code> for
-<code>true</code>, anything else for <code>false</code>. Optional. The default
-value is <code>false</code>.</td>
- </tr>
- <tr>
- <td><code>time_to_live</code></td>
- <td>Same as JSON (see previous table). Optional.</td>
- </tr>
-<tr>
- <td><code>restricted_package_name</code></td>
- <td>Same as JSON (see previous table). Optional.
- </td>
-</tr>
-<tr>
- <td><code>dry_run</code></td>
- <td>Same as JSON (see previous table). Optional.
- </td>
-</tr>
-</table>
-
-<h2 id="receive">Receiving Messages</h2>
-
-<p>This is the sequence of events that occurs when an Android application
-installed on a mobile device receives a message:</p>
-
+<p>To set up the server using a standard App Engine for Java:</p>
<ol>
- <li>The system receives the incoming message and extracts the raw key/value
-pairs from the message payload, if any.</li>
- <li>The system passes the key/value pairs to the targeted Android application
-in a <code>com.google.android.c2dm.intent.RECEIVE</code> Intent as a set of
-extras.</li>
- <li>The Android application extracts the raw data
-from the <code>com.google.android.c2dm.intent.RECEIVE</code><code> </code>Intent
-by key and processes the data.</li>
-</ol>
+ <li>Get the files from the <a href="http://code.google.com/p/gcm">open source site</a>, as described above.</p>
+ </li>
+ <li>In a text editor, edit <code>samples/gcm-demo-appengine/src/com/google/android/gcm/demo/server/ApiKeyInitializer.java</code> and replace the existing text with the API key obtained above.
-<p>See the documentation for each connection server for more detail on how it
-handles responses.</p>
+ <p class="note"><strong>Note:</strong> The API key value set in that class will be used just once to create a persistent entity on App Engine. If you deploy the application, you can use App Engine's <code>Datastore Viewer</code> to change it later.</p>
+
+ </li>
+ <li>In a shell window, go to the <code>samples/gcm-demo-appengine</code> directory.</li>
+ <li>Start the development App Engine server by <code>ant runserver</code>, using the <code>-Dsdk.dir</code> to indicate the location of the App Engine SDK and <code>-Dserver.host</code> to set your server's hostname or IP address:</li>
+
+<pre class="prettyprint">
+$ ant -Dsdk.dir=/opt/google/appengine-java-sdk runserver -Dserver.host=192.168.1.10
+Buildfile: gcm-demo-appengine/build.xml
+
+init:
+ [mkdir] Created dir: gcm-demo-appengine/dist
+
+copyjars:
+
+compile:
+
+datanucleusenhance:
+ [enhance] DataNucleus Enhancer (version 1.1.4) : Enhancement of classes
+ [enhance] DataNucleus Enhancer completed with success for 0 classes. Timings : input=28 ms, enhance=0 ms, total=28 ms. Consult the log for full details
+ [enhance] DataNucleus Enhancer completed and no classes were enhanced. Consult the log for full details
+
+runserver:
+ [java] Jun 15, 2012 8:46:06 PM com.google.apphosting.utils.jetty.JettyLogger info
+ [java] INFO: Logging to JettyLogger(null) via com.google.apphosting.utils.jetty.JettyLogger
+ [java] Jun 15, 2012 8:46:06 PM com.google.apphosting.utils.config.AppEngineWebXmlReader readAppEngineWebXml
+ [java] INFO: Successfully processed gcm-demo-appengine/WebContent/WEB-INF/appengine-web.xml
+ [java] Jun 15, 2012 8:46:06 PM com.google.apphosting.utils.config.AbstractConfigXmlReader readConfigXml
+ [java] INFO: Successfully processed gcm-demo-appengine/WebContent/WEB-INF/web.xml
+ [java] Jun 15, 2012 8:46:09 PM com.google.android.gcm.demo.server.ApiKeyInitializer contextInitialized
+ [java] SEVERE: Created fake key. Please go to App Engine admin console, change its value to your API Key (the entity type is 'Settings' and its field to be changed is 'ApiKey'), then restart the server!
+ [java] Jun 15, 2012 8:46:09 PM com.google.appengine.tools.development.DevAppServerImpl start
+ [java] INFO: The server is running at http://192.168.1.10:8080/
+ [java] Jun 15, 2012 8:46:09 PM com.google.appengine.tools.development.DevAppServerImpl start
+ [java] INFO: The admin console is running at http://192.168.1.10:8080/_ah/admin
+</pre>
+
+ <li>Open the server's main page in a browser. The URL depends on the server you're using and your machine's IP address, but it will be something like <code>http://192.168.1.10:8080/home</code>, where <code>/home</code> is the path of the main servlet.</li>
+
+ <p class="note"><strong>Note:</strong> You can get the IP by running <code>ifconfig</code> on Linux or MacOS, or <code>ipconfig</code> on Windows.</p>
+
+</ol>
+<p> You server is now ready.</p>
+
+
diff --git a/docs/html/google/google_toc.cs b/docs/html/google/google_toc.cs
index 7f76c12..999c44e 100644
--- a/docs/html/google/google_toc.cs
+++ b/docs/html/google/google_toc.cs
@@ -123,27 +123,24 @@
<span class="en">Google Cloud Messaging</span></a>
</div>
<ul>
- <li><a href="<?cs var:toroot?>google/gcm/gcm.html">
- <span class="en">Overview</span></a>
- </li>
<li><a href="<?cs var:toroot?>google/gcm/gs.html">
<span class="en">Getting Started</span></a>
</li>
- <li><a href="<?cs var:toroot?>google/gcm/client.html">
- <span class="en">Implementing GCM Client</span></a>
+ <li><a href="<?cs var:toroot?>google/gcm/gcm.html">
+ <span class="en">Architectural Overview</span></a>
</li>
- <li class="nav-section"><div class="nav-section-header"><a href="<?cs var:toroot?>google/gcm/server.html">
- <span class="en">Implementing GCM Server</span></a></div>
- <ul>
- <li><a href="<?cs var:toroot?>google/gcm/ccs.html">
- <span class="en">CCS (XMPP)</span></a></li>
- <li><a href="<?cs var:toroot?>google/gcm/http.html">
- <span class="en">HTTP</span></a></li>
- </ul>
+ <li><a href="<?cs var:toroot?>google/gcm/ccs.html">
+ <span class="en">Cloud Connection Server</span></a>
</li>
<li><a href="<?cs var:toroot?>google/gcm/notifications.html">
<span class="en">User Notifications</span></a>
</li>
+ <li><a href="<?cs var:toroot?>google/gcm/client.html">
+ <span class="en">GCM Client</span></a>
+ </li>
+ <li><a href="<?cs var:toroot?>google/gcm/server.html">
+ <span class="en">GCM Server</span></a>
+ </li>
<li><a href="<?cs var:toroot?>google/gcm/adv.html">
<span class="en">Advanced Topics</span></a>
</li>
diff --git a/docs/html/images/gcm/CCS-ack.png b/docs/html/images/gcm/CCS-ack.png
deleted file mode 100644
index bce2ab2..0000000
--- a/docs/html/images/gcm/CCS-ack.png
+++ /dev/null
Binary files differ
diff --git a/docs/html/images/gcm/GCM-arch.png b/docs/html/images/gcm/GCM-arch.png
deleted file mode 100644
index e8ffb15..0000000
--- a/docs/html/images/gcm/GCM-arch.png
+++ /dev/null
Binary files differ
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaNames.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaNames.java
index f3a91c5..ee867ff 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaNames.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/MediaNames.java
@@ -157,4 +157,11 @@
public static final int STREAM_H264_480_360_1411k_DURATION = 46000;
public static final int VIDEO_H263_AAC_DURATION = 501000;
public static final int VIDEO_H263_AMR_DURATION = 502000;
+
+ // Video files for WiFi IOT video streaming test.
+ public static final String[] NETWORK_VIDEO_FILES = {
+ "H264_BP_720x480_25fps_256kbps_AMRNB_8khz_12.2kbps_m_0_26.mp4",
+ "MPEG4_SP_720x480_30fps_280kbps_AACLC_48kHz_161kbps_s_0_26.mp4",
+ "MPEG4_SP_720x480_30fps_280kbps_AACLC_48kHz_96kbps_s_0_21.mp4"
+ };
}
diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/MediaPlayerStressTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/MediaPlayerStressTest.java
index 67da6ac..e289812 100644
--- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/MediaPlayerStressTest.java
+++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/stress/MediaPlayerStressTest.java
@@ -17,18 +17,13 @@
package com.android.mediaframeworktest.stress;
import com.android.mediaframeworktest.MediaFrameworkTest;
+import com.android.mediaframeworktest.MediaPlayerStressTestRunner;
-import android.app.Activity;
-import android.app.Instrumentation;
-import android.content.Intent;
-import android.hardware.Camera;
-import android.media.MediaPlayer;
-import android.media.MediaRecorder;
+import android.os.Bundle;
import android.os.Environment;
import android.test.ActivityInstrumentationTestCase2;
import android.test.suitebuilder.annotation.LargeTest;
import android.util.Log;
-import android.view.SurfaceHolder;
import com.android.mediaframeworktest.MediaNames;
import com.android.mediaframeworktest.functional.CodecTest;
@@ -38,14 +33,12 @@
import java.io.FileWriter;
import java.io.Writer;
-import android.test.AndroidTestCase;
-import android.test.InstrumentationTestCase;
-
/**
* Junit / Instrumentation test case for the media player
*/
public class MediaPlayerStressTest extends ActivityInstrumentationTestCase2<MediaFrameworkTest> {
private String TAG = "MediaPlayerStressTest";
+ private String mMediaSrc;
public MediaPlayerStressTest() {
super("com.android.mediaframeworktest", MediaFrameworkTest.class);
@@ -56,6 +49,12 @@
//the workaround for the race condition of requesting the updated surface.
Thread.sleep(2000);
getActivity();
+ MediaPlayerStressTestRunner mRunner = (MediaPlayerStressTestRunner)getInstrumentation();
+ Bundle arguments = mRunner.getArguments();
+ mMediaSrc = arguments.getString("media-source");
+ if (mMediaSrc == null) {
+ mMediaSrc = MediaNames.MEDIA_SAMPLE_POOL;
+ }
super.setUp();
}
@@ -119,17 +118,20 @@
boolean testResult = true;
// load directory files
boolean onCompleteSuccess = false;
- File dir = new File(MediaNames.MEDIA_SAMPLE_POOL);
- String[] children = dir.list();
+ String[] children = MediaNames.NETWORK_VIDEO_FILES;
+ if (MediaNames.MEDIA_SAMPLE_POOL.equals(mMediaSrc)) {
+ File dir = new File(mMediaSrc);
+ children = dir.list();
+ }
if (children == null) {
Log.v("MediaPlayerApiTest:testMediaSamples", "dir is empty");
return;
} else {
for (int i = 0; i < children.length; i++) {
- //Get filename of directory
+ //Get filename
String filename = children[i];
onCompleteSuccess =
- CodecTest.playMediaSamples(dir + "/" + filename);
+ CodecTest.playMediaSamples(mMediaSrc + filename);
if (!onCompleteSuccess){
//Don't fail the test right away, print out the failure file.
fileWithError += filename + '\n';
diff --git a/services/java/com/android/server/AppOpsService.java b/services/java/com/android/server/AppOpsService.java
index a402642..a55fddc 100644
--- a/services/java/com/android/server/AppOpsService.java
+++ b/services/java/com/android/server/AppOpsService.java
@@ -27,6 +27,7 @@
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import android.app.AppOpsManager;
import android.content.Context;
@@ -42,6 +43,7 @@
import android.os.UserHandle;
import android.util.AtomicFile;
import android.util.Log;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
@@ -288,6 +290,24 @@
}
}
+ private void pruneOp(Op op, int uid, String packageName) {
+ if (op.time == 0 && op.rejectTime == 0) {
+ Ops ops = getOpsLocked(uid, packageName, false);
+ if (ops != null) {
+ ops.remove(op.op);
+ if (ops.size() <= 0) {
+ HashMap<String, Ops> pkgOps = mUidOps.get(uid);
+ if (pkgOps != null) {
+ pkgOps.remove(ops.packageName);
+ if (pkgOps.size() <= 0) {
+ mUidOps.remove(uid);
+ }
+ }
+ }
+ }
+ }
+ }
+
@Override
public void setMode(int code, int uid, String packageName, int mode) {
verifyIncomingUid(uid);
@@ -316,21 +336,7 @@
if (mode == AppOpsManager.MODE_ALLOWED) {
// If going into the default mode, prune this op
// if there is nothing else interesting in it.
- if (op.time == 0 && op.rejectTime == 0) {
- Ops ops = getOpsLocked(uid, packageName, false);
- if (ops != null) {
- ops.remove(op.op);
- if (ops.size() <= 0) {
- HashMap<String, Ops> pkgOps = mUidOps.get(uid);
- if (pkgOps != null) {
- pkgOps.remove(ops.packageName);
- if (pkgOps.size() <= 0) {
- mUidOps.remove(uid);
- }
- }
- }
- }
- }
+ pruneOp(op, uid, packageName);
}
scheduleWriteNowLocked();
}
@@ -346,6 +352,82 @@
}
}
+ private static HashMap<Callback, ArrayList<Pair<String, Integer>>> addCallbacks(
+ HashMap<Callback, ArrayList<Pair<String, Integer>>> callbacks,
+ String packageName, int op, ArrayList<Callback> cbs) {
+ if (cbs == null) {
+ return callbacks;
+ }
+ if (callbacks == null) {
+ callbacks = new HashMap<Callback, ArrayList<Pair<String, Integer>>>();
+ }
+ for (int i=0; i<cbs.size(); i++) {
+ Callback cb = cbs.get(i);
+ ArrayList<Pair<String, Integer>> reports = callbacks.get(cb);
+ if (reports == null) {
+ reports = new ArrayList<Pair<String, Integer>>();
+ callbacks.put(cb, reports);
+ }
+ reports.add(new Pair<String, Integer>(packageName, op));
+ }
+ return callbacks;
+ }
+
+ @Override
+ public void resetAllModes() {
+ mContext.enforcePermission(android.Manifest.permission.UPDATE_APP_OPS_STATS,
+ Binder.getCallingPid(), Binder.getCallingUid(), null);
+ HashMap<Callback, ArrayList<Pair<String, Integer>>> callbacks = null;
+ synchronized (this) {
+ boolean changed = false;
+ for (int i=mUidOps.size()-1; i>=0; i--) {
+ HashMap<String, Ops> packages = mUidOps.valueAt(i);
+ Iterator<Map.Entry<String, Ops>> it = packages.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<String, Ops> ent = it.next();
+ String packageName = ent.getKey();
+ Ops pkgOps = ent.getValue();
+ for (int j=pkgOps.size()-1; j>=0; j--) {
+ Op curOp = pkgOps.valueAt(j);
+ if (curOp.mode != AppOpsManager.MODE_ALLOWED) {
+ curOp.mode = AppOpsManager.MODE_ALLOWED;
+ changed = true;
+ callbacks = addCallbacks(callbacks, packageName, curOp.op,
+ mOpModeWatchers.get(curOp.op));
+ callbacks = addCallbacks(callbacks, packageName, curOp.op,
+ mPackageModeWatchers.get(packageName));
+ if (curOp.time == 0 && curOp.rejectTime == 0) {
+ pkgOps.removeAt(j);
+ }
+ }
+ }
+ if (pkgOps.size() == 0) {
+ it.remove();
+ }
+ }
+ if (packages.size() == 0) {
+ mUidOps.removeAt(i);
+ }
+ }
+ if (changed) {
+ scheduleWriteNowLocked();
+ }
+ }
+ if (callbacks != null) {
+ for (Map.Entry<Callback, ArrayList<Pair<String, Integer>>> ent : callbacks.entrySet()) {
+ Callback cb = ent.getKey();
+ ArrayList<Pair<String, Integer>> reports = ent.getValue();
+ for (int i=0; i<reports.size(); i++) {
+ Pair<String, Integer> rep = reports.get(i);
+ try {
+ cb.mCallback.opChanged(rep.second, rep.first);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+ }
+ }
+
@Override
public void startWatchingMode(int op, String packageName, IAppOpsCallback callback) {
synchronized (this) {
diff --git a/services/java/com/android/server/ClipboardService.java b/services/java/com/android/server/ClipboardService.java
index 058857d..0bf03b5 100644
--- a/services/java/com/android/server/ClipboardService.java
+++ b/services/java/com/android/server/ClipboardService.java
@@ -154,31 +154,36 @@
if (clip != null && clip.getItemCount() <= 0) {
throw new IllegalArgumentException("No items");
}
- if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, Binder.getCallingUid(),
+ final int callingUid = Binder.getCallingUid();
+ if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid,
callingPackage) != AppOpsManager.MODE_ALLOWED) {
return;
}
- checkDataOwnerLocked(clip, Binder.getCallingUid());
+ checkDataOwnerLocked(clip, callingUid);
clearActiveOwnersLocked();
PerUserClipboard clipboard = getClipboard();
clipboard.primaryClip = clip;
+ final long ident = Binder.clearCallingIdentity();
final int n = clipboard.primaryClipListeners.beginBroadcast();
- for (int i = 0; i < n; i++) {
- try {
- ListenerInfo li = (ListenerInfo)
- clipboard.primaryClipListeners.getBroadcastCookie(i);
- if (mAppOps.checkOpNoThrow(AppOpsManager.OP_READ_CLIPBOARD, li.mUid,
- li.mPackageName) == AppOpsManager.MODE_ALLOWED) {
- clipboard.primaryClipListeners.getBroadcastItem(i)
- .dispatchPrimaryClipChanged();
+ try {
+ for (int i = 0; i < n; i++) {
+ try {
+ ListenerInfo li = (ListenerInfo)
+ clipboard.primaryClipListeners.getBroadcastCookie(i);
+ if (mAppOps.checkOpNoThrow(AppOpsManager.OP_READ_CLIPBOARD, li.mUid,
+ li.mPackageName) == AppOpsManager.MODE_ALLOWED) {
+ clipboard.primaryClipListeners.getBroadcastItem(i)
+ .dispatchPrimaryClipChanged();
+ }
+ } catch (RemoteException e) {
+ // The RemoteCallbackList will take care of removing
+ // the dead object for us.
}
- } catch (RemoteException e) {
-
- // The RemoteCallbackList will take care of removing
- // the dead object for us.
}
+ } finally {
+ clipboard.primaryClipListeners.finishBroadcast();
+ Binder.restoreCallingIdentity(ident);
}
- clipboard.primaryClipListeners.finishBroadcast();
}
}
diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java
index cb4e89c..a022fb1 100644
--- a/services/java/com/android/server/ConnectivityService.java
+++ b/services/java/com/android/server/ConnectivityService.java
@@ -35,6 +35,7 @@
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothTetheringDataTracker;
+import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -143,6 +144,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Random;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -383,9 +385,6 @@
TelephonyManager mTelephonyManager;
- // We only want one checkMobileProvisioning after booting.
- volatile boolean mFirstProvisioningCheckStarted = false;
-
public ConnectivityService(Context context, INetworkManagementService netd,
INetworkStatsService statsService, INetworkPolicyManager policyManager) {
// Currently, omitting a NetworkFactory will create one internally
@@ -609,8 +608,9 @@
mSettingsObserver = new SettingsObserver(mHandler, EVENT_APPLY_GLOBAL_HTTP_PROXY);
mSettingsObserver.observe(mContext);
- mCaptivePortalTracker = CaptivePortalTracker.makeCaptivePortalTracker(mContext, this);
- loadGlobalProxy();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(CONNECTED_TO_PROVISIONING_NETWORK_ACTION);
+ mContext.registerReceiver(mProvisioningReceiver, filter);
}
/**
@@ -879,6 +879,45 @@
return getNetworkInfo(mActiveDefaultNetwork, uid);
}
+ /**
+ * Find the first Provisioning network.
+ *
+ * @return NetworkInfo or null if none.
+ */
+ private NetworkInfo getProvisioningNetworkInfo() {
+ enforceAccessPermission();
+
+ // Find the first Provisioning Network
+ NetworkInfo provNi = null;
+ for (NetworkInfo ni : getAllNetworkInfo()) {
+ if (ni.isConnectedToProvisioningNetwork()) {
+ provNi = ni;
+ break;
+ }
+ }
+ if (DBG) log("getProvisioningNetworkInfo: X provNi=" + provNi);
+ return provNi;
+ }
+
+ /**
+ * Find the first Provisioning network or the ActiveDefaultNetwork
+ * if there is no Provisioning network
+ *
+ * @return NetworkInfo or null if none.
+ */
+ @Override
+ public NetworkInfo getProvisioningOrActiveNetworkInfo() {
+ enforceAccessPermission();
+
+ NetworkInfo provNi = getProvisioningNetworkInfo();
+ if (provNi == null) {
+ final int uid = Binder.getCallingUid();
+ provNi = getNetworkInfo(mActiveDefaultNetwork, uid);
+ }
+ if (DBG) log("getProvisioningOrActiveNetworkInfo: X provNi=" + provNi);
+ return provNi;
+ }
+
public NetworkInfo getActiveNetworkInfoUnfiltered() {
enforceAccessPermission();
if (isNetworkTypeValid(mActiveDefaultNetwork)) {
@@ -1241,8 +1280,10 @@
feature);
}
if (network.reconnect()) {
+ if (DBG) log("startUsingNetworkFeature X: return APN_REQUEST_STARTED");
return PhoneConstants.APN_REQUEST_STARTED;
} else {
+ if (DBG) log("startUsingNetworkFeature X: return APN_REQUEST_FAILED");
return PhoneConstants.APN_REQUEST_FAILED;
}
} else {
@@ -1254,9 +1295,11 @@
mNetRequestersPids[usedNetworkType].add(currentPid);
}
}
+ if (DBG) log("startUsingNetworkFeature X: return -1 unsupported feature.");
return -1;
}
}
+ if (DBG) log("startUsingNetworkFeature X: return APN_TYPE_NOT_AVAILABLE");
return PhoneConstants.APN_TYPE_NOT_AVAILABLE;
} finally {
if (DBG) {
@@ -1290,11 +1333,12 @@
}
}
if (found && u != null) {
+ if (VDBG) log("stopUsingNetworkFeature: X");
// stop regardless of how many other time this proc had called start
return stopUsingNetworkFeature(u, true);
} else {
// none found!
- if (VDBG) log("stopUsingNetworkFeature - not a live request, ignoring");
+ if (VDBG) log("stopUsingNetworkFeature: X not a live request, ignoring");
return 1;
}
}
@@ -1849,6 +1893,9 @@
*/
if (mNetConfigs[prevNetType].isDefault()) {
if (mActiveDefaultNetwork == prevNetType) {
+ if (DBG) {
+ log("tryFailover: set mActiveDefaultNetwork=-1, prevNetType=" + prevNetType);
+ }
mActiveDefaultNetwork = -1;
}
@@ -2041,6 +2088,9 @@
}
void systemReady() {
+ mCaptivePortalTracker = CaptivePortalTracker.makeCaptivePortalTracker(mContext, this);
+ loadGlobalProxy();
+
synchronized(this) {
mSystemReady = true;
if (mInitialBroadcast != null) {
@@ -2071,10 +2121,11 @@
};
private boolean isNewNetTypePreferredOverCurrentNetType(int type) {
- if ((type != mNetworkPreference &&
- mNetConfigs[mActiveDefaultNetwork].priority >
- mNetConfigs[type].priority) ||
- mNetworkPreference == mActiveDefaultNetwork) return false;
+ if (((type != mNetworkPreference)
+ && (mNetConfigs[mActiveDefaultNetwork].priority > mNetConfigs[type].priority))
+ || (mNetworkPreference == mActiveDefaultNetwork)) {
+ return false;
+ }
return true;
}
@@ -2088,6 +2139,11 @@
final NetworkStateTracker thisNet = mNetTrackers[newNetType];
final String thisIface = thisNet.getLinkProperties().getInterfaceName();
+ if (VDBG) {
+ log("handleConnect: E newNetType=" + newNetType + " thisIface=" + thisIface
+ + " isFailover" + isFailover);
+ }
+
// if this is a default net and other default is running
// kill the one not preferred
if (mNetConfigs[newNetType].isDefault()) {
@@ -2169,15 +2225,26 @@
}
}
+ if (DBG) log("handleCaptivePortalTrackerCheck: call captivePortalCheckComplete ni=" + info);
thisNet.captivePortalCheckComplete();
}
/** @hide */
+ @Override
public void captivePortalCheckComplete(NetworkInfo info) {
enforceConnectivityInternalPermission();
+ if (DBG) log("captivePortalCheckComplete: ni=" + info);
mNetTrackers[info.getType()].captivePortalCheckComplete();
}
+ /** @hide */
+ @Override
+ public void captivePortalCheckCompleted(NetworkInfo info, boolean isCaptivePortal) {
+ enforceConnectivityInternalPermission();
+ if (DBG) log("captivePortalCheckCompleted: ni=" + info + " captive=" + isCaptivePortal);
+ mNetTrackers[info.getType()].captivePortalCheckCompleted(isCaptivePortal);
+ }
+
/**
* Setup data activity tracking for the given network interface.
*
@@ -2238,6 +2305,10 @@
*/
private void handleConnectivityChange(int netType, boolean doReset) {
int resetMask = doReset ? NetworkUtils.RESET_ALL_ADDRESSES : 0;
+ if (VDBG) {
+ log("handleConnectivityChange: netType=" + netType + " doReset=" + doReset
+ + " resetMask=" + resetMask);
+ }
/*
* If a non-default network is enabled, add the host routes that
@@ -2305,7 +2376,9 @@
boolean resetDns = updateRoutes(newLp, curLp, mNetConfigs[netType].isDefault());
if (resetMask != 0 || resetDns) {
+ if (VDBG) log("handleConnectivityChange: resetting");
if (curLp != null) {
+ if (VDBG) log("handleConnectivityChange: resetting curLp=" + curLp);
for (String iface : curLp.getAllInterfaceNames()) {
if (TextUtils.isEmpty(iface) == false) {
if (resetMask != 0) {
@@ -2338,6 +2411,7 @@
// Update 464xlat state.
NetworkStateTracker tracker = mNetTrackers[netType];
if (mClat.requiresClat(netType, tracker)) {
+
// If the connection was previously using clat, but is not using it now, stop the clat
// daemon. Normally, this happens automatically when the connection disconnects, but if
// the disconnect is not reported, or if the connection's LinkProperties changed for
@@ -2391,6 +2465,7 @@
for (RouteInfo r : routeDiff.removed) {
if (isLinkDefault || ! r.isDefaultRoute()) {
+ if (VDBG) log("updateRoutes: default remove route r=" + r);
removeRoute(curLp, r, TO_DEFAULT_TABLE);
}
if (isLinkDefault == false) {
@@ -2435,7 +2510,6 @@
// remove the default route unless somebody else has asked for it
String ifaceName = newLp.getInterfaceName();
if (TextUtils.isEmpty(ifaceName) == false && mAddedRoutes.contains(r) == false) {
- if (VDBG) log("Removing " + r + " for interface " + ifaceName);
try {
mNetd.removeRoute(ifaceName, r);
} catch (Exception e) {
@@ -2725,27 +2799,30 @@
public void handleMessage(Message msg) {
NetworkInfo info;
switch (msg.what) {
- case NetworkStateTracker.EVENT_STATE_CHANGED:
+ case NetworkStateTracker.EVENT_STATE_CHANGED: {
info = (NetworkInfo) msg.obj;
- int type = info.getType();
NetworkInfo.State state = info.getState();
if (VDBG || (state == NetworkInfo.State.CONNECTED) ||
- (state == NetworkInfo.State.DISCONNECTED)) {
+ (state == NetworkInfo.State.DISCONNECTED) ||
+ (state == NetworkInfo.State.SUSPENDED)) {
log("ConnectivityChange for " +
info.getTypeName() + ": " +
state + "/" + info.getDetailedState());
}
- // After booting we'll check once for mobile provisioning
- // if we've provisioned by and connected.
- if (!mFirstProvisioningCheckStarted
+ // Since mobile has the notion of a network/apn that can be used for
+ // provisioning we need to check every time we're connected as
+ // CaptiveProtalTracker won't detected it because DCT doesn't report it
+ // as connected as ACTION_ANY_DATA_CONNECTION_STATE_CHANGED instead its
+ // reported as ACTION_DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN. Which
+ // is received by MDST and sent here as EVENT_STATE_CHANGED.
+ if (ConnectivityManager.isNetworkTypeMobile(info.getType())
&& (0 != Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0))
- && (state == NetworkInfo.State.CONNECTED)) {
- log("check provisioning after booting");
- mFirstProvisioningCheckStarted = true;
- checkMobileProvisioning(true, CheckMp.MAX_TIMEOUT_MS, null);
+ && ((state == NetworkInfo.State.CONNECTED)
+ || info.isConnectedToProvisioningNetwork())) {
+ checkMobileProvisioning(CheckMp.MAX_TIMEOUT_MS);
}
EventLogTags.writeConnectivityStateChanged(
@@ -2757,6 +2834,29 @@
} else if (info.getDetailedState() ==
DetailedState.CAPTIVE_PORTAL_CHECK) {
handleCaptivePortalTrackerCheck(info);
+ } else if (info.isConnectedToProvisioningNetwork()) {
+ /**
+ * TODO: Create ConnectivityManager.TYPE_MOBILE_PROVISIONING
+ * for now its an in between network, its a network that
+ * is actually a default network but we don't want it to be
+ * announced as such to keep background applications from
+ * trying to use it. It turns out that some still try so we
+ * take the additional step of clearing any default routes
+ * to the link that may have incorrectly setup by the lower
+ * levels.
+ */
+ LinkProperties lp = getLinkProperties(info.getType());
+ if (DBG) {
+ log("EVENT_STATE_CHANGED: connected to provisioning network, lp=" + lp);
+ }
+
+ // Clear any default routes setup by the radio so
+ // any activity by applications trying to use this
+ // connection will fail until the provisioning network
+ // is enabled.
+ for (RouteInfo r : lp.getRoutes()) {
+ removeRoute(lp, r, TO_DEFAULT_TABLE);
+ }
} else if (state == NetworkInfo.State.DISCONNECTED) {
handleDisconnect(info);
} else if (state == NetworkInfo.State.SUSPENDED) {
@@ -2775,18 +2875,21 @@
mLockdownTracker.onNetworkInfoChanged(info);
}
break;
- case NetworkStateTracker.EVENT_CONFIGURATION_CHANGED:
+ }
+ case NetworkStateTracker.EVENT_CONFIGURATION_CHANGED: {
info = (NetworkInfo) msg.obj;
// TODO: Temporary allowing network configuration
// change not resetting sockets.
// @see bug/4455071
handleConnectivityChange(info.getType(), false);
break;
- case NetworkStateTracker.EVENT_NETWORK_SUBTYPE_CHANGED:
+ }
+ case NetworkStateTracker.EVENT_NETWORK_SUBTYPE_CHANGED: {
info = (NetworkInfo) msg.obj;
- type = info.getType();
+ int type = info.getType();
updateNetworkSettings(mNetTrackers[type]);
break;
+ }
}
}
}
@@ -3561,72 +3664,153 @@
enabled));
}
+ private boolean isMobileDataStateTrackerReady() {
+ MobileDataStateTracker mdst =
+ (MobileDataStateTracker) mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI];
+ return (mdst != null) && (mdst.isReady());
+ }
+
+ /**
+ * The ResultReceiver resultCode for checkMobileProvisioning (CMP_RESULT_CODE)
+ */
+
+ /**
+ * No connection was possible to the network.
+ */
+ public static final int CMP_RESULT_CODE_NO_CONNECTION = 0;
+
+ /**
+ * A connection was made to the internet, all is well.
+ */
+ public static final int CMP_RESULT_CODE_CONNECTABLE = 1;
+
+ /**
+ * A connection was made but there was a redirection, we appear to be in walled garden.
+ * This is an indication of a warm sim on a mobile network.
+ */
+ public static final int CMP_RESULT_CODE_REDIRECTED = 2;
+
+ /**
+ * A connection was made but no dns server was available to resolve a name to address.
+ * This is an indication of a warm sim on a mobile network.
+ */
+ public static final int CMP_RESULT_CODE_NO_DNS = 3;
+
+ /**
+ * A connection was made but could not open a TCP connection.
+ * This is an indication of a warm sim on a mobile network.
+ */
+ public static final int CMP_RESULT_CODE_NO_TCP_CONNECTION = 4;
+
+ /**
+ * The mobile network is a provisioning network.
+ * This is an indication of a warm sim on a mobile network.
+ */
+ public static final int CMP_RESULT_CODE_PROVISIONING_NETWORK = 5;
+
+ AtomicBoolean mIsCheckingMobileProvisioning = new AtomicBoolean(false);
+
@Override
- public int checkMobileProvisioning(boolean sendNotification, int suggestedTimeOutMs,
- final ResultReceiver resultReceiver) {
- log("checkMobileProvisioning: E sendNotification=" + sendNotification
- + " suggestedTimeOutMs=" + suggestedTimeOutMs
- + " resultReceiver=" + resultReceiver);
- enforceChangePermission();
-
- mFirstProvisioningCheckStarted = true;
-
- int timeOutMs = suggestedTimeOutMs;
- if (suggestedTimeOutMs > CheckMp.MAX_TIMEOUT_MS) {
- timeOutMs = CheckMp.MAX_TIMEOUT_MS;
- }
-
- // Check that mobile networks are supported
- if (!isNetworkSupported(ConnectivityManager.TYPE_MOBILE)
- || !isNetworkSupported(ConnectivityManager.TYPE_MOBILE_HIPRI)) {
- log("checkMobileProvisioning: X no mobile network");
- if (resultReceiver != null) {
- resultReceiver.send(ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION, null);
- }
- return timeOutMs;
- }
+ public int checkMobileProvisioning(int suggestedTimeOutMs) {
+ int timeOutMs = -1;
+ if (DBG) log("checkMobileProvisioning: E suggestedTimeOutMs=" + suggestedTimeOutMs);
+ enforceConnectivityInternalPermission();
final long token = Binder.clearCallingIdentity();
try {
+ timeOutMs = suggestedTimeOutMs;
+ if (suggestedTimeOutMs > CheckMp.MAX_TIMEOUT_MS) {
+ timeOutMs = CheckMp.MAX_TIMEOUT_MS;
+ }
+
+ // Check that mobile networks are supported
+ if (!isNetworkSupported(ConnectivityManager.TYPE_MOBILE)
+ || !isNetworkSupported(ConnectivityManager.TYPE_MOBILE_HIPRI)) {
+ if (DBG) log("checkMobileProvisioning: X no mobile network");
+ return timeOutMs;
+ }
+
+ // If we're already checking don't do it again
+ // TODO: Add a queue of results...
+ if (mIsCheckingMobileProvisioning.getAndSet(true)) {
+ if (DBG) log("checkMobileProvisioning: X already checking ignore for the moment");
+ return timeOutMs;
+ }
+
+ // Start off with notification off
+ setProvNotificationVisible(false, ConnectivityManager.TYPE_NONE, null, null);
+
+ // See if we've alreadying determined if we've got a provsioning connection
+ // if so we don't need to do anything active
+ MobileDataStateTracker mdstDefault = (MobileDataStateTracker)
+ mNetTrackers[ConnectivityManager.TYPE_MOBILE];
+ boolean isDefaultProvisioning = mdstDefault.isProvisioningNetwork();
+
+ MobileDataStateTracker mdstHipri = (MobileDataStateTracker)
+ mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI];
+ boolean isHipriProvisioning = mdstHipri.isProvisioningNetwork();
+
+ if (isDefaultProvisioning || isHipriProvisioning) {
+ if (mIsNotificationVisible) {
+ if (DBG) {
+ log("checkMobileProvisioning: provisioning-ignore notification is visible");
+ }
+ } else {
+ NetworkInfo ni = null;
+ if (isDefaultProvisioning) {
+ ni = mdstDefault.getNetworkInfo();
+ }
+ if (isHipriProvisioning) {
+ ni = mdstHipri.getNetworkInfo();
+ }
+ String url = getMobileProvisioningUrl();
+ if ((ni != null) && (!TextUtils.isEmpty(url))) {
+ setProvNotificationVisible(true, ni.getType(), ni.getExtraInfo(), url);
+ } else {
+ if (DBG) log("checkMobileProvisioning: provisioning but no url, ignore");
+ }
+ }
+ mIsCheckingMobileProvisioning.set(false);
+ return timeOutMs;
+ }
+
CheckMp checkMp = new CheckMp(mContext, this);
CheckMp.CallBack cb = new CheckMp.CallBack() {
@Override
void onComplete(Integer result) {
- log("CheckMp.onComplete: result=" + result);
- if (resultReceiver != null) {
- log("CheckMp.onComplete: send result");
- resultReceiver.send(result, null);
- }
+ if (DBG) log("CheckMp.onComplete: result=" + result);
NetworkInfo ni =
mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI].getNetworkInfo();
switch(result) {
- case ConnectivityManager.CMP_RESULT_CODE_CONNECTABLE:
- case ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION: {
- log("CheckMp.onComplete: ignore, connected or no connection");
+ case CMP_RESULT_CODE_CONNECTABLE:
+ case CMP_RESULT_CODE_NO_CONNECTION: {
+ if (DBG) log("CheckMp.onComplete: ignore, connected or no connection");
break;
}
- case ConnectivityManager.CMP_RESULT_CODE_REDIRECTED: {
- log("CheckMp.onComplete: warm sim");
+ case CMP_RESULT_CODE_REDIRECTED: {
+ if (DBG) log("CheckMp.onComplete: warm sim");
String url = getMobileProvisioningUrl();
if (TextUtils.isEmpty(url)) {
url = getMobileRedirectedProvisioningUrl();
}
if (TextUtils.isEmpty(url) == false) {
- log("CheckMp.onComplete: warm sim (redirected), url=" + url);
- setNotificationVisible(true, ni, url);
+ if (DBG) log("CheckMp.onComplete: warm (redirected), url=" + url);
+ setProvNotificationVisible(true, ni.getType(), ni.getExtraInfo(),
+ url);
} else {
- log("CheckMp.onComplete: warm sim (redirected), no url");
+ if (DBG) log("CheckMp.onComplete: warm (redirected), no url");
}
break;
}
- case ConnectivityManager.CMP_RESULT_CODE_NO_DNS:
- case ConnectivityManager.CMP_RESULT_CODE_NO_TCP_CONNECTION: {
+ case CMP_RESULT_CODE_NO_DNS:
+ case CMP_RESULT_CODE_NO_TCP_CONNECTION: {
String url = getMobileProvisioningUrl();
if (TextUtils.isEmpty(url) == false) {
- log("CheckMp.onComplete: warm sim (no dns/tcp), url=" + url);
- setNotificationVisible(true, ni, url);
+ if (DBG) log("CheckMp.onComplete: warm (no dns/tcp), url=" + url);
+ setProvNotificationVisible(true, ni.getType(), ni.getExtraInfo(),
+ url);
} else {
- log("CheckMp.onComplete: warm sim (no dns/tcp), no url");
+ if (DBG) log("CheckMp.onComplete: warm (no dns/tcp), no url");
}
break;
}
@@ -3635,16 +3819,16 @@
break;
}
}
+ mIsCheckingMobileProvisioning.set(false);
}
};
CheckMp.Params params =
new CheckMp.Params(checkMp.getDefaultUrl(), timeOutMs, cb);
- log("checkMobileProvisioning: params=" + params);
- setNotificationVisible(false, null, null);
+ if (DBG) log("checkMobileProvisioning: params=" + params);
checkMp.execute(params);
} finally {
Binder.restoreCallingIdentity(token);
- log("checkMobileProvisioning: X");
+ if (DBG) log("checkMobileProvisioning: X");
}
return timeOutMs;
}
@@ -3716,27 +3900,54 @@
* a known address that fetches the data we expect.
*/
private synchronized Integer isMobileOk(Params params) {
- Integer result = ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION;
+ Integer result = CMP_RESULT_CODE_NO_CONNECTION;
Uri orgUri = Uri.parse(params.mUrl);
Random rand = new Random();
mParams = params;
+ if (mCs.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false) {
+ log("isMobileOk: not mobile capable");
+ result = CMP_RESULT_CODE_NO_CONNECTION;
+ return result;
+ }
+
try {
- if (mCs.isNetworkSupported(ConnectivityManager.TYPE_MOBILE) == false) {
- log("isMobileOk: not mobile capable");
- result = ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION;
- return result;
- }
-
- // Enable fail fast as we'll do retries here and use a
- // hipri connection so the default connection stays active.
- log("isMobileOk: start hipri url=" + params.mUrl);
- mCs.setEnableFailFastMobileData(DctConstants.ENABLED);
- mCs.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE,
- Phone.FEATURE_ENABLE_HIPRI, new Binder());
-
// Continue trying to connect until time has run out
long endTime = SystemClock.elapsedRealtime() + params.mTimeOutMs;
+
+ if (!mCs.isMobileDataStateTrackerReady()) {
+ // Wait for MobileDataStateTracker to be ready.
+ if (DBG) log("isMobileOk: mdst is not ready");
+ while(SystemClock.elapsedRealtime() < endTime) {
+ if (mCs.isMobileDataStateTrackerReady()) {
+ // Enable fail fast as we'll do retries here and use a
+ // hipri connection so the default connection stays active.
+ if (DBG) log("isMobileOk: mdst ready, enable fail fast of mobile data");
+ mCs.setEnableFailFastMobileData(DctConstants.ENABLED);
+ break;
+ }
+ sleep(1);
+ }
+ }
+
+ log("isMobileOk: start hipri url=" + params.mUrl);
+
+ // First wait until we can start using hipri
+ Binder binder = new Binder();
+ while(SystemClock.elapsedRealtime() < endTime) {
+ int ret = mCs.startUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE,
+ Phone.FEATURE_ENABLE_HIPRI, binder);
+ if ((ret == PhoneConstants.APN_ALREADY_ACTIVE)
+ || (ret == PhoneConstants.APN_REQUEST_STARTED)) {
+ log("isMobileOk: hipri started");
+ break;
+ }
+ if (VDBG) log("isMobileOk: hipri not started yet");
+ result = CMP_RESULT_CODE_NO_CONNECTION;
+ sleep(1);
+ }
+
+ // Continue trying to connect until time has run out
while(SystemClock.elapsedRealtime() < endTime) {
try {
// Wait for hipri to connect.
@@ -3745,13 +3956,26 @@
NetworkInfo.State state = mCs
.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState();
if (state != NetworkInfo.State.CONNECTED) {
- log("isMobileOk: not connected ni=" +
+ if (true/*VDBG*/) {
+ log("isMobileOk: not connected ni=" +
mCs.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI));
+ }
sleep(1);
- result = ConnectivityManager.CMP_RESULT_CODE_NO_CONNECTION;
+ result = CMP_RESULT_CODE_NO_CONNECTION;
continue;
}
+ // Hipri has started check if this is a provisioning url
+ MobileDataStateTracker mdst = (MobileDataStateTracker)
+ mCs.mNetTrackers[ConnectivityManager.TYPE_MOBILE_HIPRI];
+ if (mdst.isProvisioningNetwork()) {
+ if (DBG) log("isMobileOk: isProvisioningNetwork is true, no TCP conn");
+ result = CMP_RESULT_CODE_NO_TCP_CONNECTION;
+ return result;
+ } else {
+ if (DBG) log("isMobileOk: isProvisioningNetwork is false, continue");
+ }
+
// Get of the addresses associated with the url host. We need to use the
// address otherwise HttpURLConnection object will use the name to get
// the addresses and is will try every address but that will bypass the
@@ -3762,7 +3986,7 @@
addresses = InetAddress.getAllByName(orgUri.getHost());
} catch (UnknownHostException e) {
log("isMobileOk: UnknownHostException");
- result = ConnectivityManager.CMP_RESULT_CODE_NO_DNS;
+ result = CMP_RESULT_CODE_NO_DNS;
return result;
}
log("isMobileOk: addresses=" + inetAddressesToString(addresses));
@@ -3827,9 +4051,9 @@
urlConn.setRequestProperty("Connection", "close");
int responseCode = urlConn.getResponseCode();
if (responseCode == 204) {
- result = ConnectivityManager.CMP_RESULT_CODE_CONNECTABLE;
+ result = CMP_RESULT_CODE_CONNECTABLE;
} else {
- result = ConnectivityManager.CMP_RESULT_CODE_REDIRECTED;
+ result = CMP_RESULT_CODE_REDIRECTED;
}
log("isMobileOk: connected responseCode=" + responseCode);
urlConn.disconnect();
@@ -3843,7 +4067,7 @@
}
}
}
- result = ConnectivityManager.CMP_RESULT_CODE_NO_TCP_CONNECTION;
+ result = CMP_RESULT_CODE_NO_TCP_CONNECTION;
log("isMobileOk: loops|timed out");
return result;
} catch (Exception e) {
@@ -3857,6 +4081,23 @@
mCs.setEnableFailFastMobileData(DctConstants.DISABLED);
mCs.stopUsingNetworkFeature(ConnectivityManager.TYPE_MOBILE,
Phone.FEATURE_ENABLE_HIPRI);
+
+ // Wait for hipri to disconnect.
+ long endTime = SystemClock.elapsedRealtime() + 5000;
+
+ while(SystemClock.elapsedRealtime() < endTime) {
+ NetworkInfo.State state = mCs
+ .getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI).getState();
+ if (state != NetworkInfo.State.DISCONNECTED) {
+ if (VDBG) {
+ log("isMobileOk: connected ni=" +
+ mCs.getNetworkInfo(ConnectivityManager.TYPE_MOBILE_HIPRI));
+ }
+ sleep(1);
+ continue;
+ }
+ }
+
log("isMobileOk: X result=" + result);
}
return result;
@@ -3936,10 +4177,54 @@
}
}
- private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
+ // TODO: Move to ConnectivityManager and make public?
+ private static final String CONNECTED_TO_PROVISIONING_NETWORK_ACTION =
+ "com.android.server.connectivityservice.CONNECTED_TO_PROVISIONING_NETWORK_ACTION";
- private void setNotificationVisible(boolean visible, NetworkInfo networkInfo, String url) {
- log("setNotificationVisible: E visible=" + visible + " ni=" + networkInfo + " url=" + url);
+ private BroadcastReceiver mProvisioningReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(CONNECTED_TO_PROVISIONING_NETWORK_ACTION)) {
+ handleMobileProvisioningAction(intent.getStringExtra("EXTRA_URL"));
+ }
+ }
+ };
+
+ private void handleMobileProvisioningAction(String url) {
+ // Notication mark notification as not visible
+ setProvNotificationVisible(false, ConnectivityManager.TYPE_NONE, null, null);
+
+ // If provisioning network handle as a special case,
+ // otherwise launch browser with the intent directly.
+ NetworkInfo ni = getProvisioningNetworkInfo();
+ if ((ni != null) && ni.isConnectedToProvisioningNetwork()) {
+ if (DBG) log("handleMobileProvisioningAction: on provisioning network");
+ MobileDataStateTracker mdst = (MobileDataStateTracker)
+ mNetTrackers[ConnectivityManager.TYPE_MOBILE];
+ mdst.enableMobileProvisioning(url);
+ } else {
+ if (DBG) log("handleMobileProvisioningAction: on default network");
+ Intent newIntent =
+ new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ newIntent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ try {
+ mContext.startActivity(newIntent);
+ } catch (ActivityNotFoundException e) {
+ loge("handleMobileProvisioningAction: startActivity failed" + e);
+ }
+ }
+ }
+
+ private static final String NOTIFICATION_ID = "CaptivePortal.Notification";
+ private volatile boolean mIsNotificationVisible = false;
+
+ private void setProvNotificationVisible(boolean visible, int networkType, String extraInfo,
+ String url) {
+ if (DBG) {
+ log("setProvNotificationVisible: E visible=" + visible + " networkType=" + networkType
+ + " extraInfo=" + extraInfo + " url=" + url);
+ }
Resources r = Resources.getSystem();
NotificationManager notificationManager = (NotificationManager) mContext
@@ -3949,50 +4234,64 @@
CharSequence title;
CharSequence details;
int icon;
- switch (networkInfo.getType()) {
+ Intent intent;
+ Notification notification = new Notification();
+ switch (networkType) {
case ConnectivityManager.TYPE_WIFI:
- log("setNotificationVisible: TYPE_WIFI");
title = r.getString(R.string.wifi_available_sign_in, 0);
details = r.getString(R.string.network_available_sign_in_detailed,
- networkInfo.getExtraInfo());
+ extraInfo);
icon = R.drawable.stat_notify_wifi_in_range;
+ intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
break;
case ConnectivityManager.TYPE_MOBILE:
case ConnectivityManager.TYPE_MOBILE_HIPRI:
- log("setNotificationVisible: TYPE_MOBILE|HIPRI");
title = r.getString(R.string.network_available_sign_in, 0);
// TODO: Change this to pull from NetworkInfo once a printable
// name has been added to it
details = mTelephonyManager.getNetworkOperatorName();
icon = R.drawable.stat_notify_rssi_in_range;
+ intent = new Intent(CONNECTED_TO_PROVISIONING_NETWORK_ACTION);
+ intent.putExtra("EXTRA_URL", url);
+ intent.setFlags(0);
+ notification.contentIntent = PendingIntent.getBroadcast(mContext, 0, intent, 0);
break;
default:
- log("setNotificationVisible: other type=" + networkInfo.getType());
title = r.getString(R.string.network_available_sign_in, 0);
details = r.getString(R.string.network_available_sign_in_detailed,
- networkInfo.getExtraInfo());
+ extraInfo);
icon = R.drawable.stat_notify_rssi_in_range;
+ intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
break;
}
- Notification notification = new Notification();
notification.when = 0;
notification.icon = icon;
notification.flags = Notification.FLAG_AUTO_CANCEL;
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT |
- Intent.FLAG_ACTIVITY_NEW_TASK);
- notification.contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
notification.tickerText = title;
notification.setLatestEventInfo(mContext, title, details, notification.contentIntent);
- log("setNotificaitionVisible: notify notificaiton=" + notification);
- notificationManager.notify(NOTIFICATION_ID, 1, notification);
+ try {
+ notificationManager.notify(NOTIFICATION_ID, 1, notification);
+ } catch (NullPointerException npe) {
+ loge("setNotificaitionVisible: visible notificationManager npe=" + npe);
+ npe.printStackTrace();
+ }
} else {
- log("setNotificaitionVisible: cancel");
- notificationManager.cancel(NOTIFICATION_ID, 1);
+ try {
+ notificationManager.cancel(NOTIFICATION_ID, 1);
+ } catch (NullPointerException npe) {
+ loge("setNotificaitionVisible: cancel notificationManager npe=" + npe);
+ npe.printStackTrace();
+ }
}
- log("setNotificationVisible: X visible=" + visible + " ni=" + networkInfo + " url=" + url);
+ mIsNotificationVisible = visible;
}
/** Location to an updatable file listing carrier provisioning urls.
@@ -4086,7 +4385,9 @@
return null;
}
- private String getMobileRedirectedProvisioningUrl() {
+ @Override
+ public String getMobileRedirectedProvisioningUrl() {
+ enforceConnectivityInternalPermission();
String url = getProvisioningUrlBaseFromFile(REDIRECTED_PROVISIONING);
if (TextUtils.isEmpty(url)) {
url = mContext.getResources().getString(R.string.mobile_redirected_provisioning_url);
@@ -4094,14 +4395,15 @@
return url;
}
+ @Override
public String getMobileProvisioningUrl() {
enforceConnectivityInternalPermission();
String url = getProvisioningUrlBaseFromFile(PROVISIONING);
if (TextUtils.isEmpty(url)) {
url = mContext.getResources().getString(R.string.mobile_provisioning_url);
- log("getProvisioningUrl: mobile_provisioining_url from resource =" + url);
+ log("getMobileProvisioningUrl: mobile_provisioining_url from resource =" + url);
} else {
- log("getProvisioningUrl: mobile_provisioning_url from File =" + url);
+ log("getMobileProvisioningUrl: mobile_provisioning_url from File =" + url);
}
// populate the iccid, imei and phone number in the provisioning url.
if (!TextUtils.isEmpty(url)) {
@@ -4117,4 +4419,11 @@
return url;
}
+
+ @Override
+ public void setProvisioningNotificationVisible(boolean visible, int networkType,
+ String extraInfo, String url) {
+ enforceConnectivityInternalPermission();
+ setProvNotificationVisible(visible, networkType, extraInfo, url);
+ }
}
diff --git a/services/java/com/android/server/NotificationManagerService.java b/services/java/com/android/server/NotificationManagerService.java
index 29aaeaf..29780c0 100644
--- a/services/java/com/android/server/NotificationManagerService.java
+++ b/services/java/com/android/server/NotificationManagerService.java
@@ -1606,7 +1606,7 @@
Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification);
}
checkCallerIsSystemOrSameApp(pkg);
- final boolean isSystemNotification = isCallerSystem() || ("android".equals(pkg));
+ final boolean isSystemNotification = isUidSystem(callingUid) || ("android".equals(pkg));
userId = ActivityManager.handleIncomingUser(callingPid,
callingUid, userId, true, false, "enqueueNotification", pkg);
@@ -2084,14 +2084,18 @@
cancelAllNotificationsInt(pkg, 0, Notification.FLAG_FOREGROUND_SERVICE, true, userId);
}
- // Return true if the caller is a system or phone UID and therefore should not have
+ // Return true if the UID is a system or phone UID and therefore should not have
// any notifications or toasts blocked.
- boolean isCallerSystem() {
- final int uid = Binder.getCallingUid();
+ boolean isUidSystem(int uid) {
final int appid = UserHandle.getAppId(uid);
return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0);
}
+ // same as isUidSystem(int, int) for the Binder caller's UID.
+ boolean isCallerSystem() {
+ return isUidSystem(Binder.getCallingUid());
+ }
+
void checkCallerIsSystem() {
if (isCallerSystem()) {
return;
diff --git a/telephony/java/com/android/internal/telephony/DctConstants.java b/telephony/java/com/android/internal/telephony/DctConstants.java
index 4d8342c..e50cb95 100644
--- a/telephony/java/com/android/internal/telephony/DctConstants.java
+++ b/telephony/java/com/android/internal/telephony/DctConstants.java
@@ -94,6 +94,9 @@
public static final int EVENT_DISCONNECT_DC_RETRYING = BASE + 34;
public static final int EVENT_DATA_SETUP_COMPLETE_ERROR = BASE + 35;
public static final int CMD_SET_ENABLE_FAIL_FAST_MOBILE_DATA = BASE + 36;
+ public static final int CMD_ENABLE_MOBILE_PROVISIONING = BASE + 37;
+ public static final int CMD_IS_PROVISIONING_APN = BASE + 38;
+ public static final int EVENT_PROVISIONING_APN_ALARM = BASE + 39;
/***** Constants *****/
@@ -112,4 +115,5 @@
public static final int ENABLED = 1;
public static final String APN_TYPE_KEY = "apnType";
+ public static final String PROVISIONING_URL_KEY = "provisioningUrl";
}
diff --git a/telephony/java/com/android/internal/telephony/ISms.aidl b/telephony/java/com/android/internal/telephony/ISms.aidl
index 647f014..828e025 100644
--- a/telephony/java/com/android/internal/telephony/ISms.aidl
+++ b/telephony/java/com/android/internal/telephony/ISms.aidl
@@ -1,5 +1,6 @@
/*
** Copyright 2007, The Android Open Source Project
+** Copyright (c) 2012, The Linux Foundation. All rights reserved.
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
@@ -215,4 +216,26 @@
* Requires system permission.
*/
void setPremiumSmsPermission(String packageName, int permission);
+
+ /**
+ * SMS over IMS is supported if IMS is registered and SMS is supported
+ * on IMS.
+ *
+ * @return true if SMS over IMS is supported, false otherwise
+ *
+ * @see #getImsSmsFormat()
+ */
+ boolean isImsSmsSupported();
+
+ /**
+ * Gets SMS format supported on IMS. SMS over IMS format is
+ * either 3GPP or 3GPP2.
+ *
+ * @return android.telephony.SmsMessage.FORMAT_3GPP,
+ * android.telephony.SmsMessage.FORMAT_3GPP2
+ * or android.telephony.SmsMessage.FORMAT_UNKNOWN
+ *
+ * @see #isImsSmsSupported()
+ */
+ String getImsSmsFormat();
}
diff --git a/telephony/java/com/android/internal/telephony/RILConstants.java b/telephony/java/com/android/internal/telephony/RILConstants.java
index 9650b99..5a7f13a 100644
--- a/telephony/java/com/android/internal/telephony/RILConstants.java
+++ b/telephony/java/com/android/internal/telephony/RILConstants.java
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2006 The Android Open Source Project
+ * Copyright (c) 2012, The Linux Foundation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -262,6 +263,8 @@
int RIL_REQUEST_VOICE_RADIO_TECH = 108;
int RIL_REQUEST_GET_CELL_INFO_LIST = 109;
int RIL_REQUEST_SET_UNSOL_CELL_INFO_LIST_RATE = 110;
+ int RIL_REQUEST_IMS_REGISTRATION_STATE = 111;
+ int RIL_REQUEST_IMS_SEND_SMS = 112;
int RIL_UNSOL_RESPONSE_BASE = 1000;
int RIL_UNSOL_RESPONSE_RADIO_STATE_CHANGED = 1000;
int RIL_UNSOL_RESPONSE_CALL_STATE_CHANGED = 1001;
@@ -300,4 +303,5 @@
int RIL_UNSOL_RIL_CONNECTED = 1034;
int RIL_UNSOL_VOICE_RADIO_TECH_CHANGED = 1035;
int RIL_UNSOL_CELL_INFO_LIST = 1036;
+ int RIL_UNSOL_RESPONSE_IMS_NETWORK_STATE_CHANGED = 1037;
}
diff --git a/telephony/java/com/android/internal/telephony/SmsConstants.java b/telephony/java/com/android/internal/telephony/SmsConstants.java
index 1ccdc3b..a36697a 100644
--- a/telephony/java/com/android/internal/telephony/SmsConstants.java
+++ b/telephony/java/com/android/internal/telephony/SmsConstants.java
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2012 The Android Open Source Project
+ * Copyright (c) 2012, The Linux Foundation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,6 +63,12 @@
}
/**
+ * Indicates unknown format SMS message.
+ * @hide pending API council approval
+ */
+ public static final String FORMAT_UNKNOWN = "unknown";
+
+ /**
* Indicates a 3GPP format SMS message.
* @hide pending API council approval
*/
diff --git a/telephony/java/com/android/internal/telephony/TelephonyIntents.java b/telephony/java/com/android/internal/telephony/TelephonyIntents.java
index 3cfd0bf..40a3c8f 100644
--- a/telephony/java/com/android/internal/telephony/TelephonyIntents.java
+++ b/telephony/java/com/android/internal/telephony/TelephonyIntents.java
@@ -145,6 +145,29 @@
public static final String ACTION_ANY_DATA_CONNECTION_STATE_CHANGED
= "android.intent.action.ANY_DATA_STATE";
+ /**
+ * Broadcast Action: Occurs when a data connection connects to a provisioning apn
+ * and is broadcast by the low level data connection code.
+ * The intent will have the following extra values:</p>
+ * <ul>
+ * <li><em>apn</em> - A string that is the APN associated with this
+ * connection.</li>
+ * <li><em>apnType</em> - A string array of APN types associated with
+ * this connection. The APN type <code>"*"</code> is a special
+ * type that means this APN services all types.</li>
+ * <li><em>linkProperties</em> - The <code>LinkProperties</code> for this APN</li>
+ * <li><em>linkCapabilities</em> - The <code>linkCapabilities</code> for this APN</li>
+ * <li><em>iface</em> - A string that is the name of the interface</li>
+ * </ul>
+ *
+ * <p class="note">
+ * Requires the READ_PHONE_STATE permission.
+ *
+ * <p class="note">This is a protected intent that can only be sent
+ * by the system.
+ */
+ public static final String ACTION_DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN
+ = "android.intent.action.DATA_CONNECTION_CONNECTED_TO_PROVISIONING_APN";
/**
* Broadcast Action: An attempt to establish a data connection has failed.
diff --git a/tools/layoutlib/bridge/.classpath b/tools/layoutlib/bridge/.classpath
index 3c124d9..2e4274d 100644
--- a/tools/layoutlib/bridge/.classpath
+++ b/tools/layoutlib/bridge/.classpath
@@ -7,5 +7,6 @@
<classpathentry kind="var" path="ANDROID_PLAT_SRC/out/host/common/obj/JAVA_LIBRARIES/temp_layoutlib_intermediates/javalib.jar" sourcepath="/ANDROID_PLAT_SRC/frameworks/base"/>
<classpathentry kind="var" path="ANDROID_PLAT_SRC/prebuilts/misc/common/ninepatch/ninepatch-prebuilt.jar"/>
<classpathentry kind="var" path="ANDROID_PLAT_SRC/prebuilts/misc/common/tools-common/tools-common-prebuilt.jar"/>
+ <classpathentry kind="var" path="ANDROID_PLAT_SRC/prebuilts/misc/common/icu4j/icu4j.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>
diff --git a/tools/layoutlib/bridge/Android.mk b/tools/layoutlib/bridge/Android.mk
index 687a91f..e3d48fc 100644
--- a/tools/layoutlib/bridge/Android.mk
+++ b/tools/layoutlib/bridge/Android.mk
@@ -22,6 +22,7 @@
LOCAL_JAVA_LIBRARIES := \
kxml2-2.3.0 \
+ icu4j \
layoutlib_api-prebuilt \
tools-common-prebuilt
diff --git a/tools/layoutlib/bridge/resources/bars/ldrtl-hdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/ldrtl-hdpi/ic_sysbar_back.png
new file mode 100644
index 0000000..782ebfe
--- /dev/null
+++ b/tools/layoutlib/bridge/resources/bars/ldrtl-hdpi/ic_sysbar_back.png
Binary files differ
diff --git a/tools/layoutlib/bridge/resources/bars/ldrtl-hdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/ldrtl-hdpi/ic_sysbar_recent.png
new file mode 100644
index 0000000..677b471
--- /dev/null
+++ b/tools/layoutlib/bridge/resources/bars/ldrtl-hdpi/ic_sysbar_recent.png
Binary files differ
diff --git a/tools/layoutlib/bridge/resources/bars/ldrtl-mdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/ldrtl-mdpi/ic_sysbar_back.png
new file mode 100644
index 0000000..a1b8062
--- /dev/null
+++ b/tools/layoutlib/bridge/resources/bars/ldrtl-mdpi/ic_sysbar_back.png
Binary files differ
diff --git a/tools/layoutlib/bridge/resources/bars/ldrtl-mdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/ldrtl-mdpi/ic_sysbar_recent.png
new file mode 100644
index 0000000..fcdbefe
--- /dev/null
+++ b/tools/layoutlib/bridge/resources/bars/ldrtl-mdpi/ic_sysbar_recent.png
Binary files differ
diff --git a/tools/layoutlib/bridge/resources/bars/ldrtl-xhdpi/ic_sysbar_back.png b/tools/layoutlib/bridge/resources/bars/ldrtl-xhdpi/ic_sysbar_back.png
new file mode 100644
index 0000000..633d864
--- /dev/null
+++ b/tools/layoutlib/bridge/resources/bars/ldrtl-xhdpi/ic_sysbar_back.png
Binary files differ
diff --git a/tools/layoutlib/bridge/resources/bars/ldrtl-xhdpi/ic_sysbar_recent.png b/tools/layoutlib/bridge/resources/bars/ldrtl-xhdpi/ic_sysbar_recent.png
new file mode 100644
index 0000000..4665e2a
--- /dev/null
+++ b/tools/layoutlib/bridge/resources/bars/ldrtl-xhdpi/ic_sysbar_recent.png
Binary files differ
diff --git a/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java b/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java
new file mode 100644
index 0000000..62d0a0d
--- /dev/null
+++ b/tools/layoutlib/bridge/src/android/graphics/BidiRenderer.java
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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 android.graphics;
+
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
+import java.util.LinkedList;
+import java.util.List;
+
+import com.ibm.icu.lang.UScript;
+import com.ibm.icu.lang.UScriptRun;
+
+import android.graphics.Paint_Delegate.FontInfo;
+
+/**
+ * Render the text by breaking it into various scripts and using the right font for each script.
+ * Can be used to measure the text without actually drawing it.
+ */
+@SuppressWarnings("deprecation")
+public class BidiRenderer {
+
+ /* package */ static class ScriptRun {
+ int start;
+ int limit;
+ boolean isRtl;
+ int scriptCode;
+ FontInfo font;
+
+ public ScriptRun(int start, int limit, boolean isRtl) {
+ this.start = start;
+ this.limit = limit;
+ this.isRtl = isRtl;
+ this.scriptCode = UScript.INVALID_CODE;
+ }
+ }
+
+ /* package */ Graphics2D graphics;
+ /* package */ Paint_Delegate paint;
+ /* package */ char[] text;
+
+ /**
+ * @param graphics May be null.
+ * @param paint The Paint to use to get the fonts. Should not be null.
+ * @param text Unidirectional text. Should not be null.
+ */
+ /* package */ BidiRenderer(Graphics2D graphics, Paint_Delegate paint, char[] text) {
+ assert (paint != null);
+ this.graphics = graphics;
+ this.paint = paint;
+ this.text = text;
+ }
+
+ /**
+ * Render unidirectional text.
+ *
+ * This method can also be used to measure the width of the text without actually drawing it.
+ *
+ * @param start index of the first character
+ * @param limit index of the first character that should not be rendered.
+ * @param isRtl is the text right-to-left
+ * @param advances If not null, then advances for each character to be rendered are returned
+ * here.
+ * @param advancesIndex index into advances from where the advances need to be filled.
+ * @param draw If true and {@link graphics} is not null, draw the rendered text on the graphics
+ * at the given co-ordinates
+ * @param x The x-coordinate of the left edge of where the text should be drawn on the given
+ * graphics.
+ * @param y The y-coordinate at which to draw the text on the given graphics.
+ * @return The x-coordinate of the right edge of the drawn text. In other words,
+ * x + the width of the text.
+ */
+ /* package */ float renderText(int start, int limit, boolean isRtl, float advances[],
+ int advancesIndex, boolean draw, float x, float y) {
+ // We break the text into scripts and then select font based on it and then render each of
+ // the script runs.
+ for (ScriptRun run : getScriptRuns(text, start, limit, isRtl, paint.getFonts())) {
+ int flag = Font.LAYOUT_NO_LIMIT_CONTEXT | Font.LAYOUT_NO_START_CONTEXT;
+ flag |= isRtl ? Font.LAYOUT_RIGHT_TO_LEFT : Font.LAYOUT_LEFT_TO_RIGHT;
+ x = renderScript(run.start, run.limit, run.font, flag, advances, advancesIndex, draw,
+ x, y);
+ advancesIndex += run.limit - run.start;
+ }
+ return x;
+ }
+
+ /**
+ * Render a script run. Use the preferred font to render as much as possible. This also
+ * implements a fallback mechanism to render characters that cannot be drawn using the
+ * preferred font.
+ *
+ * @return x + width of the text drawn.
+ */
+ private float renderScript(int start, int limit, FontInfo preferredFont, int flag,
+ float advances[], int advancesIndex, boolean draw, float x, float y) {
+ List<FontInfo> fonts = paint.getFonts();
+ if (fonts == null || preferredFont == null) {
+ return x;
+ }
+
+ while (start < limit) {
+ boolean foundFont = false;
+ int canDisplayUpTo = preferredFont.mFont.canDisplayUpTo(text, start, limit);
+ if (canDisplayUpTo == -1) {
+ return render(start, limit, preferredFont, flag, advances, advancesIndex, draw,
+ x, y);
+ } else if (canDisplayUpTo > start) { // can draw something
+ x = render(start, canDisplayUpTo, preferredFont, flag, advances, advancesIndex,
+ draw, x, y);
+ advancesIndex += canDisplayUpTo - start;
+ start = canDisplayUpTo;
+ }
+
+ int charCount = Character.isHighSurrogate(text[start]) ? 2 : 1;
+ for (FontInfo font : fonts) {
+ canDisplayUpTo = font.mFont.canDisplayUpTo(text, start, start + charCount);
+ if (canDisplayUpTo == -1) {
+ x = render(start, start+charCount, font, flag, advances, advancesIndex, draw,
+ x, y);
+ start += charCount;
+ advancesIndex += charCount;
+ foundFont = true;
+ break;
+ }
+ }
+ if (!foundFont) {
+ // No font can display this char. Use the preferred font. The char will most
+ // probably appear as a box or a blank space. We could, probably, use some
+ // heuristics and break the character into the base character and diacritics and
+ // then draw it, but it's probably not worth the effort.
+ x = render(start, start + charCount, preferredFont, flag, advances, advancesIndex,
+ draw, x, y);
+ start += charCount;
+ advancesIndex += charCount;
+ }
+ }
+ return x;
+ }
+
+ /**
+ * Render the text with the given font.
+ */
+ private float render(int start, int limit, FontInfo font, int flag, float advances[],
+ int advancesIndex, boolean draw, float x, float y) {
+
+ float totalAdvance = 0;
+ // Since the metrics don't have anti-aliasing set, we create a new FontRenderContext with
+ // the anti-aliasing set.
+ FontRenderContext f = font.mMetrics.getFontRenderContext();
+ FontRenderContext frc = new FontRenderContext(f.getTransform(), paint.isAntiAliased(),
+ f.usesFractionalMetrics());
+ GlyphVector gv = font.mFont.layoutGlyphVector(frc, text, start, limit, flag);
+ int ng = gv.getNumGlyphs();
+ int[] ci = gv.getGlyphCharIndices(0, ng, null);
+ for (int i = 0; i < ng; i++) {
+ float adv = gv.getGlyphMetrics(i).getAdvanceX();
+ if (advances != null) {
+ int adv_idx = advancesIndex + ci[i];
+ advances[adv_idx] += adv;
+ }
+ totalAdvance += adv;
+ }
+ if (draw && graphics != null) {
+ graphics.drawGlyphVector(gv, x, y);
+ }
+ return x + totalAdvance;
+ }
+
+ // --- Static helper methods ---
+
+ /* package */ static List<ScriptRun> getScriptRuns(char[] text, int start, int limit,
+ boolean isRtl, List<FontInfo> fonts) {
+ LinkedList<ScriptRun> scriptRuns = new LinkedList<ScriptRun>();
+
+ int count = limit - start;
+ UScriptRun uScriptRun = new UScriptRun(text, start, count);
+ while (uScriptRun.next()) {
+ int scriptStart = uScriptRun.getScriptStart();
+ int scriptLimit = uScriptRun.getScriptLimit();
+ ScriptRun run = new ScriptRun(scriptStart, scriptLimit, isRtl);
+ run.scriptCode = uScriptRun.getScriptCode();
+ setScriptFont(text, run, fonts);
+ scriptRuns.add(run);
+ }
+
+ return scriptRuns;
+ }
+
+ // TODO: Replace this method with one which returns the font based on the scriptCode.
+ private static void setScriptFont(char[] text, ScriptRun run,
+ List<FontInfo> fonts) {
+ for (FontInfo fontInfo : fonts) {
+ if (fontInfo.mFont.canDisplayUpTo(text, run.start, run.limit) == -1) {
+ run.font = fontInfo;
+ return;
+ }
+ }
+ run.font = fonts.get(0);
+ }
+}
diff --git a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
index 4171bb5..da18864 100644
--- a/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/Canvas_Delegate.java
@@ -23,7 +23,6 @@
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
import android.graphics.Bitmap.Config;
-import android.graphics.Paint_Delegate.FontInfo;
import android.text.TextUtils;
import java.awt.Color;
@@ -35,7 +34,6 @@
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.image.BufferedImage;
-import java.util.List;
/**
@@ -978,7 +976,8 @@
@LayoutlibDelegate
/*package*/ static void native_drawText(int nativeCanvas,
final char[] text, final int index, final int count,
- final float startX, final float startY, int flags, int paint) {
+ final float startX, final float startY, final int flags, int paint) {
+
draw(nativeCanvas, paint, false /*compositeOnly*/, false /*forceSrcMode*/,
new GcSnapshot.Drawable() {
@Override
@@ -988,10 +987,10 @@
// Paint.TextAlign indicates how the text is positioned relative to X.
// LEFT is the default and there's nothing to do.
float x = startX;
- float y = startY;
+ int limit = index + count;
+ boolean isRtl = flags == Canvas.DIRECTION_RTL;
if (paintDelegate.getTextAlign() != Paint.Align.LEFT.nativeInt) {
- // TODO: check the value of bidiFlags.
- float m = paintDelegate.measureText(text, index, count, 0);
+ float m = paintDelegate.measureText(text, index, count, isRtl);
if (paintDelegate.getTextAlign() == Paint.Align.CENTER.nativeInt) {
x -= m / 2;
} else if (paintDelegate.getTextAlign() == Paint.Align.RIGHT.nativeInt) {
@@ -999,87 +998,15 @@
}
}
- List<FontInfo> fonts = paintDelegate.getFonts();
-
- if (fonts.size() > 0) {
- FontInfo mainFont = fonts.get(0);
- int i = index;
- int lastIndex = index + count;
- while (i < lastIndex) {
- // always start with the main font.
- int upTo = mainFont.mFont.canDisplayUpTo(text, i, lastIndex);
- if (upTo == -1) {
- // draw all the rest and exit.
- graphics.setFont(mainFont.mFont);
- graphics.drawChars(text, i, lastIndex - i, (int)x, (int)y);
- return;
- } else if (upTo > 0) {
- // draw what's possible
- graphics.setFont(mainFont.mFont);
- graphics.drawChars(text, i, upTo - i, (int)x, (int)y);
-
- // compute the width that was drawn to increase x
- x += mainFont.mMetrics.charsWidth(text, i, upTo - i);
-
- // move index to the first non displayed char.
- i = upTo;
-
- // don't call continue at this point. Since it is certain the main font
- // cannot display the font a index upTo (now ==i), we move on to the
- // fallback fonts directly.
- }
-
- // no char supported, attempt to read the next char(s) with the
- // fallback font. In this case we only test the first character
- // and then go back to test with the main font.
- // Special test for 2-char characters.
- boolean foundFont = false;
- for (int f = 1 ; f < fonts.size() ; f++) {
- FontInfo fontInfo = fonts.get(f);
-
- // need to check that the font can display the character. We test
- // differently if the char is a high surrogate.
- int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1;
- upTo = fontInfo.mFont.canDisplayUpTo(text, i, i + charCount);
- if (upTo == -1) {
- // draw that char
- graphics.setFont(fontInfo.mFont);
- graphics.drawChars(text, i, charCount, (int)x, (int)y);
-
- // update x
- x += fontInfo.mMetrics.charsWidth(text, i, charCount);
-
- // update the index in the text, and move on
- i += charCount;
- foundFont = true;
- break;
-
- }
- }
-
- // in case no font can display the char, display it with the main font.
- // (it'll put a square probably)
- if (foundFont == false) {
- int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1;
-
- graphics.setFont(mainFont.mFont);
- graphics.drawChars(text, i, charCount, (int)x, (int)y);
-
- // measure it to advance x
- x += mainFont.mMetrics.charsWidth(text, i, charCount);
-
- // and move to the next chars.
- i += charCount;
- }
- }
- }
+ new BidiRenderer(graphics, paintDelegate, text).renderText(
+ index, limit, isRtl, null, 0, true, x, startY);
}
});
}
@LayoutlibDelegate
/*package*/ static void native_drawText(int nativeCanvas, String text,
- int start, int end, float x, float y, int flags, int paint) {
+ int start, int end, float x, float y, final int flags, int paint) {
int count = end - start;
char[] buffer = TemporaryBuffer.obtain(count);
TextUtils.getChars(text, start, end, buffer, 0);
diff --git a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java
index c9c9800..41953ed 100644
--- a/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/graphics/Paint_Delegate.java
@@ -32,7 +32,6 @@
import java.awt.Toolkit;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
-import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -576,7 +575,7 @@
return 0;
}
- return delegate.measureText(text, index, count, bidiFlags);
+ return delegate.measureText(text, index, count, isRtl(bidiFlags));
}
@LayoutlibDelegate
@@ -615,7 +614,7 @@
}
// measure from start to end
- float res = delegate.measureText(text, start, end - start + 1, bidiFlags);
+ float res = delegate.measureText(text, start, end - start + 1, isRtl(bidiFlags));
if (measuredWidth != null) {
measuredWidth[measureIndex] = res;
@@ -980,51 +979,27 @@
/*package*/ static float native_getTextRunAdvances(int native_object,
char[] text, int index, int count, int contextIndex, int contextCount,
int flags, float[] advances, int advancesIndex) {
+
+ if (advances != null)
+ for (int i = advancesIndex; i< advancesIndex+count; i++)
+ advances[i]=0;
// get the delegate from the native int.
Paint_Delegate delegate = sManager.getDelegate(native_object);
- if (delegate == null) {
+ if (delegate == null || delegate.mFonts == null || delegate.mFonts.size() == 0) {
return 0.f;
}
+ boolean isRtl = isRtl(flags);
- if (delegate.mFonts.size() > 0) {
- // FIXME: handle multi-char characters (see measureText)
- float totalAdvance = 0;
- for (int i = 0; i < count; i++) {
- char c = text[i + index];
- boolean found = false;
- for (FontInfo info : delegate.mFonts) {
- if (info.mFont.canDisplay(c)) {
- float adv = info.mMetrics.charWidth(c);
- totalAdvance += adv;
- if (advances != null) {
- advances[i] = adv;
- }
-
- found = true;
- break;
- }
- }
-
- if (found == false) {
- // no advance for this char.
- if (advances != null) {
- advances[i] = 0.f;
- }
- }
- }
-
- return totalAdvance;
- }
-
- return 0;
-
+ int limit = index + count;
+ return new BidiRenderer(null, delegate, text).renderText(
+ index, limit, isRtl, advances, advancesIndex, false, 0, 0);
}
@LayoutlibDelegate
/*package*/ static float native_getTextRunAdvances(int native_object,
String text, int start, int end, int contextStart, int contextEnd,
int flags, float[] advances, int advancesIndex) {
- // FIXME: support contextStart, contextEnd and direction flag
+ // FIXME: support contextStart and contextEnd
int count = end - start;
char[] buffer = TemporaryBuffer.obtain(count);
TextUtils.getChars(text, start, end, buffer, 0);
@@ -1080,19 +1055,12 @@
// get the delegate from the native int.
Paint_Delegate delegate = sManager.getDelegate(nativePaint);
- if (delegate == null) {
+ if (delegate == null || delegate.mFonts == null || delegate.mFonts.size() == 0) {
return;
}
-
- // FIXME should test if the main font can display all those characters.
- // See MeasureText
- if (delegate.mFonts.size() > 0) {
- FontInfo mainInfo = delegate.mFonts.get(0);
-
- Rectangle2D rect = mainInfo.mFont.getStringBounds(text, index, index + count,
- delegate.mFontContext);
- bounds.set(0, 0, (int) rect.getWidth(), (int) rect.getHeight());
- }
+ int w = (int) delegate.measureText(text, index, count, isRtl(bidiFlags));
+ int h= delegate.getFonts().get(0).mMetrics.getHeight();
+ bounds.set(0, 0, w, h);
}
@LayoutlibDelegate
@@ -1176,6 +1144,7 @@
info.mFont = info.mFont.deriveFont(new AffineTransform(
mTextScaleX, mTextSkewX, 0, 1, 0, 0));
}
+ // The metrics here don't have anti-aliasing set.
info.mMetrics = Toolkit.getDefaultToolkit().getFontMetrics(info.mFont);
infoList.add(info);
@@ -1185,64 +1154,9 @@
}
}
- /*package*/ float measureText(char[] text, int index, int count, int bidiFlags) {
- // TODO: find out what bidiFlags actually does.
-
- // WARNING: the logic in this method is similar to Canvas_Delegate.native_drawText
- // Any change to this method should be reflected there as well
-
- if (mFonts.size() > 0) {
- FontInfo mainFont = mFonts.get(0);
- int i = index;
- int lastIndex = index + count;
- float total = 0f;
- while (i < lastIndex) {
- // always start with the main font.
- int upTo = mainFont.mFont.canDisplayUpTo(text, i, lastIndex);
- if (upTo == -1) {
- // shortcut to exit
- return total + mainFont.mMetrics.charsWidth(text, i, lastIndex - i);
- } else if (upTo > 0) {
- total += mainFont.mMetrics.charsWidth(text, i, upTo - i);
- i = upTo;
- // don't call continue at this point. Since it is certain the main font
- // cannot display the font a index upTo (now ==i), we move on to the
- // fallback fonts directly.
- }
-
- // no char supported, attempt to read the next char(s) with the
- // fallback font. In this case we only test the first character
- // and then go back to test with the main font.
- // Special test for 2-char characters.
- boolean foundFont = false;
- for (int f = 1 ; f < mFonts.size() ; f++) {
- FontInfo fontInfo = mFonts.get(f);
-
- // need to check that the font can display the character. We test
- // differently if the char is a high surrogate.
- int charCount = Character.isHighSurrogate(text[i]) ? 2 : 1;
- upTo = fontInfo.mFont.canDisplayUpTo(text, i, i + charCount);
- if (upTo == -1) {
- total += fontInfo.mMetrics.charsWidth(text, i, charCount);
- i += charCount;
- foundFont = true;
- break;
-
- }
- }
-
- // in case no font can display the char, measure it with the main font.
- if (foundFont == false) {
- int size = Character.isHighSurrogate(text[i]) ? 2 : 1;
- total += mainFont.mMetrics.charsWidth(text, i, size);
- i += size;
- }
- }
-
- return total;
- }
-
- return 0;
+ /*package*/ float measureText(char[] text, int index, int count, boolean isRtl) {
+ return new BidiRenderer(null, this, text).renderText(
+ index, index + count, isRtl, null, 0, false, 0, 0);
}
private float getFontMetrics(FontMetrics metrics) {
@@ -1281,4 +1195,14 @@
}
}
+ private static boolean isRtl(int flag) {
+ switch(flag) {
+ case Paint.BIDI_RTL:
+ case Paint.BIDI_FORCE_RTL:
+ case Paint.BIDI_DEFAULT_RTL:
+ return true;
+ default:
+ return false;
+ }
+ }
}
diff --git a/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java b/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java
index 52b8f34..973fa0e 100644
--- a/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java
+++ b/tools/layoutlib/bridge/src/android/text/AndroidBidi_Delegate.java
@@ -16,7 +16,10 @@
package android.text;
+import com.android.ide.common.rendering.api.LayoutLog;
+import com.android.layoutlib.bridge.Bridge;
import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+import com.ibm.icu.text.Bidi;
/**
@@ -29,9 +32,29 @@
public class AndroidBidi_Delegate {
@LayoutlibDelegate
- /*package*/ static int runBidi(int dir, char[] chs, byte[] chInfo, int n, boolean haveInfo) {
- // return the equivalent of Layout.DIR_LEFT_TO_RIGHT
- // TODO: actually figure the direction.
- return 0;
+ /*package*/ static int runBidi(int dir, char[] chars, byte[] charInfo, int count,
+ boolean haveInfo) {
+
+ switch (dir) {
+ case 0: // Layout.DIR_REQUEST_LTR
+ case 1: // Layout.DIR_REQUEST_RTL
+ break; // No change.
+ case -1:
+ dir = Bidi.LEVEL_DEFAULT_LTR;
+ break;
+ case -2:
+ dir = Bidi.LEVEL_DEFAULT_RTL;
+ break;
+ default:
+ // Invalid code. Log error, assume LEVEL_DEFAULT_LTR and continue.
+ Bridge.getLog().error(LayoutLog.TAG_BROKEN, "Invalid direction flag", null);
+ dir = Bidi.LEVEL_DEFAULT_LTR;
+ }
+ Bidi bidi = new Bidi(chars, 0, null, 0, count, dir);
+ if (charInfo != null) {
+ for (int i = 0; i < count; ++i)
+ charInfo[i] = bidi.getLevelAt(i);
+ }
+ return bidi.getParaLevel();
}
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java
index 42257c5..ab4be71 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/Bridge.java
@@ -35,6 +35,7 @@
import com.android.tools.layoutlib.create.MethodAdapter;
import com.android.tools.layoutlib.create.OverrideMethod;
import com.android.util.Pair;
+import com.ibm.icu.util.ULocale;
import android.content.res.BridgeAssetManager;
import android.graphics.Bitmap;
@@ -64,6 +65,8 @@
*/
public final class Bridge extends com.android.ide.common.rendering.api.Bridge {
+ private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left";
+
public static class StaticMethodNotImplementedException extends RuntimeException {
private static final long serialVersionUID = 1L;
@@ -211,7 +214,8 @@
Capability.ANIMATED_VIEW_MANIPULATION,
Capability.ADAPTER_BINDING,
Capability.EXTENDED_VIEWINFO,
- Capability.FIXED_SCALABLE_NINE_PATCH);
+ Capability.FIXED_SCALABLE_NINE_PATCH,
+ Capability.RTL);
BridgeAssetManager.initSystem();
@@ -411,6 +415,20 @@
throw new IllegalArgumentException("viewObject is not a View");
}
+ @Override
+ public boolean isRtl(String locale) {
+ return isLocaleRtl(locale);
+ }
+
+ public static boolean isLocaleRtl(String locale) {
+ if (locale == null) {
+ locale = "";
+ }
+ ULocale uLocale = new ULocale(locale);
+ return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL) ?
+ true : false;
+ }
+
/**
* Returns the lock for the bridge
*/
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
index 21bef1c..99aa228 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeContext.java
@@ -132,7 +132,8 @@
RenderResources renderResources,
IProjectCallback projectCallback,
Configuration config,
- int targetSdkVersion) {
+ int targetSdkVersion,
+ boolean hasRtlSupport) {
mProjectKey = projectKey;
mMetrics = metrics;
mProjectCallback = projectCallback;
@@ -142,6 +143,9 @@
mApplicationInfo = new ApplicationInfo();
mApplicationInfo.targetSdkVersion = targetSdkVersion;
+ if (hasRtlSupport) {
+ mApplicationInfo.flags = mApplicationInfo.flags | ApplicationInfo.FLAG_SUPPORTS_RTL;
+ }
mWindowManager = new WindowManagerImpl(mMetrics);
}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java
index ea9d8d9..17b0eb6 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/CustomBar.java
@@ -25,6 +25,7 @@
import com.android.layoutlib.bridge.impl.ParserFactory;
import com.android.layoutlib.bridge.impl.ResourceHelper;
import com.android.resources.Density;
+import com.android.resources.LayoutDirection;
import com.android.resources.ResourceType;
import org.xmlpull.v1.XmlPullParser;
@@ -86,38 +87,53 @@
}
}
- private InputStream getIcon(String iconName, Density[] densityInOut, String[] pathOut,
- boolean tryOtherDensities) {
+ private InputStream getIcon(String iconName, Density[] densityInOut, LayoutDirection direction,
+ String[] pathOut, boolean tryOtherDensities) {
// current density
Density density = densityInOut[0];
// bitmap url relative to this class
- pathOut[0] = "/bars/" + density.getResourceValue() + "/" + iconName;
+ if (direction != null) {
+ pathOut[0] = "/bars/" + direction.getResourceValue() + "-" + density.getResourceValue()
+ + "/" + iconName;
+ } else {
+ pathOut[0] = "/bars/" + density.getResourceValue() + "/" + iconName;
+ }
InputStream stream = getClass().getResourceAsStream(pathOut[0]);
if (stream == null && tryOtherDensities) {
for (Density d : Density.values()) {
if (d != density) {
densityInOut[0] = d;
- stream = getIcon(iconName, densityInOut, pathOut, false /*tryOtherDensities*/);
+ stream = getIcon(iconName, densityInOut, direction, pathOut,
+ false /*tryOtherDensities*/);
if (stream != null) {
return stream;
}
}
}
+ // couldn't find resource with direction qualifier. try without.
+ if (direction != null) {
+ return getIcon(iconName, densityInOut, null, pathOut, true);
+ }
}
return stream;
}
protected void loadIcon(int index, String iconName, Density density) {
+ loadIcon(index, iconName, density, false);
+ }
+
+ protected void loadIcon(int index, String iconName, Density density, boolean isRtl) {
View child = getChildAt(index);
if (child instanceof ImageView) {
ImageView imageView = (ImageView) child;
String[] pathOut = new String[1];
Density[] densityInOut = new Density[] { density };
- InputStream stream = getIcon(iconName, densityInOut, pathOut,
+ LayoutDirection dir = isRtl ? LayoutDirection.RTL : LayoutDirection.LTR;
+ InputStream stream = getIcon(iconName, densityInOut, dir, pathOut,
true /*tryOtherDensities*/);
density = densityInOut[0];
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java
index cc90d6b..84e676e 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/NavigationBar.java
@@ -17,6 +17,7 @@
package com.android.layoutlib.bridge.bars;
import com.android.resources.Density;
+import com.android.layoutlib.bridge.Bridge;
import org.xmlpull.v1.XmlPullParserException;
@@ -26,7 +27,8 @@
public class NavigationBar extends CustomBar {
- public NavigationBar(Context context, Density density, int orientation) throws XmlPullParserException {
+ public NavigationBar(Context context, Density density, int orientation, boolean isRtl,
+ boolean rtlEnabled) throws XmlPullParserException {
super(context, density, orientation, "/bars/navigation_bar.xml", "navigation_bar.xml");
setBackgroundColor(0xFF000000);
@@ -37,14 +39,15 @@
// 0 is a spacer.
int back = 1;
int recent = 3;
- if (orientation == LinearLayout.VERTICAL) {
+ if (orientation == LinearLayout.VERTICAL || (isRtl && !rtlEnabled)) {
+ // If RTL is enabled, then layoutlib mirrors the layout for us.
back = 3;
recent = 1;
}
- loadIcon(back, "ic_sysbar_back.png", density);
- loadIcon(2, "ic_sysbar_home.png", density);
- loadIcon(recent, "ic_sysbar_recent.png", density);
+ loadIcon(back, "ic_sysbar_back.png", density, isRtl);
+ loadIcon(2, "ic_sysbar_home.png", density, isRtl);
+ loadIcon(recent, "ic_sysbar_recent.png", density, isRtl);
}
@Override
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/StatusBar.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/StatusBar.java
index 5c08412..baa956d 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/StatusBar.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/bars/StatusBar.java
@@ -30,7 +30,10 @@
public class StatusBar extends CustomBar {
- public StatusBar(Context context, Density density) throws XmlPullParserException {
+ public StatusBar(Context context, Density density, int direction, boolean RtlEnabled)
+ throws XmlPullParserException {
+ // FIXME: if direction is RTL but it's not enabled in application manifest, mirror this bar.
+
super(context, density, LinearLayout.HORIZONTAL, "/bars/status_bar.xml", "status_bar.xml");
// FIXME: use FILL_H?
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/FontLoader.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/FontLoader.java
index 081ce67..108b651 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/FontLoader.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/FontLoader.java
@@ -52,6 +52,8 @@
private static final String NODE_NAME = "name";
private static final String NODE_FILE = "file";
+ private static final String ATTRIBUTE_VARIANT = "variant";
+ private static final String ATTRIBUTE_VALUE_ELEGANT = "elegant";
private static final String FONT_SUFFIX_NONE = ".ttf";
private static final String FONT_SUFFIX_REGULAR = "-Regular.ttf";
private static final String FONT_SUFFIX_BOLD = "-Bold.ttf";
@@ -189,6 +191,7 @@
private FontInfo mFontInfo = null;
private final StringBuilder mBuilder = new StringBuilder();
private List<FontInfo> mFontList = new ArrayList<FontInfo>();
+ private boolean isCompactFont = true;
private FontHandler(String osFontsLocation) {
super();
@@ -209,8 +212,21 @@
mFontList = new ArrayList<FontInfo>();
} else if (NODE_FAMILY.equals(localName)) {
if (mFontList != null) {
+ mFontInfo = null;
+ }
+ } else if (NODE_NAME.equals(localName)) {
+ if (mFontList != null && mFontInfo == null) {
mFontInfo = new FontInfo();
}
+ } else if (NODE_FILE.equals(localName)) {
+ if (mFontList != null && mFontInfo == null) {
+ mFontInfo = new FontInfo();
+ }
+ if (ATTRIBUTE_VALUE_ELEGANT.equals(attributes.getValue(ATTRIBUTE_VARIANT))) {
+ isCompactFont = false;
+ } else {
+ isCompactFont = true;
+ }
}
mBuilder.setLength(0);
@@ -223,7 +239,9 @@
*/
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
- mBuilder.append(ch, start, length);
+ if (isCompactFont) {
+ mBuilder.append(ch, start, length);
+ }
}
/* (non-Javadoc)
@@ -259,7 +277,7 @@
}
} else if (NODE_FILE.equals(localName)) {
// handle a new file for an existing Font Info
- if (mFontInfo != null) {
+ if (isCompactFont && mFontInfo != null) {
String fileName = trimXmlWhitespaces(mBuilder.toString());
Font font = getFont(fileName);
if (font != null) {
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java
index b909bec..87047b3 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderAction.java
@@ -121,7 +121,8 @@
// build the context
mContext = new BridgeContext(mParams.getProjectKey(), metrics, resources,
- mParams.getProjectCallback(), getConfiguration(), mParams.getTargetSdkVersion());
+ mParams.getProjectCallback(), getConfiguration(), mParams.getTargetSdkVersion(),
+ mParams.isRtlSupported());
setUp();
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
index c14af4a..9ddbbf1 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/RenderSessionImpl.java
@@ -225,13 +225,15 @@
SessionParams params = getParams();
HardwareConfig hardwareConfig = params.getHardwareConfig();
BridgeContext context = getContext();
-
+ boolean isRtl = Bridge.isLocaleRtl(params.getLocale());
+ int direction = isRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR;
// the view group that receives the window background.
ViewGroup backgroundView = null;
if (mWindowIsFloating || params.isForceNoDecor()) {
backgroundView = mViewRoot = mContentRoot = new FrameLayout(context);
+ mViewRoot.setLayoutDirection(direction);
} else {
if (hasSoftwareButtons() && mNavigationBarOrientation == LinearLayout.VERTICAL) {
/*
@@ -253,12 +255,14 @@
the bottom
*/
LinearLayout topLayout = new LinearLayout(context);
+ topLayout.setLayoutDirection(direction);
mViewRoot = topLayout;
topLayout.setOrientation(LinearLayout.HORIZONTAL);
try {
NavigationBar navigationBar = new NavigationBar(context,
- hardwareConfig.getDensity(), LinearLayout.VERTICAL);
+ hardwareConfig.getDensity(), LinearLayout.VERTICAL, isRtl,
+ params.isRtlSupported());
navigationBar.setLayoutParams(
new LinearLayout.LayoutParams(
mNavigationBarSize,
@@ -290,6 +294,7 @@
LinearLayout topLayout = new LinearLayout(context);
topLayout.setOrientation(LinearLayout.VERTICAL);
+ topLayout.setLayoutDirection(direction);
// if we don't already have a view root this is it
if (mViewRoot == null) {
mViewRoot = topLayout;
@@ -301,13 +306,22 @@
// this is the case of soft buttons + vertical bar.
// this top layout is the first layout in the horizontal layout. see above)
- mViewRoot.addView(topLayout, 0);
+ if (isRtl && params.isRtlSupported()) {
+ // If RTL is enabled, layoutlib will mirror the layouts. So, add the
+ // topLayout to the right of Navigation Bar and layoutlib will draw it
+ // to the left.
+ mViewRoot.addView(topLayout);
+ } else {
+ // Add the top layout to the left of the Navigation Bar.
+ mViewRoot.addView(topLayout, 0);
+ }
}
if (mStatusBarSize > 0) {
// system bar
try {
- StatusBar systemBar = new StatusBar(context, hardwareConfig.getDensity());
+ StatusBar systemBar = new StatusBar(context, hardwareConfig.getDensity(),
+ direction, params.isRtlSupported());
systemBar.setLayoutParams(
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, mStatusBarSize));
@@ -366,7 +380,8 @@
// system bar
try {
NavigationBar navigationBar = new NavigationBar(context,
- hardwareConfig.getDensity(), LinearLayout.HORIZONTAL);
+ hardwareConfig.getDensity(), LinearLayout.HORIZONTAL, isRtl,
+ params.isRtlSupported());
navigationBar.setLayoutParams(
new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, mNavigationBarSize));
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/BaseAdapter.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterHelper.java
similarity index 62%
rename from tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/BaseAdapter.java
rename to tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterHelper.java
index e0414fe..6c998af 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/BaseAdapter.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package com.android.layoutlib.bridge.impl.binding;
-import com.android.ide.common.rendering.api.AdapterBinding;
import com.android.ide.common.rendering.api.DataBindingItem;
import com.android.ide.common.rendering.api.IProjectCallback;
import com.android.ide.common.rendering.api.LayoutLog;
@@ -27,7 +26,6 @@
import com.android.layoutlib.bridge.impl.RenderAction;
import com.android.util.Pair;
-import android.database.DataSetObserver;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
@@ -35,124 +33,27 @@
import android.widget.ImageView;
import android.widget.TextView;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
/**
- * Base adapter to do fake data binding in {@link AdapterView} objects.
+ * A Helper class to do fake data binding in {@link AdapterView} objects.
*/
-public class BaseAdapter {
+@SuppressWarnings("deprecation")
+public class AdapterHelper {
- /**
- * This is the items provided by the adapter. They are dynamically generated.
- */
- protected final static class AdapterItem {
- private final DataBindingItem mItem;
- private final int mType;
- private final int mFullPosition;
- private final int mPositionPerType;
- private List<AdapterItem> mChildren;
-
- protected AdapterItem(DataBindingItem item, int type, int fullPosition,
- int positionPerType) {
- mItem = item;
- mType = type;
- mFullPosition = fullPosition;
- mPositionPerType = positionPerType;
- }
-
- void addChild(AdapterItem child) {
- if (mChildren == null) {
- mChildren = new ArrayList<AdapterItem>();
- }
-
- mChildren.add(child);
- }
-
- List<AdapterItem> getChildren() {
- if (mChildren != null) {
- return mChildren;
- }
-
- return Collections.emptyList();
- }
-
- int getType() {
- return mType;
- }
-
- int getFullPosition() {
- return mFullPosition;
- }
-
- int getPositionPerType() {
- return mPositionPerType;
- }
-
- DataBindingItem getDataBindingItem() {
- return mItem;
- }
- }
-
- private final AdapterBinding mBinding;
- private final IProjectCallback mCallback;
- private final ResourceReference mAdapterRef;
- private boolean mSkipCallbackParser = false;
-
- protected final List<AdapterItem> mItems = new ArrayList<AdapterItem>();
-
- protected BaseAdapter(ResourceReference adapterRef, AdapterBinding binding,
- IProjectCallback callback) {
- mAdapterRef = adapterRef;
- mBinding = binding;
- mCallback = callback;
- }
-
- // ------- Some Adapter method used by all children classes.
-
- public boolean areAllItemsEnabled() {
- return true;
- }
-
- public boolean hasStableIds() {
- return true;
- }
-
- public boolean isEmpty() {
- return mItems.size() == 0;
- }
-
- public void registerDataSetObserver(DataSetObserver observer) {
- // pass
- }
-
- public void unregisterDataSetObserver(DataSetObserver observer) {
- // pass
- }
-
- // -------
-
-
- protected AdapterBinding getBinding() {
- return mBinding;
- }
-
- protected View getView(AdapterItem item, AdapterItem parentItem, View convertView,
- ViewGroup parent) {
+ static Pair<View, Boolean> getView(AdapterItem item, AdapterItem parentItem, ViewGroup parent,
+ IProjectCallback callback, ResourceReference adapterRef, boolean skipCallbackParser) {
// we don't care about recycling here because we never scroll.
DataBindingItem dataBindingItem = item.getDataBindingItem();
BridgeContext context = RenderAction.getCurrentContext();
Pair<View, Boolean> pair = context.inflateView(dataBindingItem.getViewReference(),
- parent, false /*attachToRoot*/, mSkipCallbackParser);
+ parent, false /*attachToRoot*/, skipCallbackParser);
View view = pair.getFirst();
- mSkipCallbackParser |= pair.getSecond();
+ skipCallbackParser |= pair.getSecond();
if (view != null) {
- fillView(context, view, item, parentItem);
+ fillView(context, view, item, parentItem, callback, adapterRef);
} else {
// create a text view to display an error.
TextView tv = new TextView(context);
@@ -160,16 +61,16 @@
view = tv;
}
- return view;
+ return Pair.of(view, skipCallbackParser);
}
- private void fillView(BridgeContext context, View view, AdapterItem item,
- AdapterItem parentItem) {
+ private static void fillView(BridgeContext context, View view, AdapterItem item,
+ AdapterItem parentItem, IProjectCallback callback, ResourceReference adapterRef) {
if (view instanceof ViewGroup) {
ViewGroup group = (ViewGroup) view;
final int count = group.getChildCount();
for (int i = 0 ; i < count ; i++) {
- fillView(context, group.getChildAt(i), item, parentItem);
+ fillView(context, group.getChildAt(i), item, parentItem, callback, adapterRef);
}
} else {
int id = view.getId();
@@ -184,8 +85,8 @@
if (view instanceof TextView) {
TextView tv = (TextView) view;
- Object value = mCallback.getAdapterItemValue(
- mAdapterRef, context.getViewKey(view),
+ Object value = callback.getAdapterItemValue(
+ adapterRef, context.getViewKey(view),
item.getDataBindingItem().getViewReference(),
fullPosition, positionPerType,
fullParentPosition, parentPositionPerType,
@@ -204,8 +105,8 @@
if (view instanceof Checkable) {
Checkable cb = (Checkable) view;
- Object value = mCallback.getAdapterItemValue(
- mAdapterRef, context.getViewKey(view),
+ Object value = callback.getAdapterItemValue(
+ adapterRef, context.getViewKey(view),
item.getDataBindingItem().getViewReference(),
fullPosition, positionPerType,
fullParentPosition, parentPositionPerType,
@@ -224,8 +125,8 @@
if (view instanceof ImageView) {
ImageView iv = (ImageView) view;
- Object value = mCallback.getAdapterItemValue(
- mAdapterRef, context.getViewKey(view),
+ Object value = callback.getAdapterItemValue(
+ adapterRef, context.getViewKey(view),
item.getDataBindingItem().getViewReference(),
fullPosition, positionPerType,
fullParentPosition, parentPositionPerType,
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterItem.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterItem.java
new file mode 100644
index 0000000..8e28dba
--- /dev/null
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/AdapterItem.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * 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.android.layoutlib.bridge.impl.binding;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.android.ide.common.rendering.api.DataBindingItem;
+
+/**
+ * This is the items provided by the adapter. They are dynamically generated.
+ */
+final class AdapterItem {
+ private final DataBindingItem mItem;
+ private final int mType;
+ private final int mFullPosition;
+ private final int mPositionPerType;
+ private List<AdapterItem> mChildren;
+
+ protected AdapterItem(DataBindingItem item, int type, int fullPosition,
+ int positionPerType) {
+ mItem = item;
+ mType = type;
+ mFullPosition = fullPosition;
+ mPositionPerType = positionPerType;
+ }
+
+ void addChild(AdapterItem child) {
+ if (mChildren == null) {
+ mChildren = new ArrayList<AdapterItem>();
+ }
+
+ mChildren.add(child);
+ }
+
+ List<AdapterItem> getChildren() {
+ if (mChildren != null) {
+ return mChildren;
+ }
+
+ return Collections.emptyList();
+ }
+
+ int getType() {
+ return mType;
+ }
+
+ int getFullPosition() {
+ return mFullPosition;
+ }
+
+ int getPositionPerType() {
+ return mPositionPerType;
+ }
+
+ DataBindingItem getDataBindingItem() {
+ return mItem;
+ }
+}
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeAdapter.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeAdapter.java
index 22570b9..9a13f5a 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeAdapter.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeAdapter.java
@@ -20,10 +20,12 @@
import com.android.ide.common.rendering.api.DataBindingItem;
import com.android.ide.common.rendering.api.IProjectCallback;
import com.android.ide.common.rendering.api.ResourceReference;
+import com.android.util.Pair;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
+import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.SpinnerAdapter;
@@ -35,17 +37,23 @@
* and {@link SpinnerAdapter}.
*
*/
-public class FakeAdapter extends BaseAdapter implements ListAdapter, SpinnerAdapter {
+@SuppressWarnings("deprecation")
+public class FakeAdapter extends BaseAdapter {
// don't use a set because the order is important.
private final List<ResourceReference> mTypes = new ArrayList<ResourceReference>();
+ private final IProjectCallback mCallback;
+ private final ResourceReference mAdapterRef;
+ private final List<AdapterItem> mItems = new ArrayList<AdapterItem>();
+ private boolean mSkipCallbackParser = false;
public FakeAdapter(ResourceReference adapterRef, AdapterBinding binding,
IProjectCallback callback) {
- super(adapterRef, binding, callback);
+ mAdapterRef = adapterRef;
+ mCallback = callback;
- final int repeatCount = getBinding().getRepeatCount();
- final int itemCount = getBinding().getItemCount();
+ final int repeatCount = binding.getRepeatCount();
+ final int itemCount = binding.getItemCount();
// Need an array to count for each type.
// This is likely too big, but is the max it can be.
@@ -54,7 +62,7 @@
// We put several repeating sets.
for (int r = 0 ; r < repeatCount ; r++) {
// loop on the type of list items, and add however many for each type.
- for (DataBindingItem dataBindingItem : getBinding()) {
+ for (DataBindingItem dataBindingItem : binding) {
ResourceReference viewRef = dataBindingItem.getViewReference();
int typeIndex = mTypes.indexOf(viewRef);
if (typeIndex == -1) {
@@ -103,7 +111,11 @@
public View getView(int position, View convertView, ViewGroup parent) {
// we don't care about recycling here because we never scroll.
AdapterItem item = mItems.get(position);
- return getView(item, null /*parentGroup*/, convertView, parent);
+ Pair<View, Boolean> pair = AdapterHelper.getView(item, null /*parentGroup*/, parent,
+ mCallback, mAdapterRef, mSkipCallbackParser);
+ mSkipCallbackParser = pair.getSecond();
+ return pair.getFirst();
+
}
@Override
diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeExpandableAdapter.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeExpandableAdapter.java
index 199e040..e539579 100644
--- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeExpandableAdapter.java
+++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/impl/binding/FakeExpandableAdapter.java
@@ -20,7 +20,9 @@
import com.android.ide.common.rendering.api.DataBindingItem;
import com.android.ide.common.rendering.api.IProjectCallback;
import com.android.ide.common.rendering.api.ResourceReference;
+import com.android.util.Pair;
+import android.database.DataSetObserver;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ExpandableListAdapter;
@@ -29,8 +31,14 @@
import java.util.ArrayList;
import java.util.List;
-public class FakeExpandableAdapter extends BaseAdapter implements ExpandableListAdapter,
- HeterogeneousExpandableList {
+@SuppressWarnings("deprecation")
+public class FakeExpandableAdapter implements ExpandableListAdapter, HeterogeneousExpandableList {
+
+ private final IProjectCallback mCallback;
+ private final ResourceReference mAdapterRef;
+ private boolean mSkipCallbackParser = false;
+
+ protected final List<AdapterItem> mItems = new ArrayList<AdapterItem>();
// don't use a set because the order is important.
private final List<ResourceReference> mGroupTypes = new ArrayList<ResourceReference>();
@@ -38,7 +46,8 @@
public FakeExpandableAdapter(ResourceReference adapterRef, AdapterBinding binding,
IProjectCallback callback) {
- super(adapterRef, binding, callback);
+ mAdapterRef = adapterRef;
+ mCallback = callback;
createItems(binding, binding.getItemCount(), binding.getRepeatCount(), mGroupTypes, 1);
}
@@ -125,7 +134,10 @@
ViewGroup parent) {
// we don't care about recycling here because we never scroll.
AdapterItem item = mItems.get(groupPosition);
- return getView(item, null /*parentItem*/, convertView, parent);
+ Pair<View, Boolean> pair = AdapterHelper.getView(item, null /*parentItem*/, parent,
+ mCallback, mAdapterRef, mSkipCallbackParser);
+ mSkipCallbackParser = pair.getSecond();
+ return pair.getFirst();
}
@Override
@@ -134,7 +146,10 @@
// we don't care about recycling here because we never scroll.
AdapterItem parentItem = mItems.get(groupPosition);
AdapterItem item = getChildItem(groupPosition, childPosition);
- return getView(item, parentItem, convertView, parent);
+ Pair<View, Boolean> pair = AdapterHelper.getView(item, parentItem, parent, mCallback,
+ mAdapterRef, mSkipCallbackParser);
+ mSkipCallbackParser = pair.getSecond();
+ return pair.getFirst();
}
@Override
@@ -172,6 +187,31 @@
// pass
}
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ // pass
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ // pass
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return true;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return mItems.isEmpty();
+ }
+
// ---- HeterogeneousExpandableList
@Override
diff --git a/wifi/java/android/net/wifi/WifiStateMachine.java b/wifi/java/android/net/wifi/WifiStateMachine.java
index 0077354..6589ff5 100644
--- a/wifi/java/android/net/wifi/WifiStateMachine.java
+++ b/wifi/java/android/net/wifi/WifiStateMachine.java
@@ -3166,6 +3166,7 @@
class VerifyingLinkState extends State {
@Override
public void enter() {
+ log(getName() + " enter");
setNetworkDetailedState(DetailedState.VERIFYING_POOR_LINK);
mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.VERIFYING_POOR_LINK);
sendNetworkStateChangeBroadcast(mLastBssid);
@@ -3175,11 +3176,14 @@
switch (message.what) {
case WifiWatchdogStateMachine.POOR_LINK_DETECTED:
//stay here
+ log(getName() + " POOR_LINK_DETECTED: no transition");
break;
case WifiWatchdogStateMachine.GOOD_LINK_DETECTED:
+ log(getName() + " GOOD_LINK_DETECTED: transition to captive portal check");
transitionTo(mCaptivePortalCheckState);
break;
default:
+ log(getName() + " what=" + message.what + " NOT_HANDLED");
return NOT_HANDLED;
}
return HANDLED;
@@ -3189,6 +3193,7 @@
class CaptivePortalCheckState extends State {
@Override
public void enter() {
+ log(getName() + " enter");
setNetworkDetailedState(DetailedState.CAPTIVE_PORTAL_CHECK);
mWifiConfigStore.updateStatus(mLastNetworkId, DetailedState.CAPTIVE_PORTAL_CHECK);
sendNetworkStateChangeBroadcast(mLastBssid);
@@ -3197,6 +3202,7 @@
public boolean processMessage(Message message) {
switch (message.what) {
case CMD_CAPTIVE_CHECK_COMPLETE:
+ log(getName() + " CMD_CAPTIVE_CHECK_COMPLETE");
try {
mNwService.enableIpv6(mInterfaceName);
} catch (RemoteException re) {
diff --git a/wifi/java/android/net/wifi/WifiStateTracker.java b/wifi/java/android/net/wifi/WifiStateTracker.java
index cf75381..461dedb 100644
--- a/wifi/java/android/net/wifi/WifiStateTracker.java
+++ b/wifi/java/android/net/wifi/WifiStateTracker.java
@@ -120,6 +120,11 @@
mWifiManager.captivePortalCheckComplete();
}
+ @Override
+ public void captivePortalCheckCompleted(boolean isCaptivePortal) {
+ // not implemented
+ }
+
/**
* Turn the wireless radio off for a network.
* @param turnOn {@code true} to turn the radio on, {@code false}