diff options
47 files changed, 1851 insertions, 586 deletions
diff --git a/api/current.txt b/api/current.txt index d8387c1..d95868f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -1993,6 +1993,7 @@ package android.accessibilityservice { public abstract class AccessibilityService extends android.app.Service { ctor public AccessibilityService(); + method public android.view.accessibility.AccessibilityNodeInfo getRootInActiveWindow(); method public final android.accessibilityservice.AccessibilityServiceInfo getServiceInfo(); method public abstract void onAccessibilityEvent(android.view.accessibility.AccessibilityEvent); method public final android.os.IBinder onBind(android.content.Intent); @@ -3767,7 +3768,6 @@ package android.app { method public android.app.Notification.Builder setDefaults(int); method public android.app.Notification.Builder setDeleteIntent(android.app.PendingIntent); method public android.app.Notification.Builder setFullScreenIntent(android.app.PendingIntent, boolean); - method public android.app.Notification.Builder setIntruderActionsShowText(boolean); method public android.app.Notification.Builder setLargeIcon(android.graphics.Bitmap); method public android.app.Notification.Builder setLights(int, int, int); method public android.app.Notification.Builder setNumber(int); @@ -3783,7 +3783,6 @@ package android.app { method public android.app.Notification.Builder setTicker(java.lang.CharSequence); method public android.app.Notification.Builder setTicker(java.lang.CharSequence, android.widget.RemoteViews); method public android.app.Notification.Builder setUsesChronometer(boolean); - method public android.app.Notification.Builder setUsesIntruderAlert(boolean); method public android.app.Notification.Builder setVibrate(long[]); method public android.app.Notification.Builder setWhen(long); } @@ -10725,6 +10724,7 @@ package android.media { public class AudioRecord { ctor public AudioRecord(int, int, int, int, int) throws java.lang.IllegalArgumentException; method public int getAudioFormat(); + method public int getAudioSessionId(); method public int getAudioSource(); method public int getChannelConfiguration(); method public int getChannelCount(); @@ -11516,6 +11516,11 @@ package android.media { package android.media.audiofx { + public class AcousticEchoCanceler extends android.media.audiofx.AudioEffect { + method public static android.media.audiofx.AcousticEchoCanceler create(int); + method public static boolean isAvailable(); + } + public class AudioEffect { method public android.media.audiofx.AudioEffect.Descriptor getDescriptor() throws java.lang.IllegalStateException; method public boolean getEnabled() throws java.lang.IllegalStateException; @@ -11566,6 +11571,11 @@ package android.media.audiofx { method public abstract void onEnableStatusChange(android.media.audiofx.AudioEffect, boolean); } + public class AutomaticGainControl extends android.media.audiofx.AudioEffect { + method public static android.media.audiofx.AutomaticGainControl create(int); + method public static boolean isAvailable(); + } + public class BassBoost extends android.media.audiofx.AudioEffect { ctor public BassBoost(int, int) throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.RuntimeException, java.lang.UnsupportedOperationException; method public android.media.audiofx.BassBoost.Settings getProperties() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; @@ -11684,6 +11694,11 @@ package android.media.audiofx { field public short numBands; } + public class NoiseSuppressor extends android.media.audiofx.AudioEffect { + method public static android.media.audiofx.NoiseSuppressor create(int); + method public static boolean isAvailable(); + } + public class PresetReverb extends android.media.audiofx.AudioEffect { ctor public PresetReverb(int, int) throws java.lang.IllegalArgumentException, java.lang.RuntimeException, java.lang.UnsupportedOperationException; method public short getPreset() throws java.lang.IllegalArgumentException, java.lang.IllegalStateException, java.lang.UnsupportedOperationException; @@ -12470,10 +12485,14 @@ package android.net.nsd { method public void resolveService(android.net.nsd.NsdManager.Channel, java.lang.String, java.lang.String, android.net.nsd.NsdManager.DnsSdResolveListener); method public void stopServiceDiscovery(android.net.nsd.NsdManager.Channel, android.net.nsd.NsdManager.ActionListener); method public void unregisterService(android.net.nsd.NsdManager.Channel, int, android.net.nsd.NsdManager.ActionListener); + field public static final java.lang.String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED"; field public static final int ALREADY_ACTIVE = 3; // 0x3 field public static final int BUSY = 2; // 0x2 field public static final int ERROR = 0; // 0x0 + field public static final java.lang.String EXTRA_NSD_STATE = "nsd_state"; field public static final int MAX_REGS_REACHED = 4; // 0x4 + field public static final int NSD_STATE_DISABLED = 1; // 0x1 + field public static final int NSD_STATE_ENABLED = 2; // 0x2 field public static final int UNSUPPORTED = 1; // 0x1 } @@ -17844,9 +17863,11 @@ package android.provider { field public static final java.lang.String DATE_ADDED = "date_added"; field public static final java.lang.String DATE_MODIFIED = "date_modified"; field public static final java.lang.String DISPLAY_NAME = "_display_name"; + field public static final java.lang.String HEIGHT = "height"; field public static final java.lang.String MIME_TYPE = "mime_type"; field public static final java.lang.String SIZE = "_size"; field public static final java.lang.String TITLE = "title"; + field public static final java.lang.String WIDTH = "width"; } public static final class MediaStore.Video { diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index eed8aa6..4e340c0 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -426,13 +426,11 @@ public abstract class AccessibilityService extends Service { throw new IllegalStateException("AccessibilityService not connected." + " Did you receive a call of onServiceConnected()?"); } - AccessibilityNodeInfo root = AccessibilityInteractionClient.getInstance() - .findAccessibilityNodeInfoByAccessibilityId(connectionId, - AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, - AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); + AccessibilityNodeInfo root = getRootInActiveWindow(); if (root == null) { return; } + AccessibilityNodeInfo current = root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY); if (current == null) { current = root; @@ -480,6 +478,19 @@ public abstract class AccessibilityService extends Service { } /** + * Gets the root node in the currently active window if this service + * can retrieve window content. + * + * @return The root node if this service can retrieve window content. + */ + public AccessibilityNodeInfo getRootInActiveWindow() { + return AccessibilityInteractionClient.getInstance() + .findAccessibilityNodeInfoByAccessibilityId(mConnectionId, + AccessibilityNodeInfo.ACTIVE_WINDOW_ID, AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityNodeInfo.FLAG_PREFETCH_DESCENDANTS); + } + + /** * Performs a global action. Such an action can be performed * at any moment regardless of the current application or user * location in that application. For example going back, going diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 4d5238c..6f95e26 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -34,5 +34,8 @@ interface INotificationManager void cancelToast(String pkg, ITransientNotification callback); void enqueueNotificationWithTag(String pkg, String tag, int id, in Notification notification, inout int[] idReceived); void cancelNotificationWithTag(String pkg, String tag, int id); + + void setNotificationsEnabledForPackage(String pkg, boolean enabled); + boolean areNotificationsEnabledForPackage(String pkg); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 22d84f0..b581f99 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -190,12 +190,6 @@ public class Notification implements Parcelable public RemoteViews contentView; /** - * The view that will represent this notification in the pop-up "intruder alert" dialog. - * @hide - */ - public RemoteViews intruderView; - - /** * A large-format version of {@link #contentView}, giving the Notification an * opportunity to show more detail. The system UI may choose to show this * instead of the normal content view at its discretion. @@ -590,9 +584,6 @@ public class Notification implements Parcelable actions = parcel.createTypedArray(Action.CREATOR); if (parcel.readInt() != 0) { - intruderView = RemoteViews.CREATOR.createFromParcel(parcel); - } - if (parcel.readInt() != 0) { bigContentView = RemoteViews.CREATOR.createFromParcel(parcel); } } @@ -658,9 +649,6 @@ public class Notification implements Parcelable for(int i=0; i<this.actions.length; i++) { that.actions[i] = this.actions[i].clone(); } - if (this.intruderView != null) { - that.intruderView = this.intruderView.clone(); - } if (this.bigContentView != null) { that.bigContentView = this.bigContentView.clone(); } @@ -755,13 +743,6 @@ public class Notification implements Parcelable parcel.writeTypedArray(actions, 0); - if (intruderView != null) { - parcel.writeInt(1); - intruderView.writeToParcel(parcel, 0); - } else { - parcel.writeInt(0); - } - if (bigContentView != null) { parcel.writeInt(1); bigContentView.writeToParcel(parcel, 0); @@ -942,8 +923,6 @@ public class Notification implements Parcelable private Bundle mExtras; private int mPriority; private ArrayList<Action> mActions = new ArrayList<Action>(3); - private boolean mCanHasIntruder; - private boolean mIntruderActionsShowText; private boolean mUseChronometer; /** @@ -1349,38 +1328,6 @@ public class Notification implements Parcelable return this; } - /** - * Specify whether this notification should pop up as an - * "intruder alert" (a small window that shares the screen with the - * current activity). This sort of notification is (as the name implies) - * very intrusive, so use it sparingly for notifications that require - * the user's attention. - * - * Notes: - * <ul> - * <li>Intruder alerts only show when the screen is on.</li> - * <li>Intruder alerts take precedence over fullScreenIntents.</li> - * </ul> - * - * @param intrude Whether to pop up an intruder alert (default false). - */ - public Builder setUsesIntruderAlert(boolean intrude) { - mCanHasIntruder = intrude; - return this; - } - - /** - * Control text on intruder alert action buttons. By default, action - * buttons in intruders do not show textual labels. - * - * @param showActionText Whether to show text labels beneath action - * icons (default false). - */ - public Builder setIntruderActionsShowText(boolean showActionText) { - mIntruderActionsShowText = showActionText; - return this; - } - private void setFlag(int mask, boolean value) { if (value) { mFlags |= mask; @@ -1506,45 +1453,6 @@ public class Notification implements Parcelable return applyStandardTemplateWithActions(R.layout.notification_template_base); } - private RemoteViews makeIntruderView(boolean showLabels) { - RemoteViews intruderView = new RemoteViews(mContext.getPackageName(), - R.layout.notification_intruder_content); - if (mLargeIcon != null) { - intruderView.setImageViewBitmap(R.id.icon, mLargeIcon); - intruderView.setViewVisibility(R.id.icon, View.VISIBLE); - } else if (mSmallIcon != 0) { - intruderView.setImageViewResource(R.id.icon, mSmallIcon); - intruderView.setViewVisibility(R.id.icon, View.VISIBLE); - } else { - intruderView.setViewVisibility(R.id.icon, View.GONE); - } - if (mContentTitle != null) { - intruderView.setTextViewText(R.id.title, mContentTitle); - } - if (mContentText != null) { - intruderView.setTextViewText(R.id.text, mContentText); - } - if (mActions.size() > 0) { - intruderView.setViewVisibility(R.id.actions, View.VISIBLE); - int N = mActions.size(); - if (N>3) N=3; - final int[] BUTTONS = { R.id.action0, R.id.action1, R.id.action2 }; - for (int i=0; i<N; i++) { - final Action action = mActions.get(i); - final int buttonId = BUTTONS[i]; - - intruderView.setViewVisibility(buttonId, View.VISIBLE); - intruderView.setTextViewText(buttonId, showLabels ? action.title : null); - intruderView.setTextViewCompoundDrawables(buttonId, 0, action.icon, 0, 0); - intruderView.setContentDescription(buttonId, action.title); - intruderView.setOnClickPendingIntent(buttonId, action.actionIntent); - } - } else { - intruderView.setViewVisibility(R.id.actions, View.GONE); - } - return intruderView; - } - private RemoteViews generateActionButton(Action action) { RemoteViews button = new RemoteViews(mContext.getPackageName(), R.layout.notification_action); button.setTextViewCompoundDrawables(R.id.action0, action.icon, 0, 0, 0); @@ -1579,9 +1487,6 @@ public class Notification implements Parcelable n.ledOffMS = mLedOffMs; n.defaults = mDefaults; n.flags = mFlags; - if (mCanHasIntruder) { - n.intruderView = makeIntruderView(mIntruderActionsShowText); - } n.bigContentView = makeBigContentView(); if (mLedOnMs != 0 && mLedOffMs != 0) { n.flags |= FLAG_SHOW_LIGHTS; diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 640b47b..4fb710e 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -234,48 +234,6 @@ public class Camera { }; /** - * Creates a new Camera object to access a particular hardware camera. - * - * <p>When <code>force</code> is set to false, this will throw an exception - * if the same camera is already opened by other clients. If true, the other - * client will be disconnected from the camera they opened. If the device - * can only support one camera running at a time, all camera-using clients - * will be disconnected from their cameras. - * - * <p>A camera being held by an application can be taken away by other - * applications at any time. Before the camera is taken, applications will - * get {@link #CAMERA_ERROR_RELEASED} and have some time to clean up. Apps - * receiving this callback must immediately stop video recording and then - * call {@link #release()} on their camera object. Otherwise, it will be - * released by the frameworks in a short time. After receiving - * CAMERA_ERROR_RELEASED, apps should not call any method except <code> - * release</code> and {@link #isReleased()}. After a camera is taken away, - * all methods will throw exceptions except <code>isReleased</code> and - * <code>release</code>. Apps can use <code>isReleased</code> to see if the - * camera has been taken away. If the camera is taken away, the apps can - * silently finish themselves or show a dialog. - * - * <p>Applications with android.permission.KEEP_CAMERA can request to keep - * the camera. That is, the camera will not be taken by other applications - * while it is opened. The permission can only be obtained by trusted - * platform applications, such as those implementing lock screen security - * features. - * - * @param cameraId the hardware camera to access, between 0 and - * {@link #getNumberOfCameras()}-1. - * @param force true to take the ownership from the existing client if the - * camera has been opened by other clients. - * @param keep true if the applications do not want other apps to take the - * camera. Only the apps with android.permission.KEEP_CAMERA can keep - * the camera. - * @return a new Camera object, connected, locked and ready for use. - * @hide - */ - public static Camera open(int cameraId, boolean force, boolean keep) { - return new Camera(cameraId, force, keep); - } - - /** * Creates a new Camera object to access a particular hardware camera. If * the same camera is opened by other applications, this will throw a * RuntimeException. @@ -305,7 +263,7 @@ public class Camera { * @see android.app.admin.DevicePolicyManager#getCameraDisabled(android.content.ComponentName) */ public static Camera open(int cameraId) { - return new Camera(cameraId, false, false); + return new Camera(cameraId); } /** @@ -320,13 +278,13 @@ public class Camera { for (int i = 0; i < numberOfCameras; i++) { getCameraInfo(i, cameraInfo); if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) { - return new Camera(i, false, false); + return new Camera(i); } } return null; } - Camera(int cameraId, boolean force, boolean keep) { + Camera(int cameraId) { mShutterCallback = null; mRawImageCallback = null; mJpegCallback = null; @@ -343,7 +301,7 @@ public class Camera { mEventHandler = null; } - native_setup(new WeakReference<Camera>(this), cameraId, force, keep); + native_setup(new WeakReference<Camera>(this), cameraId); } /** @@ -356,8 +314,7 @@ public class Camera { release(); } - private native final void native_setup(Object camera_this, int cameraId, - boolean force, boolean keep); + private native final void native_setup(Object camera_this, int cameraId); private native final void native_release(); @@ -372,18 +329,6 @@ public class Camera { } /** - * Whether the camera is released. When any camera method throws an - * exception, applications can use this to check whether the camera has been - * taken by other clients. If true, it means other clients have taken the - * camera. The applications can silently finish themselves or show a dialog. - * - * @return whether the camera is released. - * @see #open(int, boolean, boolean) - * @hide - */ - public native final boolean isReleased(); - - /** * Unlocks the camera to allow another process to access it. * Normally, the camera is locked to the process with an active Camera * object until {@link #release()} is called. To allow rapid handoff @@ -1377,17 +1322,6 @@ public class Camera { public static final int CAMERA_ERROR_UNKNOWN = 1; /** - * Camera was released because another client has opened the camera. The - * application should call {@link #release()} after getting this. The apps - * should not call any method except <code>release</code> and {@link #isReleased()} - * after this. - * - * @see Camera.ErrorCallback - * @hide - */ - public static final int CAMERA_ERROR_RELEASED = 2; - - /** * Media server died. In this case, the application must release the * Camera object and instantiate a new one. * @see Camera.ErrorCallback diff --git a/core/java/android/net/nsd/INsdManager.aidl b/core/java/android/net/nsd/INsdManager.aidl index 077a675..3361a7b 100644 --- a/core/java/android/net/nsd/INsdManager.aidl +++ b/core/java/android/net/nsd/INsdManager.aidl @@ -26,4 +26,5 @@ import android.os.Messenger; interface INsdManager { Messenger getMessenger(); + void setEnabled(boolean enable); } diff --git a/core/java/android/net/nsd/NsdManager.java b/core/java/android/net/nsd/NsdManager.java index dac8d20..ecf5e20 100644 --- a/core/java/android/net/nsd/NsdManager.java +++ b/core/java/android/net/nsd/NsdManager.java @@ -16,6 +16,8 @@ package android.net.nsd; +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; import android.content.Context; import android.os.Binder; import android.os.IBinder; @@ -133,6 +135,40 @@ public class NsdManager { private static final String TAG = "NsdManager"; INsdManager mService; + /** + * Broadcast intent action to indicate whether network service discovery is + * enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state + * information as int. + * + * @see #EXTRA_NSD_STATE + */ + @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_NSD_STATE_CHANGED = + "android.net.nsd.STATE_CHANGED"; + + /** + * The lookup key for an int that indicates whether network service discovery is enabled + * or disabled. Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}. + * + * @see #NSD_STATE_DISABLED + * @see #NSD_STATE_ENABLED + */ + public static final String EXTRA_NSD_STATE = "nsd_state"; + + /** + * Network service discovery is disabled + * + * @see #NSD_STATE_CHANGED_ACTION + */ + public static final int NSD_STATE_DISABLED = 1; + + /** + * Network service discovery is enabled + * + * @see #NSD_STATE_CHANGED_ACTION + */ + public static final int NSD_STATE_ENABLED = 2; + private static final int BASE = Protocol.BASE_NSD_MANAGER; /** @hide */ @@ -188,6 +224,12 @@ public class NsdManager { /** @hide */ public static final int STOP_RESOLVE_SUCCEEDED = BASE + 23; + /** @hide */ + public static final int ENABLE = BASE + 24; + /** @hide */ + public static final int DISABLE = BASE + 25; + + /** * Create a new Nsd instance. Applications use * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve @@ -312,8 +354,8 @@ public class NsdManager { private DnsSdResolveListener mDnsSdResolveListener; private ActionListener mDnsSdStopResolveListener; - AsyncChannel mAsyncChannel; - ServiceHandler mHandler; + private AsyncChannel mAsyncChannel; + private ServiceHandler mHandler; class ServiceHandler extends Handler { ServiceHandler(Looper looper) { super(looper); @@ -594,6 +636,13 @@ public class NsdManager { c.mAsyncChannel.sendMessage(STOP_RESOLVE); } + /** Internal use only @hide */ + public void setEnabled(boolean enabled) { + try { + mService.setEnabled(enabled); + } catch (RemoteException e) { } + } + /** * Get a reference to NetworkService handler. This is used to establish * an AsyncChannel communication with the service diff --git a/core/java/android/provider/MediaStore.java b/core/java/android/provider/MediaStore.java index 38945c2..79d0144 100644 --- a/core/java/android/provider/MediaStore.java +++ b/core/java/android/provider/MediaStore.java @@ -306,13 +306,11 @@ public final class MediaStore { /** * The width of the image/video in pixels. - * @hide */ public static final String WIDTH = "width"; /** * The height of the image/video in pixels. - * @hide */ public static final String HEIGHT = "height"; } diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 6dfbb2f..3a5fdd1 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -3277,6 +3277,12 @@ public final class Settings { "wifi_mobile_data_transition_wakelock_timeout_ms"; /** + * Whether network service discovery is enabled. + * @hide + */ + public static final String NSD_ON = "nsd_on"; + + /** * Whether background data usage is allowed by the user. See * ConnectivityManager for more info. */ diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java index d8d5185..1c61c6c 100644 --- a/core/java/android/view/KeyCharacterMap.java +++ b/core/java/android/view/KeyCharacterMap.java @@ -24,6 +24,7 @@ import android.util.SparseIntArray; import android.hardware.input.InputManager; import java.lang.Character; +import java.text.Normalizer; /** * Describes the keys provided by a keyboard device and their associated labels. @@ -149,9 +150,22 @@ public class KeyCharacterMap implements Parcelable { /* Characters used to display placeholders for dead keys. */ private static final int ACCENT_ACUTE = '\u00B4'; + private static final int ACCENT_BREVE = '\u02D8'; + private static final int ACCENT_CARON = '\u02C7'; + private static final int ACCENT_CEDILLA = '\u00B8'; + private static final int ACCENT_COMMA_ABOVE = '\u1FBD'; + private static final int ACCENT_COMMA_ABOVE_RIGHT = '\u02BC'; + private static final int ACCENT_DOT_ABOVE = '\u02D9'; + private static final int ACCENT_DOUBLE_ACUTE = '\u02DD'; private static final int ACCENT_GRAVE = '\u02CB'; private static final int ACCENT_CIRCUMFLEX = '\u02C6'; + private static final int ACCENT_MACRON = '\u00AF'; + private static final int ACCENT_MACRON_BELOW = '\u02CD'; + private static final int ACCENT_OGONEK = '\u02DB'; + private static final int ACCENT_REVERSED_COMMA_ABOVE = '\u02BD'; + private static final int ACCENT_RING_ABOVE = '\u02DA'; private static final int ACCENT_TILDE = '\u02DC'; + private static final int ACCENT_TURNED_COMMA_ABOVE = '\u02BB'; private static final int ACCENT_UMLAUT = '\u00A8'; /* Legacy dead key display characters used in previous versions of the API. @@ -161,136 +175,66 @@ public class KeyCharacterMap implements Parcelable { private static final int ACCENT_TILDE_LEGACY = '~'; /** - * Maps Unicode combining diacritical to display-form dead key - * (display character shifted left 16 bits). + * Maps Unicode combining diacritical to display-form dead key. */ - private static final SparseIntArray COMBINING = new SparseIntArray(); + private static final SparseIntArray sCombiningToAccent = new SparseIntArray(); + private static final SparseIntArray sAccentToCombining = new SparseIntArray(); static { - COMBINING.put('\u0300', ACCENT_GRAVE); - COMBINING.put('\u0301', ACCENT_ACUTE); - COMBINING.put('\u0302', ACCENT_CIRCUMFLEX); - COMBINING.put('\u0303', ACCENT_TILDE); - COMBINING.put('\u0308', ACCENT_UMLAUT); + addCombining('\u0300', ACCENT_GRAVE); + addCombining('\u0301', ACCENT_ACUTE); + addCombining('\u0302', ACCENT_CIRCUMFLEX); + addCombining('\u0303', ACCENT_TILDE); + addCombining('\u0304', ACCENT_MACRON); + addCombining('\u0306', ACCENT_BREVE); + addCombining('\u0307', ACCENT_DOT_ABOVE); + addCombining('\u0308', ACCENT_UMLAUT); + //addCombining('\u0309', ACCENT_HOOK_ABOVE); + addCombining('\u030A', ACCENT_RING_ABOVE); + addCombining('\u030B', ACCENT_DOUBLE_ACUTE); + addCombining('\u030C', ACCENT_CARON); + //addCombining('\u030D', ACCENT_VERTICAL_LINE_ABOVE); + //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE); + //addCombining('\u030F', ACCENT_DOUBLE_GRAVE); + //addCombining('\u0310', ACCENT_CANDRABINDU); + //addCombining('\u0311', ACCENT_INVERTED_BREVE); + addCombining('\u0312', ACCENT_TURNED_COMMA_ABOVE); + addCombining('\u0313', ACCENT_COMMA_ABOVE); + addCombining('\u0314', ACCENT_REVERSED_COMMA_ABOVE); + addCombining('\u0315', ACCENT_COMMA_ABOVE_RIGHT); + //addCombining('\u031B', ACCENT_HORN); + //addCombining('\u0323', ACCENT_DOT_BELOW); + //addCombining('\u0326', ACCENT_COMMA_BELOW); + addCombining('\u0327', ACCENT_CEDILLA); + addCombining('\u0328', ACCENT_OGONEK); + //addCombining('\u0329', ACCENT_VERTICAL_LINE_BELOW); + addCombining('\u0331', ACCENT_MACRON_BELOW); + //addCombining('\u0342', ACCENT_PERISPOMENI); + //addCombining('\u0344', ACCENT_DIALYTIKA_TONOS); + //addCombining('\u0345', ACCENT_YPOGEGRAMMENI); + + // One-way mappings to equivalent preferred accents. + sCombiningToAccent.append('\u0340', ACCENT_GRAVE); + sCombiningToAccent.append('\u0341', ACCENT_ACUTE); + sCombiningToAccent.append('\u0343', ACCENT_COMMA_ABOVE); + + // One-way legacy mappings to preserve compatibility with older applications. + sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300'); + sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302'); + sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303'); + } + + private static void addCombining(int combining, int accent) { + sCombiningToAccent.append(combining, accent); + sAccentToCombining.append(accent, combining); } /** - * Maps combinations of (display-form) dead key and second character + * Maps combinations of (display-form) combining key and second character * to combined output character. + * These mappings are derived from the Unicode NFC tables as needed. */ - private static final SparseIntArray DEAD = new SparseIntArray(); - static { - addDeadChar(ACCENT_ACUTE, 'A', '\u00C1'); - addDeadChar(ACCENT_ACUTE, 'C', '\u0106'); - addDeadChar(ACCENT_ACUTE, 'E', '\u00C9'); - addDeadChar(ACCENT_ACUTE, 'G', '\u01F4'); - addDeadChar(ACCENT_ACUTE, 'I', '\u00CD'); - addDeadChar(ACCENT_ACUTE, 'K', '\u1E30'); - addDeadChar(ACCENT_ACUTE, 'L', '\u0139'); - addDeadChar(ACCENT_ACUTE, 'M', '\u1E3E'); - addDeadChar(ACCENT_ACUTE, 'N', '\u0143'); - addDeadChar(ACCENT_ACUTE, 'O', '\u00D3'); - addDeadChar(ACCENT_ACUTE, 'P', '\u1E54'); - addDeadChar(ACCENT_ACUTE, 'R', '\u0154'); - addDeadChar(ACCENT_ACUTE, 'S', '\u015A'); - addDeadChar(ACCENT_ACUTE, 'U', '\u00DA'); - addDeadChar(ACCENT_ACUTE, 'W', '\u1E82'); - addDeadChar(ACCENT_ACUTE, 'Y', '\u00DD'); - addDeadChar(ACCENT_ACUTE, 'Z', '\u0179'); - addDeadChar(ACCENT_ACUTE, 'a', '\u00E1'); - addDeadChar(ACCENT_ACUTE, 'c', '\u0107'); - addDeadChar(ACCENT_ACUTE, 'e', '\u00E9'); - addDeadChar(ACCENT_ACUTE, 'g', '\u01F5'); - addDeadChar(ACCENT_ACUTE, 'i', '\u00ED'); - addDeadChar(ACCENT_ACUTE, 'k', '\u1E31'); - addDeadChar(ACCENT_ACUTE, 'l', '\u013A'); - addDeadChar(ACCENT_ACUTE, 'm', '\u1E3F'); - addDeadChar(ACCENT_ACUTE, 'n', '\u0144'); - addDeadChar(ACCENT_ACUTE, 'o', '\u00F3'); - addDeadChar(ACCENT_ACUTE, 'p', '\u1E55'); - addDeadChar(ACCENT_ACUTE, 'r', '\u0155'); - addDeadChar(ACCENT_ACUTE, 's', '\u015B'); - addDeadChar(ACCENT_ACUTE, 'u', '\u00FA'); - addDeadChar(ACCENT_ACUTE, 'w', '\u1E83'); - addDeadChar(ACCENT_ACUTE, 'y', '\u00FD'); - addDeadChar(ACCENT_ACUTE, 'z', '\u017A'); - addDeadChar(ACCENT_CIRCUMFLEX, 'A', '\u00C2'); - addDeadChar(ACCENT_CIRCUMFLEX, 'C', '\u0108'); - addDeadChar(ACCENT_CIRCUMFLEX, 'E', '\u00CA'); - addDeadChar(ACCENT_CIRCUMFLEX, 'G', '\u011C'); - addDeadChar(ACCENT_CIRCUMFLEX, 'H', '\u0124'); - addDeadChar(ACCENT_CIRCUMFLEX, 'I', '\u00CE'); - addDeadChar(ACCENT_CIRCUMFLEX, 'J', '\u0134'); - addDeadChar(ACCENT_CIRCUMFLEX, 'O', '\u00D4'); - addDeadChar(ACCENT_CIRCUMFLEX, 'S', '\u015C'); - addDeadChar(ACCENT_CIRCUMFLEX, 'U', '\u00DB'); - addDeadChar(ACCENT_CIRCUMFLEX, 'W', '\u0174'); - addDeadChar(ACCENT_CIRCUMFLEX, 'Y', '\u0176'); - addDeadChar(ACCENT_CIRCUMFLEX, 'Z', '\u1E90'); - addDeadChar(ACCENT_CIRCUMFLEX, 'a', '\u00E2'); - addDeadChar(ACCENT_CIRCUMFLEX, 'c', '\u0109'); - addDeadChar(ACCENT_CIRCUMFLEX, 'e', '\u00EA'); - addDeadChar(ACCENT_CIRCUMFLEX, 'g', '\u011D'); - addDeadChar(ACCENT_CIRCUMFLEX, 'h', '\u0125'); - addDeadChar(ACCENT_CIRCUMFLEX, 'i', '\u00EE'); - addDeadChar(ACCENT_CIRCUMFLEX, 'j', '\u0135'); - addDeadChar(ACCENT_CIRCUMFLEX, 'o', '\u00F4'); - addDeadChar(ACCENT_CIRCUMFLEX, 's', '\u015D'); - addDeadChar(ACCENT_CIRCUMFLEX, 'u', '\u00FB'); - addDeadChar(ACCENT_CIRCUMFLEX, 'w', '\u0175'); - addDeadChar(ACCENT_CIRCUMFLEX, 'y', '\u0177'); - addDeadChar(ACCENT_CIRCUMFLEX, 'z', '\u1E91'); - addDeadChar(ACCENT_GRAVE, 'A', '\u00C0'); - addDeadChar(ACCENT_GRAVE, 'E', '\u00C8'); - addDeadChar(ACCENT_GRAVE, 'I', '\u00CC'); - addDeadChar(ACCENT_GRAVE, 'N', '\u01F8'); - addDeadChar(ACCENT_GRAVE, 'O', '\u00D2'); - addDeadChar(ACCENT_GRAVE, 'U', '\u00D9'); - addDeadChar(ACCENT_GRAVE, 'W', '\u1E80'); - addDeadChar(ACCENT_GRAVE, 'Y', '\u1EF2'); - addDeadChar(ACCENT_GRAVE, 'a', '\u00E0'); - addDeadChar(ACCENT_GRAVE, 'e', '\u00E8'); - addDeadChar(ACCENT_GRAVE, 'i', '\u00EC'); - addDeadChar(ACCENT_GRAVE, 'n', '\u01F9'); - addDeadChar(ACCENT_GRAVE, 'o', '\u00F2'); - addDeadChar(ACCENT_GRAVE, 'u', '\u00F9'); - addDeadChar(ACCENT_GRAVE, 'w', '\u1E81'); - addDeadChar(ACCENT_GRAVE, 'y', '\u1EF3'); - addDeadChar(ACCENT_TILDE, 'A', '\u00C3'); - addDeadChar(ACCENT_TILDE, 'E', '\u1EBC'); - addDeadChar(ACCENT_TILDE, 'I', '\u0128'); - addDeadChar(ACCENT_TILDE, 'N', '\u00D1'); - addDeadChar(ACCENT_TILDE, 'O', '\u00D5'); - addDeadChar(ACCENT_TILDE, 'U', '\u0168'); - addDeadChar(ACCENT_TILDE, 'V', '\u1E7C'); - addDeadChar(ACCENT_TILDE, 'Y', '\u1EF8'); - addDeadChar(ACCENT_TILDE, 'a', '\u00E3'); - addDeadChar(ACCENT_TILDE, 'e', '\u1EBD'); - addDeadChar(ACCENT_TILDE, 'i', '\u0129'); - addDeadChar(ACCENT_TILDE, 'n', '\u00F1'); - addDeadChar(ACCENT_TILDE, 'o', '\u00F5'); - addDeadChar(ACCENT_TILDE, 'u', '\u0169'); - addDeadChar(ACCENT_TILDE, 'v', '\u1E7D'); - addDeadChar(ACCENT_TILDE, 'y', '\u1EF9'); - addDeadChar(ACCENT_UMLAUT, 'A', '\u00C4'); - addDeadChar(ACCENT_UMLAUT, 'E', '\u00CB'); - addDeadChar(ACCENT_UMLAUT, 'H', '\u1E26'); - addDeadChar(ACCENT_UMLAUT, 'I', '\u00CF'); - addDeadChar(ACCENT_UMLAUT, 'O', '\u00D6'); - addDeadChar(ACCENT_UMLAUT, 'U', '\u00DC'); - addDeadChar(ACCENT_UMLAUT, 'W', '\u1E84'); - addDeadChar(ACCENT_UMLAUT, 'X', '\u1E8C'); - addDeadChar(ACCENT_UMLAUT, 'Y', '\u0178'); - addDeadChar(ACCENT_UMLAUT, 'a', '\u00E4'); - addDeadChar(ACCENT_UMLAUT, 'e', '\u00EB'); - addDeadChar(ACCENT_UMLAUT, 'h', '\u1E27'); - addDeadChar(ACCENT_UMLAUT, 'i', '\u00EF'); - addDeadChar(ACCENT_UMLAUT, 'o', '\u00F6'); - addDeadChar(ACCENT_UMLAUT, 't', '\u1E97'); - addDeadChar(ACCENT_UMLAUT, 'u', '\u00FC'); - addDeadChar(ACCENT_UMLAUT, 'w', '\u1E85'); - addDeadChar(ACCENT_UMLAUT, 'x', '\u1E8D'); - addDeadChar(ACCENT_UMLAUT, 'y', '\u00FF'); - } + private static final SparseIntArray sDeadKeyCache = new SparseIntArray(); + private static final StringBuilder sDeadKeyBuilder = new StringBuilder(); public static final Parcelable.Creator<KeyCharacterMap> CREATOR = new Parcelable.Creator<KeyCharacterMap>() { @@ -387,7 +331,7 @@ public class KeyCharacterMap implements Parcelable { metaState = KeyEvent.normalizeMetaState(metaState); char ch = nativeGetCharacter(mPtr, keyCode, metaState); - int map = COMBINING.get(ch); + int map = sCombiningToAccent.get(ch); if (map != 0) { return map | COMBINING_ACCENT; } else { @@ -503,14 +447,25 @@ public class KeyCharacterMap implements Parcelable { * @return The combined character, or 0 if the characters cannot be combined. */ public static int getDeadChar(int accent, int c) { - if (accent == ACCENT_CIRCUMFLEX_LEGACY) { - accent = ACCENT_CIRCUMFLEX; - } else if (accent == ACCENT_GRAVE_LEGACY) { - accent = ACCENT_GRAVE; - } else if (accent == ACCENT_TILDE_LEGACY) { - accent = ACCENT_TILDE; + int combining = sAccentToCombining.get(accent); + if (combining == 0) { + return 0; } - return DEAD.get((accent << 16) | c); + + final int combination = (combining << 16) | c; + int combined; + synchronized (sDeadKeyCache) { + combined = sDeadKeyCache.get(combination, -1); + if (combined == -1) { + sDeadKeyBuilder.setLength(0); + sDeadKeyBuilder.append((char)c); + sDeadKeyBuilder.append((char)combining); + String result = Normalizer.normalize(sDeadKeyBuilder, Normalizer.Form.NFC); + combined = result.length() == 1 ? result.charAt(0) : 0; + sDeadKeyCache.put(combination, combined); + } + } + return combined; } /** @@ -723,10 +678,6 @@ public class KeyCharacterMap implements Parcelable { return 0; } - private static void addDeadChar(int accent, int c, char combinedResult) { - DEAD.put((accent << 16) | c, combinedResult); - } - /** * Thrown by {@link KeyCharacterMap#load} when a key character map could not be loaded. */ diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index bd054bc..7e5fe63 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12297,7 +12297,7 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal final int flags = parent.mGroupFlags; final boolean initialized = a.isInitialized(); if (!initialized) { - a.initialize(mRight - mLeft, mBottom - mTop, getWidth(), getHeight()); + a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight()); a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop); onAnimationStart(); } diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index ca5648a..ae68794 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -123,7 +123,7 @@ public abstract class AbsSeekBar extends ProgressBar { invalidate(); if (needUpdate) { updateThumbPos(getWidth(), getHeight()); - if (thumb.isStateful()) { + if (thumb != null && thumb.isStateful()) { // Note that if the states are different this won't work. // For now, let's consider that an app bug. int[] state = getDrawableState(); diff --git a/core/java/android/widget/ExpandableListConnector.java b/core/java/android/widget/ExpandableListConnector.java index 2ff6b70..bda64ba 100644 --- a/core/java/android/widget/ExpandableListConnector.java +++ b/core/java/android/widget/ExpandableListConnector.java @@ -372,7 +372,8 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { @Override public boolean isEnabled(int flatListPos) { - final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position; + final PositionMetadata metadata = getUnflattenedPos(flatListPos); + final ExpandableListPosition pos = metadata.position; boolean retValue; if (pos.type == ExpandableListPosition.CHILD) { @@ -382,7 +383,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { retValue = true; } - pos.recycle(); + metadata.recycle(); return retValue; } @@ -461,7 +462,8 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { @Override public int getItemViewType(int flatListPos) { - final ExpandableListPosition pos = getUnflattenedPos(flatListPos).position; + final PositionMetadata metadata = getUnflattenedPos(flatListPos); + final ExpandableListPosition pos = metadata.position; int retValue; if (mExpandableListAdapter instanceof HeterogeneousExpandableList) { @@ -481,7 +483,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { } } - pos.recycle(); + metadata.recycle(); return retValue; } @@ -590,8 +592,10 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * @param groupPos position of the group to collapse */ boolean collapseGroup(int groupPos) { - PositionMetadata pm = getFlattenedPos(ExpandableListPosition.obtain( - ExpandableListPosition.GROUP, groupPos, -1, -1)); + ExpandableListPosition elGroupPos = ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1); + PositionMetadata pm = getFlattenedPos(elGroupPos); + elGroupPos.recycle(); if (pm == null) return false; boolean retValue = collapseGroup(pm); @@ -631,8 +635,10 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { * @param groupPos the group to be expanded */ boolean expandGroup(int groupPos) { - PositionMetadata pm = getFlattenedPos(ExpandableListPosition.obtain( - ExpandableListPosition.GROUP, groupPos, -1, -1)); + ExpandableListPosition elGroupPos = ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1); + PositionMetadata pm = getFlattenedPos(elGroupPos); + elGroupPos.recycle(); boolean retValue = expandGroup(pm); pm.recycle(); return retValue; @@ -971,7 +977,10 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { public int groupInsertIndex; private void resetState() { - position = null; + if (position != null) { + position.recycle(); + position = null; + } groupMetadata = null; groupInsertIndex = 0; } @@ -1005,6 +1014,7 @@ class ExpandableListConnector extends BaseAdapter implements Filterable { } public void recycle() { + resetState(); synchronized (sPool) { if (sPool.size() < MAX_POOL_SIZE) { sPool.add(this); diff --git a/core/java/android/widget/ExpandableListPosition.java b/core/java/android/widget/ExpandableListPosition.java index e8d6113..bb68da6 100644 --- a/core/java/android/widget/ExpandableListPosition.java +++ b/core/java/android/widget/ExpandableListPosition.java @@ -125,6 +125,10 @@ class ExpandableListPosition { return elp; } + /** + * Do not call this unless you obtained this via ExpandableListPosition.obtain(). + * PositionMetadata will handle recycling its own children. + */ public void recycle() { synchronized (sPool) { if (sPool.size() < MAX_POOL_SIZE) { diff --git a/core/java/android/widget/ExpandableListView.java b/core/java/android/widget/ExpandableListView.java index c2d8bda..a746370 100644 --- a/core/java/android/widget/ExpandableListView.java +++ b/core/java/android/widget/ExpandableListView.java @@ -326,7 +326,6 @@ public class ExpandableListView extends ListView { indicator.draw(canvas); } } - pos.recycle(); } @@ -613,8 +612,10 @@ public class ExpandableListView extends ListView { * was already expanded, this will return false) */ public boolean expandGroup(int groupPos, boolean animate) { - PositionMetadata pm = mConnector.getFlattenedPos(ExpandableListPosition.obtain( - ExpandableListPosition.GROUP, groupPos, -1, -1)); + ExpandableListPosition elGroupPos = ExpandableListPosition.obtain( + ExpandableListPosition.GROUP, groupPos, -1, -1); + PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos); + elGroupPos.recycle(); boolean retValue = mConnector.expandGroup(pm); if (mOnGroupExpandListener != null) { @@ -776,8 +777,10 @@ public class ExpandableListView extends ListView { * @return The flat list position for the given child or group. */ public int getFlatListPosition(long packedPosition) { - PositionMetadata pm = mConnector.getFlattenedPos(ExpandableListPosition - .obtainPosition(packedPosition)); + ExpandableListPosition elPackedPos = ExpandableListPosition + .obtainPosition(packedPosition); + PositionMetadata pm = mConnector.getFlattenedPos(elPackedPos); + elPackedPos.recycle(); final int flatListPosition = pm.position.flatListPos; pm.recycle(); return getAbsoluteFlatPosition(flatListPosition); @@ -988,11 +991,11 @@ public class ExpandableListView extends ListView { final int adjustedPosition = getFlatPositionForConnector(flatListPosition); PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition); ExpandableListPosition pos = pm.position; - pm.recycle(); id = getChildOrGroupId(pos); long packedPosition = pos.getPackedPosition(); - pos.recycle(); + + pm.recycle(); return new ExpandableListContextMenuInfo(view, packedPosition, id); } diff --git a/core/java/com/android/internal/util/AsyncChannel.java b/core/java/com/android/internal/util/AsyncChannel.java index 0c5d5ef..d1c2d2e 100644 --- a/core/java/com/android/internal/util/AsyncChannel.java +++ b/core/java/com/android/internal/util/AsyncChannel.java @@ -150,7 +150,7 @@ public class AsyncChannel { */ public static final int CMD_CHANNEL_DISCONNECTED = BASE + 4; - private static final int CMD_TO_STRING_COUNT = CMD_CHANNEL_DISCONNECTED + 1; + private static final int CMD_TO_STRING_COUNT = CMD_CHANNEL_DISCONNECTED - BASE + 1; private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT]; static { sCmdToString[CMD_CHANNEL_HALF_CONNECTED - BASE] = "CMD_CHANNEL_HALF_CONNECTED"; diff --git a/core/jni/android_hardware_Camera.cpp b/core/jni/android_hardware_Camera.cpp index 36b4b45..6cd8955 100644 --- a/core/jni/android_hardware_Camera.cpp +++ b/core/jni/android_hardware_Camera.cpp @@ -457,9 +457,9 @@ static void android_hardware_Camera_getCameraInfo(JNIEnv *env, jobject thiz, // connect to camera service static void android_hardware_Camera_native_setup(JNIEnv *env, jobject thiz, - jobject weak_this, jint cameraId, jboolean force, jboolean keep) + jobject weak_this, jint cameraId) { - sp<Camera> camera = Camera::connect(cameraId, force, keep); + sp<Camera> camera = Camera::connect(cameraId); if (camera == NULL) { jniThrowRuntimeException(env, "Fail to connect to camera service"); @@ -821,15 +821,6 @@ static void android_hardware_Camera_enableFocusMoveCallback(JNIEnv *env, jobject } } -static bool android_hardware_Camera_isReleased(JNIEnv *env, jobject thiz) -{ - ALOGV("isReleased"); - sp<Camera> camera = get_native_camera(env, thiz, NULL); - if (camera == 0) return true; - - return (camera->sendCommand(CAMERA_CMD_PING, 0, 0) != NO_ERROR); -} - //------------------------------------------------- static JNINativeMethod camMethods[] = { @@ -840,7 +831,7 @@ static JNINativeMethod camMethods[] = { "(ILandroid/hardware/Camera$CameraInfo;)V", (void*)android_hardware_Camera_getCameraInfo }, { "native_setup", - "(Ljava/lang/Object;IZZ)V", + "(Ljava/lang/Object;I)V", (void*)android_hardware_Camera_native_setup }, { "native_release", "()V", @@ -908,9 +899,6 @@ static JNINativeMethod camMethods[] = { { "enableFocusMoveCallback", "(I)V", (void *)android_hardware_Camera_enableFocusMoveCallback}, - { "isReleased", - "()Z", - (void *)android_hardware_Camera_isReleased}, }; struct field { diff --git a/core/res/res/layout/notification_template_base.xml b/core/res/res/layout/notification_template_base.xml index 74b3fa9..bd26232 100644 --- a/core/res/res/layout/notification_template_base.xml +++ b/core/res/res/layout/notification_template_base.xml @@ -19,7 +19,7 @@ android:background="@android:color/background_dark" android:id="@+id/status_bar_latest_event_content" android:layout_width="match_parent" - android:layout_height="wrap_content" + android:layout_height="64dp" internal:layout_minHeight="64dp" internal:layout_maxHeight="64dp" > diff --git a/docs/html/resources/resources_toc.cs b/docs/html/resources/resources_toc.cs index 686bde3..a21708c 100644 --- a/docs/html/resources/resources_toc.cs +++ b/docs/html/resources/resources_toc.cs @@ -124,6 +124,23 @@ class="new"> new!</span></span> </li> <li class="toggle-list"> + <div><a href="<?cs var:toroot ?>training/cloudsync/index.html"> + <span class="en">Syncing to the Cloud<span class="new"> new!</span></span> + </a></div> + <ul> + <li><a href="<?cs var:toroot ?>training/cloudsync/aesync.html"> + <span class="en">Syncing with App Engine</span> + </a> + </li> + <li><a href="<?cs var:toroot ?>training/cloudsync/backupapi.html"> + <span class="en">Using the Backup API</span> + </a> + </li> + </ul> + </li> + + + <li class="toggle-list"> <div><a href="<?cs var:toroot ?>training/search/index.html"> <span class="en">Adding Search Functionality<span class="new"> new!</span></span> </a> @@ -369,11 +386,11 @@ class="new"> new!</span></span> </li> <li><a href="<?cs var:toroot ?>training/displaying-bitmaps/display-bitmap.html"> <span class="en">Displaying Bitmaps in Your UI</span> - </a> </li> - </ul> + <ul> </li> + <li class="toggle-list"> <div><a href="<?cs var:toroot ?>training/accessibility/index.html"> <span class="en">Implementing Accessibility<span class="new"> new!</span></span> @@ -391,9 +408,11 @@ class="new"> new!</span></span> </li> </ul> - </li> - - + </li> + + + + <li> <span class="heading"> <span class="en">Technical Resources</span> diff --git a/docs/html/training/location/currentlocation.jd b/docs/html/training/basics/location/currentlocation.jd index 4692530..4692530 100644 --- a/docs/html/training/location/currentlocation.jd +++ b/docs/html/training/basics/location/currentlocation.jd diff --git a/docs/html/training/location/geocoding.jd b/docs/html/training/basics/location/geocoding.jd index 6364976..6364976 100644 --- a/docs/html/training/location/geocoding.jd +++ b/docs/html/training/basics/location/geocoding.jd diff --git a/docs/html/training/location/index.jd b/docs/html/training/basics/location/index.jd index 48cfbc3..48cfbc3 100644 --- a/docs/html/training/location/index.jd +++ b/docs/html/training/basics/location/index.jd diff --git a/docs/html/training/location/locationmanager.jd b/docs/html/training/basics/location/locationmanager.jd index 61abcbd..61abcbd 100644 --- a/docs/html/training/location/locationmanager.jd +++ b/docs/html/training/basics/location/locationmanager.jd diff --git a/docs/html/training/cloudsync/aesync.jd b/docs/html/training/cloudsync/aesync.jd new file mode 100644 index 0000000..c60d28b --- /dev/null +++ b/docs/html/training/cloudsync/aesync.jd @@ -0,0 +1,432 @@ +page.title=Syncing with App Engine +parent.title=Syncing to the Cloud +parent.link=index.html + +trainingnavtop=true +next.title=Using the Backup API +next.link=backupapi.html + +@jd:body + +<div id="tb-wrapper"> +<div id="tb"> + +<!-- table of contents --> +<h2>This lesson teaches you how to</h2> +<ol> + <li><a href="#prepare">Prepare Your Environment</a></li> + <li><a href="#project">Create Your Project</a></li> + <li><a href="#data">Create the Data Layer</a></li> + <li><a href="#persistence">Create the Persistence Layer</a></li> + <li><a href="#androidapp">Query and Update from the Android App</a></li> + <li><a href="#serverc2dm">Configure the C2DM Server-Side</a></li> + <li><a href="#clientc2dm">Configure the C2DM Client-Side</a></li> +</ol> +<h2>You should also read</h2> + <ul> + <li><a + href="http://developers.google.com/appengine/">App Engine</a></li> + <li><a href="http://code.google.com/android/c2dm/">Android Cloud to Device + Messaging Framework</a></li> + </ul> +<h2>Try it out</h2> + +<p>This lesson uses the Cloud Tasks sample code, originally shown at the +<a href="http://www.youtube.com/watch?v=M7SxNNC429U">Android + AppEngine: A Developer's Dream Combination</a> +talk at Google I/O. You can use the sample application as a source of reusable code for your own +application, or simply as a reference for how the Android and cloud pieces of the overall +application fit together. You can also build the sample application and see how it runs +on your own device or emulator.</p> +<p> + <a href="http://code.google.com/p/cloud-tasks-io/" class="button">Cloud Tasks + App</a> +</p> + +</div> +</div> + +<p>Writing an app that syncs to the cloud can be a challenge. There are many little +details to get right, like server-side auth, client-side auth, a shared data +model, and an API. One way to make this much easier is to use the Google Plugin +for Eclipse, which handles a lot of the plumbing for you when building Android +and App Engine applications that talk to each other. This lesson walks you through building such a project.</p> + +<p>Following this lesson shows you how to:</p> +<ul> + <li>Build Android and Appengine apps that can communicate with each other</li> + <li>Take advantage of Cloud to Device Messaging (C2DM) so your Android app doesn't have to poll for updates</li> +</ul> + +<p>This lesson focuses on local development, and does not cover distribution +(i.e, pushing your App Engine app live, or publishing your Android App to +market), as those topics are covered extensively elsewhere.</p> + +<h2 id="prepare">Prepare Your Environment</h2> +<p>If you want to follow along with the code example in this lesson, you must do +the following to prepare your development environment:</p> +<ul> +<li>Install the <a href="http://code.google.com/eclipse/">Google Plugin for + Eclipse.</a></li> +<li>Install the <a + href="http://code.google.com/webtoolkit/download.html">GWT SDK</a> and the <a + href="http://code.google.com/appengine/">Java App Engine SDK</a>. The <a + href="http://code.google.com/eclipse/docs/getting_started.html">Quick Start + Guide</a> shows you how to install these components.</li> +<li>Sign up for <a href="http://code.google.com/android/c2dm/signup.html">C2DM + access</a>. We strongly recommend <a + href="https://accounts.google.com/SignUp">creating a new Google account</a> specifically for +connecting to C2DM. The server component in this lesson uses this <em>role + account</em> repeatedly to authenticate with Google servers. +</li> +</ul> + +<h2 id="project">Create Your Projects</h2> +<p>After installing the Google Plugin for Eclipse, notice that a new kind of Android project +exists when you create a new Eclipse project: The <strong>App Engine Connected + Android Project</strong> (under the <strong>Google</strong> project category). +A wizard guides you through creating this project, +during the course of which you are prompted to enter the account credentials for the role +account you created.</p> + +<p class="note"><strong>Note:</strong> Remember to enter the credentials for +your <i>role account</i> (the one you created to access C2DM services), not an +account you'd log into as a user, or as an admin.</p> + +<p>Once you're done, you'll see two projects waiting for you in your +workspace—An Android application and an App Engine application. Hooray! +These two applications are already fully functional— the wizard has +created a sample application which lets you authenticate to the App Engine +application from your Android device using AccountManager (no need to type in +your credentials), and an App Engine app that can send messages to any logged-in +device using C2DM. In order to spin up your application and take it for a test +drive, do the following:</p> + +<p>To spin up the Android application, make sure you have an AVD with a platform +version of <em>at least</em> Android 2.2 (API Level 8). Right click on the Android project in +Eclipse, and go to <strong>Debug As > Local App Engine Connected Android + Application</strong>. This launches the emulator in such a way that it can +test C2DM functionality (which typically works through Google Play). It'll +also launch a local instance of App Engine containing your awesome +application.</p> + +<h2 id="data">Create the Data Layer</h2> + +<p>At this point you have a fully functional sample application running. Now +it's time to start changing the code to create your own application.</p> + +<p>First, create the data model that defines the data shared between +the App Engine and Android applications. To start, open up the source folder of +your App Engine project, and navigate down to the <strong>(yourApp)-AppEngine + > src > (yourapp) > server</strong> package. Create a new class in there containing some data you want to +store server-side. The code ends up looking something like this:</p> +<pre> +package com.cloudtasks.server; + +import javax.persistence.*; + +@Entity +public class Task { + + private String emailAddress; + private String name; + private String userId; + private String note; + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + public Task() { + } + + public String getEmailAddress() { + return this.emailAddress; + } + + public Long getId() { + return this.id; + } + ... +} +</pre> +<p>Note the use of annotations: <code>Entity</code>, <code>Id</code> and +<code>GeneratedValue</code> are all part of the <a + href="http://www.oracle.com/technetwork/articles/javaee/jpa-137156.html">Java + Persistence API</a>. Essentially, the <code>Entity</code> annotation goes +above the class declaration, and indicates that this class represents an entity +in your data layer. The <code>Id</code> and <code>GeneratedValue</code> +annotations, respectively, indicate the field used as a lookup key for this +class, and how that id is generated (in this case, +<code>GenerationType.IDENTITY</code> indicates that the is generated by +the database). You can find more on this topic in the App Engine documentation, +on the page <a + href="http://code.google.com/appengine/docs/java/datastore/jpa/overview.html">Using + JPA with App Engine</a>.</p> + +<p>Once you've written all the classes that represent entities in your data +layer, you need a way for the Android and App Engine applications to communicate +about this data. This communication is enabled by creating a Remote Procedure +Call (RPC) service. +Typically, this involves a lot of monotonous code. Fortunately, there's an easy way! Right +click on the server project in your App Engine source folder, and in the context +menu, navigate to <strong>New > Other</strong> and then, in the resulting +screen, select <strong>Google > RPC Service.</strong> A wizard appears, pre-populated +with all the Entities you created in the previous step, +which it found by seeking out the <code>@Entity</code> annotation in the +source files you added. Pretty neat, right? Click <strong>Finish</strong>, and the wizard +creates a Service class with stub methods for the Create, Retrieve, Update and +Delete (CRUD) operations of all your entities.</p> + +<h2 id="persistence">Create the Persistence Layer</h2> + +<p>The persistence layer is where your application data is stored +long-term, so any information you want to keep for your users needs to go here. +You have several options for writing your persistence layer, depending on +what kind of data you want to store. A few of the options hosted by Google +(though you don't have to use these services) include <a + href="http://code.google.com/apis/storage/">Google Storage for Developers</a> +and App Engine's built-in <a + href="http://code.google.com/appengine/docs/java/gettingstarted/usingdatastore.html">Datastore</a>. +The sample code for this lesson uses DataStore code.</p> + +<p>Create a class in your <code>com.cloudtasks.server</code> package to handle +persistence layer input and output. In order to access the data store, use the <a + href="http://db.apache.org/jdo/api20/apidocs/javax/jdo/PersistenceManager.html">PersistenceManager</a> +class. You can generate an instance of this class using the PMF class in the +<code>com.google.android.c2dm.server.PMF</code> package, and then use that to +perform basic CRUD operations on your data store, like this:</p> +<pre> +/** +* Remove this object from the data store. +*/ +public void delete(Long id) { + PersistenceManager pm = PMF.get().getPersistenceManager(); + try { + Task item = pm.getObjectById(Task.class, id); + pm.deletePersistent(item); + } finally { + pm.close(); + } +} +</pre> + +<p>You can also use <a + href="http://code.google.com/appengine/docs/python/datastore/queryclass.html">Query</a> +objects to retrieve data from your Datastore. Here's an example of a method +that searches out an object by its ID.</p> + +<pre> +public Task find(Long id) { + if (id == null) { + return null; + } + + PersistenceManager pm = PMF.get().getPersistenceManager(); + try { + Query query = pm.newQuery("select from " + Task.class.getName() + + " where id==" + id.toString() + " && emailAddress=='" + getUserEmail() + "'"); + List<Task> list = (List<Task>) query.execute(); + return list.size() == 0 ? null : list.get(0); + } catch (RuntimeException e) { + System.out.println(e); + throw e; + } finally { + pm.close(); + } +} +</pre> + +<p>For a good example of a class that encapsulates the persistence layer for +you, check out the <a + href="http://code.google.com/p/cloud-tasks-io/source/browse/trunk/CloudTasks-AppEngine/src/com/cloudtasks/server/DataStore.java">DataStore</a> +class in the Cloud Tasks app.</p> + + + +<h2 id="androidapp">Query and Update from the Android App</h2> + +<p>In order to keep in sync with the App Engine application, your Android application +needs to know how to do two things: Pull data from the cloud, and send data up +to the cloud. Much of the plumbing for this is generated by the +plugin, but you need to wire it up to your Android user interface yourself.</p> + +<p>Pop open the source code for the main Activity in your project and look for +<code><YourProjectName> Activity.java</code>, then for the method +<code>setHelloWorldScreenContent()</code>. Obviously you're not building a +HelloWorld app, so delete this method entirely and replace it +with something relevant. However, the boilerplate code has some very important +characteristics. For one, the code that communicates with the cloud is wrapped +in an {@link android.os.AsyncTask} and therefore <em>not</em> hitting the +network on the UI thread. Also, it gives an easy template for how to access +the cloud in your own code, using the <a + href="http://code.google.com/webtoolkit/doc/latest/DevGuideRequestFactory.html">RequestFactory</a> +class generated that was auto-generated for you by the Eclipse plugin (called +MyRequestFactory in the example below), and various {@code Request} types.</p> + +<p>For instance, if your server-side data model included an object called {@code +Task} when you generated an RPC layer it automatically created a +{@code TaskRequest} class for you, as well as a {@code TaskProxy} representing the individual +task. In code, requesting a list of all these tasks from the server looks +like this:</p> + +<pre> +public void fetchTasks (Long id) { + // Request is wrapped in an AsyncTask to avoid making a network request + // on the UI thread. + new AsyncTask<Long, Void, List<TaskProxy>>() { + @Override + protected List<TaskProxy> doInBackground(Long... arguments) { + final List<TaskProxy> list = new ArrayList<TaskProxy>(); + MyRequestFactory factory = Util.getRequestFactory(mContext, + MyRequestFactory.class); + TaskRequest taskRequest = factory.taskNinjaRequest(); + + if (arguments.length == 0 || arguments[0] == -1) { + factory.taskRequest().queryTasks().fire(new Receiver<List<TaskProxy>>() { + @Override + public void onSuccess(List<TaskProxy> arg0) { + list.addAll(arg0); + } + }); + } else { + newTask = true; + factory.taskRequest().readTask(arguments[0]).fire(new Receiver<TaskProxy>() { + @Override + public void onSuccess(TaskProxy arg0) { + list.add(arg0); + } + }); + } + return list; + } + + @Override + protected void onPostExecute(List<TaskProxy> result) { + TaskNinjaActivity.this.dump(result); + } + + }.execute(id); +} +... + +public void dump (List<TaskProxy> tasks) { + for (TaskProxy task : tasks) { + Log.i("Task output", task.getName() + "\n" + task.getNote()); + } +} +</pre> + +<p>This {@link android.os.AsyncTask} returns a list of +<code>TaskProxy</code> objects, and sends it to the debug {@code dump()} method +upon completion. Note that if the argument list is empty, or the first argument +is a -1, all tasks are retrieved from the server. Otherwise, only the ones with +IDs in the supplied list are returned. All the fields you added to the task +entity when building out the App Engine application are available via get/set +methods in the <code>TaskProxy</code> class.</p> + +<p>In order to create new tasks and send them to the cloud, create a request +object and use it to create a proxy object. Then populate the proxy object and +call its update method. Once again, this should be done in an +<code>AsyncTask</code> to avoid doing networking on the UI thread. The end +result looks something like this.</p> + +<pre> +new AsyncTask<Void, Void, Void>() { + @Override + protected Void doInBackground(Void... arg0) { + MyRequestFactory factory = (MyRequestFactory) + Util.getRequestFactory(TasksActivity.this, + MyRequestFactory.class); + TaskRequest request = factory.taskRequest(); + + // Create your local proxy object, populate it + TaskProxy task = request.create(TaskProxy.class); + task.setName(taskName); + task.setNote(taskDetails); + task.setDueDate(dueDate); + + // To the cloud! + request.updateTask(task).fire(); + return null; + } +}.execute(); +</pre> + +<h2 id="serverc2dm">Configure the C2DM Server-Side</h2> + +<p>In order to set up C2DM messages to be sent to your Android device, go back +into your App Engine codebase, and open up the service class that was created +when you generated your RPC layer. If the name of your project is Foo, +this class is called FooService. Add a line to each of the methods for +adding, deleting, or updating data so that a C2DM message is sent to the +user's device. Here's an example of an update task: +</p> + +<pre> +public static Task updateTask(Task task) { + task.setEmailAddress(DataStore.getUserEmail()); + task = db.update(task); + DataStore.sendC2DMUpdate(TaskChange.UPDATE + TaskChange.SEPARATOR + task.getId()); + return task; +} + +// Helper method. Given a String, send it to the current user's device via C2DM. +public static void sendC2DMUpdate(String message) { + UserService userService = UserServiceFactory.getUserService(); + User user = userService.getCurrentUser(); + ServletContext context = RequestFactoryServlet.getThreadLocalRequest().getSession().getServletContext(); + SendMessage.sendMessage(context, user.getEmail(), message); +} +</pre> + +<p>In the following example, a helper class, {@code TaskChange}, has been created with a few +constants. Creating such a helper class makes managing the communication +between App Engine and Android apps much easier. Just create it in the shared +folder, define a few constants (flags for what kind of message you're sending +and a seperator is typically enough), and you're done. By way of example, +the above code works off of a {@code TaskChange} class defined as this:</p> + +<pre> +public class TaskChange { + public static String UPDATE = "Update"; + public static String DELETE = "Delete"; + public static String SEPARATOR = ":"; +} +</pre> + +<h2 id="clientc2dm">Configure the C2DM Client-Side</h2> + +<p>In order to define the Android applications behavior when a C2DM is recieved, +open up the <code>C2DMReceiver</code> class, and browse to the +<code>onMessage()</code> method. Tweak this method to update based on the content +of the message.</p> +<pre> +//In your C2DMReceiver class + +public void notifyListener(Intent intent) { + if (listener != null) { + Bundle extras = intent.getExtras(); + if (extras != null) { + String message = (String) extras.get("message"); + String[] messages = message.split(Pattern.quote(TaskChange.SEPARATOR)); + listener.onTaskUpdated(messages[0], Long.parseLong(messages[1])); + } + } +} +</pre> + +<pre> +// Elsewhere in your code, wherever it makes sense to perform local updates +public void onTasksUpdated(String messageType, Long id) { + if (messageType.equals(TaskChange.DELETE)) { + // Delete this task from your local data store + ... + } else { + // Call that monstrous Asynctask defined earlier. + fetchTasks(id); + } +} +</pre> +<p> +Once you have C2DM set up to trigger local updates, you're all done. +Congratulations, you have a cloud-connected Android application!</p> diff --git a/docs/html/training/cloudsync/backupapi.jd b/docs/html/training/cloudsync/backupapi.jd new file mode 100644 index 0000000..3055596 --- /dev/null +++ b/docs/html/training/cloudsync/backupapi.jd @@ -0,0 +1,193 @@ +page.title=Using the Backup API +parent.title=Syncing to the Cloud +parent.link=index.html + +trainingnavtop=true +previous.title=Syncing with App Engine +previous.link=aesync.html + +@jd:body + +<div id="tb-wrapper"> + <div id="tb"> + <h2>This lesson teaches you to</h2> + <ol> + <li><a href="#register">Register for the Android Backup Service</a></li> + <li><a href="#manifest">Configure Your Manifest</a></li> + <li><a href="#agent">Write Your Backup Agent</a></li> + <li><a href="#backup">Request a Backup</a></li> + <li><a href="#restore">Restore from a Backup</a></li> + </ol> + <h2>You should also read</h2> + <ul> + <li><a + href="http://developer.android.com/guide/topics/data/backup.html">Data + Backup</a></li> + </ul> + </div> +</div> + +<p>When a user purchases a new device or resets their existing one, they might +expect that when Google Play restores your app back to their device during the +initial setup, the previous data associated with the app restores as well. By +default, that doesn't happen and all the user's accomplishments or settings in +your app are lost.</p> +<p>For situations where the volume of data is relatively light (less than a +megabyte), like the user's preferences, notes, game high scores or other +stats, the Backup API provides a lightweight solution. This lesson walks you +through integrating the Backup API into your application, and restoring data to +new devices using the Backup API.</p> + +<h2 id="register">Register for the Android Backup Service</h2> +<p>This lesson requires the use of the <a + href="http://code.google.com/android/backup/index.html">Android Backup + Service</a>, which requires registration. Go ahead and <a + href="http://code.google.com/android/backup/signup.html">register here</a>. Once +that's done, the service pre-populates an XML tag for insertion in your Android +Manifest, which looks like this:</p> +<pre> +<meta-data android:name="com.google.android.backup.api_key" +android:value="ABcDe1FGHij2KlmN3oPQRs4TUvW5xYZ" /> +</pre> +<p>Note that each backup key works with a specific package name. If you have +different applications, register separate keys for each one.</p> + + +<h2 id="manifest">Configure Your Manifest</h2> +<p>Use of the Android Backup Service requires two additions to your application +manifest. First, declare the name of the class that acts as your backup agent, +then add the snippet above as a child element of the Application tag. Assuming +your backup agent is going to be called {@code TheBackupAgent}, here's an example of +what the manifest looks like with this tag included:</p> + +<pre> +<application android:label="MyApp" + android:backupAgent="TheBackupAgent"> + ... + <meta-data android:name="com.google.android.backup.api_key" + android:value="ABcDe1FGHij2KlmN3oPQRs4TUvW5xYZ" /> + ... +</application> +</pre> +<h2 id="agent">Write Your Backup Agent</h2> +<p>The easiest way to create your backup agent is by extending the wrapper class +{@link android.app.backup.BackupAgentHelper}. Creating this helper class is +actually a very simple process. Just create a class with the same name as you +used in the manifest in the previous step (in this example, {@code +TheBackupAgent}), +and extend {@code BackupAgentHelper}. Then override the {@link +android.app.backup.BackupAgent#onCreate()}.</p> + +<p>Inside the {@link android.app.backup.BackupAgent#onCreate()} method, create a {@link +android.app.backup.BackupHelper}. These helpers are +specialized classes for backing up certain kinds of data. The Android framework +currently includes two such helpers: {@link +android.app.backup.FileBackupHelper} and {@link +android.app.backup.SharedPreferencesBackupHelper}. After you create the helper +and point it at the data you want to back up, just add it to the +BackupAgentHelper using the {@link android.app.backup.BackupAgentHelper#addHelper(String, BackupHelper) addHelper()} +method, adding a key which is used to +retrieve the data later. In most cases the entire +implementation is perhaps 10 lines of code.</p> + +<p>Here's an example that backs up a high scores file.</p> + +<pre> + import android.app.backup.BackupAgentHelper; + import android.app.backup.FileBackupHelper; + + + public class TheBackupAgent extends BackupAgentHelper { + // The name of the SharedPreferences file + static final String HIGH_SCORES_FILENAME = "scores"; + + // A key to uniquely identify the set of backup data + static final String FILES_BACKUP_KEY = "myfiles"; + + // Allocate a helper and add it to the backup agent + @Override + void onCreate() { + FileBackupHelper helper = new FileBackupHelper(this, HIGH_SCORES_FILENAME); + addHelper(FILES_BACKUP_KEY, helper); + } +} +</pre> +<p>For added flexibility, {@link android.app.backup.FileBackupHelper}'s +constructor can take a variable number of filenames. You could just as easily +have backed up both a high scores file and a game progress file just by adding +an extra parameter, like this:</p> +<pre> + @Override + void onCreate() { + FileBackupHelper helper = new FileBackupHelper(this, HIGH_SCORES_FILENAME, PROGRESS_FILENAME); + addHelper(FILES_BACKUP_KEY, helper); + } +</pre> +<p>Backing up preferences is similarly easy. Create a {@link +android.app.backup.SharedPreferencesBackupHelper} the same way you did a {@link +android.app.backup.FileBackupHelper}. In this case, instead of adding filenames +to the constructor, add the names of the shared preference groups being used by +your application. Here's an example of how your backup agent helper might look if +high scores are implemented as preferences instead of a flat file:</p> + +<pre> + import android.app.backup.BackupAgentHelper; + import android.app.backup.SharedPreferencesBackupHelper; + + public class TheBackupAgent extends BackupAgentHelper { + // The names of the SharedPreferences groups that the application maintains. These + // are the same strings that are passed to getSharedPreferences(String, int). + static final String PREFS_DISPLAY = "displayprefs"; + static final String PREFS_SCORES = "highscores"; + + // An arbitrary string used within the BackupAgentHelper implementation to + // identify the SharedPreferencesBackupHelper's data. + static final String MY_PREFS_BACKUP_KEY = "myprefs"; + + // Simply allocate a helper and install it + void onCreate() { + SharedPreferencesBackupHelper helper = + new SharedPreferencesBackupHelper(this, PREFS_DISPLAY, PREFS_SCORES); + addHelper(MY_PREFS_BACKUP_KEY, helper); + } + } +</pre> + +<p>You can add as many backup helper instances to your backup agent helper as you +like, but remember that you only need one of each type. One {@link +android.app.backup.FileBackupHelper} handles all the files that you need to back up, and one +{@link android.app.backup.SharedPreferencesBackupHelper} handles all the shared +preferencegroups you need backed up. +</p> + + +<h2 id="backup">Request a Backup</h2> +<p>In order to request a backup, just create an instance of the {@link +android.app.backup.BackupManager}, and call it's {@link +android.app.backup.BackupManager#dataChanged()} method.</p> + +<pre> + import android.app.backup.BackupManager; + ... + + public void requestBackup() { + BackupManager bm = new BackupManager(this); + bm.dataChanged(); + } +</pre> + +<p>This call notifies the backup manager that there is data ready to be backed +up to the cloud. At some point in the future, the backup manager then calls +your backup agent's {@link +android.app.backup.BackupAgent#onBackup(ParcelFileDescriptor, BackupDataOutput, +ParcelFileDescriptor) onBackup()} method. You can make +the call whenever your data has changed, without having to worry about causing +excessive network activity. If you request a backup twice before a backup +occurs, the backup only occurs once.</p> + + +<h2 id="restore">Restore from a Backup</h2> +<p>Typically you shouldn't ever have to manually request a restore, as it +happens automatically when your application is installed on a device. However, +if it <em>is</em> necessary to trigger a manual restore, just call the +{@link android.app.backup.BackupManager#requestRestore(RestoreObserver) requestRestore()} method.</p> diff --git a/docs/html/training/cloudsync/index.jd b/docs/html/training/cloudsync/index.jd new file mode 100644 index 0000000..e53844b --- /dev/null +++ b/docs/html/training/cloudsync/index.jd @@ -0,0 +1,34 @@ +page.title=Syncing to the Cloud + +trainingnavtop=true +startpage=true +next.title=Syncing with App Engine +next.link=aesync.html + +@jd:body + +<p>By providing powerful APIs for internet connectivity, the Android framework +helps you build rich cloud-enabled apps that sync their data to a remote web +service, making sure all your devices always stay in sync, and your valuable +data is always backed up to the cloud.</p> + +<p>This class covers different strategies for cloud enabled applications. It +covers syncing data with the cloud using your own back-end web application, and +backing up data using the cloud so that users can restore their data when +installing your application on a new device. +</p> + +<h2>Lessons</h2> + +<dl> + <dt><strong><a href="aesync.html">Syncing with App Engine.</a></strong></dt> + <dd>Learn how to create a paired App Engine app and Android app which share a + data model, authenticates using the AccountManager, and communicate with each + other via REST and C2DM.</dd> + <dt><strong><a href="backupapi.html">Using the Backup + API</a></strong></dt> + <dd>Learn how to integrate the Backup API into your Android Application, so + that user data such as preferences, notes, and high scores update seamlessly + across all of a user's devices</dd> +</dl> + diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index 4e112af..34bf368 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -497,7 +497,6 @@ public class AudioRecord * Returns the audio session ID. * * @return the ID of the audio session this AudioRecord belongs to. - * @hide */ public int getAudioSessionId() { return mSessionId; diff --git a/media/java/android/media/audiofx/AcousticEchoCanceler.java b/media/java/android/media/audiofx/AcousticEchoCanceler.java index e31f84c..4b59c88 100644 --- a/media/java/android/media/audiofx/AcousticEchoCanceler.java +++ b/media/java/android/media/audiofx/AcousticEchoCanceler.java @@ -37,7 +37,6 @@ import android.util.Log; * state on a particular AudioRecord session. * <p>See {@link android.media.audiofx.AudioEffect} class for more details on * controlling audio effects. - * @hide */ public class AcousticEchoCanceler extends AudioEffect { @@ -90,9 +89,8 @@ public class AcousticEchoCanceler extends AudioEffect { * @throws java.lang.IllegalArgumentException * @throws java.lang.UnsupportedOperationException * @throws java.lang.RuntimeException - * @hide */ - public AcousticEchoCanceler(int audioSession) + private AcousticEchoCanceler(int audioSession) throws IllegalArgumentException, UnsupportedOperationException, RuntimeException { super(EFFECT_TYPE_AEC, EFFECT_TYPE_NULL, 0, audioSession); } diff --git a/media/java/android/media/audiofx/AutomaticGainControl.java b/media/java/android/media/audiofx/AutomaticGainControl.java index eca7eec..83eb4e9 100644 --- a/media/java/android/media/audiofx/AutomaticGainControl.java +++ b/media/java/android/media/audiofx/AutomaticGainControl.java @@ -37,7 +37,6 @@ import android.util.Log; * state on a particular AudioRecord session. * <p>See {@link android.media.audiofx.AudioEffect} class for more details on * controlling audio effects. - * @hide */ public class AutomaticGainControl extends AudioEffect { @@ -90,9 +89,8 @@ public class AutomaticGainControl extends AudioEffect { * @throws java.lang.IllegalArgumentException * @throws java.lang.UnsupportedOperationException * @throws java.lang.RuntimeException - * @hide */ - public AutomaticGainControl(int audioSession) + private AutomaticGainControl(int audioSession) throws IllegalArgumentException, UnsupportedOperationException, RuntimeException { super(EFFECT_TYPE_AGC, EFFECT_TYPE_NULL, 0, audioSession); } diff --git a/media/java/android/media/audiofx/NoiseSuppressor.java b/media/java/android/media/audiofx/NoiseSuppressor.java index a2d3386..0ea42ab 100644 --- a/media/java/android/media/audiofx/NoiseSuppressor.java +++ b/media/java/android/media/audiofx/NoiseSuppressor.java @@ -38,7 +38,6 @@ import android.util.Log; * state on a particular AudioRecord session. * <p>See {@link android.media.audiofx.AudioEffect} class for more details on * controlling audio effects. - * @hide */ public class NoiseSuppressor extends AudioEffect { @@ -92,7 +91,6 @@ public class NoiseSuppressor extends AudioEffect { * @throws java.lang.IllegalArgumentException * @throws java.lang.UnsupportedOperationException * @throws java.lang.RuntimeException - * @hide */ private NoiseSuppressor(int audioSession) throws IllegalArgumentException, UnsupportedOperationException, RuntimeException { diff --git a/media/java/android/media/audiofx/Visualizer.java b/media/java/android/media/audiofx/Visualizer.java index 91d0add..47c0d57 100755 --- a/media/java/android/media/audiofx/Visualizer.java +++ b/media/java/android/media/audiofx/Visualizer.java @@ -81,6 +81,20 @@ public class Visualizer { */ public static final int STATE_ENABLED = 2; + // to keep in sync with system/media/audio_effects/include/audio_effects/effect_visualizer.h + /** + * @hide + * Defines a capture mode where amplification is applied based on the content of the captured + * data. This is the default Visualizer mode, and is suitable for music visualization. + */ + public static final int SCALING_MODE_NORMALIZED = 0; + /** + * @hide + * Defines a capture mode where the playback volume will affect (scale) the range of the + * captured data. A low playback volume will lead to low sample and fft values, and vice-versa. + */ + public static final int SCALING_MODE_AS_PLAYED = 1; + // to keep in sync with frameworks/base/media/jni/audioeffect/android_media_Visualizer.cpp private static final int NATIVE_EVENT_PCM_CAPTURE = 0; private static final int NATIVE_EVENT_FFT_CAPTURE = 1; @@ -302,6 +316,44 @@ public class Visualizer { } /** + * @hide + * Set the type of scaling applied on the captured visualization data. + * @param mode see {@link #SCALING_MODE_NORMALIZED} + * and {@link #SCALING_MODE_AS_PLAYED} + * @return {@link #SUCCESS} in case of success, + * {@link #ERROR_BAD_VALUE} in case of failure. + * @throws IllegalStateException + */ + public int setScalingMode(int mode) + throws IllegalStateException { + synchronized (mStateLock) { + if (mState == STATE_UNINITIALIZED) { + throw(new IllegalStateException("setScalingMode() called in wrong state: " + + mState)); + } + return native_setScalingMode(mode); + } + } + + /** + * @hide + * Returns the current scaling mode on the captured visualization data. + * @return the scaling mode, see {@link #SCALING_MODE_NORMALIZED} + * and {@link #SCALING_MODE_AS_PLAYED}. + * @throws IllegalStateException + */ + public int getScalingMode() + throws IllegalStateException { + synchronized (mStateLock) { + if (mState == STATE_UNINITIALIZED) { + throw(new IllegalStateException("getScalingMode() called in wrong state: " + + mState)); + } + return native_getScalingMode(); + } + } + + /** * Returns the sampling rate of the captured audio. * @return the sampling rate in milliHertz. */ @@ -588,6 +640,10 @@ public class Visualizer { private native final int native_getCaptureSize(); + private native final int native_setScalingMode(int mode); + + private native final int native_getScalingMode(); + private native final int native_getSamplingRate(); private native final int native_getWaveForm(byte[] waveform); diff --git a/media/jni/audioeffect/android_media_Visualizer.cpp b/media/jni/audioeffect/android_media_Visualizer.cpp index f015afb..c2655c7 100644 --- a/media/jni/audioeffect/android_media_Visualizer.cpp +++ b/media/jni/audioeffect/android_media_Visualizer.cpp @@ -491,6 +491,27 @@ android_media_visualizer_native_getCaptureSize(JNIEnv *env, jobject thiz) } static jint +android_media_visualizer_native_setScalingMode(JNIEnv *env, jobject thiz, jint mode) +{ + Visualizer* lpVisualizer = getVisualizer(env, thiz); + if (lpVisualizer == NULL) { + return VISUALIZER_ERROR_NO_INIT; + } + + return translateError(lpVisualizer->setScalingMode(mode)); +} + +static jint +android_media_visualizer_native_getScalingMode(JNIEnv *env, jobject thiz) +{ + Visualizer* lpVisualizer = getVisualizer(env, thiz); + if (lpVisualizer == NULL) { + return -1; + } + return lpVisualizer->getScalingMode(); +} + +static jint android_media_visualizer_native_getSamplingRate(JNIEnv *env, jobject thiz) { Visualizer* lpVisualizer = getVisualizer(env, thiz); @@ -582,6 +603,8 @@ static JNINativeMethod gMethods[] = { {"getMaxCaptureRate", "()I", (void *)android_media_visualizer_native_getMaxCaptureRate}, {"native_setCaptureSize", "(I)I", (void *)android_media_visualizer_native_setCaptureSize}, {"native_getCaptureSize", "()I", (void *)android_media_visualizer_native_getCaptureSize}, + {"native_setScalingMode", "(I)I", (void *)android_media_visualizer_native_setScalingMode}, + {"native_getScalingMode", "()I", (void *)android_media_visualizer_native_getScalingMode}, {"native_getSamplingRate", "()I", (void *)android_media_visualizer_native_getSamplingRate}, {"native_getWaveForm", "([B)I", (void *)android_media_visualizer_native_getWaveForm}, {"native_getFft", "([B)I", (void *)android_media_visualizer_native_getFft}, diff --git a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/audio/MediaVisualizerTest.java b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/audio/MediaVisualizerTest.java index b0bf654..abf85d7 100644 --- a/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/audio/MediaVisualizerTest.java +++ b/media/tests/MediaFrameworkTest/src/com/android/mediaframeworktest/functional/audio/MediaVisualizerTest.java @@ -188,6 +188,37 @@ public class MediaVisualizerTest extends ActivityInstrumentationTestCase2<MediaF assertTrue(msg, result); } + //Test case 1.2: check scaling mode + @LargeTest + public void test1_2ScalingMode() throws Exception { + boolean result = false; + String msg = "test1_2ScalingMode()"; + getVisualizer(0); + try { + int res = mVisualizer.setScalingMode(Visualizer.SCALING_MODE_AS_PLAYED); + assertEquals(msg + ": setting SCALING_MODE_AS_PLAYED failed", + res, Visualizer.SUCCESS); + int mode = mVisualizer.getScalingMode(); + assertEquals(msg + ": setting SCALING_MODE_AS_PLAYED didn't stick", + mode, Visualizer.SCALING_MODE_AS_PLAYED); + + res = mVisualizer.setScalingMode(Visualizer.SCALING_MODE_NORMALIZED); + assertEquals(msg + ": setting SCALING_MODE_NORMALIZED failed", + res, Visualizer.SUCCESS); + mode = mVisualizer.getScalingMode(); + assertEquals(msg + ": setting SCALING_MODE_NORMALIZED didn't stick", + mode, Visualizer.SCALING_MODE_NORMALIZED); + + result = true; + } catch (IllegalStateException e) { + msg = msg.concat("IllegalStateException"); + loge(msg, "set/get parameter() called in wrong state: " + e); + } finally { + releaseVisualizer(); + } + assertTrue(msg, result); + } + //----------------------------------------------------------------- // 2 - check capture //---------------------------------- @@ -403,6 +434,91 @@ public class MediaVisualizerTest extends ActivityInstrumentationTestCase2<MediaF assertTrue(msg, result); } + //Test case 2.2: test capture in polling mode with volume scaling + @LargeTest + public void test2_2PollingCaptureVolumeScaling() throws Exception { + // test that when playing a sound, the energy measured with Visualizer in + // SCALING_MODE_AS_PLAYED mode decreases when lowering the volume + boolean result = false; + String msg = "test2_2PollingCaptureVolumeScaling()"; + AudioEffect vc = null; + MediaPlayer mp = null; + AudioManager am = (AudioManager) getActivity().getSystemService(Context.AUDIO_SERVICE); + int ringerMode = am.getRingerMode(); + am.setRingerMode(AudioManager.RINGER_MODE_NORMAL); + final int volMax = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + am.setStreamVolume(AudioManager.STREAM_MUSIC, volMax, 0); + + try { + // test setup not related to tested functionality: + // creating a volume controller on output mix ensures that ro.audio.silent mutes + // audio after the effects and not before + vc = new AudioEffect( + AudioEffect.EFFECT_TYPE_NULL, + VOLUME_EFFECT_UUID, + 0, + 0); + vc.setEnabled(true); + + mp = new MediaPlayer(); + mp.setDataSource(MediaNames.SINE_200_1000); + mp.setAudioStreamType(AudioManager.STREAM_MUSIC); + getVisualizer(mp.getAudioSessionId()); + + // verify we successfully set the Visualizer in SCALING_MODE_AS_PLAYED mode + mVisualizer.setScalingMode(Visualizer.SCALING_MODE_AS_PLAYED); + assertTrue(msg + " get volume scaling doesn't return SCALING_MODE_AS_PLAYED", + mVisualizer.getScalingMode() == Visualizer.SCALING_MODE_AS_PLAYED); + mVisualizer.setEnabled(true); + mp.prepare(); + mp.start(); + Thread.sleep(500); + + // check capture on sound with music volume at max + byte[] data = new byte[mVisualizer.getCaptureSize()]; + mVisualizer.getWaveForm(data); + int energyAtVolMax = computeEnergy(data, true); + assertTrue(msg +": getWaveForm reads insufficient level", + energyAtVolMax > 0); + log(msg, " engergy at max volume = "+energyAtVolMax); + + // check capture on sound with music volume lowered from max + am.setStreamVolume(AudioManager.STREAM_MUSIC, (volMax * 2) / 3, 0); + Thread.sleep(500); + mVisualizer.getWaveForm(data); + int energyAtLowerVol = computeEnergy(data, true); + assertTrue(msg +": getWaveForm at lower volume reads insufficient level", + energyAtLowerVol > 0); + log(msg, "energy at lower volume = "+energyAtLowerVol); + assertTrue(msg +": getWaveForm didn't report lower energy when volume decreases", + energyAtVolMax > energyAtLowerVol); + + result = true; + } catch (IllegalArgumentException e) { + msg = msg.concat(": IllegalArgumentException"); + loge(msg, " hit exception " + e); + } catch (UnsupportedOperationException e) { + msg = msg.concat(": UnsupportedOperationException"); + loge(msg, " hit exception " + e); + } catch (IllegalStateException e) { + msg = msg.concat("IllegalStateException"); + loge(msg, " hit exception " + e); + } catch (InterruptedException e) { + loge(msg, " sleep() interrupted"); + } + finally { + releaseVisualizer(); + if (mp != null) { + mp.release(); + } + if (vc != null) { + vc.release(); + } + am.setRingerMode(ringerMode); + } + assertTrue(msg, result); + } + //----------------------------------------------------------------- // private methods //---------------------------------- diff --git a/packages/SystemUI/res/menu/notification_popup_menu.xml b/packages/SystemUI/res/menu/notification_popup_menu.xml new file mode 100644 index 0000000..8923fb6 --- /dev/null +++ b/packages/SystemUI/res/menu/notification_popup_menu.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* apps/common/assets/default/default/skins/StatusBar.xml +** +** Copyright 2012, 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. +*/ +--> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@+id/notification_inspect_item" android:title="@string/status_bar_notification_inspect_item_title" /> +</menu> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 6dbe9d3..236ca6b 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -365,4 +365,8 @@ <!-- Description of the desk dock action that invokes the Android Dreams screen saver feature --> <string name="dreams_dock_launcher">Activate screen saver</string> + + <!-- Title shown in notification popup for inspecting the responsible + application --> + <string name="status_bar_notification_inspect_item_title">App info</string> </resources> diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index 19657a9..414af89 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -23,11 +23,14 @@ import android.animation.Animator.AnimatorListener; import android.animation.ValueAnimator; import android.animation.ValueAnimator.AnimatorUpdateListener; import android.graphics.RectF; +import android.os.Handler; import android.util.Log; +import android.view.accessibility.AccessibilityEvent; import android.view.animation.LinearInterpolator; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; +import android.view.ViewConfiguration; public class SwipeHelper implements Gefingerpoken { static final String TAG = "com.android.systemui.SwipeHelper"; @@ -57,6 +60,7 @@ public class SwipeHelper implements Gefingerpoken { private float mPagingTouchSlop; private Callback mCallback; + private Handler mHandler; private int mSwipeDirection; private VelocityTracker mVelocityTracker; @@ -67,15 +71,24 @@ public class SwipeHelper implements Gefingerpoken { private boolean mCanCurrViewBeDimissed; private float mDensityScale; + private boolean mLongPressSent; + private View.OnLongClickListener mLongPressListener; + private Runnable mWatchLongPress; + public SwipeHelper(int swipeDirection, Callback callback, float densityScale, float pagingTouchSlop) { mCallback = callback; + mHandler = new Handler(); mSwipeDirection = swipeDirection; mVelocityTracker = VelocityTracker.obtain(); mDensityScale = densityScale; mPagingTouchSlop = pagingTouchSlop; } + public void setLongPressListener(View.OnLongClickListener listener) { + mLongPressListener = listener; + } + public void setDensityScale(float densityScale) { mDensityScale = densityScale; } @@ -167,12 +180,19 @@ public class SwipeHelper implements Gefingerpoken { } } + private void removeLongPressCallback() { + if (mWatchLongPress != null) { + mHandler.removeCallbacks(mWatchLongPress); + } + } + public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mDragging = false; + mLongPressSent = false; mCurrView = mCallback.getChildAtPosition(ev); mVelocityTracker.clear(); if (mCurrView != null) { @@ -180,10 +200,28 @@ public class SwipeHelper implements Gefingerpoken { mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); mVelocityTracker.addMovement(ev); mInitialTouchPos = getPos(ev); + + if (mLongPressListener != null) { + if (mWatchLongPress == null) { + mWatchLongPress = new Runnable() { + @Override + public void run() { + if (mCurrView != null && !mLongPressSent) { + mLongPressSent = true; + mCurrView.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); + mLongPressListener.onLongClick(mCurrView); + } + } + }; + } + mHandler.postDelayed(mWatchLongPress, ViewConfiguration.getLongPressTimeout()); + } + } break; + case MotionEvent.ACTION_MOVE: - if (mCurrView != null) { + if (mCurrView != null && !mLongPressSent) { mVelocityTracker.addMovement(ev); float pos = getPos(ev); float delta = pos - mInitialTouchPos; @@ -191,14 +229,19 @@ public class SwipeHelper implements Gefingerpoken { mCallback.onBeginDrag(mCurrView); mDragging = true; mInitialTouchPos = getPos(ev) - getTranslation(mCurrAnimView); + + removeLongPressCallback(); } } + break; + case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mDragging = false; mCurrView = null; mCurrAnimView = null; + mLongPressSent = false; break; } return mDragging; @@ -269,6 +312,10 @@ public class SwipeHelper implements Gefingerpoken { } public boolean onTouchEvent(MotionEvent ev) { + if (mLongPressSent) { + return true; + } + if (!mDragging) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index ede8e7a..3803092 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -26,17 +26,20 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Rect; +import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; +import android.provider.Settings; import android.util.Log; import android.util.Slog; import android.view.Display; import android.view.IWindowManager; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -45,6 +48,7 @@ import android.view.WindowManager; import android.view.WindowManagerImpl; import android.widget.LinearLayout; import android.widget.RemoteViews; +import android.widget.PopupMenu; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.StatusBarIcon; @@ -214,6 +218,39 @@ public abstract class BaseStatusBar extends SystemUI implements } } + private void startApplicationDetailsActivity(String packageName) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", packageName, null)); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(intent); + } + + protected View.OnLongClickListener getNotificationLongClicker() { + return new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + final String packageNameF = (String) v.getTag(); + if (packageNameF == null) return false; + PopupMenu popup = new PopupMenu(mContext, v); + popup.getMenuInflater().inflate(R.menu.notification_popup_menu, popup.getMenu()); + popup.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { + public boolean onMenuItemClick(MenuItem item) { + if (item.getItemId() == R.id.notification_inspect_item) { + startApplicationDetailsActivity(packageNameF); + animateCollapse(); + } else { + return false; + } + return true; + } + }); + popup.show(); + + return true; + } + }; + } + public void dismissIntruder() { // pass } @@ -354,9 +391,13 @@ public abstract class BaseStatusBar extends SystemUI implements LayoutInflater inflater = (LayoutInflater)mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); View row = inflater.inflate(R.layout.status_bar_notification_row, parent, false); + + // for blaming (see SwipeHelper.setLongPressListener) + row.setTag(sbn.pkg); + // XXX: temporary: while testing big notifications, auto-expand all of them ViewGroup.LayoutParams lp = row.getLayoutParams(); - if (sbn.notification.bigContentView != null) { + if (large != null) { lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; } else { lp.height = minHeight; @@ -372,9 +413,16 @@ public abstract class BaseStatusBar extends SystemUI implements // bind the click event to the content area ViewGroup content = (ViewGroup)row.findViewById(R.id.content); ViewGroup adaptive = (ViewGroup)row.findViewById(R.id.adaptive); - // XXX: update to allow controls within notification views + + // Ensure that R.id.content is properly set to 64dp high if 1U + lp = content.getLayoutParams(); + if (large == null) { + lp.height = minHeight; + } + content.setLayoutParams(lp); + content.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS); -// content.setOnFocusChangeListener(mFocusChangeListener); + PendingIntent contentIntent = sbn.notification.contentIntent; if (contentIntent != null) { final View.OnClickListener listener = new NotificationClicker(contentIntent, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index f45b3ad..da98c80 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -94,7 +94,7 @@ public class PhoneStatusBar extends BaseStatusBar { public static final String ACTION_STATUSBAR_START = "com.android.internal.policy.statusbar.START"; - private static final boolean ENABLE_INTRUDERS = true; + private static final boolean ENABLE_INTRUDERS = false; static final int EXPANDED_LEAVE_ALONE = -10000; static final int EXPANDED_FULL_OPEN = -10001; @@ -218,11 +218,6 @@ public class PhoneStatusBar extends BaseStatusBar { private int mNavigationIconHints = 0; - // TODO(dsandler): codify this stuff in NotificationManager's header somewhere - private int mDisplayMinScore = Notification.PRIORITY_LOW * 10; - private int mIntruderMinScore = Notification.PRIORITY_HIGH * 10; - private int mIntruderInImmersiveMinScore = Notification.PRIORITY_HIGH * 10 + 5; - private class ExpandedDialog extends Dialog { ExpandedDialog(Context context) { super(context, com.android.internal.R.style.Theme_Translucent_NoTitleBar); @@ -280,9 +275,11 @@ public class PhoneStatusBar extends BaseStatusBar { } mNotificationPanel = expanded.findViewById(R.id.notification_panel); - mIntruderAlertView = (IntruderAlertView) View.inflate(context, R.layout.intruder_alert, null); - mIntruderAlertView.setVisibility(View.GONE); - mIntruderAlertView.setBar(this); + if (ENABLE_INTRUDERS) { + mIntruderAlertView = (IntruderAlertView) View.inflate(context, R.layout.intruder_alert, null); + mIntruderAlertView.setVisibility(View.GONE); + mIntruderAlertView.setBar(this); + } PhoneStatusBarView sb = (PhoneStatusBarView)View.inflate(context, R.layout.status_bar, null); @@ -312,6 +309,7 @@ public class PhoneStatusBar extends BaseStatusBar { mExpandedDialog = new ExpandedDialog(context); mPile = (NotificationRowLayout)expanded.findViewById(R.id.latestItems); + mPile.setLongPressListener(getNotificationLongClicker()); mExpandedContents = mPile; // was: expanded.findViewById(R.id.notificationLinearLayout); mClearButton = expanded.findViewById(R.id.clear_all_button); @@ -520,12 +518,12 @@ public class PhoneStatusBar extends BaseStatusBar { } } catch (RemoteException ex) { } + + /* + * DISABLED due to missing API if (ENABLE_INTRUDERS && ( // TODO(dsandler): Only if the screen is on notification.notification.intruderView != null)) { -// notification.notification.fullScreenIntent != null -// || (notification.score >= mIntruderInImmersiveMinScore) -// || (!immersive && (notification.score > mIntruderMinScore)))) { Slog.d(TAG, "Presenting high-priority notification"); // special new transient ticker mode // 1. Populate mIntruderAlertView @@ -554,7 +552,10 @@ public class PhoneStatusBar extends BaseStatusBar { if (INTRUDER_ALERT_DECAY_MS > 0) { mHandler.sendEmptyMessageDelayed(MSG_HIDE_INTRUDER, INTRUDER_ALERT_DECAY_MS); } - } else if (notification.notification.fullScreenIntent != null) { + } else + */ + + if (notification.notification.fullScreenIntent != null) { // not immersive & a full-screen alert should be shown Slog.d(TAG, "Notification has fullScreenIntent; sending fullScreenIntent"); try { @@ -675,7 +676,7 @@ public class PhoneStatusBar extends BaseStatusBar { updateExpandedViewPos(EXPANDED_LEAVE_ALONE); // See if we need to update the intruder. - if (oldNotification == mCurrentlyIntrudingNotification) { + if (ENABLE_INTRUDERS && oldNotification == mCurrentlyIntrudingNotification) { if (DEBUG) Slog.d(TAG, "updating the current intruder:" + notification); // XXX: this is a hack for Alarms. The real implementation will need to *update* // the intruder. @@ -697,7 +698,7 @@ public class PhoneStatusBar extends BaseStatusBar { // Recalculate the position of the sliding windows and the titles. updateExpandedViewPos(EXPANDED_LEAVE_ALONE); - if (old == mCurrentlyIntrudingNotification) { + if (ENABLE_INTRUDERS && old == mCurrentlyIntrudingNotification) { mHandler.sendEmptyMessage(MSG_HIDE_INTRUDER); } @@ -2039,6 +2040,7 @@ public class PhoneStatusBar extends BaseStatusBar { }; private void setIntruderAlertVisibility(boolean vis) { + if (!ENABLE_INTRUDERS) return; if (DEBUG) { Slog.v(TAG, (vis ? "showing" : "hiding") + " intruder alert window"); } @@ -2125,30 +2127,6 @@ public class PhoneStatusBar extends BaseStatusBar { vib.vibrate(250); } - public int getScoreThreshold() { - return mDisplayMinScore; - } - - public void setScoreThreshold(int score) { - // XXX HAX - if (mDisplayMinScore != score) { - this.mDisplayMinScore = score; - applyScoreThreshold(); - } - } - - private void applyScoreThreshold() { - int N = mNotificationData.size(); - for (int i=0; i<N; i++) { - NotificationData.Entry entry = mNotificationData.get(i); - int vis = (entry.notification.score < mDisplayMinScore) - ? View.GONE - : View.VISIBLE; - entry.row.setVisibility(vis); - entry.icon.setVisibility(vis); - } - } - Runnable mStartTracing = new Runnable() { public void run() { vibrate(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java index 5369317..5c38db5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NotificationRowLayout.java @@ -104,6 +104,10 @@ public class NotificationRowLayout mExpandHelper = new ExpandHelper(mContext, this, minHeight, maxHeight); } + public void setLongPressListener(View.OnLongClickListener listener) { + mSwipeHelper.setLongPressListener(listener); + } + public void setAnimateBounds(boolean anim) { mAnimateBounds = anim; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java index 8c1509b..c868f78 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/tablet/TabletStatusBar.java @@ -74,6 +74,7 @@ import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.CompatModeButton; import com.android.systemui.statusbar.policy.LocationController; import com.android.systemui.statusbar.policy.NetworkController; +import com.android.systemui.statusbar.policy.NotificationRowLayout; import com.android.systemui.statusbar.policy.Prefs; import java.io.FileDescriptor; @@ -153,7 +154,7 @@ public class TabletStatusBar extends BaseStatusBar implements int mNotificationPeekTapDuration; int mNotificationFlingVelocity; - ViewGroup mPile; + NotificationRowLayout mPile; BatteryController mBatteryController; BluetoothController mBluetoothController; @@ -375,8 +376,9 @@ public class TabletStatusBar extends BaseStatusBar implements mRecentButton.setOnTouchListener(mRecentsPanel); - mPile = (ViewGroup)mNotificationPanel.findViewById(R.id.content); + mPile = (NotificationRowLayout)mNotificationPanel.findViewById(R.id.content); mPile.removeAllViews(); + mPile.setLongPressListener(getNotificationLongClicker()); ScrollView scroller = (ScrollView)mPile.getParent(); scroller.setFillViewport(true); diff --git a/services/java/com/android/server/NotificationManagerService.java b/services/java/com/android/server/NotificationManagerService.java index b22be76..1ba7e79 100755 --- a/services/java/com/android/server/NotificationManagerService.java +++ b/services/java/com/android/server/NotificationManagerService.java @@ -16,7 +16,9 @@ package com.android.server; +import com.android.internal.os.AtomicFile; import com.android.internal.statusbar.StatusBarNotification; +import com.android.internal.util.FastXmlSerializer; import android.app.ActivityManagerNative; import android.app.IActivityManager; @@ -37,9 +39,10 @@ import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.ContentObserver; import android.media.AudioManager; +import android.net.NetworkPolicy; +import android.net.NetworkTemplate; import android.net.Uri; import android.os.Binder; -import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; @@ -53,14 +56,36 @@ import android.text.TextUtils; import android.util.EventLog; import android.util.Log; import android.util.Slog; +import android.util.Xml; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.widget.Toast; +import java.io.File; import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; + +import libcore.io.IoUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import static android.net.NetworkPolicyManager.POLICY_NONE; +import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeBooleanAttribute; +import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeIntAttribute; +import static com.android.server.net.NetworkPolicyManagerService.XmlUtils.writeLongAttribute; +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + /** {@hide} */ public class NotificationManagerService extends INotificationManager.Stub @@ -81,6 +106,13 @@ public class NotificationManagerService extends INotificationManager.Stub private static final int DEFAULT_STREAM_TYPE = AudioManager.STREAM_NOTIFICATION; private static final boolean SCORE_ONGOING_HIGHER = false; + private static final int JUNK_SCORE = -1000; + private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; + private static final int SCORE_DISPLAY_THRESHOLD = Notification.PRIORITY_MIN * NOTIFICATION_PRIORITY_MULTIPLIER; + + private static final boolean ENABLE_BLOCKED_NOTIFICATIONS = true; + private static final boolean ENABLE_BLOCKED_TOASTS = true; + final Context mContext; final IActivityManager mAm; final IBinder mForegroundToken = new Binder(); @@ -115,6 +147,144 @@ public class NotificationManagerService extends INotificationManager.Stub private ArrayList<NotificationRecord> mLights = new ArrayList<NotificationRecord>(); private NotificationRecord mLedNotification; + // Notification control database. For now just contains disabled packages. + private AtomicFile mPolicyFile; + private HashSet<String> mBlockedPackages = new HashSet<String>(); + + private static final int DB_VERSION = 1; + + private static final String TAG_BODY = "notification-policy"; + private static final String ATTR_VERSION = "version"; + + private static final String TAG_BLOCKED_PKGS = "blocked-packages"; + private static final String TAG_PACKAGE = "package"; + private static final String ATTR_NAME = "name"; + + private void loadBlockDb() { + synchronized(mBlockedPackages) { + if (mPolicyFile == null) { + File dir = new File("/data/system"); + mPolicyFile = new AtomicFile(new File(dir, "notification_policy.xml")); + + mBlockedPackages.clear(); + + FileInputStream infile = null; + try { + infile = mPolicyFile.openRead(); + final XmlPullParser parser = Xml.newPullParser(); + parser.setInput(infile, null); + + int type; + String tag; + int version = DB_VERSION; + while ((type = parser.next()) != END_DOCUMENT) { + tag = parser.getName(); + if (type == START_TAG) { + if (TAG_BODY.equals(tag)) { + version = Integer.parseInt(parser.getAttributeValue(null, ATTR_VERSION)); + } else if (TAG_BLOCKED_PKGS.equals(tag)) { + while ((type = parser.next()) != END_DOCUMENT) { + tag = parser.getName(); + if (TAG_PACKAGE.equals(tag)) { + mBlockedPackages.add(parser.getAttributeValue(null, ATTR_NAME)); + } else if (TAG_BLOCKED_PKGS.equals(tag) && type == END_TAG) { + break; + } + } + } + } + } + } catch (FileNotFoundException e) { + // No data yet + } catch (IOException e) { + Log.wtf(TAG, "Unable to read blocked notifications database", e); + } catch (NumberFormatException e) { + Log.wtf(TAG, "Unable to parse blocked notifications database", e); + } catch (XmlPullParserException e) { + Log.wtf(TAG, "Unable to parse blocked notifications database", e); + } finally { + IoUtils.closeQuietly(infile); + } + } + } + } + + private void writeBlockDb() { + synchronized(mBlockedPackages) { + FileOutputStream outfile = null; + try { + outfile = mPolicyFile.startWrite(); + + XmlSerializer out = new FastXmlSerializer(); + out.setOutput(outfile, "utf-8"); + + out.startDocument(null, true); + + out.startTag(null, TAG_BODY); { + out.attribute(null, ATTR_VERSION, String.valueOf(DB_VERSION)); + out.startTag(null, TAG_BLOCKED_PKGS); { + // write all known network policies + for (String pkg : mBlockedPackages) { + out.startTag(null, TAG_PACKAGE); { + out.attribute(null, ATTR_NAME, pkg); + } out.endTag(null, TAG_PACKAGE); + } + } out.endTag(null, TAG_BLOCKED_PKGS); + } out.endTag(null, TAG_BODY); + + out.endDocument(); + + mPolicyFile.finishWrite(outfile); + } catch (IOException e) { + if (outfile != null) { + mPolicyFile.failWrite(outfile); + } + } + } + } + + public boolean areNotificationsEnabledForPackage(String pkg) { + checkCallerIsSystem(); + return areNotificationsEnabledForPackageInt(pkg); + } + + // Unchecked. Not exposed via Binder, but can be called in the course of enqueue*(). + private boolean areNotificationsEnabledForPackageInt(String pkg) { + final boolean enabled = !mBlockedPackages.contains(pkg); + if (DBG) { + Slog.v(TAG, "notifications are " + (enabled?"en":"dis") + "abled for " + pkg); + } + return enabled; + } + + public void setNotificationsEnabledForPackage(String pkg, boolean enabled) { + checkCallerIsSystem(); + if (DBG) { + Slog.v(TAG, (enabled?"en":"dis") + "abling notifications for " + pkg); + } + if (enabled) { + mBlockedPackages.remove(pkg); + } else { + mBlockedPackages.add(pkg); + + // Now, cancel any outstanding notifications that are part of a just-disabled app + if (ENABLE_BLOCKED_NOTIFICATIONS) { + synchronized (mNotificationList) { + final int N = mNotificationList.size(); + for (int i=0; i<N; i++) { + final NotificationRecord r = mNotificationList.get(i); + if (r.pkg.equals(pkg)) { + cancelNotificationLocked(r, false); + } + } + } + } + // Don't bother canceling toasts, they'll go away soon enough. + } + writeBlockDb(); + } + + private static String idDebugString(Context baseContext, String packageName, int id) { Context c = null; @@ -405,6 +575,8 @@ public class NotificationManagerService extends INotificationManager.Stub mToastQueue = new ArrayList<ToastRecord>(); mHandler = new WorkerHandler(); + loadBlockDb(); + mStatusBar = statusBar; statusBar.setNotificationCallbacks(mNotificationCallbacks); @@ -465,6 +637,13 @@ public class NotificationManagerService extends INotificationManager.Stub return ; } + final boolean isSystemToast = ("android".equals(pkg)); + + if (ENABLE_BLOCKED_TOASTS && !isSystemToast && !areNotificationsEnabledForPackageInt(pkg)) { + Slog.e(TAG, "Suppressing toast from package " + pkg + " by user request."); + return; + } + synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); @@ -479,7 +658,7 @@ public class NotificationManagerService extends INotificationManager.Stub } else { // Limit the number of toasts that any given package except the android // package can enqueue. Prevents DOS attacks and deals with leaks. - if (!"android".equals(pkg)) { + if (!isSystemToast) { int count = 0; final int N = mToastQueue.size(); for (int i=0; i<N; i++) { @@ -675,11 +854,15 @@ public class NotificationManagerService extends INotificationManager.Stub public void enqueueNotificationInternal(String pkg, int callingUid, int callingPid, String tag, int id, Notification notification, int[] idOut) { - checkIncomingCall(pkg); + if (DBG) { + Slog.v(TAG, "enqueueNotificationInternal: pkg=" + pkg + " id=" + id + " notification=" + notification); + } + checkCallerIsSystemOrSameApp(pkg); + final boolean isSystemNotification = ("android".equals(pkg)); // Limit the number of notifications that any given package except the android // package can enqueue. Prevents DOS attacks and deals with leaks. - if (!"android".equals(pkg)) { + if (!isSystemNotification) { synchronized (mNotificationList) { int count = 0; final int N = mNotificationList.size(); @@ -717,7 +900,7 @@ public class NotificationManagerService extends INotificationManager.Stub } // === Scoring === - + // 0. Sanitize inputs notification.priority = clamp(notification.priority, Notification.PRIORITY_MIN, Notification.PRIORITY_MAX); // Migrate notification flags to scores @@ -726,19 +909,27 @@ public class NotificationManagerService extends INotificationManager.Stub } else if (SCORE_ONGOING_HIGHER && 0 != (notification.flags & Notification.FLAG_ONGOING_EVENT)) { if (notification.priority < Notification.PRIORITY_HIGH) notification.priority = Notification.PRIORITY_HIGH; } - + // 1. initial score: buckets of 10, around the app - int score = notification.priority * 10; //[-20..20] + int score = notification.priority * NOTIFICATION_PRIORITY_MULTIPLIER; //[-20..20] - // 2. Consult oracles (external heuristics) - // TODO(dsandler): oracles + // 2. Consult external heuristics (TBD) - // 3. Apply local heuristics & overrides + // 3. Apply local rules // blocked apps - // TODO(dsandler): add block db - if (pkg.startsWith("com.test.spammer.")) { - score = -1000; + if (ENABLE_BLOCKED_NOTIFICATIONS && !isSystemNotification && !areNotificationsEnabledForPackageInt(pkg)) { + score = JUNK_SCORE; + Slog.e(TAG, "Suppressing notification from package " + pkg + " by user request."); + } + + if (DBG) { + Slog.v(TAG, "Assigned score=" + score + " to " + notification); + } + + if (score < SCORE_DISPLAY_THRESHOLD) { + // Notification will be blocked because the score is too low. + return; } synchronized (mNotificationList) { @@ -1030,7 +1221,7 @@ public class NotificationManagerService extends INotificationManager.Stub } public void cancelNotificationWithTag(String pkg, String tag, int id) { - checkIncomingCall(pkg); + checkCallerIsSystemOrSameApp(pkg); // Don't allow client applications to cancel foreground service notis. cancelNotification(pkg, tag, id, 0, Binder.getCallingUid() == Process.SYSTEM_UID @@ -1038,14 +1229,22 @@ public class NotificationManagerService extends INotificationManager.Stub } public void cancelAllNotifications(String pkg) { - checkIncomingCall(pkg); + checkCallerIsSystemOrSameApp(pkg); // Calling from user space, don't allow the canceling of actively // running foreground services. cancelAllNotificationsInt(pkg, 0, Notification.FLAG_FOREGROUND_SERVICE, true); } - void checkIncomingCall(String pkg) { + void checkCallerIsSystem() { + int uid = Binder.getCallingUid(); + if (uid == Process.SYSTEM_UID || uid == 0) { + return; + } + throw new SecurityException("Disallowed call for uid " + uid); + } + + void checkCallerIsSystemOrSameApp(String pkg) { int uid = Binder.getCallingUid(); if (uid == Process.SYSTEM_UID || uid == 0) { return; diff --git a/services/java/com/android/server/NsdService.java b/services/java/com/android/server/NsdService.java index 8014e27..839fbe2 100644 --- a/services/java/com/android/server/NsdService.java +++ b/services/java/com/android/server/NsdService.java @@ -17,6 +17,8 @@ package com.android.server; import android.content.Context; +import android.content.ContentResolver; +import android.content.Intent; import android.content.pm.PackageManager; import android.net.nsd.DnsSdServiceInfo; import android.net.nsd.DnsSdTxtRecord; @@ -28,6 +30,7 @@ import android.os.HandlerThread; import android.os.Message; import android.os.Messenger; import android.os.IBinder; +import android.provider.Settings; import android.util.Slog; import java.io.FileDescriptor; @@ -41,6 +44,9 @@ import java.util.concurrent.CountDownLatch; import com.android.internal.app.IBatteryStats; import com.android.internal.telephony.TelephonyIntents; import com.android.internal.util.AsyncChannel; +import com.android.internal.util.Protocol; +import com.android.internal.util.State; +import com.android.internal.util.StateMachine; import com.android.server.am.BatteryStatsService; import com.android.server.NativeDaemonConnector.Command; import com.android.internal.R; @@ -58,6 +64,8 @@ public class NsdService extends INsdManager.Stub { private static final boolean DBG = true; private Context mContext; + private ContentResolver mContentResolver; + private NsdStateMachine mNsdStateMachine; /** * Clients receiving asynchronous messages @@ -69,189 +77,342 @@ public class NsdService extends INsdManager.Stub { private int INVALID_ID = 0; private int mUniqueId = 1; - /** - * Handles client(app) connections - */ - private class AsyncServiceHandler extends Handler { + private static final int BASE = Protocol.BASE_NSD_MANAGER; + private static final int CMD_TO_STRING_COUNT = NsdManager.STOP_RESOLVE - BASE + 1; + private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT]; + + static { + sCmdToString[NsdManager.DISCOVER_SERVICES - BASE] = "DISCOVER"; + sCmdToString[NsdManager.STOP_DISCOVERY - BASE] = "STOP-DISCOVER"; + sCmdToString[NsdManager.REGISTER_SERVICE - BASE] = "REGISTER"; + sCmdToString[NsdManager.UNREGISTER_SERVICE - BASE] = "UNREGISTER"; + sCmdToString[NsdManager.RESOLVE_SERVICE - BASE] = "RESOLVE"; + sCmdToString[NsdManager.STOP_RESOLVE - BASE] = "STOP-RESOLVE"; + } - AsyncServiceHandler(android.os.Looper looper) { - super(looper); + private static String cmdToString(int cmd) { + cmd -= BASE; + if ((cmd >= 0) && (cmd < sCmdToString.length)) { + return sCmdToString[cmd]; + } else { + return null; } + } + + private class NsdStateMachine extends StateMachine { + + private DefaultState mDefaultState = new DefaultState(); + private DisabledState mDisabledState = new DisabledState(); + private EnabledState mEnabledState = new EnabledState(); @Override - public void handleMessage(Message msg) { - ClientInfo clientInfo; - DnsSdServiceInfo servInfo; - switch (msg.what) { - case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: - if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) { - AsyncChannel c = (AsyncChannel) msg.obj; - if (DBG) Slog.d(TAG, "New client listening to asynchronous messages"); - c.sendMessage(AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED); - ClientInfo cInfo = new ClientInfo(c, msg.replyTo); - if (mClients.size() == 0) { - startMDnsDaemon(); + protected String getMessageInfo(Message msg) { + return cmdToString(msg.what); + } + + NsdStateMachine(String name) { + super(name); + addState(mDefaultState); + addState(mDisabledState, mDefaultState); + addState(mEnabledState, mDefaultState); + if (isNsdEnabled()) { + setInitialState(mEnabledState); + } else { + setInitialState(mDisabledState); + } + setProcessedMessagesSize(25); + } + + class DefaultState extends State { + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: + if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) { + AsyncChannel c = (AsyncChannel) msg.obj; + if (DBG) Slog.d(TAG, "New client listening to asynchronous messages"); + c.sendMessage(AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED); + ClientInfo cInfo = new ClientInfo(c, msg.replyTo); + mClients.put(msg.replyTo, cInfo); + } else { + Slog.e(TAG, "Client connection failure, error=" + msg.arg1); } - mClients.put(msg.replyTo, cInfo); - } else { - Slog.e(TAG, "Client connection failure, error=" + msg.arg1); - } - break; - case AsyncChannel.CMD_CHANNEL_DISCONNECTED: - if (msg.arg1 == AsyncChannel.STATUS_SEND_UNSUCCESSFUL) { - Slog.e(TAG, "Send failed, client connection lost"); - } else { - if (DBG) Slog.d(TAG, "Client connection lost with reason: " + msg.arg1); - } - mClients.remove(msg.replyTo); - if (mClients.size() == 0) { - stopMDnsDaemon(); - } - break; - case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: - AsyncChannel ac = new AsyncChannel(); - ac.connect(mContext, this, msg.replyTo); - break; - case NsdManager.DISCOVER_SERVICES: - if (DBG) Slog.d(TAG, "Discover services"); - servInfo = (DnsSdServiceInfo) msg.obj; - clientInfo = mClients.get(msg.replyTo); - if (clientInfo.mDiscoveryId != INVALID_ID) { - //discovery already in progress - if (DBG) Slog.d(TAG, "discovery in progress"); - mReplyChannel.replyToMessage(msg, NsdManager.DISCOVER_SERVICES_FAILED, - NsdManager.ALREADY_ACTIVE); break; - } - clientInfo.mDiscoveryId = getUniqueId(); - if (discoverServices(clientInfo.mDiscoveryId, servInfo.getServiceType())) { - mReplyChannel.replyToMessage(msg, NsdManager.DISCOVER_SERVICES_STARTED); - } else { + case AsyncChannel.CMD_CHANNEL_DISCONNECTED: + if (msg.arg1 == AsyncChannel.STATUS_SEND_UNSUCCESSFUL) { + Slog.e(TAG, "Send failed, client connection lost"); + } else { + if (DBG) Slog.d(TAG, "Client connection lost with reason: " + msg.arg1); + } + mClients.remove(msg.replyTo); + break; + case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: + AsyncChannel ac = new AsyncChannel(); + ac.connect(mContext, getHandler(), msg.replyTo); + break; + case NsdManager.DISCOVER_SERVICES: mReplyChannel.replyToMessage(msg, NsdManager.DISCOVER_SERVICES_FAILED, - NsdManager.ERROR); - clientInfo.mDiscoveryId = INVALID_ID; - } - break; - case NsdManager.STOP_DISCOVERY: - if (DBG) Slog.d(TAG, "Stop service discovery"); - clientInfo = mClients.get(msg.replyTo); - if (clientInfo.mDiscoveryId == INVALID_ID) { - //already stopped - if (DBG) Slog.d(TAG, "discovery already stopped"); - mReplyChannel.replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED, - NsdManager.ALREADY_ACTIVE); + NsdManager.BUSY); + break; + case NsdManager.STOP_DISCOVERY: + mReplyChannel.replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED, + NsdManager.ERROR); break; - } - if (stopServiceDiscovery(clientInfo.mDiscoveryId)) { - clientInfo.mDiscoveryId = INVALID_ID; - mReplyChannel.replyToMessage(msg, NsdManager.STOP_DISCOVERY_SUCCEEDED); - } else { - mReplyChannel.replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED, - NsdManager.ERROR); - } - break; - case NsdManager.REGISTER_SERVICE: - if (DBG) Slog.d(TAG, "Register service"); - clientInfo = mClients.get(msg.replyTo); - if (clientInfo.mRegisteredIds.size() >= ClientInfo.MAX_REG) { - if (DBG) Slog.d(TAG, "register service exceeds limit"); - mReplyChannel.replyToMessage(msg, NsdManager.REGISTER_SERVICE_FAILED, - NsdManager.MAX_REGS_REACHED); - } - - int id = getUniqueId(); - if (registerService(id, (DnsSdServiceInfo) msg.obj)) { - clientInfo.mRegisteredIds.add(id); - } else { + case NsdManager.REGISTER_SERVICE: mReplyChannel.replyToMessage(msg, NsdManager.REGISTER_SERVICE_FAILED, NsdManager.ERROR); - } - break; - case NsdManager.UNREGISTER_SERVICE: - if (DBG) Slog.d(TAG, "unregister service"); - clientInfo = mClients.get(msg.replyTo); - int regId = msg.arg1; - if (clientInfo.mRegisteredIds.remove(new Integer(regId)) && - unregisterService(regId)) { - mReplyChannel.replyToMessage(msg, NsdManager.UNREGISTER_SERVICE_SUCCEEDED); - } else { + break; + case NsdManager.UNREGISTER_SERVICE: mReplyChannel.replyToMessage(msg, NsdManager.UNREGISTER_SERVICE_FAILED, NsdManager.ERROR); - } - break; - case NsdManager.UPDATE_SERVICE: - if (DBG) Slog.d(TAG, "Update service"); - //TODO: implement - mReplyChannel.replyToMessage(msg, NsdManager.UPDATE_SERVICE_FAILED); - break; - case NsdManager.RESOLVE_SERVICE: - if (DBG) Slog.d(TAG, "Resolve service"); - servInfo = (DnsSdServiceInfo) msg.obj; - clientInfo = mClients.get(msg.replyTo); - if (clientInfo.mResolveId != INVALID_ID) { - //first cancel existing resolve - stopResolveService(clientInfo.mResolveId); - } - - clientInfo.mResolveId = getUniqueId(); - if (!resolveService(clientInfo.mResolveId, servInfo)) { + break; + case NsdManager.RESOLVE_SERVICE: mReplyChannel.replyToMessage(msg, NsdManager.RESOLVE_SERVICE_FAILED, NsdManager.ERROR); - clientInfo.mResolveId = INVALID_ID; - } - break; - case NsdManager.STOP_RESOLVE: - if (DBG) Slog.d(TAG, "Stop resolve"); - clientInfo = mClients.get(msg.replyTo); - if (clientInfo.mResolveId == INVALID_ID) { - //already stopped - if (DBG) Slog.d(TAG, "resolve already stopped"); - mReplyChannel.replyToMessage(msg, NsdManager.STOP_RESOLVE_FAILED, - NsdManager.ALREADY_ACTIVE); break; - } - if (stopResolveService(clientInfo.mResolveId)) { - clientInfo.mResolveId = INVALID_ID; - mReplyChannel.replyToMessage(msg, NsdManager.STOP_RESOLVE_SUCCEEDED); - } else { + case NsdManager.STOP_RESOLVE: mReplyChannel.replyToMessage(msg, NsdManager.STOP_RESOLVE_FAILED, NsdManager.ERROR); - } - break; - default: - Slog.d(TAG, "NsdServicehandler.handleMessage ignoring msg=" + msg); - break; + break; + default: + Slog.e(TAG, "Unhandled " + msg); + return NOT_HANDLED; + } + return HANDLED; } } + + class DisabledState extends State { + @Override + public void enter() { + sendNsdStateChangeBroadcast(false); + } + + @Override + public boolean processMessage(Message msg) { + switch (msg.what) { + case NsdManager.ENABLE: + transitionTo(mEnabledState); + break; + default: + return NOT_HANDLED; + } + return HANDLED; + } + } + + class EnabledState extends State { + @Override + public void enter() { + sendNsdStateChangeBroadcast(true); + if (mClients.size() > 0) { + startMDnsDaemon(); + } + } + + @Override + public void exit() { + if (mClients.size() > 0) { + stopMDnsDaemon(); + } + } + + @Override + public boolean processMessage(Message msg) { + ClientInfo clientInfo; + DnsSdServiceInfo servInfo; + boolean result = HANDLED; + switch (msg.what) { + case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: + //First client + if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL && + mClients.size() == 0) { + startMDnsDaemon(); + } + result = NOT_HANDLED; + break; + case AsyncChannel.CMD_CHANNEL_DISCONNECTED: + //Last client + if (mClients.size() == 1) { + stopMDnsDaemon(); + } + result = NOT_HANDLED; + break; + case NsdManager.DISABLE: + //TODO: cleanup clients + transitionTo(mDisabledState); + break; + case NsdManager.DISCOVER_SERVICES: + if (DBG) Slog.d(TAG, "Discover services"); + servInfo = (DnsSdServiceInfo) msg.obj; + clientInfo = mClients.get(msg.replyTo); + if (clientInfo.mDiscoveryId != INVALID_ID) { + //discovery already in progress + if (DBG) Slog.d(TAG, "discovery in progress"); + mReplyChannel.replyToMessage(msg, NsdManager.DISCOVER_SERVICES_FAILED, + NsdManager.ALREADY_ACTIVE); + break; + } + clientInfo.mDiscoveryId = getUniqueId(); + if (discoverServices(clientInfo.mDiscoveryId, servInfo.getServiceType())) { + mReplyChannel.replyToMessage(msg, NsdManager.DISCOVER_SERVICES_STARTED); + } else { + mReplyChannel.replyToMessage(msg, NsdManager.DISCOVER_SERVICES_FAILED, + NsdManager.ERROR); + clientInfo.mDiscoveryId = INVALID_ID; + } + break; + case NsdManager.STOP_DISCOVERY: + if (DBG) Slog.d(TAG, "Stop service discovery"); + clientInfo = mClients.get(msg.replyTo); + if (clientInfo.mDiscoveryId == INVALID_ID) { + //already stopped + if (DBG) Slog.d(TAG, "discovery already stopped"); + mReplyChannel.replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED, + NsdManager.ALREADY_ACTIVE); + break; + } + if (stopServiceDiscovery(clientInfo.mDiscoveryId)) { + clientInfo.mDiscoveryId = INVALID_ID; + mReplyChannel.replyToMessage(msg, NsdManager.STOP_DISCOVERY_SUCCEEDED); + } else { + mReplyChannel.replyToMessage(msg, NsdManager.STOP_DISCOVERY_FAILED, + NsdManager.ERROR); + } + break; + case NsdManager.REGISTER_SERVICE: + if (DBG) Slog.d(TAG, "Register service"); + clientInfo = mClients.get(msg.replyTo); + if (clientInfo.mRegisteredIds.size() >= ClientInfo.MAX_REG) { + if (DBG) Slog.d(TAG, "register service exceeds limit"); + mReplyChannel.replyToMessage(msg, NsdManager.REGISTER_SERVICE_FAILED, + NsdManager.MAX_REGS_REACHED); + } + + int id = getUniqueId(); + if (registerService(id, (DnsSdServiceInfo) msg.obj)) { + clientInfo.mRegisteredIds.add(id); + } else { + mReplyChannel.replyToMessage(msg, NsdManager.REGISTER_SERVICE_FAILED, + NsdManager.ERROR); + } + break; + case NsdManager.UNREGISTER_SERVICE: + if (DBG) Slog.d(TAG, "unregister service"); + clientInfo = mClients.get(msg.replyTo); + int regId = msg.arg1; + if (clientInfo.mRegisteredIds.remove(new Integer(regId)) && + unregisterService(regId)) { + mReplyChannel.replyToMessage(msg, + NsdManager.UNREGISTER_SERVICE_SUCCEEDED); + } else { + mReplyChannel.replyToMessage(msg, NsdManager.UNREGISTER_SERVICE_FAILED, + NsdManager.ERROR); + } + break; + case NsdManager.UPDATE_SERVICE: + if (DBG) Slog.d(TAG, "Update service"); + //TODO: implement + mReplyChannel.replyToMessage(msg, NsdManager.UPDATE_SERVICE_FAILED); + break; + case NsdManager.RESOLVE_SERVICE: + if (DBG) Slog.d(TAG, "Resolve service"); + servInfo = (DnsSdServiceInfo) msg.obj; + clientInfo = mClients.get(msg.replyTo); + if (clientInfo.mResolveId != INVALID_ID) { + //first cancel existing resolve + stopResolveService(clientInfo.mResolveId); + } + + clientInfo.mResolveId = getUniqueId(); + if (!resolveService(clientInfo.mResolveId, servInfo)) { + mReplyChannel.replyToMessage(msg, NsdManager.RESOLVE_SERVICE_FAILED, + NsdManager.ERROR); + clientInfo.mResolveId = INVALID_ID; + } + break; + case NsdManager.STOP_RESOLVE: + if (DBG) Slog.d(TAG, "Stop resolve"); + clientInfo = mClients.get(msg.replyTo); + if (clientInfo.mResolveId == INVALID_ID) { + //already stopped + if (DBG) Slog.d(TAG, "resolve already stopped"); + mReplyChannel.replyToMessage(msg, NsdManager.STOP_RESOLVE_FAILED, + NsdManager.ALREADY_ACTIVE); + break; + } + if (stopResolveService(clientInfo.mResolveId)) { + clientInfo.mResolveId = INVALID_ID; + mReplyChannel.replyToMessage(msg, NsdManager.STOP_RESOLVE_SUCCEEDED); + } else { + mReplyChannel.replyToMessage(msg, NsdManager.STOP_RESOLVE_FAILED, + NsdManager.ERROR); + } + break; + default: + result = NOT_HANDLED; + break; + } + return result; + } + } } - private AsyncServiceHandler mAsyncServiceHandler; private NativeDaemonConnector mNativeConnector; private final CountDownLatch mNativeDaemonConnected = new CountDownLatch(1); private NsdService(Context context) { mContext = context; - - HandlerThread nsdThread = new HandlerThread("NsdService"); - nsdThread.start(); - mAsyncServiceHandler = new AsyncServiceHandler(nsdThread.getLooper()); + mContentResolver = context.getContentResolver(); mNativeConnector = new NativeDaemonConnector(new NativeCallbackReceiver(), "mdns", 10, MDNS_TAG, 25); + + mNsdStateMachine = new NsdStateMachine(TAG); + mNsdStateMachine.start(); + Thread th = new Thread(mNativeConnector, MDNS_TAG); th.start(); } public static NsdService create(Context context) throws InterruptedException { NsdService service = new NsdService(context); - /* service.mNativeDaemonConnected.await(); */ + service.mNativeDaemonConnected.await(); return service; } public Messenger getMessenger() { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService"); - return new Messenger(mAsyncServiceHandler); + return new Messenger(mNsdStateMachine.getHandler()); + } + + public void setEnabled(boolean enable) { + mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CONNECTIVITY_INTERNAL, + "NsdService"); + Settings.Secure.putInt(mContentResolver, Settings.Secure.NSD_ON, enable ? 1 : 0); + if (enable) { + mNsdStateMachine.sendMessage(NsdManager.ENABLE); + } else { + mNsdStateMachine.sendMessage(NsdManager.DISABLE); + } + } + + private void sendNsdStateChangeBroadcast(boolean enabled) { + final Intent intent = new Intent(NsdManager.NSD_STATE_CHANGED_ACTION); + intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT); + if (enabled) { + intent.putExtra(NsdManager.EXTRA_NSD_STATE, NsdManager.NSD_STATE_ENABLED); + } else { + intent.putExtra(NsdManager.EXTRA_NSD_STATE, NsdManager.NSD_STATE_DISABLED); + } + mContext.sendStickyBroadcast(intent); + } + + private boolean isNsdEnabled() { + boolean ret = Settings.Secure.getInt(mContentResolver, Settings.Secure.NSD_ON, 1) == 1; + if (DBG) Slog.d(TAG, "Network service discovery enabled " + ret); + return ret; } private int getUniqueId() { @@ -522,7 +683,7 @@ public class NsdService extends INsdManager.Stub { } @Override - protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { pw.println("Permission Denial: can't dump ServiceDiscoverService from from pid=" @@ -531,7 +692,12 @@ public class NsdService extends INsdManager.Stub { return; } - pw.println("Internal state:"); + for (ClientInfo client : mClients.values()) { + pw.println("Client Info"); + pw.println(client); + } + + mNsdStateMachine.dump(fd, pw, args); } private ClientInfo getClientByDiscovery(int discoveryId) { @@ -579,5 +745,19 @@ public class NsdService extends INsdManager.Stub { mDiscoveryId = mResolveId = INVALID_ID; if (DBG) Slog.d(TAG, "New client, channel: " + c + " messenger: " + m); } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("mChannel ").append(mChannel).append("\n"); + sb.append("mMessenger ").append(mMessenger).append("\n"); + sb.append("mDiscoveryId ").append(mDiscoveryId).append("\n"); + sb.append("mResolveId ").append(mResolveId).append("\n"); + sb.append("mResolvedService ").append(mResolvedService).append("\n"); + for(int regId : mRegisteredIds) { + sb.append("regId ").append(regId).append("\n"); + } + return sb.toString(); + } } } diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java index 7aee8d2..b4a458f 100644..100755 --- a/services/java/com/android/server/wm/WindowManagerService.java +++ b/services/java/com/android/server/wm/WindowManagerService.java @@ -3557,17 +3557,7 @@ public class WindowManagerService extends IWindowManager.Stub continue; } - if (!haveGroup) { - // We ignore any hidden applications on the top. - if (wtoken.hiddenRequested || wtoken.willBeHidden) { - if (DEBUG_ORIENTATION) Slog.v(TAG, "Skipping " + wtoken - + " -- hidden on top"); - continue; - } - haveGroup = true; - curGroup = wtoken.groupId; - lastOrientation = wtoken.requestedOrientation; - } else if (curGroup != wtoken.groupId) { + if (haveGroup == true && curGroup != wtoken.groupId) { // If we have hit a new application group, and the bottom // of the previous group didn't explicitly say to use // the orientation behind it, and the last app was @@ -3580,6 +3570,20 @@ public class WindowManagerService extends IWindowManager.Stub return lastOrientation; } } + + // We ignore any hidden applications on the top. + if (wtoken.hiddenRequested || wtoken.willBeHidden) { + if (DEBUG_ORIENTATION) Slog.v(TAG, "Skipping " + wtoken + + " -- hidden on top"); + continue; + } + + if (!haveGroup) { + haveGroup = true; + curGroup = wtoken.groupId; + lastOrientation = wtoken.requestedOrientation; + } + int or = wtoken.requestedOrientation; // If this application is fullscreen, and didn't explicitly say // to use the orientation behind it, then just take whatever diff --git a/telephony/java/com/android/internal/telephony/DataConnection.java b/telephony/java/com/android/internal/telephony/DataConnection.java index a124c7f..da03f76 100644 --- a/telephony/java/com/android/internal/telephony/DataConnection.java +++ b/telephony/java/com/android/internal/telephony/DataConnection.java @@ -209,7 +209,7 @@ public abstract class DataConnection extends StateMachine { protected static final int EVENT_RIL_CONNECTED = BASE + 5; protected static final int EVENT_DISCONNECT_ALL = BASE + 6; - private static final int CMD_TO_STRING_COUNT = EVENT_DISCONNECT_ALL + 1; + private static final int CMD_TO_STRING_COUNT = EVENT_DISCONNECT_ALL - BASE + 1; private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT]; static { sCmdToString[EVENT_CONNECT - BASE] = "EVENT_CONNECT"; diff --git a/telephony/java/com/android/internal/telephony/DataConnectionAc.java b/telephony/java/com/android/internal/telephony/DataConnectionAc.java index 4744ff0..96419ae 100644 --- a/telephony/java/com/android/internal/telephony/DataConnectionAc.java +++ b/telephony/java/com/android/internal/telephony/DataConnectionAc.java @@ -82,7 +82,7 @@ public class DataConnectionAc extends AsyncChannel { public static final int REQ_GET_RECONNECT_INTENT = BASE + 26; public static final int RSP_GET_RECONNECT_INTENT = BASE + 27; - private static final int CMD_TO_STRING_COUNT = RSP_GET_RECONNECT_INTENT + 1; + private static final int CMD_TO_STRING_COUNT = RSP_GET_RECONNECT_INTENT - BASE + 1; private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT]; static { sCmdToString[REQ_IS_INACTIVE - BASE] = "REQ_IS_INACTIVE"; diff --git a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java index 5eac1f2..a90af15 100644 --- a/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java +++ b/tests/StatusBar/src/com/android/statusbartest/NotificationTestList.java @@ -24,6 +24,7 @@ import android.content.ContentResolver; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; +import android.os.Bundle; import android.os.Environment; import android.os.Vibrator; import android.os.Handler; @@ -44,10 +45,10 @@ public class NotificationTestList extends TestActivity private final static String TAG = "NotificationTestList"; NotificationManager mNM; - Vibrator mVibrator = (Vibrator)getSystemService(VIBRATOR_SERVICE); + Vibrator mVibrator; Handler mHandler = new Handler(); - long mActivityCreateTime = System.currentTimeMillis(); + long mActivityCreateTime; long mChronometerBase = 0; boolean mProgressDone = true; @@ -67,6 +68,13 @@ public class NotificationTestList extends TestActivity final int kUnnumberedIconResID = R.drawable.notificationx; @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + mVibrator = (Vibrator)getSystemService(VIBRATOR_SERVICE); + mActivityCreateTime = System.currentTimeMillis(); + } + + @Override protected String tag() { return TAG; } diff --git a/wifi/java/android/net/wifi/p2p/WifiP2pService.java b/wifi/java/android/net/wifi/p2p/WifiP2pService.java index 314e33e..6168f0e 100644 --- a/wifi/java/android/net/wifi/p2p/WifiP2pService.java +++ b/wifi/java/android/net/wifi/p2p/WifiP2pService.java @@ -1500,8 +1500,7 @@ public class WifiP2pService extends IWifiP2pManager.Stub { int key; WifiP2pServiceRequest req; for (int i=0; i < c.mReqList.size(); i++) { - key = c.mReqList.keyAt(i); - req = c.mReqList.get(key); + req = c.mReqList.valueAt(i); if (req != null) { sb.append(req.getSupplicantQuery()); } @@ -1539,7 +1538,10 @@ public class WifiP2pService extends IWifiP2pManager.Stub { return false; } - req.setTransactionId(++mServiceTransactionId); + ++mServiceTransactionId; + //The Wi-Fi p2p spec says transaction id should be non-zero + if (mServiceTransactionId == 0) ++mServiceTransactionId; + req.setTransactionId(mServiceTransactionId); clientInfo.mReqList.put(mServiceTransactionId, req); if (mServiceDiscReqId == null) { @@ -1550,13 +1552,23 @@ public class WifiP2pService extends IWifiP2pManager.Stub { } private void removeServiceRequest(Messenger m, WifiP2pServiceRequest req) { - ClientInfo clientInfo = getClientInfo(m, false); if (clientInfo == null) { return; } - clientInfo.mReqList.remove(req.getTransactionId()); + //Application does not have transaction id information + //go through stored requests to remove + boolean removed = false; + for (int i=0; i < clientInfo.mReqList.size(); i++) { + if (req.equals(clientInfo.mReqList.valueAt(i))) { + removed = true; + clientInfo.mReqList.removeAt(i); + break; + } + } + + if (!removed) return; if (clientInfo.mReqList.size() == 0 && clientInfo.mServList.size() == 0) { if (DBG) logd("remove client information from framework"); @@ -1670,6 +1682,7 @@ public class WifiP2pService extends IWifiP2pManager.Stub { } catch (RemoteException e) { if (DBG) logd("detect dead channel"); clearClientInfo(c.mMessenger); + return; } } } @@ -1683,6 +1696,8 @@ public class WifiP2pService extends IWifiP2pManager.Stub { * TODO: This can be done better with full async channels. */ private void clearClientDeadChannels() { + ArrayList<Messenger> deadClients = new ArrayList<Messenger>(); + for (ClientInfo c : mClientInfoList.values()) { Message msg = Message.obtain(); msg.what = WifiP2pManager.PING; @@ -1693,9 +1708,13 @@ public class WifiP2pService extends IWifiP2pManager.Stub { c.mMessenger.send(msg); } catch (RemoteException e) { if (DBG) logd("detect dead channel"); - clearClientInfo(c.mMessenger); + deadClients.add(c.mMessenger); } } + + for (Messenger m : deadClients) { + clearClientInfo(m); + } } /** |
