diff options
92 files changed, 2212 insertions, 1041 deletions
@@ -19690,6 +19690,7 @@ package android.speech { method public static final android.content.Intent getVoiceDetailsIntent(android.content.Context); field public static final java.lang.String ACTION_GET_LANGUAGE_DETAILS = "android.speech.action.GET_LANGUAGE_DETAILS"; field public static final java.lang.String ACTION_RECOGNIZE_SPEECH = "android.speech.action.RECOGNIZE_SPEECH"; + field public static final java.lang.String ACTION_VOICE_SEARCH_HANDS_FREE = "android.speech.action.VOICE_SEARCH_HANDS_FREE"; field public static final java.lang.String ACTION_WEB_SEARCH = "android.speech.action.WEB_SEARCH"; field public static final java.lang.String DETAILS_META_DATA = "android.speech.DETAILS"; field public static final java.lang.String EXTRA_CALLING_PACKAGE = "calling_package"; @@ -19705,6 +19706,7 @@ package android.speech { field public static final java.lang.String EXTRA_RESULTS = "android.speech.extra.RESULTS"; field public static final java.lang.String EXTRA_RESULTS_PENDINGINTENT = "android.speech.extra.RESULTS_PENDINGINTENT"; field public static final java.lang.String EXTRA_RESULTS_PENDINGINTENT_BUNDLE = "android.speech.extra.RESULTS_PENDINGINTENT_BUNDLE"; + field public static final java.lang.String EXTRA_SECURE = "android.speech.extras.EXTRA_SECURE"; field public static final java.lang.String EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS = "android.speech.extras.SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS"; field public static final java.lang.String EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS = "android.speech.extras.SPEECH_INPUT_MINIMUM_LENGTH_MILLIS"; field public static final java.lang.String EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS = "android.speech.extras.SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS"; diff --git a/api/current.txt b/api/current.txt index 1a694a8..c467c3b 100644 --- a/api/current.txt +++ b/api/current.txt @@ -19690,6 +19690,7 @@ package android.speech { method public static final android.content.Intent getVoiceDetailsIntent(android.content.Context); field public static final java.lang.String ACTION_GET_LANGUAGE_DETAILS = "android.speech.action.GET_LANGUAGE_DETAILS"; field public static final java.lang.String ACTION_RECOGNIZE_SPEECH = "android.speech.action.RECOGNIZE_SPEECH"; + field public static final java.lang.String ACTION_VOICE_SEARCH_HANDS_FREE = "android.speech.action.VOICE_SEARCH_HANDS_FREE"; field public static final java.lang.String ACTION_WEB_SEARCH = "android.speech.action.WEB_SEARCH"; field public static final java.lang.String DETAILS_META_DATA = "android.speech.DETAILS"; field public static final java.lang.String EXTRA_CALLING_PACKAGE = "calling_package"; @@ -19705,6 +19706,7 @@ package android.speech { field public static final java.lang.String EXTRA_RESULTS = "android.speech.extra.RESULTS"; field public static final java.lang.String EXTRA_RESULTS_PENDINGINTENT = "android.speech.extra.RESULTS_PENDINGINTENT"; field public static final java.lang.String EXTRA_RESULTS_PENDINGINTENT_BUNDLE = "android.speech.extra.RESULTS_PENDINGINTENT_BUNDLE"; + field public static final java.lang.String EXTRA_SECURE = "android.speech.extras.EXTRA_SECURE"; field public static final java.lang.String EXTRA_SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS = "android.speech.extras.SPEECH_INPUT_COMPLETE_SILENCE_LENGTH_MILLIS"; field public static final java.lang.String EXTRA_SPEECH_INPUT_MINIMUM_LENGTH_MILLIS = "android.speech.extras.SPEECH_INPUT_MINIMUM_LENGTH_MILLIS"; field public static final java.lang.String EXTRA_SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS = "android.speech.extras.SPEECH_INPUT_POSSIBLY_COMPLETE_SILENCE_LENGTH_MILLIS"; diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 9a8d802..2eea171 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1852,14 +1852,17 @@ public class Notification implements Parcelable int[] rowIds = {R.id.inbox_text0, R.id.inbox_text1, R.id.inbox_text2, R.id.inbox_text3, R.id.inbox_text4}; + // Make sure all rows are gone in case we reuse a view. + for (int rowId : rowIds) { + contentView.setViewVisibility(rowId, View.GONE); + } + int i=0; while (i < mTexts.size() && i < rowIds.length) { CharSequence str = mTexts.get(i); if (str != null && !str.equals("")) { contentView.setViewVisibility(rowIds[i], View.VISIBLE); contentView.setTextViewText(rowIds[i], str); - } else { - contentView.setViewVisibility(rowIds[i], View.GONE); } i++; } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 6448b55..dfd35e1 100755 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -77,7 +77,8 @@ public final class InputManager { * The meta-data specifies a resource that contains a description of each keyboard * layout that is provided by the application. * <pre><code> - * <receiver android:name=".InputDeviceReceiver"> + * <receiver android:name=".InputDeviceReceiver" + * android:label="@string/keyboard_layouts_label"> * <intent-filter> * <action android:name="android.hardware.input.action.QUERY_KEYBOARD_LAYOUTS" /> * </intent-filter> @@ -90,7 +91,9 @@ public final class InputManager { * an XML resource whose root element is <code><keyboard-layouts></code> that * contains zero or more <code><keyboard-layout></code> elements. * Each <code><keyboard-layout></code> element specifies the name, label, and location - * of a key character map for a particular keyboard layout. + * of a key character map for a particular keyboard layout. The label on the receiver + * is used to name the collection of keyboard layouts provided by this receiver in the + * keyboard layout settings. * <pre></code> * <?xml version="1.0" encoding="utf-8"?> * <keyboard-layouts xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/core/java/android/hardware/input/KeyboardLayout.java b/core/java/android/hardware/input/KeyboardLayout.java index e75a6dc..5402e75 100644 --- a/core/java/android/hardware/input/KeyboardLayout.java +++ b/core/java/android/hardware/input/KeyboardLayout.java @@ -28,6 +28,7 @@ public final class KeyboardLayout implements Parcelable, Comparable<KeyboardLayout> { private final String mDescriptor; private final String mLabel; + private final String mCollection; public static final Parcelable.Creator<KeyboardLayout> CREATOR = new Parcelable.Creator<KeyboardLayout>() { @@ -39,14 +40,16 @@ public final class KeyboardLayout implements Parcelable, } }; - public KeyboardLayout(String descriptor, String label) { + public KeyboardLayout(String descriptor, String label, String collection) { mDescriptor = descriptor; mLabel = label; + mCollection = collection; } private KeyboardLayout(Parcel source) { mDescriptor = source.readString(); mLabel = source.readString(); + mCollection = source.readString(); } /** @@ -68,6 +71,15 @@ public final class KeyboardLayout implements Parcelable, return mLabel; } + /** + * Gets the name of the collection to which the keyboard layout belongs. This is + * the label of the broadcast receiver or application that provided the keyboard layout. + * @return The keyboard layout collection name. + */ + public String getCollection() { + return mCollection; + } + @Override public int describeContents() { return 0; @@ -77,15 +89,23 @@ public final class KeyboardLayout implements Parcelable, public void writeToParcel(Parcel dest, int flags) { dest.writeString(mDescriptor); dest.writeString(mLabel); + dest.writeString(mCollection); } @Override public int compareTo(KeyboardLayout another) { - return mLabel.compareToIgnoreCase(another.mLabel); + int result = mLabel.compareToIgnoreCase(another.mLabel); + if (result == 0) { + result = mCollection.compareToIgnoreCase(another.mCollection); + } + return result; } @Override public String toString() { - return mLabel; + if (mCollection.isEmpty()) { + return mLabel; + } + return mLabel + " - " + mCollection; } }
\ No newline at end of file diff --git a/core/java/android/net/SSLCertificateSocketFactory.java b/core/java/android/net/SSLCertificateSocketFactory.java index 6a4f1f2..2703f1d 100644 --- a/core/java/android/net/SSLCertificateSocketFactory.java +++ b/core/java/android/net/SSLCertificateSocketFactory.java @@ -261,8 +261,8 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * server then the first protocol in the client's list will be selected. * The order of the client's protocols is otherwise insignificant. * - * @param npnProtocols a possibly-empty list of protocol byte arrays. All - * arrays must be non-empty and of length less than 256. + * @param npnProtocols a non-empty list of protocol byte arrays. All arrays + * must be non-empty and of length less than 256. */ public void setNpnProtocols(byte[][] npnProtocols) { this.mNpnProtocols = toNpnProtocolsList(npnProtocols); @@ -273,6 +273,9 @@ public class SSLCertificateSocketFactory extends SSLSocketFactory { * strings. */ static byte[] toNpnProtocolsList(byte[]... npnProtocols) { + if (npnProtocols.length == 0) { + throw new IllegalArgumentException("npnProtocols.length == 0"); + } int totalLength = 0; for (byte[] s : npnProtocols) { if (s.length == 0 || s.length > 255) { diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java index 7ffa575..53b41d5 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -299,7 +299,23 @@ public final class NfcActivityManager extends INdefPushCallback.Stub callback = state.uriCallback; } if (callback != null) { - return callback.createBeamUris(mDefaultEvent); + uris = callback.createBeamUris(mDefaultEvent); + if (uris != null) { + for (Uri uri : uris) { + if (uri == null) { + Log.e(TAG, "Uri not allowed to be null."); + return null; + } + String scheme = uri.getScheme(); + if (scheme == null || (!scheme.equalsIgnoreCase("file") && + !scheme.equalsIgnoreCase("content"))) { + Log.e(TAG, "Uri needs to have " + + "either scheme file or scheme content"); + return null; + } + } + } + return uris; } else { return uris; } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 7bf9feb..4464d58 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -584,17 +584,138 @@ public final class NfcAdapter { } } - //TODO: make sure NFC service has permission for URI - //TODO: see if we will eventually support multiple URIs - //TODO: javadoc + /** + * Set one or more {@link Uri}s to send using Android Beam (TM). Every + * Uri you provide must have either scheme 'file' or scheme 'content'. + * + * <p>For the data provided through this method, Android Beam tries to + * switch to alternate transports such as Bluetooth to achieve a fast + * transfer speed. Hence this method is very suitable + * for transferring large files such as pictures or songs. + * + * <p>The receiving side will store the content of each Uri in + * a file and present a notification to the user to open the file + * with a {@link android.content.Intent} with action + * {@link android.content.Intent#ACTION_VIEW}. + * If multiple URIs are sent, the {@link android.content.Intent} will refer + * to the first of the stored files. + * + * <p>This method may be called at any time before {@link Activity#onDestroy}, + * but the URI(s) are only made available for Android Beam when the + * specified activity(s) are in resumed (foreground) state. The recommended + * approach is to call this method during your Activity's + * {@link Activity#onCreate} - see sample + * code below. This method does not immediately perform any I/O or blocking work, + * so is safe to call on your main thread. + * + * <p>{@link #setBeamPushUris} and {@link #setBeamPushUrisCallback} + * have priority over both {@link #setNdefPushMessage} and + * {@link #setNdefPushMessageCallback}. + * + * <p>If {@link #setBeamPushUris} is called with a null Uri array, + * and/or {@link #setBeamPushUrisCallback} is called with a null callback, + * then the Uri push will be completely disabled for the specified activity(s). + * + * <p>Code example: + * <pre> + * protected void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + * if (nfcAdapter == null) return; // NFC not available on this device + * nfcAdapter.setBeamPushUris(new Uri[] {uri1, uri2}, this); + * } + * </pre> + * And that is it. Only one call per activity is necessary. The Android + * OS will automatically release its references to the Uri(s) and the + * Activity object when it is destroyed if you follow this pattern. + * + * <p>If your Activity wants to dynamically supply Uri(s), + * then set a callback using {@link #setBeamPushUrisCallback} instead + * of using this method. + * + * <p class="note">Do not pass in an Activity that has already been through + * {@link Activity#onDestroy}. This is guaranteed if you call this API + * during {@link Activity#onCreate}. + * + * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param uris an array of Uri(s) to push over Android Beam + * @param activity activity for which the Uri(s) will be pushed + */ public void setBeamPushUris(Uri[] uris, Activity activity) { if (activity == null) { throw new NullPointerException("activity cannot be null"); } + if (uris != null) { + for (Uri uri : uris) { + if (uri == null) throw new NullPointerException("Uri not " + + "allowed to be null"); + String scheme = uri.getScheme(); + if (scheme == null || (!scheme.equalsIgnoreCase("file") && + !scheme.equalsIgnoreCase("content"))) { + throw new IllegalArgumentException("URI needs to have " + + "either scheme file or scheme content"); + } + } + } mNfcActivityManager.setNdefPushContentUri(activity, uris); } - // TODO javadoc + /** + * Set a callback that will dynamically generate one or more {@link Uri}s + * to send using Android Beam (TM). Every Uri the callback provides + * must have either scheme 'file' or scheme 'content'. + * + * <p>For the data provided through this callback, Android Beam tries to + * switch to alternate transports such as Bluetooth to achieve a fast + * transfer speed. Hence this method is very suitable + * for transferring large files such as pictures or songs. + * + * <p>The receiving side will store the content of each Uri in + * a file and present a notification to the user to open the file + * with a {@link android.content.Intent} with action + * {@link android.content.Intent#ACTION_VIEW}. + * If multiple URIs are sent, the {@link android.content.Intent} will refer + * to the first of the stored files. + * + * <p>This method may be called at any time before {@link Activity#onDestroy}, + * but the URI(s) are only made available for Android Beam when the + * specified activity(s) are in resumed (foreground) state. The recommended + * approach is to call this method during your Activity's + * {@link Activity#onCreate} - see sample + * code below. This method does not immediately perform any I/O or blocking work, + * so is safe to call on your main thread. + * + * <p>{@link #setBeamPushUris} and {@link #setBeamPushUrisCallback} + * have priority over both {@link #setNdefPushMessage} and + * {@link #setNdefPushMessageCallback}. + * + * <p>If {@link #setBeamPushUris} is called with a null Uri array, + * and/or {@link #setBeamPushUrisCallback} is called with a null callback, + * then the Uri push will be completely disabled for the specified activity(s). + * + * <p>Code example: + * <pre> + * protected void onCreate(Bundle savedInstanceState) { + * super.onCreate(savedInstanceState); + * NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + * if (nfcAdapter == null) return; // NFC not available on this device + * nfcAdapter.setBeamPushUrisCallback(callback, this); + * } + * </pre> + * And that is it. Only one call per activity is necessary. The Android + * OS will automatically release its references to the Uri(s) and the + * Activity object when it is destroyed if you follow this pattern. + * + * <p class="note">Do not pass in an Activity that has already been through + * {@link Activity#onDestroy}. This is guaranteed if you call this API + * during {@link Activity#onCreate}. + * + * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. + * + * @param callback callback, or null to disable + * @param activity activity for which the Uri(s) will be pushed + */ public void setBeamPushUrisCallback(CreateBeamUrisCallback callback, Activity activity) { if (activity == null) { throw new NullPointerException("activity cannot be null"); @@ -663,6 +784,10 @@ public final class NfcAdapter { * {@link Activity#onDestroy}. This is guaranteed if you call this API * during {@link Activity#onCreate}. * + * <p class="note">For sending large content such as pictures and songs, + * consider using {@link #setBeamPushUris}, which switches to alternate transports + * such as Bluetooth to achieve a fast transfer rate. + * * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @param message NDEF message to push over NFC, or null to disable @@ -753,7 +878,9 @@ public final class NfcAdapter { * <p class="note">Do not pass in an Activity that has already been through * {@link Activity#onDestroy}. This is guaranteed if you call this API * during {@link Activity#onCreate}. - * + * <p class="note">For sending large content such as pictures and songs, + * consider using {@link #setBeamPushUris}, which switches to alternate transports + * such as Bluetooth to achieve a fast transfer rate. * <p class="note">Requires the {@link android.Manifest.permission#NFC} permission. * * @param callback callback, or null to disable diff --git a/core/java/android/os/storage/IMountService.java b/core/java/android/os/storage/IMountService.java index f4abda6..ab64866 100644 --- a/core/java/android/os/storage/IMountService.java +++ b/core/java/android/os/storage/IMountService.java @@ -1360,7 +1360,14 @@ public interface IMountService extends IInterface { */ public Parcelable[] getVolumeList() throws RemoteException; - public String getSecureContainerFilesystemPath(String id) throws RemoteException; + /** + * Gets the path on the filesystem for the ASEC container itself. + * + * @param cid ASEC container ID + * @return path to filesystem or {@code null} if it's not found + * @throws RemoteException + */ + public String getSecureContainerFilesystemPath(String cid) throws RemoteException; /* * Fix permissions in a container which has just been created and populated. diff --git a/core/java/android/preference/DialogPreference.java b/core/java/android/preference/DialogPreference.java index c59ed18..a643c8a 100644 --- a/core/java/android/preference/DialogPreference.java +++ b/core/java/android/preference/DialogPreference.java @@ -261,6 +261,8 @@ public abstract class DialogPreference extends Preference implements @Override protected void onClick() { + if (mDialog != null && mDialog.isShowing()) return; + showDialog(null); } diff --git a/core/java/android/speech/RecognizerIntent.java b/core/java/android/speech/RecognizerIntent.java index fd709f2..457e66c 100644 --- a/core/java/android/speech/RecognizerIntent.java +++ b/core/java/android/speech/RecognizerIntent.java @@ -115,6 +115,45 @@ public class RecognizerIntent { public static final String ACTION_WEB_SEARCH = "android.speech.action.WEB_SEARCH"; /** + * Starts an activity that will prompt the user for speech without requiring the user's + * visual attention or touch input. It will send it through a speech recognizer, + * and either synthesize speech for a web search result or trigger + * another type of action based on the user's speech. + * + * This activity may be launched while device is locked in a secure mode. + * Special care must be taken to ensure that the voice actions that are performed while + * hands free cannot compromise the device's security. + * The activity should check the value of the {@link #EXTRA_SECURE} extra to determine + * whether the device has been securely locked. If so, the activity should either restrict + * the set of voice actions that are permitted or require some form of secure + * authentication before proceeding. + * + * To ensure that the activity's user interface is visible while the lock screen is showing, + * the activity should set the + * {@link android.view.WindowManager.LayoutParams#FLAG_SHOW_WHEN_LOCKED} window flag. + * Otherwise the activity's user interface may be hidden by the lock screen. The activity + * should take care not to leak private information when the device is securely locked. + * + * <p>Optional extras: + * <ul> + * <li>{@link #EXTRA_SECURE} + * </ul> + */ + public static final String ACTION_VOICE_SEARCH_HANDS_FREE = + "android.speech.action.VOICE_SEARCH_HANDS_FREE"; + + /** + * Optional boolean to indicate that a "hands free" voice search was performed while the device + * was in a secure mode. An example of secure mode is when the device's screen lock is active, + * and it requires some form of authentication to be unlocked. + * + * When the device is securely locked, the voice search activity should either restrict + * the set of voice actions that are permitted, or require some form of secure authentication + * before proceeding. + */ + public static final String EXTRA_SECURE = "android.speech.extras.EXTRA_SECURE"; + + /** * The minimum length of an utterance. We will not stop recording before this amount of time. * * Note that it is extremely rare you'd want to specify this value in an intent. If you don't diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 62c267f..e4062e6 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -2255,9 +2255,27 @@ public class View implements Drawable.Callback, Drawable.Callback2, KeyEvent.Cal * flags, we would like a stable view of the content insets given to * {@link #fitSystemWindows(Rect)}. This means that the insets seen there * will always represent the worst case that the application can expect - * as a continue state. In practice this means with any of system bar, - * nav bar, and status bar shown, but not the space that would be needed - * for an input method. + * as a continuous state. In the stock Android UI this is the space for + * the system bar, nav bar, and status bar, but not more transient elements + * such as an input method. + * + * The stable layout your UI sees is based on the system UI modes you can + * switch to. That is, if you specify {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} + * then you will get a stable layout for changes of the + * {@link #SYSTEM_UI_FLAG_FULLSCREEN} mode; if you specify + * {@link #SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} and + * {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}, then you can transition + * to {@link #SYSTEM_UI_FLAG_FULLSCREEN} and {@link #SYSTEM_UI_FLAG_HIDE_NAVIGATION} + * with a stable layout. (Note that you should avoid using + * {@link #SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION} by itself.) + * + * If you have set the window flag {@ WindowManager.LayoutParams#FLAG_FULLSCREEN} + * to hide the status bar (instead of using {@link #SYSTEM_UI_FLAG_FULLSCREEN}), + * then a hidden status bar will be considered a "stable" state for purposes + * here. This allows your UI to continually hide the status bar, while still + * using the system UI flags to hide the action bar while still retaining + * a stable layout. Note that changing the window fullscreen flag will never + * provide a stable layout for a clean transition. * * <p>If you are using ActionBar in * overlay mode with {@link Window#FEATURE_ACTION_BAR_OVERLAY diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index d62f513..d94275b 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -691,13 +691,6 @@ public interface WindowManager extends ViewManager { */ public static final int FLAG_NEEDS_MENU_KEY = 0x08000000; - /** Window flag: *sigh* The lock screen wants to continue running its - * animation while it is fading. A kind-of hack to allow this. Maybe - * in the future we just make this the default behavior. - * - * {@hide} */ - public static final int FLAG_KEEP_SURFACE_WHILE_ANIMATING = 0x10000000; - /** Window flag: special flag to limit the size of the window to be * original size ([320x480] x density). Used to create window for applications * running under compatibility mode. diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java index 11bd815..cc490bd 100644 --- a/core/java/android/webkit/AccessibilityInjector.java +++ b/core/java/android/webkit/AccessibilityInjector.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010 The Android Open Source Project + * Copyright (C) 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. @@ -16,484 +16,615 @@ package android.webkit; +import android.content.Context; +import android.os.Bundle; +import android.os.SystemClock; import android.provider.Settings; -import android.text.TextUtils; -import android.text.TextUtils.SimpleStringSplitter; -import android.util.Log; +import android.speech.tts.TextToSpeech; import android.view.KeyEvent; -import android.view.accessibility.AccessibilityEvent; +import android.view.View; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; import android.webkit.WebViewCore.EventHub; -import java.util.ArrayList; -import java.util.Stack; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; /** - * This class injects accessibility into WebViews with disabled JavaScript or - * WebViews with enabled JavaScript but for which we have no accessibility - * script to inject. - * </p> - * Note: To avoid changes in the framework upon changing the available - * navigation axis, or reordering the navigation axis, or changing - * the key bindings, or defining sequence of actions to be bound to - * a given key this class is navigation axis agnostic. It is only - * aware of one navigation axis which is in fact the default behavior - * of webViews while using the DPAD/TrackBall. - * </p> - * In general a key binding is a mapping from modifiers + key code to - * a sequence of actions. For more detail how to specify key bindings refer to - * {@link android.provider.Settings.Secure#ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS}. - * </p> - * The possible actions are invocations to - * {@link #setCurrentAxis(int, boolean, String)}, or - * {@link #traverseCurrentAxis(int, boolean, String)} - * {@link #traverseGivenAxis(int, int, boolean, String)} - * {@link #prefromAxisTransition(int, int, boolean, String)} - * referred via the values of: - * {@link #ACTION_SET_CURRENT_AXIS}, - * {@link #ACTION_TRAVERSE_CURRENT_AXIS}, - * {@link #ACTION_TRAVERSE_GIVEN_AXIS}, - * {@link #ACTION_PERFORM_AXIS_TRANSITION}, - * respectively. - * The arguments for the action invocation are specified as offset - * hexademical pairs. Note the last argument of the invocation - * should NOT be specified in the binding as it is provided by - * this class. For details about the key binding implementation - * refer to {@link AccessibilityWebContentKeyBinding}. + * Handles injecting accessibility JavaScript and related JavaScript -> Java + * APIs. */ class AccessibilityInjector { - private static final String LOG_TAG = "AccessibilityInjector"; + // Default result returned from AndroidVox. Using true here means if the + // script fails, an accessibility service will always think that traversal + // has succeeded. + private static final String DEFAULT_ANDROIDVOX_RESULT = "true"; + + // The WebViewClassic this injector is responsible for managing. + private final WebViewClassic mWebViewClassic; - private static final boolean DEBUG = true; + // Cached reference to mWebViewClassic.getContext(), for convenience. + private final Context mContext; - private static final int ACTION_SET_CURRENT_AXIS = 0; - private static final int ACTION_TRAVERSE_CURRENT_AXIS = 1; - private static final int ACTION_TRAVERSE_GIVEN_AXIS = 2; - private static final int ACTION_PERFORM_AXIS_TRANSITION = 3; - private static final int ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS = 4; + // Cached reference to mWebViewClassic.getWebView(), for convenience. + private final WebView mWebView; - // the default WebView behavior abstracted as a navigation axis - private static final int NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR = 7; + // The Java objects that are exposed to JavaScript. + private TextToSpeech mTextToSpeech; + private CallbackHandler mCallback; - // these are the same for all instances so make them process wide - private static ArrayList<AccessibilityWebContentKeyBinding> sBindings = - new ArrayList<AccessibilityWebContentKeyBinding>(); + // Lazily loaded helper objects. + private AccessibilityManager mAccessibilityManager; + private AccessibilityInjectorFallback mAccessibilityInjectorFallback; + private JSONObject mAccessibilityJSONObject; - // handle to the WebViewClassic this injector is associated with. - private final WebViewClassic mWebView; + // Whether the accessibility script has been injected into the current page. + private boolean mAccessibilityScriptInjected; - // events scheduled for sending as soon as we receive the selected text - private final Stack<AccessibilityEvent> mScheduledEventStack = new Stack<AccessibilityEvent>(); + // Constants for determining script injection strategy. + private static final int ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED = -1; + private static final int ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT = 0; + @SuppressWarnings("unused") + private static final int ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED = 1; - // the current traversal axis - private int mCurrentAxis = 2; // sentence + // Alias for TTS API exposed to JavaScript. + private static final String ALIAS_TTS_JS_INTERFACE = "accessibility"; - // we need to consume the up if we have handled the last down - private boolean mLastDownEventHandled; + // Alias for traversal callback exposed to JavaScript. + private static final String ALIAS_TRAVERSAL_JS_INTERFACE = "accessibilityTraversal"; - // getting two empty selection strings in a row we let the WebView handle the event - private boolean mIsLastSelectionStringNull; + // Template for JavaScript that injects a screen-reader. + private static final String ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE = + "javascript:(function() {" + + " var chooser = document.createElement('script');" + + " chooser.type = 'text/javascript';" + + " chooser.src = '%1s';" + + " document.getElementsByTagName('head')[0].appendChild(chooser);" + + " })();"; - // keep track of last direction - private int mLastDirection; + // Template for JavaScript that performs AndroidVox actions. + private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE = + "cvox.AndroidVox.performAction('%1s')"; /** - * Creates a new injector associated with a given {@link WebViewClassic}. + * Creates an instance of the AccessibilityInjector based on + * {@code webViewClassic}. * - * @param webView The associated WebViewClassic. + * @param webViewClassic The WebViewClassic that this AccessibilityInjector + * manages. */ - public AccessibilityInjector(WebViewClassic webView) { - mWebView = webView; - ensureWebContentKeyBindings(); + public AccessibilityInjector(WebViewClassic webViewClassic) { + mWebViewClassic = webViewClassic; + mWebView = webViewClassic.getWebView(); + mContext = webViewClassic.getContext(); + mAccessibilityManager = AccessibilityManager.getInstance(mContext); } /** - * Processes a key down <code>event</code>. - * - * @return True if the event was processed. + * Attempts to load scripting interfaces for accessibility. + * <p> + * This should be called when the window is attached. + * </p> */ - public boolean onKeyEvent(KeyEvent event) { - // We do not handle ENTER in any circumstances. - if (isEnterActionKey(event.getKeyCode())) { - return false; + public void addAccessibilityApisIfNecessary() { + if (!isAccessibilityEnabled() || !isJavaScriptEnabled()) { + return; } - if (event.getAction() == KeyEvent.ACTION_UP) { - return mLastDownEventHandled; + addTtsApis(); + addCallbackApis(); + } + + /** + * Attempts to unload scripting interfaces for accessibility. + * <p> + * This should be called when the window is detached. + * </p> + */ + public void removeAccessibilityApisIfNecessary() { + removeTtsApis(); + removeCallbackApis(); + } + + /** + * Initializes an {@link AccessibilityNodeInfo} with the actions and + * movement granularity levels supported by this + * {@link AccessibilityInjector}. + * <p> + * If an action identifier is added in this method, this + * {@link AccessibilityInjector} should also return {@code true} from + * {@link #supportsAccessibilityAction(int)}. + * </p> + * + * @param info The info to initialize. + * @see View#onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo) + */ + public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER + | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD + | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE + | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH + | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE); + info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY); + info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); + info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT); + info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT); + info.addAction(AccessibilityNodeInfo.ACTION_CLICK); + info.setClickable(true); + } + + /** + * Returns {@code true} if this {@link AccessibilityInjector} should handle + * the specified action. + * + * @param action An accessibility action identifier. + * @return {@code true} if this {@link AccessibilityInjector} should handle + * the specified action. + */ + public boolean supportsAccessibilityAction(int action) { + switch (action) { + case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: + case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: + case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT: + case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: + case AccessibilityNodeInfo.ACTION_CLICK: + return true; + default: + return false; } + } - mLastDownEventHandled = false; + /** + * Performs the specified accessibility action. + * + * @param action The identifier of the action to perform. + * @param arguments The action arguments, or {@code null} if no arguments. + * @return {@code true} if the action was successful. + * @see View#performAccessibilityAction(int, Bundle) + */ + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (!isAccessibilityEnabled()) { + mAccessibilityScriptInjected = false; + toggleFallbackAccessibilityInjector(false); + return false; + } - AccessibilityWebContentKeyBinding binding = null; - for (AccessibilityWebContentKeyBinding candidate : sBindings) { - if (event.getKeyCode() == candidate.getKeyCode() - && event.hasModifiers(candidate.getModifiers())) { - binding = candidate; - break; - } + if (mAccessibilityScriptInjected) { + return sendActionToAndroidVox(action, arguments); } + + if (mAccessibilityInjectorFallback != null) { + return mAccessibilityInjectorFallback.performAccessibilityAction(action, arguments); + } + + return false; + } - if (binding == null) { + /** + * Attempts to handle key events when accessibility is turned on. + * + * @param event The key event to handle. + * @return {@code true} if the event was handled. + */ + public boolean handleKeyEventIfNecessary(KeyEvent event) { + if (!isAccessibilityEnabled()) { + mAccessibilityScriptInjected = false; + toggleFallbackAccessibilityInjector(false); return false; } - for (int i = 0, count = binding.getActionCount(); i < count; i++) { - int actionCode = binding.getActionCode(i); - String contentDescription = Integer.toHexString(binding.getAction(i)); - switch (actionCode) { - case ACTION_SET_CURRENT_AXIS: - int axis = binding.getFirstArgument(i); - boolean sendEvent = (binding.getSecondArgument(i) == 1); - setCurrentAxis(axis, sendEvent, contentDescription); - mLastDownEventHandled = true; - break; - case ACTION_TRAVERSE_CURRENT_AXIS: - int direction = binding.getFirstArgument(i); - // on second null selection string in same direction - WebView handles the event - if (direction == mLastDirection && mIsLastSelectionStringNull) { - mIsLastSelectionStringNull = false; - return false; - } - mLastDirection = direction; - sendEvent = (binding.getSecondArgument(i) == 1); - mLastDownEventHandled = traverseCurrentAxis(direction, sendEvent, - contentDescription); - break; - case ACTION_TRAVERSE_GIVEN_AXIS: - direction = binding.getFirstArgument(i); - // on second null selection string in same direction => WebView handle the event - if (direction == mLastDirection && mIsLastSelectionStringNull) { - mIsLastSelectionStringNull = false; - return false; - } - mLastDirection = direction; - axis = binding.getSecondArgument(i); - sendEvent = (binding.getThirdArgument(i) == 1); - traverseGivenAxis(direction, axis, sendEvent, contentDescription); - mLastDownEventHandled = true; - break; - case ACTION_PERFORM_AXIS_TRANSITION: - int fromAxis = binding.getFirstArgument(i); - int toAxis = binding.getSecondArgument(i); - sendEvent = (binding.getThirdArgument(i) == 1); - prefromAxisTransition(fromAxis, toAxis, sendEvent, contentDescription); - mLastDownEventHandled = true; - break; - case ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS: - // This is a special case since we treat the default WebView navigation - // behavior as one of the possible navigation axis the user can use. - // If we are not on the default WebView navigation axis this is NOP. - if (mCurrentAxis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) { - // While WebVew handles navigation we do not get null selection - // strings so do not check for that here as the cases above. - mLastDirection = binding.getFirstArgument(i); - sendEvent = (binding.getSecondArgument(i) == 1); - traverseGivenAxis(mLastDirection, NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR, - sendEvent, contentDescription); - mLastDownEventHandled = false; - } else { - mLastDownEventHandled = true; - } - break; - default: - Log.w(LOG_TAG, "Unknown action code: " + actionCode); + if (mAccessibilityScriptInjected) { + // if an accessibility script is injected we delegate to it the key + // handling. this script is a screen reader which is a fully fledged + // solution for blind users to navigate in and interact with web + // pages. + if (event.getAction() == KeyEvent.ACTION_UP) { + mWebViewClassic.sendBatchableInputMessage(EventHub.KEY_UP, 0, 0, event); + } else if (event.getAction() == KeyEvent.ACTION_DOWN) { + mWebViewClassic.sendBatchableInputMessage(EventHub.KEY_DOWN, 0, 0, event); + } else { + return false; } + + return true; } - return mLastDownEventHandled; + if (mAccessibilityInjectorFallback != null) { + // if an accessibility injector is present (no JavaScript enabled or + // the site opts out injecting our JavaScript screen reader) we let + // it decide whether to act on and consume the event. + return mAccessibilityInjectorFallback.onKeyEvent(event); + } + + return false; } /** - * Set the current navigation axis which will be used while - * calling {@link #traverseCurrentAxis(int, boolean, String)}. + * Attempts to handle selection change events when accessibility is using a + * non-JavaScript method. * - * @param axis The axis to set. - * @param sendEvent Whether to send an accessibility event to - * announce the change. + * @param selectionString The selection string. */ - private void setCurrentAxis(int axis, boolean sendEvent, String contentDescription) { - mCurrentAxis = axis; - if (sendEvent) { - AccessibilityEvent event = getPartialyPopulatedAccessibilityEvent(); - event.getText().add(String.valueOf(axis)); - event.setContentDescription(contentDescription); - sendAccessibilityEvent(event); + public void handleSelectionChangedIfNecessary(String selectionString) { + if (mAccessibilityInjectorFallback != null) { + mAccessibilityInjectorFallback.onSelectionStringChange(selectionString); } } /** - * Performs conditional transition one axis to another. + * Prepares for injecting accessibility scripts into a new page. + * + * @param url The URL that will be loaded. + */ + public void onPageStarted(String url) { + mAccessibilityScriptInjected = false; + } + + /** + * Attempts to inject the accessibility script using a {@code <script>} tag. + * <p> + * This should be called after a page has finished loading. + * </p> * - * @param fromAxis The axis which must be the current for the transition to occur. - * @param toAxis The axis to which to transition. - * @param sendEvent Flag if to send an event to announce successful transition. - * @param contentDescription A description of the performed action. + * @param url The URL that just finished loading. */ - private void prefromAxisTransition(int fromAxis, int toAxis, boolean sendEvent, - String contentDescription) { - if (mCurrentAxis == fromAxis) { - setCurrentAxis(toAxis, sendEvent, contentDescription); + public void onPageFinished(String url) { + if (!isAccessibilityEnabled()) { + mAccessibilityScriptInjected = false; + toggleFallbackAccessibilityInjector(false); + return; } + + if (!shouldInjectJavaScript(url)) { + toggleFallbackAccessibilityInjector(true); + return; + } + + toggleFallbackAccessibilityInjector(false); + + final String injectionUrl = getScreenReaderInjectionUrl(); + mWebView.loadUrl(injectionUrl); + + mAccessibilityScriptInjected = true; } /** - * Traverse the document along the current navigation axis. + * Toggles the non-JavaScript method for handling accessibility. * - * @param direction The direction of traversal. - * @param sendEvent Whether to send an accessibility event to - * announce the change. - * @param contentDescription A description of the performed action. - * @see #setCurrentAxis(int, boolean, String) + * @param enabled {@code true} to enable the non-JavaScript method, or + * {@code false} to disable it. */ - private boolean traverseCurrentAxis(int direction, boolean sendEvent, - String contentDescription) { - return traverseGivenAxis(direction, mCurrentAxis, sendEvent, contentDescription); + private void toggleFallbackAccessibilityInjector(boolean enabled) { + if (enabled && (mAccessibilityInjectorFallback == null)) { + mAccessibilityInjectorFallback = new AccessibilityInjectorFallback(mWebViewClassic); + } else { + mAccessibilityInjectorFallback = null; + } } /** - * Traverse the document along the given navigation axis. + * Determines whether it's okay to inject JavaScript into a given URL. * - * @param direction The direction of traversal. - * @param axis The axis along which to traverse. - * @param sendEvent Whether to send an accessibility event to - * announce the change. - * @param contentDescription A description of the performed action. + * @param url The URL to check. + * @return {@code true} if JavaScript should be injected, {@code false} if a + * non-JavaScript method should be used. */ - private boolean traverseGivenAxis(int direction, int axis, boolean sendEvent, - String contentDescription) { - WebViewCore webViewCore = mWebView.getWebViewCore(); - if (webViewCore == null) { + private boolean shouldInjectJavaScript(String url) { + // Respect the WebView's JavaScript setting. + if (!isJavaScriptEnabled()) { return false; } - AccessibilityEvent event = null; - if (sendEvent) { - event = getPartialyPopulatedAccessibilityEvent(); - // the text will be set upon receiving the selection string - event.setContentDescription(contentDescription); + // Allow the page to opt out of Accessibility script injection. + if (getAxsUrlParameterValue(url) == ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT) { + return false; } - mScheduledEventStack.push(event); - // if the axis is the default let WebView handle the event which will - // result in cursor ring movement and selection of its content - if (axis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) { + // The user must explicitly enable Accessibility script injection. + if (!isScriptInjectionEnabled()) { return false; } - webViewCore.sendMessage(EventHub.MODIFY_SELECTION, direction, axis); return true; } /** - * Called when the <code>selectionString</code> has changed. + * @return {@code true} if the user has explicitly enabled Accessibility + * script injection. */ - public void onSelectionStringChange(String selectionString) { - if (DEBUG) { - Log.d(LOG_TAG, "Selection string: " + selectionString); - } - mIsLastSelectionStringNull = (selectionString == null); - if (mScheduledEventStack.isEmpty()) { + private boolean isScriptInjectionEnabled() { + final int injectionSetting = Settings.Secure.getInt( + mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 0); + return (injectionSetting == 1); + } + + /** + * Attempts to initialize and add interfaces for TTS, if that hasn't already + * been done. + */ + private void addTtsApis() { + if (mTextToSpeech != null) { return; } - AccessibilityEvent event = mScheduledEventStack.pop(); - if (event != null) { - event.getText().add(selectionString); - sendAccessibilityEvent(event); - } + + final String pkgName = mContext.getPackageName(); + + mTextToSpeech = new TextToSpeech(mContext, null, null, pkgName + ".**webview**", true); + mWebView.addJavascriptInterface(mTextToSpeech, ALIAS_TTS_JS_INTERFACE); } /** - * Sends an {@link AccessibilityEvent}. - * - * @param event The event to send. + * Attempts to shutdown and remove interfaces for TTS, if that hasn't + * already been done. */ - private void sendAccessibilityEvent(AccessibilityEvent event) { - if (DEBUG) { - Log.d(LOG_TAG, "Dispatching: " + event); + private void removeTtsApis() { + if (mTextToSpeech == null) { + return; } - // accessibility may be disabled while waiting for the selection string - AccessibilityManager accessibilityManager = - AccessibilityManager.getInstance(mWebView.getContext()); - if (accessibilityManager.isEnabled()) { - accessibilityManager.sendAccessibilityEvent(event); + + mWebView.removeJavascriptInterface(ALIAS_TTS_JS_INTERFACE); + mTextToSpeech.stop(); + mTextToSpeech.shutdown(); + mTextToSpeech = null; + } + + private void addCallbackApis() { + if (mCallback != null) { + return; } + + mCallback = new CallbackHandler(ALIAS_TRAVERSAL_JS_INTERFACE); + mWebView.addJavascriptInterface(mCallback, ALIAS_TRAVERSAL_JS_INTERFACE); } - /** - * @return An accessibility event whose members are populated except its - * text and content description. - */ - private AccessibilityEvent getPartialyPopulatedAccessibilityEvent() { - AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SELECTED); - event.setClassName(mWebView.getClass().getName()); - event.setPackageName(mWebView.getContext().getPackageName()); - event.setEnabled(mWebView.getWebView().isEnabled()); - return event; + private void removeCallbackApis() { + if (mCallback == null) { + return; + } + + mWebView.removeJavascriptInterface(ALIAS_TRAVERSAL_JS_INTERFACE); + mCallback = null; } /** - * Ensures that the Web content key bindings are loaded. + * Returns the script injection preference requested by the URL, or + * {@link #ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED} if the page has no + * preference. + * + * @param url The URL to check. + * @return A script injection preference. */ - private void ensureWebContentKeyBindings() { - if (sBindings.size() > 0) { - return; + private int getAxsUrlParameterValue(String url) { + if (url == null) { + return ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED; } - String webContentKeyBindingsString = Settings.Secure.getString( - mWebView.getContext().getContentResolver(), - Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS); + try { + final List<NameValuePair> params = URLEncodedUtils.parse(new URI(url), null); - SimpleStringSplitter semiColonSplitter = new SimpleStringSplitter(';'); - semiColonSplitter.setString(webContentKeyBindingsString); - - while (semiColonSplitter.hasNext()) { - String bindingString = semiColonSplitter.next(); - if (TextUtils.isEmpty(bindingString)) { - Log.e(LOG_TAG, "Disregarding malformed Web content key binding: " - + webContentKeyBindingsString); - continue; - } - String[] keyValueArray = bindingString.split("="); - if (keyValueArray.length != 2) { - Log.e(LOG_TAG, "Disregarding malformed Web content key binding: " + bindingString); - continue; - } - try { - long keyCodeAndModifiers = Long.decode(keyValueArray[0].trim()); - String[] actionStrings = keyValueArray[1].split(":"); - int[] actions = new int[actionStrings.length]; - for (int i = 0, count = actions.length; i < count; i++) { - actions[i] = Integer.decode(actionStrings[i].trim()); + for (NameValuePair param : params) { + if ("axs".equals(param.getName())) { + return verifyInjectionValue(param.getValue()); } - sBindings.add(new AccessibilityWebContentKeyBinding(keyCodeAndModifiers, actions)); - } catch (NumberFormatException nfe) { - Log.e(LOG_TAG, "Disregarding malformed key binding: " + bindingString); } + } catch (URISyntaxException e) { + // Do nothing. } + + return ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED; } - private boolean isEnterActionKey(int keyCode) { - return keyCode == KeyEvent.KEYCODE_DPAD_CENTER - || keyCode == KeyEvent.KEYCODE_ENTER - || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER; + private int verifyInjectionValue(String value) { + try { + final int parsed = Integer.parseInt(value); + + switch (parsed) { + case ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT: + return ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT; + case ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED: + return ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED; + } + } catch (NumberFormatException e) { + // Do nothing. + } + + return ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED; } /** - * Represents a web content key-binding. + * @return The URL for injecting the screen reader. */ - private static final class AccessibilityWebContentKeyBinding { + private String getScreenReaderInjectionUrl() { + final String screenReaderUrl = Settings.Secure.getString( + mContext.getContentResolver(), Settings.Secure.ACCESSIBILITY_SCREEN_READER_URL); + return String.format(ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE, screenReaderUrl); + } - private static final int MODIFIERS_OFFSET = 32; - private static final long MODIFIERS_MASK = 0xFFFFFFF00000000L; + /** + * @return {@code true} if JavaScript is enabled in the {@link WebView} + * settings. + */ + private boolean isJavaScriptEnabled() { + return mWebView.getSettings().getJavaScriptEnabled(); + } - private static final int KEY_CODE_OFFSET = 0; - private static final long KEY_CODE_MASK = 0x00000000FFFFFFFFL; + /** + * @return {@code true} if accessibility is enabled. + */ + private boolean isAccessibilityEnabled() { + return mAccessibilityManager.isEnabled(); + } - private static final int ACTION_OFFSET = 24; - private static final int ACTION_MASK = 0xFF000000; + /** + * Packs an accessibility action into a JSON object and sends it to AndroidVox. + * + * @param action The action identifier. + * @param arguments The action arguments, if applicable. + * @return The result of the action. + */ + private boolean sendActionToAndroidVox(int action, Bundle arguments) { + if (mAccessibilityJSONObject == null) { + mAccessibilityJSONObject = new JSONObject(); + } else { + // Remove all keys from the object. + final Iterator<?> keys = mAccessibilityJSONObject.keys(); + while (keys.hasNext()) { + keys.next(); + keys.remove(); + } + } - private static final int FIRST_ARGUMENT_OFFSET = 16; - private static final int FIRST_ARGUMENT_MASK = 0x00FF0000; + try { + mAccessibilityJSONObject.accumulate("action", action); - private static final int SECOND_ARGUMENT_OFFSET = 8; - private static final int SECOND_ARGUMENT_MASK = 0x0000FF00; + switch (action) { + case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: + case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: + final int granularity = arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT); + mAccessibilityJSONObject.accumulate("granularity", granularity); + break; + case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT: + case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT: + final String element = arguments.getString( + AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING); + mAccessibilityJSONObject.accumulate("element", element); + break; + } + } catch (JSONException e) { + return false; + } - private static final int THIRD_ARGUMENT_OFFSET = 0; - private static final int THIRD_ARGUMENT_MASK = 0x000000FF; + final String jsonString = mAccessibilityJSONObject.toString(); + final String jsCode = String.format(ACCESSIBILITY_ANDROIDVOX_TEMPLATE, jsonString); + final String result = mCallback.performAction(mWebView, jsCode, DEFAULT_ANDROIDVOX_RESULT); - private final long mKeyCodeAndModifiers; + return ("true".equalsIgnoreCase(result)); + } - private final int [] mActionSequence; + /** + * Exposes result interface to JavaScript. + */ + private static class CallbackHandler { + private static final String JAVASCRIPT_ACTION_TEMPLATE = + "javascript:(function() { %s.onResult(%d, %s); })();"; - /** - * @return The key code of the binding key. - */ - public int getKeyCode() { - return (int) ((mKeyCodeAndModifiers & KEY_CODE_MASK) >> KEY_CODE_OFFSET); - } + // Time in milliseconds to wait for a result before failing. + private static final long RESULT_TIMEOUT = 200; - /** - * @return The meta state of the binding key. - */ - public int getModifiers() { - return (int) ((mKeyCodeAndModifiers & MODIFIERS_MASK) >> MODIFIERS_OFFSET); - } + private final AtomicInteger mResultIdCounter = new AtomicInteger(); + private final Object mResultLock = new Object(); + private final String mInterfaceName; - /** - * @return The number of actions in the key binding. - */ - public int getActionCount() { - return mActionSequence.length; - } + private String mResult = null; + private long mResultId = -1; - /** - * @param index The action for a given action <code>index</code>. - */ - public int getAction(int index) { - return mActionSequence[index]; + private CallbackHandler(String interfaceName) { + mInterfaceName = interfaceName; } /** - * @param index The action code for a given action <code>index</code>. + * Performs an action and attempts to wait for a result. + * + * @param webView The WebView to perform the action on. + * @param code JavaScript code that evaluates to a result. + * @param defaultResult The result to return if the action times out. + * @return The result of the action, or false if it timed out. */ - public int getActionCode(int index) { - return (mActionSequence[index] & ACTION_MASK) >> ACTION_OFFSET; + private String performAction(WebView webView, String code, String defaultResult) { + final int resultId = mResultIdCounter.getAndIncrement(); + final String url = String.format( + JAVASCRIPT_ACTION_TEMPLATE, mInterfaceName, resultId, code); + webView.loadUrl(url); + + return getResultAndClear(resultId, defaultResult); } /** - * @param index The first argument for a given action <code>index</code>. + * Gets the result of a request to perform an accessibility action. + * + * @param resultId The result id to match the result with the request. + * @param defaultResult The default result to return on timeout. + * @return The result of the request. */ - public int getFirstArgument(int index) { - return (mActionSequence[index] & FIRST_ARGUMENT_MASK) >> FIRST_ARGUMENT_OFFSET; + private String getResultAndClear(int resultId, String defaultResult) { + synchronized (mResultLock) { + final boolean success = waitForResultTimedLocked(resultId); + final String result = success ? mResult : defaultResult; + clearResultLocked(); + return result; + } } /** - * @param index The second argument for a given action <code>index</code>. + * Clears the result state. */ - public int getSecondArgument(int index) { - return (mActionSequence[index] & SECOND_ARGUMENT_MASK) >> SECOND_ARGUMENT_OFFSET; + private void clearResultLocked() { + mResultId = -1; + mResult = null; } /** - * @param index The third argument for a given action <code>index</code>. + * Waits up to a given bound for a result of a request and returns it. + * + * @param resultId The result id to match the result with the request. + * @return Whether the result was received. */ - public int getThirdArgument(int index) { - return (mActionSequence[index] & THIRD_ARGUMENT_MASK) >> THIRD_ARGUMENT_OFFSET; + private boolean waitForResultTimedLocked(int resultId) { + long waitTimeMillis = RESULT_TIMEOUT; + final long startTimeMillis = SystemClock.uptimeMillis(); + while (true) { + try { + if (mResultId == resultId) { + return true; + } + if (mResultId > resultId) { + return false; + } + final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; + waitTimeMillis = RESULT_TIMEOUT - elapsedTimeMillis; + if (waitTimeMillis <= 0) { + return false; + } + mResultLock.wait(waitTimeMillis); + } catch (InterruptedException ie) { + /* ignore */ + } + } } /** - * Creates a new instance. - * @param keyCodeAndModifiers The key for the binding (key and modifiers). - * @param actionSequence The sequence of action for the binding. + * Callback exposed to JavaScript. Handles returning the result of a + * request to a waiting (or potentially timed out) thread. + * + * @param id The result id of the request as a {@link String}. + * @param result The result of the request as a {@link String}. */ - public AccessibilityWebContentKeyBinding(long keyCodeAndModifiers, int[] actionSequence) { - mKeyCodeAndModifiers = keyCodeAndModifiers; - mActionSequence = actionSequence; - } + @SuppressWarnings("unused") + public void onResult(String id, String result) { + final long resultId; - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); - builder.append("modifiers: "); - builder.append(getModifiers()); - builder.append(", keyCode: "); - builder.append(getKeyCode()); - builder.append(", actions["); - for (int i = 0, count = getActionCount(); i < count; i++) { - builder.append("{actionCode"); - builder.append(i); - builder.append(": "); - builder.append(getActionCode(i)); - builder.append(", firstArgument: "); - builder.append(getFirstArgument(i)); - builder.append(", secondArgument: "); - builder.append(getSecondArgument(i)); - builder.append(", thirdArgument: "); - builder.append(getThirdArgument(i)); - builder.append("}"); + try { + resultId = Long.parseLong(id); + } catch (NumberFormatException e) { + return; + } + + synchronized (mResultLock) { + if (resultId > mResultId) { + mResult = result; + mResultId = resultId; + } + mResultLock.notifyAll(); } - builder.append("]"); - return builder.toString(); } } } diff --git a/core/java/android/webkit/AccessibilityInjectorFallback.java b/core/java/android/webkit/AccessibilityInjectorFallback.java new file mode 100644 index 0000000..4d9c26c --- /dev/null +++ b/core/java/android/webkit/AccessibilityInjectorFallback.java @@ -0,0 +1,575 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.webkit; + +import android.os.Bundle; +import android.provider.Settings; +import android.text.TextUtils; +import android.text.TextUtils.SimpleStringSplitter; +import android.util.Log; +import android.view.KeyEvent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.webkit.WebViewCore.EventHub; + +import java.util.ArrayList; +import java.util.Stack; + +/** + * This class injects accessibility into WebViews with disabled JavaScript or + * WebViews with enabled JavaScript but for which we have no accessibility + * script to inject. + * </p> + * Note: To avoid changes in the framework upon changing the available + * navigation axis, or reordering the navigation axis, or changing + * the key bindings, or defining sequence of actions to be bound to + * a given key this class is navigation axis agnostic. It is only + * aware of one navigation axis which is in fact the default behavior + * of webViews while using the DPAD/TrackBall. + * </p> + * In general a key binding is a mapping from modifiers + key code to + * a sequence of actions. For more detail how to specify key bindings refer to + * {@link android.provider.Settings.Secure#ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS}. + * </p> + * The possible actions are invocations to + * {@link #setCurrentAxis(int, boolean, String)}, or + * {@link #traverseCurrentAxis(int, boolean, String)} + * {@link #traverseGivenAxis(int, int, boolean, String)} + * {@link #performAxisTransition(int, int, boolean, String)} + * referred via the values of: + * {@link #ACTION_SET_CURRENT_AXIS}, + * {@link #ACTION_TRAVERSE_CURRENT_AXIS}, + * {@link #ACTION_TRAVERSE_GIVEN_AXIS}, + * {@link #ACTION_PERFORM_AXIS_TRANSITION}, + * respectively. + * The arguments for the action invocation are specified as offset + * hexademical pairs. Note the last argument of the invocation + * should NOT be specified in the binding as it is provided by + * this class. For details about the key binding implementation + * refer to {@link AccessibilityWebContentKeyBinding}. + */ +class AccessibilityInjectorFallback { + private static final String LOG_TAG = "AccessibilityInjector"; + + private static final boolean DEBUG = true; + + private static final int ACTION_SET_CURRENT_AXIS = 0; + private static final int ACTION_TRAVERSE_CURRENT_AXIS = 1; + private static final int ACTION_TRAVERSE_GIVEN_AXIS = 2; + private static final int ACTION_PERFORM_AXIS_TRANSITION = 3; + private static final int ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS = 4; + + // WebView navigation axes from WebViewCore.h, plus an additional axis for + // the default behavior. + private static final int NAVIGATION_AXIS_CHARACTER = 0; + private static final int NAVIGATION_AXIS_WORD = 1; + private static final int NAVIGATION_AXIS_SENTENCE = 2; + @SuppressWarnings("unused") + private static final int NAVIGATION_AXIS_HEADING = 3; + private static final int NAVIGATION_AXIS_SIBLING = 5; + @SuppressWarnings("unused") + private static final int NAVIGATION_AXIS_PARENT_FIRST_CHILD = 5; + private static final int NAVIGATION_AXIS_DOCUMENT = 6; + private static final int NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR = 7; + + // WebView navigation directions from WebViewCore.h. + private static final int NAVIGATION_DIRECTION_BACKWARD = 0; + private static final int NAVIGATION_DIRECTION_FORWARD = 1; + + // these are the same for all instances so make them process wide + private static ArrayList<AccessibilityWebContentKeyBinding> sBindings = + new ArrayList<AccessibilityWebContentKeyBinding>(); + + // handle to the WebViewClassic this injector is associated with. + private final WebViewClassic mWebView; + private final WebView mWebViewInternal; + + // events scheduled for sending as soon as we receive the selected text + private final Stack<AccessibilityEvent> mScheduledEventStack = new Stack<AccessibilityEvent>(); + + // the current traversal axis + private int mCurrentAxis = 2; // sentence + + // we need to consume the up if we have handled the last down + private boolean mLastDownEventHandled; + + // getting two empty selection strings in a row we let the WebView handle the event + private boolean mIsLastSelectionStringNull; + + // keep track of last direction + private int mLastDirection; + + /** + * Creates a new injector associated with a given {@link WebViewClassic}. + * + * @param webView The associated WebViewClassic. + */ + public AccessibilityInjectorFallback(WebViewClassic webView) { + mWebView = webView; + mWebViewInternal = mWebView.getWebView(); + ensureWebContentKeyBindings(); + } + + /** + * Processes a key down <code>event</code>. + * + * @return True if the event was processed. + */ + public boolean onKeyEvent(KeyEvent event) { + // We do not handle ENTER in any circumstances. + if (isEnterActionKey(event.getKeyCode())) { + return false; + } + + if (event.getAction() == KeyEvent.ACTION_UP) { + return mLastDownEventHandled; + } + + mLastDownEventHandled = false; + + AccessibilityWebContentKeyBinding binding = null; + for (AccessibilityWebContentKeyBinding candidate : sBindings) { + if (event.getKeyCode() == candidate.getKeyCode() + && event.hasModifiers(candidate.getModifiers())) { + binding = candidate; + break; + } + } + + if (binding == null) { + return false; + } + + for (int i = 0, count = binding.getActionCount(); i < count; i++) { + int actionCode = binding.getActionCode(i); + String contentDescription = Integer.toHexString(binding.getAction(i)); + switch (actionCode) { + case ACTION_SET_CURRENT_AXIS: + int axis = binding.getFirstArgument(i); + boolean sendEvent = (binding.getSecondArgument(i) == 1); + setCurrentAxis(axis, sendEvent, contentDescription); + mLastDownEventHandled = true; + break; + case ACTION_TRAVERSE_CURRENT_AXIS: + int direction = binding.getFirstArgument(i); + // on second null selection string in same direction - WebView handles the event + if (direction == mLastDirection && mIsLastSelectionStringNull) { + mIsLastSelectionStringNull = false; + return false; + } + mLastDirection = direction; + sendEvent = (binding.getSecondArgument(i) == 1); + mLastDownEventHandled = traverseCurrentAxis(direction, sendEvent, + contentDescription); + break; + case ACTION_TRAVERSE_GIVEN_AXIS: + direction = binding.getFirstArgument(i); + // on second null selection string in same direction => WebView handle the event + if (direction == mLastDirection && mIsLastSelectionStringNull) { + mIsLastSelectionStringNull = false; + return false; + } + mLastDirection = direction; + axis = binding.getSecondArgument(i); + sendEvent = (binding.getThirdArgument(i) == 1); + traverseGivenAxis(direction, axis, sendEvent, contentDescription); + mLastDownEventHandled = true; + break; + case ACTION_PERFORM_AXIS_TRANSITION: + int fromAxis = binding.getFirstArgument(i); + int toAxis = binding.getSecondArgument(i); + sendEvent = (binding.getThirdArgument(i) == 1); + performAxisTransition(fromAxis, toAxis, sendEvent, contentDescription); + mLastDownEventHandled = true; + break; + case ACTION_TRAVERSE_DEFAULT_WEB_VIEW_BEHAVIOR_AXIS: + // This is a special case since we treat the default WebView navigation + // behavior as one of the possible navigation axis the user can use. + // If we are not on the default WebView navigation axis this is NOP. + if (mCurrentAxis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) { + // While WebVew handles navigation we do not get null selection + // strings so do not check for that here as the cases above. + mLastDirection = binding.getFirstArgument(i); + sendEvent = (binding.getSecondArgument(i) == 1); + traverseGivenAxis(mLastDirection, NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR, + sendEvent, contentDescription); + mLastDownEventHandled = false; + } else { + mLastDownEventHandled = true; + } + break; + default: + Log.w(LOG_TAG, "Unknown action code: " + actionCode); + } + } + + return mLastDownEventHandled; + } + + /** + * Set the current navigation axis which will be used while + * calling {@link #traverseCurrentAxis(int, boolean, String)}. + * + * @param axis The axis to set. + * @param sendEvent Whether to send an accessibility event to + * announce the change. + */ + private void setCurrentAxis(int axis, boolean sendEvent, String contentDescription) { + mCurrentAxis = axis; + if (sendEvent) { + final AccessibilityEvent event = getPartialyPopulatedAccessibilityEvent( + AccessibilityEvent.TYPE_ANNOUNCEMENT); + event.getText().add(String.valueOf(axis)); + event.setContentDescription(contentDescription); + sendAccessibilityEvent(event); + } + } + + /** + * Performs conditional transition one axis to another. + * + * @param fromAxis The axis which must be the current for the transition to occur. + * @param toAxis The axis to which to transition. + * @param sendEvent Flag if to send an event to announce successful transition. + * @param contentDescription A description of the performed action. + */ + private void performAxisTransition(int fromAxis, int toAxis, boolean sendEvent, + String contentDescription) { + if (mCurrentAxis == fromAxis) { + setCurrentAxis(toAxis, sendEvent, contentDescription); + } + } + + /** + * Traverse the document along the current navigation axis. + * + * @param direction The direction of traversal. + * @param sendEvent Whether to send an accessibility event to + * announce the change. + * @param contentDescription A description of the performed action. + * @see #setCurrentAxis(int, boolean, String) + */ + private boolean traverseCurrentAxis(int direction, boolean sendEvent, + String contentDescription) { + return traverseGivenAxis(direction, mCurrentAxis, sendEvent, contentDescription); + } + + boolean performAccessibilityAction(int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: + case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: + final int direction = getDirectionForAction(action); + final int axis = getAxisForGranularity(arguments.getInt( + AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT)); + return traverseGivenAxis(direction, axis, true, null); + default: + return false; + } + } + + /** + * Returns the {@link WebView}-defined direction for the given + * {@link AccessibilityNodeInfo}-defined action. + * + * @param action An accessibility action identifier. + * @return A web view navigation direction. + */ + private static int getDirectionForAction(int action) { + switch (action) { + case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY: + return NAVIGATION_DIRECTION_FORWARD; + case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: + return NAVIGATION_DIRECTION_BACKWARD; + default: + return -1; + } + } + + /** + * Returns the {@link WebView}-defined axis for the given + * {@link AccessibilityNodeInfo}-defined granularity. + * + * @param granularity An accessibility granularity identifier. + * @return A web view navigation axis. + */ + private static int getAxisForGranularity(int granularity) { + switch (granularity) { + case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER: + return NAVIGATION_AXIS_CHARACTER; + case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD: + return NAVIGATION_AXIS_WORD; + case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE: + return NAVIGATION_AXIS_SENTENCE; + case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH: + // TODO: Figure out what nextSibling() actually means. + return NAVIGATION_AXIS_SIBLING; + case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE: + return NAVIGATION_AXIS_DOCUMENT; + default: + return -1; + } + } + + /** + * Traverse the document along the given navigation axis. + * + * @param direction The direction of traversal. + * @param axis The axis along which to traverse. + * @param sendEvent Whether to send an accessibility event to + * announce the change. + * @param contentDescription A description of the performed action. + */ + private boolean traverseGivenAxis(int direction, int axis, boolean sendEvent, + String contentDescription) { + WebViewCore webViewCore = mWebView.getWebViewCore(); + if (webViewCore == null) { + return false; + } + + AccessibilityEvent event = null; + if (sendEvent) { + event = getPartialyPopulatedAccessibilityEvent( + AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); + // the text will be set upon receiving the selection string + event.setContentDescription(contentDescription); + } + mScheduledEventStack.push(event); + + // if the axis is the default let WebView handle the event which will + // result in cursor ring movement and selection of its content + if (axis == NAVIGATION_AXIS_DEFAULT_WEB_VIEW_BEHAVIOR) { + return false; + } + + webViewCore.sendMessage(EventHub.MODIFY_SELECTION, direction, axis); + return true; + } + + /** + * Called when the <code>selectionString</code> has changed. + */ + public void onSelectionStringChange(String selectionString) { + if (DEBUG) { + Log.d(LOG_TAG, "Selection string: " + selectionString); + } + mIsLastSelectionStringNull = (selectionString == null); + if (mScheduledEventStack.isEmpty()) { + return; + } + AccessibilityEvent event = mScheduledEventStack.pop(); + if ((event != null) && (selectionString != null)) { + event.getText().add(selectionString); + event.setFromIndex(0); + event.setToIndex(selectionString.length()); + sendAccessibilityEvent(event); + } + } + + /** + * Sends an {@link AccessibilityEvent}. + * + * @param event The event to send. + */ + private void sendAccessibilityEvent(AccessibilityEvent event) { + if (DEBUG) { + Log.d(LOG_TAG, "Dispatching: " + event); + } + // accessibility may be disabled while waiting for the selection string + AccessibilityManager accessibilityManager = + AccessibilityManager.getInstance(mWebView.getContext()); + if (accessibilityManager.isEnabled()) { + accessibilityManager.sendAccessibilityEvent(event); + } + } + + /** + * @return An accessibility event whose members are populated except its + * text and content description. + */ + private AccessibilityEvent getPartialyPopulatedAccessibilityEvent(int eventType) { + AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + mWebViewInternal.onInitializeAccessibilityEvent(event); + return event; + } + + /** + * Ensures that the Web content key bindings are loaded. + */ + private void ensureWebContentKeyBindings() { + if (sBindings.size() > 0) { + return; + } + + String webContentKeyBindingsString = Settings.Secure.getString( + mWebView.getContext().getContentResolver(), + Settings.Secure.ACCESSIBILITY_WEB_CONTENT_KEY_BINDINGS); + + SimpleStringSplitter semiColonSplitter = new SimpleStringSplitter(';'); + semiColonSplitter.setString(webContentKeyBindingsString); + + while (semiColonSplitter.hasNext()) { + String bindingString = semiColonSplitter.next(); + if (TextUtils.isEmpty(bindingString)) { + Log.e(LOG_TAG, "Disregarding malformed Web content key binding: " + + webContentKeyBindingsString); + continue; + } + String[] keyValueArray = bindingString.split("="); + if (keyValueArray.length != 2) { + Log.e(LOG_TAG, "Disregarding malformed Web content key binding: " + bindingString); + continue; + } + try { + long keyCodeAndModifiers = Long.decode(keyValueArray[0].trim()); + String[] actionStrings = keyValueArray[1].split(":"); + int[] actions = new int[actionStrings.length]; + for (int i = 0, count = actions.length; i < count; i++) { + actions[i] = Integer.decode(actionStrings[i].trim()); + } + sBindings.add(new AccessibilityWebContentKeyBinding(keyCodeAndModifiers, actions)); + } catch (NumberFormatException nfe) { + Log.e(LOG_TAG, "Disregarding malformed key binding: " + bindingString); + } + } + } + + private boolean isEnterActionKey(int keyCode) { + return keyCode == KeyEvent.KEYCODE_DPAD_CENTER + || keyCode == KeyEvent.KEYCODE_ENTER + || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER; + } + + /** + * Represents a web content key-binding. + */ + private static final class AccessibilityWebContentKeyBinding { + + private static final int MODIFIERS_OFFSET = 32; + private static final long MODIFIERS_MASK = 0xFFFFFFF00000000L; + + private static final int KEY_CODE_OFFSET = 0; + private static final long KEY_CODE_MASK = 0x00000000FFFFFFFFL; + + private static final int ACTION_OFFSET = 24; + private static final int ACTION_MASK = 0xFF000000; + + private static final int FIRST_ARGUMENT_OFFSET = 16; + private static final int FIRST_ARGUMENT_MASK = 0x00FF0000; + + private static final int SECOND_ARGUMENT_OFFSET = 8; + private static final int SECOND_ARGUMENT_MASK = 0x0000FF00; + + private static final int THIRD_ARGUMENT_OFFSET = 0; + private static final int THIRD_ARGUMENT_MASK = 0x000000FF; + + private final long mKeyCodeAndModifiers; + + private final int [] mActionSequence; + + /** + * @return The key code of the binding key. + */ + public int getKeyCode() { + return (int) ((mKeyCodeAndModifiers & KEY_CODE_MASK) >> KEY_CODE_OFFSET); + } + + /** + * @return The meta state of the binding key. + */ + public int getModifiers() { + return (int) ((mKeyCodeAndModifiers & MODIFIERS_MASK) >> MODIFIERS_OFFSET); + } + + /** + * @return The number of actions in the key binding. + */ + public int getActionCount() { + return mActionSequence.length; + } + + /** + * @param index The action for a given action <code>index</code>. + */ + public int getAction(int index) { + return mActionSequence[index]; + } + + /** + * @param index The action code for a given action <code>index</code>. + */ + public int getActionCode(int index) { + return (mActionSequence[index] & ACTION_MASK) >> ACTION_OFFSET; + } + + /** + * @param index The first argument for a given action <code>index</code>. + */ + public int getFirstArgument(int index) { + return (mActionSequence[index] & FIRST_ARGUMENT_MASK) >> FIRST_ARGUMENT_OFFSET; + } + + /** + * @param index The second argument for a given action <code>index</code>. + */ + public int getSecondArgument(int index) { + return (mActionSequence[index] & SECOND_ARGUMENT_MASK) >> SECOND_ARGUMENT_OFFSET; + } + + /** + * @param index The third argument for a given action <code>index</code>. + */ + public int getThirdArgument(int index) { + return (mActionSequence[index] & THIRD_ARGUMENT_MASK) >> THIRD_ARGUMENT_OFFSET; + } + + /** + * Creates a new instance. + * @param keyCodeAndModifiers The key for the binding (key and modifiers). + * @param actionSequence The sequence of action for the binding. + */ + public AccessibilityWebContentKeyBinding(long keyCodeAndModifiers, int[] actionSequence) { + mKeyCodeAndModifiers = keyCodeAndModifiers; + mActionSequence = actionSequence; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("modifiers: "); + builder.append(getModifiers()); + builder.append(", keyCode: "); + builder.append(getKeyCode()); + builder.append(", actions["); + for (int i = 0, count = getActionCount(); i < count; i++) { + builder.append("{actionCode"); + builder.append(i); + builder.append(": "); + builder.append(getActionCode(i)); + builder.append(", firstArgument: "); + builder.append(getFirstArgument(i)); + builder.append(", secondArgument: "); + builder.append(getSecondArgument(i)); + builder.append(", thirdArgument: "); + builder.append(getThirdArgument(i)); + builder.append("}"); + } + builder.append("]"); + return builder.toString(); + } + } +} diff --git a/core/java/android/webkit/WebCoreThreadWatchdog.java b/core/java/android/webkit/WebCoreThreadWatchdog.java index 655db31..a22e6e8 100644 --- a/core/java/android/webkit/WebCoreThreadWatchdog.java +++ b/core/java/android/webkit/WebCoreThreadWatchdog.java @@ -26,6 +26,10 @@ import android.os.Message; import android.os.Process; import android.webkit.WebViewCore.EventHub; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + // A Runnable that will monitor if the WebCore thread is still // processing messages by pinging it every so often. It is safe // to call the public methods of this class from any thread. @@ -51,25 +55,31 @@ class WebCoreThreadWatchdog implements Runnable { // After the first timeout, use a shorter period before re-prompting the user. private static final int SUBSEQUENT_TIMEOUT_PERIOD = 15 * 1000; - private Context mContext; private Handler mWebCoreThreadHandler; private Handler mHandler; private boolean mPaused; + private Set<WebViewClassic> mWebViews; + private static WebCoreThreadWatchdog sInstance; - public synchronized static WebCoreThreadWatchdog start(Context context, - Handler webCoreThreadHandler) { + public synchronized static WebCoreThreadWatchdog start(Handler webCoreThreadHandler) { if (sInstance == null) { - sInstance = new WebCoreThreadWatchdog(context, webCoreThreadHandler); + sInstance = new WebCoreThreadWatchdog(webCoreThreadHandler); new Thread(sInstance, "WebCoreThreadWatchdog").start(); } return sInstance; } - public synchronized static void updateContext(Context context) { + public synchronized static void registerWebView(WebViewClassic w) { if (sInstance != null) { - sInstance.setContext(context); + sInstance.addWebView(w); + } + } + + public synchronized static void unregisterWebView(WebViewClassic w) { + if (sInstance != null) { + sInstance.removeWebView(w); } } @@ -85,12 +95,18 @@ class WebCoreThreadWatchdog implements Runnable { } } - private void setContext(Context context) { - mContext = context; + private void addWebView(WebViewClassic w) { + if (mWebViews == null) { + mWebViews = new HashSet<WebViewClassic>(); + } + mWebViews.add(w); } - private WebCoreThreadWatchdog(Context context, Handler webCoreThreadHandler) { - mContext = context; + private void removeWebView(WebViewClassic w) { + mWebViews.remove(w); + } + + private WebCoreThreadWatchdog(Handler webCoreThreadHandler) { mWebCoreThreadHandler = webCoreThreadHandler; } @@ -147,39 +163,41 @@ class WebCoreThreadWatchdog implements Runnable { break; case TIMED_OUT: - if ((mContext == null) || !(mContext instanceof Activity)) return; - new AlertDialog.Builder(mContext) - .setMessage(com.android.internal.R.string.webpage_unresponsive) - .setPositiveButton(com.android.internal.R.string.force_close, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // User chose to force close. - Process.killProcess(Process.myPid()); - } - }) - .setNegativeButton(com.android.internal.R.string.wait, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - // The user chose to wait. The last HEARTBEAT message - // will still be in the WebCore thread's queue, so all - // we need to do is post another TIMED_OUT so that the - // user will get prompted again if the WebCore thread - // doesn't sort itself out. - sendMessageDelayed(obtainMessage(TIMED_OUT), - SUBSEQUENT_TIMEOUT_PERIOD); - } - }) - .setOnCancelListener(new DialogInterface.OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - sendMessageDelayed(obtainMessage(TIMED_OUT), - SUBSEQUENT_TIMEOUT_PERIOD); + boolean postedDialog = false; + synchronized (WebCoreThreadWatchdog.class) { + Iterator<WebViewClassic> it = mWebViews.iterator(); + // Check each WebView we are aware of and find one that is capable of + // showing the user a prompt dialog. + while (it.hasNext()) { + WebView activeView = it.next().getWebView(); + + if (activeView.getWindowToken() != null && + activeView.getViewRootImpl() != null) { + postedDialog = activeView.post(new PageNotRespondingRunnable( + activeView.getContext(), this)); + + if (postedDialog) { + // We placed the message into the UI thread for an attached + // WebView so we've made our best attempt to display the + // "page not responding" dialog to the user. Although the + // message is in the queue, there is no guarantee when/if + // the runnable will execute. In the case that the runnable + // never executes, the user will need to terminate the + // process manually. + break; } - }) - .setIcon(android.R.drawable.ic_dialog_alert) - .show(); + } + } + + if (!postedDialog) { + // There's no active webview we can use to show the dialog, so + // wait again. If we never get a usable view, the user will + // never get the chance to terminate the process, and will + // need to do it manually. + sendMessageDelayed(obtainMessage(TIMED_OUT), + SUBSEQUENT_TIMEOUT_PERIOD); + } + } break; } } @@ -205,4 +223,55 @@ class WebCoreThreadWatchdog implements Runnable { Looper.loop(); } + + private class PageNotRespondingRunnable implements Runnable { + Context mContext; + private Handler mWatchdogHandler; + + public PageNotRespondingRunnable(Context context, Handler watchdogHandler) { + mContext = context; + mWatchdogHandler = watchdogHandler; + } + + @Override + public void run() { + // This must run on the UI thread as it is displaying an AlertDialog. + assert Looper.getMainLooper().getThread() == Thread.currentThread(); + new AlertDialog.Builder(mContext) + .setMessage(com.android.internal.R.string.webpage_unresponsive) + .setPositiveButton(com.android.internal.R.string.force_close, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // User chose to force close. + Process.killProcess(Process.myPid()); + } + }) + .setNegativeButton(com.android.internal.R.string.wait, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // The user chose to wait. The last HEARTBEAT message + // will still be in the WebCore thread's queue, so all + // we need to do is post another TIMED_OUT so that the + // user will get prompted again if the WebCore thread + // doesn't sort itself out. + mWatchdogHandler.sendMessageDelayed( + mWatchdogHandler.obtainMessage(TIMED_OUT), + SUBSEQUENT_TIMEOUT_PERIOD); + } + }) + .setOnCancelListener( + new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + mWatchdogHandler.sendMessageDelayed( + mWatchdogHandler.obtainMessage(TIMED_OUT), + SUBSEQUENT_TIMEOUT_PERIOD); + } + }) + .setIcon(android.R.drawable.ic_dialog_alert) + .show(); + } + } } diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index ba5a417..cbb3011 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -1686,6 +1686,10 @@ public class WebView extends AbsoluteLayout WebView.super.computeScroll(); } + public boolean super_performAccessibilityAction(int action, Bundle arguments) { + return WebView.super.performAccessibilityAction(action, arguments); + } + public boolean super_performLongClick() { return WebView.super.performLongClick(); } @@ -1938,6 +1942,11 @@ public class WebView extends AbsoluteLayout mProvider.getViewDelegate().onInitializeAccessibilityEvent(event); } + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + return mProvider.getViewDelegate().performAccessibilityAction(action, arguments); + } + /** @hide */ @Override protected void onDrawVerticalScrollBar(Canvas canvas, Drawable scrollBar, diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index 7786564..7f43552 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -60,9 +60,7 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.SystemClock; -import android.provider.Settings; import android.security.KeyChain; -import android.speech.tts.TextToSpeech; import android.text.Editable; import android.text.InputType; import android.text.Selection; @@ -102,7 +100,6 @@ import android.webkit.WebViewCore.DrawData; import android.webkit.WebViewCore.EventHub; import android.webkit.WebViewCore.TextFieldInitData; import android.webkit.WebViewCore.TextSelectionData; -import android.webkit.WebViewCore.TouchHighlightData; import android.webkit.WebViewCore.WebKitHitTest; import android.widget.AbsoluteLayout; import android.widget.Adapter; @@ -276,6 +273,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc newCursorPosition -= text.length() - limitedText.length(); } super.setComposingText(limitedText, newCursorPosition); + updateSelection(); if (limitedText != text) { restartInput(); int lastCaret = start + limitedText.length(); @@ -288,19 +286,44 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc @Override public boolean commitText(CharSequence text, int newCursorPosition) { setComposingText(text, newCursorPosition); - int cursorPosition = Selection.getSelectionEnd(getEditable()); - setComposingRegion(cursorPosition, cursorPosition); + finishComposingText(); return true; } @Override public boolean deleteSurroundingText(int leftLength, int rightLength) { - Editable editable = getEditable(); - int cursorPosition = Selection.getSelectionEnd(editable); - int startDelete = Math.max(0, cursorPosition - leftLength); - int endDelete = Math.min(editable.length(), - cursorPosition + rightLength); - setNewText(startDelete, endDelete, ""); + // This code is from BaseInputConnection#deleteSurroundText. + // We have to delete the same text in webkit. + Editable content = getEditable(); + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + if (a > b) { + int tmp = a; + a = b; + b = tmp; + } + + int ca = getComposingSpanStart(content); + int cb = getComposingSpanEnd(content); + if (cb < ca) { + int tmp = ca; + ca = cb; + cb = tmp; + } + if (ca != -1 && cb != -1) { + if (ca < a) a = ca; + if (cb > b) b = cb; + } + + int endDelete = Math.min(content.length(), b + rightLength); + if (endDelete > b) { + setNewText(b, endDelete, ""); + } + int startDelete = Math.max(0, a - leftLength); + if (startDelete < a) { + setNewText(startDelete, a, ""); + } return super.deleteSurroundingText(leftLength, rightLength); } @@ -413,6 +436,46 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc outAttrs.imeOptions = mImeOptions; outAttrs.hintText = mHint; outAttrs.initialCapsMode = getCursorCapsMode(InputType.TYPE_CLASS_TEXT); + + Editable editable = getEditable(); + int selectionStart = Selection.getSelectionStart(editable); + int selectionEnd = Selection.getSelectionEnd(editable); + if (selectionStart < 0 || selectionEnd < 0) { + selectionStart = editable.length(); + selectionEnd = selectionStart; + } + outAttrs.initialSelStart = selectionStart; + outAttrs.initialSelEnd = selectionEnd; + } + + @Override + public boolean setSelection(int start, int end) { + boolean result = super.setSelection(start, end); + updateSelection(); + return result; + } + + @Override + public boolean setComposingRegion(int start, int end) { + boolean result = super.setComposingRegion(start, end); + updateSelection(); + return result; + } + + /** + * Send the selection and composing spans to the IME. + */ + private void updateSelection() { + Editable editable = getEditable(); + int selectionStart = Selection.getSelectionStart(editable); + int selectionEnd = Selection.getSelectionEnd(editable); + int composingStart = getComposingSpanStart(editable); + int composingEnd = getComposingSpanEnd(editable); + InputMethodManager imm = InputMethodManager.peekInstance(); + if (imm != null) { + imm.updateSelection(mWebView, selectionStart, selectionEnd, + composingStart, composingEnd); + } } /** @@ -431,14 +494,18 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc boolean isCharacterDelete = false; int textLength = text.length(); int originalLength = original.length(); - if (textLength > originalLength) { - isCharacterAdd = (textLength == originalLength + 1) - && TextUtils.regionMatches(text, 0, original, 0, - originalLength); - } else if (originalLength > textLength) { - isCharacterDelete = (textLength == originalLength - 1) - && TextUtils.regionMatches(text, 0, original, 0, - textLength); + int selectionStart = Selection.getSelectionStart(editable); + int selectionEnd = Selection.getSelectionEnd(editable); + if (selectionStart == selectionEnd) { + if (textLength > originalLength) { + isCharacterAdd = (textLength == originalLength + 1) + && TextUtils.regionMatches(text, 0, original, 0, + originalLength); + } else if (originalLength > textLength) { + isCharacterDelete = (textLength == originalLength - 1) + && TextUtils.regionMatches(text, 0, original, 0, + textLength); + } } if (isCharacterAdd) { sendCharacter(text.charAt(textLength - 1)); @@ -867,15 +934,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc private static final int MOTIONLESS_IGNORE = 3; private int mHeldMotionless; - // An instance for injecting accessibility in WebViews with disabled - // JavaScript or ones for which no accessibility script exists + // Lazily-instantiated instance for injecting accessibility. private AccessibilityInjector mAccessibilityInjector; - // flag indicating if accessibility script is injected so we - // know to handle Shift and arrows natively first - private boolean mAccessibilityScriptInjected; - - /** * How long the caret handle will last without being touched. */ @@ -1084,34 +1145,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc private int mHorizontalScrollBarMode = SCROLLBAR_AUTO; private int mVerticalScrollBarMode = SCROLLBAR_AUTO; - // constants for determining script injection strategy - private static final int ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED = -1; - private static final int ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT = 0; - private static final int ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED = 1; - - // the alias via which accessibility JavaScript interface is exposed - private static final String ALIAS_ACCESSIBILITY_JS_INTERFACE = "accessibility"; - - // Template for JavaScript that injects a screen-reader. - private static final String ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE = - "javascript:(function() {" + - " var chooser = document.createElement('script');" + - " chooser.type = 'text/javascript';" + - " chooser.src = '%1s';" + - " document.getElementsByTagName('head')[0].appendChild(chooser);" + - " })();"; - - // Regular expression that matches the "axs" URL parameter. - // The value of 0 means the accessibility script is opted out - // The value of 1 means the accessibility script is already injected - private static final String PATTERN_MATCH_AXS_URL_PARAMETER = "(\\?axs=(0|1))|(&axs=(0|1))"; - - // TextToSpeech instance exposed to JavaScript to the injected screenreader. - private TextToSpeech mTextToSpeech; - - // variable to cache the above pattern in case accessibility is enabled. - private Pattern mMatchAxsUrlParameterPattern; - /** * Max distance to overscroll by in pixels. * This how far content can be pulled beyond its normal bounds by the user. @@ -1630,43 +1663,66 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc return true; } - /** - * Adds accessibility APIs to JavaScript. - * - * Note: This method is responsible to performing the necessary - * check if the accessibility APIs should be exposed. - */ - private void addAccessibilityApisToJavaScript() { - if (AccessibilityManager.getInstance(mContext).isEnabled() - && getSettings().getJavaScriptEnabled()) { - // exposing the TTS for now ... - final Context ctx = mContext; - if (ctx != null) { - final String packageName = ctx.getPackageName(); - if (packageName != null) { - mTextToSpeech = new TextToSpeech(ctx, null, null, - packageName + ".**webview**", true); - addJavascriptInterface(mTextToSpeech, ALIAS_ACCESSIBILITY_JS_INTERFACE); + @Override + public boolean performAccessibilityAction(int action, Bundle arguments) { + if (!mWebView.isEnabled()) { + // Only default actions are supported while disabled. + return mWebViewPrivate.super_performAccessibilityAction(action, arguments); + } + + if (mAccessibilityInjector.supportsAccessibilityAction(action)) { + return mAccessibilityInjector.performAccessibilityAction(action, arguments); + } + + switch (action) { + case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: + case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: { + final int convertedContentHeight = contentToViewY(getContentHeight()); + final int adjustedViewHeight = getHeight() - mWebView.getPaddingTop() + - mWebView.getPaddingBottom(); + final int maxScrollY = Math.max(convertedContentHeight - adjustedViewHeight, 0); + final boolean canScrollBackward = (getScrollY() > 0); + final boolean canScrollForward = ((getScrollY() - maxScrollY) > 0); + if ((action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) && canScrollBackward) { + mWebView.scrollBy(0, adjustedViewHeight); + return true; + } + if ((action == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD) && canScrollForward) { + mWebView.scrollBy(0, -adjustedViewHeight); + return true; } + return false; } } - } - /** - * Removes accessibility APIs from JavaScript. - */ - private void removeAccessibilityApisFromJavaScript() { - // exposing the TTS for now ... - if (mTextToSpeech != null) { - removeJavascriptInterface(ALIAS_ACCESSIBILITY_JS_INTERFACE); - mTextToSpeech.shutdown(); - mTextToSpeech = null; - } + return mWebViewPrivate.super_performAccessibilityAction(action, arguments); } @Override public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { + if (!mWebView.isEnabled()) { + // Only default actions are supported while disabled. + return; + } + info.setScrollable(isScrollableForAccessibility()); + + final int convertedContentHeight = contentToViewY(getContentHeight()); + final int adjustedViewHeight = getHeight() - mWebView.getPaddingTop() + - mWebView.getPaddingBottom(); + final int maxScrollY = Math.max(convertedContentHeight - adjustedViewHeight, 0); + final boolean canScrollBackward = (getScrollY() > 0); + final boolean canScrollForward = ((getScrollY() - maxScrollY) > 0); + + if (canScrollForward) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD); + } + + if (canScrollForward) { + info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD); + } + + mAccessibilityInjector.onInitializeAccessibilityNodeInfo(info); } @Override @@ -1684,6 +1740,17 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc event.setMaxScrollY(Math.max(convertedContentHeight - adjustedViewHeight, 0)); } + private boolean isAccessibilityEnabled() { + return AccessibilityManager.getInstance(mContext).isEnabled(); + } + + private AccessibilityInjector getAccessibilityInjector() { + if (mAccessibilityInjector == null) { + mAccessibilityInjector = new AccessibilityInjector(this); + } + return mAccessibilityInjector; + } + private boolean isScrollableForAccessibility() { return (contentToViewX(getContentWidth()) > getWidth() - mWebView.getPaddingLeft() - mWebView.getPaddingRight() @@ -3365,11 +3432,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc nativeSetPauseDrawing(mNativeClass, false); } } - // Ensure that the watchdog has a currently valid Context to be able to display - // a prompt dialog. For example, if the Activity was finished whilst the WebCore - // thread was blocked and the Activity is started again, we may reuse the blocked - // thread, but we'll have a new Activity. - WebCoreThreadWatchdog.updateContext(mContext); // We get a call to onResume for new WebViews (i.e. mIsPaused will be false). We need // to ensure that the Watchdog thread is running for the new WebView, so call // it outside the if block above. @@ -3824,7 +3886,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // reset the flag since we set to true in if need after // loading is see onPageFinished(Url) - mAccessibilityScriptInjected = false; + if (isAccessibilityEnabled()) { + getAccessibilityInjector().onPageStarted(url); + } // Don't start out editing. mIsEditingText = false; @@ -3836,114 +3900,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc */ /* package */ void onPageFinished(String url) { mZoomManager.onPageFinished(url); - injectAccessibilityForUrl(url); - } - - /** - * This method injects accessibility in the loaded document if accessibility - * is enabled. If JavaScript is enabled we try to inject a URL specific script. - * If no URL specific script is found or JavaScript is disabled we fallback to - * the default {@link AccessibilityInjector} implementation. - * </p> - * If the URL has the "axs" paramter set to 1 it has already done the - * script injection so we do nothing. If the parameter is set to 0 - * the URL opts out accessibility script injection so we fall back to - * the default {@link AccessibilityInjector}. - * </p> - * Note: If the user has not opted-in the accessibility script injection no scripts - * are injected rather the default {@link AccessibilityInjector} implementation - * is used. - * - * @param url The URL loaded by this {@link WebView}. - */ - private void injectAccessibilityForUrl(String url) { - if (mWebViewCore == null) { - return; - } - AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(mContext); - - if (!accessibilityManager.isEnabled()) { - // it is possible that accessibility was turned off between reloads - ensureAccessibilityScriptInjectorInstance(false); - return; - } - - if (!getSettings().getJavaScriptEnabled()) { - // no JS so we fallback to the basic buil-in support - ensureAccessibilityScriptInjectorInstance(true); - return; - } - - // check the URL "axs" parameter to choose appropriate action - int axsParameterValue = getAxsUrlParameterValue(url); - if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_UNDEFINED) { - boolean onDeviceScriptInjectionEnabled = (Settings.Secure.getInt(mContext - .getContentResolver(), Settings.Secure.ACCESSIBILITY_SCRIPT_INJECTION, 0) == 1); - if (onDeviceScriptInjectionEnabled) { - ensureAccessibilityScriptInjectorInstance(false); - // neither script injected nor script injection opted out => we inject - mWebView.loadUrl(getScreenReaderInjectingJs()); - // TODO: Set this flag after successfull script injection. Maybe upon injection - // the chooser should update the meta tag and we check it to declare success - mAccessibilityScriptInjected = true; - } else { - // injection disabled so we fallback to the basic built-in support - ensureAccessibilityScriptInjectorInstance(true); - } - } else if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_OPTED_OUT) { - // injection opted out so we fallback to the basic buil-in support - ensureAccessibilityScriptInjectorInstance(true); - } else if (axsParameterValue == ACCESSIBILITY_SCRIPT_INJECTION_PROVIDED) { - ensureAccessibilityScriptInjectorInstance(false); - // the URL provides accessibility but we still need to add our generic script - mWebView.loadUrl(getScreenReaderInjectingJs()); - } else { - Log.e(LOGTAG, "Unknown URL value for the \"axs\" URL parameter: " + axsParameterValue); - } - } - - /** - * Ensures the instance of the {@link AccessibilityInjector} to be present ot not. - * - * @param present True to ensure an insance, false to ensure no instance. - */ - private void ensureAccessibilityScriptInjectorInstance(boolean present) { - if (present) { - if (mAccessibilityInjector == null) { - mAccessibilityInjector = new AccessibilityInjector(this); - } - } else { - mAccessibilityInjector = null; - } - } - - /** - * Gets JavaScript that injects a screen-reader. - * - * @return The JavaScript snippet. - */ - private String getScreenReaderInjectingJs() { - String screenReaderUrl = Settings.Secure.getString(mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_SCREEN_READER_URL); - return String.format(ACCESSIBILITY_SCREEN_READER_JAVASCRIPT_TEMPLATE, screenReaderUrl); - } - /** - * Gets the "axs" URL parameter value. - * - * @param url A url to fetch the paramter from. - * @return The parameter value if such, -1 otherwise. - */ - private int getAxsUrlParameterValue(String url) { - if (mMatchAxsUrlParameterPattern == null) { - mMatchAxsUrlParameterPattern = Pattern.compile(PATTERN_MATCH_AXS_URL_PARAMETER); - } - Matcher matcher = mMatchAxsUrlParameterPattern.matcher(url); - if (matcher.find()) { - String keyValuePair = url.substring(matcher.start(), matcher.end()); - return Integer.parseInt(keyValuePair.split("=")[1]); + if (isAccessibilityEnabled()) { + getAccessibilityInjector().onPageFinished(url); } - return -1; } // scale from content to view coordinates, and pin @@ -4901,30 +4861,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc return false; } - // accessibility support - if (accessibilityScriptInjected()) { - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - // if an accessibility script is injected we delegate to it the key handling. - // this script is a screen reader which is a fully fledged solution for blind - // users to navigate in and interact with web pages. - sendBatchableInputMessage(EventHub.KEY_DOWN, 0, 0, event); - return true; - } else { - // Clean up if accessibility was disabled after loading the current URL. - mAccessibilityScriptInjected = false; - } - } else if (mAccessibilityInjector != null) { - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - if (mAccessibilityInjector.onKeyEvent(event)) { - // if an accessibility injector is present (no JavaScript enabled or the site - // opts out injecting our JavaScript screen reader) we let it decide whether - // to act on and consume the event. - return true; - } - } else { - // Clean up if accessibility was disabled after loading the current URL. - mAccessibilityInjector = null; - } + // See if the accessibility injector needs to handle this event. + if (isAccessibilityEnabled() + && getAccessibilityInjector().handleKeyEventIfNecessary(event)) { + return true; } if (keyCode == KeyEvent.KEYCODE_PAGE_UP) { @@ -5028,30 +4968,10 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc return false; } - // accessibility support - if (accessibilityScriptInjected()) { - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - // if an accessibility script is injected we delegate to it the key handling. - // this script is a screen reader which is a fully fledged solution for blind - // users to navigate in and interact with web pages. - sendBatchableInputMessage(EventHub.KEY_UP, 0, 0, event); - return true; - } else { - // Clean up if accessibility was disabled after loading the current URL. - mAccessibilityScriptInjected = false; - } - } else if (mAccessibilityInjector != null) { - if (AccessibilityManager.getInstance(mContext).isEnabled()) { - if (mAccessibilityInjector.onKeyEvent(event)) { - // if an accessibility injector is present (no JavaScript enabled or the site - // opts out injecting our JavaScript screen reader) we let it decide whether to - // act on and consume the event. - return true; - } - } else { - // Clean up if accessibility was disabled after loading the current URL. - mAccessibilityInjector = null; - } + // See if the accessibility injector needs to handle this event. + if (isAccessibilityEnabled() + && getAccessibilityInjector().handleKeyEventIfNecessary(event)) { + return true; } if (isEnterActionKey(keyCode)) { @@ -5167,6 +5087,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc private void adjustSelectionCursors() { if (mIsCaretSelection) { + syncSelectionCursors(); return; // no need to swap left and right handles. } @@ -5352,7 +5273,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc public void onAttachedToWindow() { if (mWebView.hasWindowFocus()) setActive(true); - addAccessibilityApisToJavaScript(); + if (isAccessibilityEnabled()) { + getAccessibilityInjector().addAccessibilityApisIfNecessary(); + } updateHwAccelerated(); } @@ -5363,7 +5286,14 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc mZoomManager.dismissZoomPicker(); if (mWebView.hasWindowFocus()) setActive(false); - removeAccessibilityApisFromJavaScript(); + if (isAccessibilityEnabled()) { + getAccessibilityInjector().removeAccessibilityApisIfNecessary(); + } else { + // Ensure the injector is cleared if we're detaching from the window + // and accessibility is disabled. + mAccessibilityInjector = null; + } + updateHwAccelerated(); if (mWebView.isHardwareAccelerated()) { @@ -7415,9 +7345,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc break; case SELECTION_STRING_CHANGED: - if (mAccessibilityInjector != null) { - String selectionString = (String) msg.obj; - mAccessibilityInjector.onSelectionStringChange(selectionString); + if (isAccessibilityEnabled()) { + getAccessibilityInjector() + .handleSelectionChangedIfNecessary((String) msg.obj); } break; @@ -7486,6 +7416,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc int cursorPosition = start + text.length(); replaceTextfieldText(start, end, text, cursorPosition, cursorPosition); + selectionDone(); break; } @@ -7512,7 +7443,9 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc } case CLEAR_CARET_HANDLE: - selectionDone(); + if (mIsCaretSelection) { + selectionDone(); + } break; case KEY_PRESS: @@ -7600,6 +7533,11 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc invalidate(); } } + + @Override + public void clearPreviousHitTest() { + setHitTestResult(null); + } } private void setHitTestTypeFromUrl(String url) { @@ -7973,7 +7911,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc mIsBatchingTextChanges = false; } - private void sendBatchableInputMessage(int what, int arg1, int arg2, + void sendBatchableInputMessage(int what, int arg1, int arg2, Object obj) { if (mWebViewCore == null) { return; @@ -8394,16 +8332,6 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc } /** - * @return Whether accessibility script has been injected. - */ - private boolean accessibilityScriptInjected() { - // TODO: Maybe the injected script should announce its presence in - // the page meta-tag so the nativePageShouldHandleShiftAndArrows - // will check that as one of the conditions it looks for - return mAccessibilityScriptInjected; - } - - /** * See {@link WebView#setBackgroundColor(int)} */ @Override diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java index 76cd1c9..af7914e 100644 --- a/core/java/android/webkit/WebViewCore.java +++ b/core/java/android/webkit/WebViewCore.java @@ -178,8 +178,10 @@ public final class WebViewCore { // Start the singleton watchdog which will monitor the WebCore thread // to verify it's still processing messages. - WebCoreThreadWatchdog.start(context, sWebCoreHandler); + WebCoreThreadWatchdog.start(sWebCoreHandler); } + // Make sure the Watchdog is aware of this new WebView. + WebCoreThreadWatchdog.registerWebView(w); } // Create an EventHub to handle messages before and after the thread is // ready. @@ -1979,6 +1981,7 @@ public final class WebViewCore { mEventHub.sendMessageAtFrontOfQueue( Message.obtain(null, EventHub.DESTROY)); mEventHub.blockMessages(); + WebCoreThreadWatchdog.unregisterWebView(mWebViewClassic); } } diff --git a/core/java/android/webkit/WebViewInputDispatcher.java b/core/java/android/webkit/WebViewInputDispatcher.java index 9eeb311..d118eac 100644 --- a/core/java/android/webkit/WebViewInputDispatcher.java +++ b/core/java/android/webkit/WebViewInputDispatcher.java @@ -399,7 +399,6 @@ final class WebViewInputDispatcher { unscheduleHideTapHighlightLocked(); unscheduleShowTapHighlightLocked(); mUiCallbacks.showTapHighlight(true); - scheduleHideTapHighlightLocked(); } private void scheduleShowTapHighlightLocked() { @@ -466,13 +465,13 @@ final class WebViewInputDispatcher { return; } mPostClickScheduled = false; - showTapCandidateLocked(); MotionEvent event = mPostTouchStream.getLastEvent(); if (event == null || event.getAction() != MotionEvent.ACTION_UP) { return; } + showTapCandidateLocked(); MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event); DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_CLICK, 0, mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale); @@ -511,6 +510,7 @@ final class WebViewInputDispatcher { } private void enqueueHitTestLocked(MotionEvent event) { + mUiCallbacks.clearPreviousHitTest(); MotionEvent eventToEnqueue = MotionEvent.obtainNoHistory(event); DispatchEvent d = obtainDispatchEventLocked(eventToEnqueue, EVENT_TYPE_HIT_TEST, 0, mPostLastWebKitXOffset, mPostLastWebKitYOffset, mPostLastWebKitScale); @@ -666,6 +666,10 @@ final class WebViewInputDispatcher { if (event != null && recycleEvent) { event.recycle(); } + + if (eventType == EVENT_TYPE_CLICK) { + scheduleHideTapHighlightLocked(); + } } } } @@ -802,6 +806,10 @@ final class WebViewInputDispatcher { d.mEvent = null; // retain ownership of event, don't recycle it yet } recycleDispatchEventLocked(d); + + if (eventType == EVENT_TYPE_CLICK) { + scheduleHideTapHighlightLocked(); + } } // Handle the event. @@ -822,21 +830,31 @@ final class WebViewInputDispatcher { } private void enqueueEventLocked(DispatchEvent d) { - if (!shouldSkipWebKit(d.mEventType)) { + if (!shouldSkipWebKit(d)) { enqueueWebKitEventLocked(d); } else { enqueueUiEventLocked(d); } } - private boolean shouldSkipWebKit(int eventType) { - switch (eventType) { + private boolean shouldSkipWebKit(DispatchEvent d) { + switch (d.mEventType) { case EVENT_TYPE_CLICK: case EVENT_TYPE_HOVER: case EVENT_TYPE_SCROLL: case EVENT_TYPE_HIT_TEST: return false; case EVENT_TYPE_TOUCH: + // TODO: This should be cleaned up. We now have WebViewInputDispatcher + // and WebViewClassic both checking for slop and doing their own + // thing - they should be consolidated. And by consolidated, I mean + // WebViewClassic's version should just be deleted. + // The reason this is done is because webpages seem to expect + // that they only get an ontouchmove if the slop has been exceeded. + if (mIsTapCandidate && d.mEvent != null + && d.mEvent.getActionMasked() == MotionEvent.ACTION_MOVE) { + return true; + } return !mPostSendTouchEventsToWebKit || mPostDoNotSendTouchEventsToWebKitUntilNextGesture; } @@ -1040,6 +1058,12 @@ final class WebViewInputDispatcher { * @param show True if it should show the highlight, false if it should hide it */ public void showTapHighlight(boolean show); + + /** + * Called when we are sending a new EVENT_TYPE_HIT_TEST to WebKit, so + * previous hit tests should be cleared as they are obsolete. + */ + public void clearPreviousHitTest(); } /* Implemented by {@link WebViewCore} to perform operations on the web kit thread. */ diff --git a/core/java/android/webkit/WebViewProvider.java b/core/java/android/webkit/WebViewProvider.java index 74a215c..867ee54 100644 --- a/core/java/android/webkit/WebViewProvider.java +++ b/core/java/android/webkit/WebViewProvider.java @@ -276,6 +276,8 @@ public interface WebViewProvider { public void onInitializeAccessibilityEvent(AccessibilityEvent event); + public boolean performAccessibilityAction(int action, Bundle arguments); + public void setOverScrollMode(int mode); public void setScrollBarStyle(int style); diff --git a/core/jni/android/graphics/BitmapFactory.h b/core/jni/android/graphics/BitmapFactory.h index 9ae61bc..f2aaab7 100644 --- a/core/jni/android/graphics/BitmapFactory.h +++ b/core/jni/android/graphics/BitmapFactory.h @@ -16,6 +16,7 @@ extern jfieldID gOptions_widthFieldID; extern jfieldID gOptions_heightFieldID; extern jfieldID gOptions_mimeFieldID; extern jfieldID gOptions_mCancelID; +extern jfieldID gOptions_bitmapFieldID; jstring getMimeTypeString(JNIEnv* env, SkImageDecoder::Format format); diff --git a/core/jni/android/graphics/BitmapRegionDecoder.cpp b/core/jni/android/graphics/BitmapRegionDecoder.cpp index dd8e84f..b218bcd 100644 --- a/core/jni/android/graphics/BitmapRegionDecoder.cpp +++ b/core/jni/android/graphics/BitmapRegionDecoder.cpp @@ -29,6 +29,7 @@ #include "CreateJavaOutputStreamAdaptor.h" #include "Utils.h" #include "JNIHelp.h" +#include "SkTScopedPtr.h" #include <android_runtime/AndroidRuntime.h> #include "android_util_Binder.h" @@ -180,7 +181,8 @@ static jobject nativeNewInstanceFromAsset(JNIEnv* env, jobject clazz, * reportSizeToVM not supported */ static jobject nativeDecodeRegion(JNIEnv* env, jobject, SkBitmapRegionDecoder *brd, - int start_x, int start_y, int width, int height, jobject options) { + int start_x, int start_y, int width, int height, jobject options) { + jobject tileBitmap = NULL; SkImageDecoder *decoder = brd->getDecoder(); int sampleSize = 1; SkBitmap::Config prefConfig = SkBitmap::kNo_Config; @@ -199,12 +201,12 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, SkBitmapRegionDecoder *b doDither = env->GetBooleanField(options, gOptions_ditherFieldID); preferQualityOverSpeed = env->GetBooleanField(options, gOptions_preferQualityOverSpeedFieldID); + // Get the bitmap for re-use if it exists. + tileBitmap = env->GetObjectField(options, gOptions_bitmapFieldID); } decoder->setDitherImage(doDither); decoder->setPreferQualityOverSpeed(preferQualityOverSpeed); - SkBitmap* bitmap = new SkBitmap; - SkAutoTDelete<SkBitmap> adb(bitmap); AutoDecoderCancel adc(options, decoder); // To fix the race condition in case "requestCancelDecode" @@ -219,6 +221,17 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, SkBitmapRegionDecoder *b region.fTop = start_y; region.fRight = start_x + width; region.fBottom = start_y + height; + SkBitmap* bitmap = NULL; + SkTScopedPtr<SkBitmap> adb; + + if (tileBitmap != NULL) { + // Re-use bitmap. + bitmap = GraphicsJNI::getNativeBitmap(env, tileBitmap); + } + if (bitmap == NULL) { + bitmap = new SkBitmap; + adb.reset(bitmap); + } if (!brd->decodeRegion(bitmap, region, prefConfig, sampleSize)) { return nullObjectReturn("decoder->decodeRegion returned false"); @@ -235,12 +248,12 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, SkBitmapRegionDecoder *b getMimeTypeString(env, decoder->getFormat())); } - // detach bitmap from its autodeleter, since we want to own it now - adb.detach(); + if (tileBitmap != NULL) { + return tileBitmap; + } - SkPixelRef* pr = bitmap->pixelRef(); - // promise we will never change our pixels (great for sharing and pictures) - pr->setImmutable(); + // detach bitmap from its autodeleter, since we want to own it now + adb.release(); JavaPixelAllocator* allocator = (JavaPixelAllocator*) decoder->getAllocator(); jbyteArray buff = allocator->getStorageObjAndReset(); diff --git a/core/res/res/drawable-hdpi/switch_thumb_activated_holo_dark.9.png b/core/res/res/drawable-hdpi/switch_thumb_activated_holo_dark.9.png Binary files differindex 2fc475b..9c5147e 100644 --- a/core/res/res/drawable-hdpi/switch_thumb_activated_holo_dark.9.png +++ b/core/res/res/drawable-hdpi/switch_thumb_activated_holo_dark.9.png diff --git a/core/res/res/drawable-hdpi/switch_thumb_activated_holo_light.9.png b/core/res/res/drawable-hdpi/switch_thumb_activated_holo_light.9.png Binary files differindex 5adecf1..9c5147e 100644 --- a/core/res/res/drawable-hdpi/switch_thumb_activated_holo_light.9.png +++ b/core/res/res/drawable-hdpi/switch_thumb_activated_holo_light.9.png diff --git a/core/res/res/drawable-hdpi/switch_thumb_disabled_holo_dark.9.png b/core/res/res/drawable-hdpi/switch_thumb_disabled_holo_dark.9.png Binary files differindex 457fa84..a257e26 100644 --- a/core/res/res/drawable-hdpi/switch_thumb_disabled_holo_dark.9.png +++ b/core/res/res/drawable-hdpi/switch_thumb_disabled_holo_dark.9.png diff --git a/core/res/res/drawable-hdpi/switch_thumb_disabled_holo_light.9.png b/core/res/res/drawable-hdpi/switch_thumb_disabled_holo_light.9.png Binary files differindex c3cfc29..a257e26 100644 --- a/core/res/res/drawable-hdpi/switch_thumb_disabled_holo_light.9.png +++ b/core/res/res/drawable-hdpi/switch_thumb_disabled_holo_light.9.png diff --git a/core/res/res/drawable-hdpi/switch_thumb_holo_dark.9.png b/core/res/res/drawable-hdpi/switch_thumb_holo_dark.9.png Binary files differindex d0e1806..dd999d6 100644 --- a/core/res/res/drawable-hdpi/switch_thumb_holo_dark.9.png +++ b/core/res/res/drawable-hdpi/switch_thumb_holo_dark.9.png diff --git a/core/res/res/drawable-hdpi/switch_thumb_holo_light.9.png b/core/res/res/drawable-hdpi/switch_thumb_holo_light.9.png Binary files differindex c30506d..dd999d6 100644 --- a/core/res/res/drawable-hdpi/switch_thumb_holo_light.9.png +++ b/core/res/res/drawable-hdpi/switch_thumb_holo_light.9.png diff --git a/core/res/res/drawable-hdpi/switch_thumb_pressed_holo_dark.9.png b/core/res/res/drawable-hdpi/switch_thumb_pressed_holo_dark.9.png Binary files differindex 9106687..ea54380 100644 --- a/core/res/res/drawable-hdpi/switch_thumb_pressed_holo_dark.9.png +++ b/core/res/res/drawable-hdpi/switch_thumb_pressed_holo_dark.9.png diff --git a/core/res/res/drawable-hdpi/switch_thumb_pressed_holo_light.9.png b/core/res/res/drawable-hdpi/switch_thumb_pressed_holo_light.9.png Binary files differindex 2bdda56..ea54380 100644 --- a/core/res/res/drawable-hdpi/switch_thumb_pressed_holo_light.9.png +++ b/core/res/res/drawable-hdpi/switch_thumb_pressed_holo_light.9.png diff --git a/core/res/res/drawable-mdpi/switch_thumb_activated_holo_dark.9.png b/core/res/res/drawable-mdpi/switch_thumb_activated_holo_dark.9.png Binary files differindex 0787d16..3d7c236 100644 --- a/core/res/res/drawable-mdpi/switch_thumb_activated_holo_dark.9.png +++ b/core/res/res/drawable-mdpi/switch_thumb_activated_holo_dark.9.png diff --git a/core/res/res/drawable-mdpi/switch_thumb_activated_holo_light.9.png b/core/res/res/drawable-mdpi/switch_thumb_activated_holo_light.9.png Binary files differindex 0157e68..3d7c236 100644 --- a/core/res/res/drawable-mdpi/switch_thumb_activated_holo_light.9.png +++ b/core/res/res/drawable-mdpi/switch_thumb_activated_holo_light.9.png diff --git a/core/res/res/drawable-mdpi/switch_thumb_disabled_holo_dark.9.png b/core/res/res/drawable-mdpi/switch_thumb_disabled_holo_dark.9.png Binary files differindex 51b14d0..82f05d6 100644 --- a/core/res/res/drawable-mdpi/switch_thumb_disabled_holo_dark.9.png +++ b/core/res/res/drawable-mdpi/switch_thumb_disabled_holo_dark.9.png diff --git a/core/res/res/drawable-mdpi/switch_thumb_disabled_holo_light.9.png b/core/res/res/drawable-mdpi/switch_thumb_disabled_holo_light.9.png Binary files differindex d68568a..82f05d6 100644 --- a/core/res/res/drawable-mdpi/switch_thumb_disabled_holo_light.9.png +++ b/core/res/res/drawable-mdpi/switch_thumb_disabled_holo_light.9.png diff --git a/core/res/res/drawable-mdpi/switch_thumb_holo_dark.9.png b/core/res/res/drawable-mdpi/switch_thumb_holo_dark.9.png Binary files differindex 6bf153a..9bc7a68 100644 --- a/core/res/res/drawable-mdpi/switch_thumb_holo_dark.9.png +++ b/core/res/res/drawable-mdpi/switch_thumb_holo_dark.9.png diff --git a/core/res/res/drawable-mdpi/switch_thumb_holo_light.9.png b/core/res/res/drawable-mdpi/switch_thumb_holo_light.9.png Binary files differindex 0d98983..9bc7a68 100644 --- a/core/res/res/drawable-mdpi/switch_thumb_holo_light.9.png +++ b/core/res/res/drawable-mdpi/switch_thumb_holo_light.9.png diff --git a/core/res/res/drawable-mdpi/switch_thumb_pressed_holo_dark.9.png b/core/res/res/drawable-mdpi/switch_thumb_pressed_holo_dark.9.png Binary files differindex 3cee7b8..670dc2e 100644 --- a/core/res/res/drawable-mdpi/switch_thumb_pressed_holo_dark.9.png +++ b/core/res/res/drawable-mdpi/switch_thumb_pressed_holo_dark.9.png diff --git a/core/res/res/drawable-mdpi/switch_thumb_pressed_holo_light.9.png b/core/res/res/drawable-mdpi/switch_thumb_pressed_holo_light.9.png Binary files differindex 43a7c4c..670dc2e 100644 --- a/core/res/res/drawable-mdpi/switch_thumb_pressed_holo_light.9.png +++ b/core/res/res/drawable-mdpi/switch_thumb_pressed_holo_light.9.png diff --git a/core/res/res/drawable-xhdpi/switch_thumb_activated_holo_dark.9.png b/core/res/res/drawable-xhdpi/switch_thumb_activated_holo_dark.9.png Binary files differindex a0e6b20..ca48bd8 100644 --- a/core/res/res/drawable-xhdpi/switch_thumb_activated_holo_dark.9.png +++ b/core/res/res/drawable-xhdpi/switch_thumb_activated_holo_dark.9.png diff --git a/core/res/res/drawable-xhdpi/switch_thumb_activated_holo_light.9.png b/core/res/res/drawable-xhdpi/switch_thumb_activated_holo_light.9.png Binary files differindex 88235fe..ca48bd8 100644 --- a/core/res/res/drawable-xhdpi/switch_thumb_activated_holo_light.9.png +++ b/core/res/res/drawable-xhdpi/switch_thumb_activated_holo_light.9.png diff --git a/core/res/res/drawable-xhdpi/switch_thumb_disabled_holo_dark.9.png b/core/res/res/drawable-xhdpi/switch_thumb_disabled_holo_dark.9.png Binary files differindex 04fb9a1..c3d80f0 100644 --- a/core/res/res/drawable-xhdpi/switch_thumb_disabled_holo_dark.9.png +++ b/core/res/res/drawable-xhdpi/switch_thumb_disabled_holo_dark.9.png diff --git a/core/res/res/drawable-xhdpi/switch_thumb_disabled_holo_light.9.png b/core/res/res/drawable-xhdpi/switch_thumb_disabled_holo_light.9.png Binary files differindex 06a14f3..c3d80f0 100644 --- a/core/res/res/drawable-xhdpi/switch_thumb_disabled_holo_light.9.png +++ b/core/res/res/drawable-xhdpi/switch_thumb_disabled_holo_light.9.png diff --git a/core/res/res/drawable-xhdpi/switch_thumb_holo_dark.9.png b/core/res/res/drawable-xhdpi/switch_thumb_holo_dark.9.png Binary files differindex af7d631..df236df 100644 --- a/core/res/res/drawable-xhdpi/switch_thumb_holo_dark.9.png +++ b/core/res/res/drawable-xhdpi/switch_thumb_holo_dark.9.png diff --git a/core/res/res/drawable-xhdpi/switch_thumb_holo_light.9.png b/core/res/res/drawable-xhdpi/switch_thumb_holo_light.9.png Binary files differindex d6ab3ea..df236df 100644 --- a/core/res/res/drawable-xhdpi/switch_thumb_holo_light.9.png +++ b/core/res/res/drawable-xhdpi/switch_thumb_holo_light.9.png diff --git a/core/res/res/drawable-xhdpi/switch_thumb_pressed_holo_dark.9.png b/core/res/res/drawable-xhdpi/switch_thumb_pressed_holo_dark.9.png Binary files differindex 5a8e807..4acb32b 100644 --- a/core/res/res/drawable-xhdpi/switch_thumb_pressed_holo_dark.9.png +++ b/core/res/res/drawable-xhdpi/switch_thumb_pressed_holo_dark.9.png diff --git a/core/res/res/drawable-xhdpi/switch_thumb_pressed_holo_light.9.png b/core/res/res/drawable-xhdpi/switch_thumb_pressed_holo_light.9.png Binary files differindex 392f3dc..4acb32b 100644 --- a/core/res/res/drawable-xhdpi/switch_thumb_pressed_holo_light.9.png +++ b/core/res/res/drawable-xhdpi/switch_thumb_pressed_holo_light.9.png diff --git a/core/res/res/drawable/switch_track_holo_dark.xml b/core/res/res/drawable/switch_track_holo_dark.xml index c9a940d..5f796c1 100644 --- a/core/res/res/drawable/switch_track_holo_dark.xml +++ b/core/res/res/drawable/switch_track_holo_dark.xml @@ -15,7 +15,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_enabled="false" android:drawable="@drawable/switch_bg_disabled_holo_dark" /> <item android:state_focused="true" android:drawable="@drawable/switch_bg_focused_holo_dark" /> <item android:drawable="@drawable/switch_bg_holo_dark" /> </selector> diff --git a/core/res/res/drawable/switch_track_holo_light.xml b/core/res/res/drawable/switch_track_holo_light.xml index 98e53b5..39bee37 100644 --- a/core/res/res/drawable/switch_track_holo_light.xml +++ b/core/res/res/drawable/switch_track_holo_light.xml @@ -15,7 +15,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_enabled="false" android:drawable="@drawable/switch_bg_disabled_holo_light" /> <item android:state_focused="true" android:drawable="@drawable/switch_bg_focused_holo_light" /> <item android:drawable="@drawable/switch_bg_holo_light" /> </selector> diff --git a/core/tests/coretests/src/android/net/SSLTest.java b/core/tests/coretests/src/android/net/SSLTest.java index c573498..27b699d 100644 --- a/core/tests/coretests/src/android/net/SSLTest.java +++ b/core/tests/coretests/src/android/net/SSLTest.java @@ -59,17 +59,20 @@ public class SSLTest extends TestCase { new byte[] { 'h', 't', 't', 'p', '/', '1', '.', '1' }))); } - public void testStringsToNpnBytesEmptyByteArray() { + public void testStringsToNpnBytesEmptyArray() { try { - SSLCertificateSocketFactory.toNpnProtocolsList(new byte[0]); + SSLCertificateSocketFactory.toNpnProtocolsList(); fail(); } catch (IllegalArgumentException expected) { } } - public void testStringsToNpnBytesEmptyArray() { - byte[] expected = {}; - assertTrue(Arrays.equals(expected, SSLCertificateSocketFactory.toNpnProtocolsList())); + public void testStringsToNpnBytesEmptyByteArray() { + try { + SSLCertificateSocketFactory.toNpnProtocolsList(new byte[0]); + fail(); + } catch (IllegalArgumentException expected) { + } } public void testStringsToNpnBytesOversizedInput() { diff --git a/graphics/java/android/graphics/BitmapRegionDecoder.java b/graphics/java/android/graphics/BitmapRegionDecoder.java index 496e0c7..c1d3407 100644 --- a/graphics/java/android/graphics/BitmapRegionDecoder.java +++ b/graphics/java/android/graphics/BitmapRegionDecoder.java @@ -180,9 +180,9 @@ public final class BitmapRegionDecoder { */ public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) { checkRecycled("decodeRegion called on recycled region decoder"); - if (rect.left < 0 || rect.top < 0 || rect.right > getWidth() - || rect.bottom > getHeight()) - throw new IllegalArgumentException("rectangle is not inside the image"); + if (rect.right <= 0 || rect.bottom <= 0 || rect.left >= getWidth() + || rect.top >= getHeight()) + throw new IllegalArgumentException("rectangle is outside the image"); return nativeDecodeRegion(mNativeBitmapRegionDecoder, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, options); } diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index ef5da5b..16cfa92 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -2000,6 +2000,37 @@ public class AudioManager { } /** + * @hide + * Used internally by telephony package to register an intent receiver for ACTION_MEDIA_BUTTON. + * @param eventReceiver the component that will receive the media button key events, + * no-op if eventReceiver is null + */ + public void registerMediaButtonEventReceiverForCalls(ComponentName eventReceiver) { + if (eventReceiver == null) { + return; + } + IAudioService service = getService(); + try { + // eventReceiver != null + service.registerMediaButtonEventReceiverForCalls(eventReceiver); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in registerMediaButtonEventReceiverForCalls", e); + } + } + + /** + * @hide + */ + public void unregisterMediaButtonEventReceiverForCalls() { + IAudioService service = getService(); + try { + service.unregisterMediaButtonEventReceiverForCalls(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in unregisterMediaButtonEventReceiverForCalls", e); + } + } + + /** * Unregister the receiver of MEDIA_BUTTON intents. * @param eventReceiver identifier of a {@link android.content.BroadcastReceiver} * that was registered with {@link #registerMediaButtonEventReceiver(ComponentName)}. diff --git a/media/java/android/media/AudioService.java b/media/java/android/media/AudioService.java index 8da7d0f..aa29444 100644 --- a/media/java/android/media/AudioService.java +++ b/media/java/android/media/AudioService.java @@ -3625,12 +3625,14 @@ public class AudioService extends IAudioService.Stub implements OnFinished { Log.e(TAG, "not dispatching invalid media key event " + keyEvent); return; } - // event filtering based on audio mode + // event filtering for telephony synchronized(mRingingLock) { - if (mIsRinging || (getMode() == AudioSystem.MODE_IN_CALL) || - (getMode() == AudioSystem.MODE_IN_COMMUNICATION) || - (getMode() == AudioSystem.MODE_RINGTONE) ) { - return; + synchronized(mRCStack) { + if ((mMediaReceiverForCalls != null) && + (mIsRinging || (getMode() == AudioSystem.MODE_IN_CALL))) { + dispatchMediaKeyEventForCalls(keyEvent, needWakeLock); + return; + } } } // event filtering based on voice-based interactions @@ -3642,6 +3644,25 @@ public class AudioService extends IAudioService.Stub implements OnFinished { } /** + * Handles the dispatching of the media button events to the telephony package. + * Precondition: mMediaReceiverForCalls != null + * @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons + * @param needWakeLock true if a PARTIAL_WAKE_LOCK needs to be held while this key event + * is dispatched. + */ + private void dispatchMediaKeyEventForCalls(KeyEvent keyEvent, boolean needWakeLock) { + Intent keyIntent = new Intent(Intent.ACTION_MEDIA_BUTTON, null); + keyIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + keyIntent.setPackage(mMediaReceiverForCalls.getPackageName()); + if (needWakeLock) { + mMediaEventWakeLock.acquire(); + keyIntent.putExtra(EXTRA_WAKELOCK_ACQUIRED, WAKELOCK_RELEASE_ON_FINISHED); + } + mContext.sendOrderedBroadcast(keyIntent, null, mKeyEventDone, + mAudioHandler, Activity.RESULT_OK, null, null); + } + + /** * Handles the dispatching of the media button events to one of the registered listeners, * or if there was none, broadcast an ACTION_MEDIA_BUTTON intent to the rest of the system. * @param keyEvent a non-null KeyEvent whose key code is one of the supported media buttons @@ -3678,38 +3699,15 @@ public class AudioService extends IAudioService.Stub implements OnFinished { } /** - * The minimum duration during which a user must press to trigger voice-based interactions - */ - private final static int MEDIABUTTON_LONG_PRESS_DURATION_MS = 300; - /** - * The different states of the state machine to handle the launch of voice-based interactions, - * stored in mVoiceButtonState. - */ - private final static int VOICEBUTTON_STATE_IDLE = 0; - private final static int VOICEBUTTON_STATE_DOWN = 1; - private final static int VOICEBUTTON_STATE_DOWN_IGNORE_NEW = 2; - /** - * The different actions after state transitions on mVoiceButtonState. + * The different actions performed in response to a voice button key event. */ private final static int VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS = 1; private final static int VOICEBUTTON_ACTION_START_VOICE_INPUT = 2; private final static int VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS = 3; private final Object mVoiceEventLock = new Object(); - private int mVoiceButtonState = VOICEBUTTON_STATE_IDLE; - private long mVoiceButtonDownTime = 0; - - /** - * Log an error when an unexpected action is encountered in the state machine to filter - * key events. - * @param keyAction the unexpected action of the key event being filtered - * @param stateName the string corresponding to the state in which the error occurred - */ - private static void logErrorForKeyAction(int keyAction, String stateName) { - Log.e(TAG, "unexpected action " - + KeyEvent.actionToString(keyAction) - + " in " + stateName + " state"); - } + private boolean mVoiceButtonDown; + private boolean mVoiceButtonHandled; /** * Filter key events that may be used for voice-based interactions @@ -3719,67 +3717,32 @@ public class AudioService extends IAudioService.Stub implements OnFinished { * is dispatched. */ private void filterVoiceInputKeyEvent(KeyEvent keyEvent, boolean needWakeLock) { + if (DEBUG_RC) { + Log.v(TAG, "voice input key event: " + keyEvent + ", needWakeLock=" + needWakeLock); + } + int voiceButtonAction = VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS; int keyAction = keyEvent.getAction(); synchronized (mVoiceEventLock) { - // state machine on mVoiceButtonState - switch (mVoiceButtonState) { - - case VOICEBUTTON_STATE_IDLE: - if (keyAction == KeyEvent.ACTION_DOWN) { - mVoiceButtonDownTime = keyEvent.getDownTime(); - // valid state transition - mVoiceButtonState = VOICEBUTTON_STATE_DOWN; - } else if (keyAction == KeyEvent.ACTION_UP) { - // no state transition - // action is still VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS - } else { - logErrorForKeyAction(keyAction, "VOICEBUTTON_STATE_IDLE"); - } - break; - - case VOICEBUTTON_STATE_DOWN: - if ((keyEvent.getEventTime() - mVoiceButtonDownTime) - >= MEDIABUTTON_LONG_PRESS_DURATION_MS) { - // press was long enough, start voice-based interactions, regardless of - // whether this was a DOWN or UP key event - voiceButtonAction = VOICEBUTTON_ACTION_START_VOICE_INPUT; - if (keyAction == KeyEvent.ACTION_UP) { - // done tracking the key press, so transition back to idle state - mVoiceButtonState = VOICEBUTTON_STATE_IDLE; - } else if (keyAction == KeyEvent.ACTION_DOWN) { - // no need to observe the upcoming key events - mVoiceButtonState = VOICEBUTTON_STATE_DOWN_IGNORE_NEW; - } else { - logErrorForKeyAction(keyAction, "VOICEBUTTON_STATE_DOWN"); - } - } else { - if (keyAction == KeyEvent.ACTION_UP) { - // press wasn't long enough, simulate complete key press - voiceButtonAction = VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS; - // not tracking the key press anymore, so transition back to idle state - mVoiceButtonState = VOICEBUTTON_STATE_IDLE; - } else if (keyAction == KeyEvent.ACTION_DOWN) { - // no state transition - // action is still VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS - } else { - logErrorForKeyAction(keyAction, "VOICEBUTTON_STATE_DOWN"); - } - } - break; - - case VOICEBUTTON_STATE_DOWN_IGNORE_NEW: - if (keyAction == KeyEvent.ACTION_UP) { - // done tracking the key press, so transition back to idle state - mVoiceButtonState = VOICEBUTTON_STATE_IDLE; - // action is still VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS - } else if (keyAction == KeyEvent.ACTION_DOWN) { - // no state transition: we've already launched voice-based interactions - // action is still VOICEBUTTON_ACTION_DISCARD_CURRENT_KEY_PRESS - } else { - logErrorForKeyAction(keyAction, "VOICEBUTTON_STATE_DOWN_IGNORE_NEW"); + if (keyAction == KeyEvent.ACTION_DOWN) { + if (keyEvent.getRepeatCount() == 0) { + // initial down + mVoiceButtonDown = true; + mVoiceButtonHandled = false; + } else if (mVoiceButtonDown && !mVoiceButtonHandled + && (keyEvent.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) { + // long-press, start voice-based interactions + mVoiceButtonHandled = true; + voiceButtonAction = VOICEBUTTON_ACTION_START_VOICE_INPUT; + } + } else if (keyAction == KeyEvent.ACTION_UP) { + if (mVoiceButtonDown) { + // voice button up + mVoiceButtonDown = false; + if (!mVoiceButtonHandled && !keyEvent.isCanceled()) { + voiceButtonAction = VOICEBUTTON_ACTION_SIMULATE_KEY_PRESS; } - break; + } } }//synchronized (mVoiceEventLock) @@ -4028,6 +3991,12 @@ public class AudioService extends IAudioService.Stub implements OnFinished { private final Stack<RemoteControlStackEntry> mRCStack = new Stack<RemoteControlStackEntry>(); /** + * The component the telephony package can register so telephony calls have priority to + * handle media button events + */ + private ComponentName mMediaReceiverForCalls = null; + + /** * Helper function: * Display in the log the current entries in the remote control focus stack */ @@ -4381,6 +4350,35 @@ public class AudioService extends IAudioService.Stub implements OnFinished { } /** + * see AudioManager.registerMediaButtonEventReceiverForCalls(ComponentName c) + * precondition: c != null + */ + public void registerMediaButtonEventReceiverForCalls(ComponentName c) { + if (mContext.checkCallingPermission("android.permission.MODIFY_PHONE_STATE") + != PackageManager.PERMISSION_GRANTED) { + Log.e(TAG, "Invalid permissions to register media button receiver for calls"); + return; + } + synchronized(mRCStack) { + mMediaReceiverForCalls = c; + } + } + + /** + * see AudioManager.unregisterMediaButtonEventReceiverForCalls() + */ + public void unregisterMediaButtonEventReceiverForCalls() { + if (mContext.checkCallingPermission("android.permission.MODIFY_PHONE_STATE") + != PackageManager.PERMISSION_GRANTED) { + Log.e(TAG, "Invalid permissions to unregister media button receiver for calls"); + return; + } + synchronized(mRCStack) { + mMediaReceiverForCalls = null; + } + } + + /** * see AudioManager.registerRemoteControlClient(ComponentName eventReceiver, ...) * Note: using this method with rcClient == null is a way to "disable" the IRemoteControlClient * without modifying the RC stack, but while still causing the display to refresh (will diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 48f091c..6753ad3 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -109,6 +109,9 @@ interface IAudioService { oneway void registerMediaButtonIntent(in PendingIntent pi, in ComponentName c); oneway void unregisterMediaButtonIntent(in PendingIntent pi, in ComponentName c); + oneway void registerMediaButtonEventReceiverForCalls(in ComponentName c); + oneway void unregisterMediaButtonEventReceiverForCalls(); + oneway void registerRemoteControlClient(in PendingIntent mediaIntent, in IRemoteControlClient rcClient, in String callingPackageName); oneway void unregisterRemoteControlClient(in PendingIntent mediaIntent, diff --git a/media/java/android/media/MediaPlayer.java b/media/java/android/media/MediaPlayer.java index aa4cdbe..9ed9de0 100644 --- a/media/java/android/media/MediaPlayer.java +++ b/media/java/android/media/MediaPlayer.java @@ -737,7 +737,7 @@ public class MediaPlayer * @see MediaPlayer#VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING */ public void setVideoScalingMode(int mode) { - if (isVideoScalingModeSupported(mode)) { + if (!isVideoScalingModeSupported(mode)) { final String msg = "Scaling mode " + mode + " is not supported"; throw new IllegalArgumentException(msg); } diff --git a/media/java/android/media/MediaScanner.java b/media/java/android/media/MediaScanner.java index 821a251b..987c0ac 100644 --- a/media/java/android/media/MediaScanner.java +++ b/media/java/android/media/MediaScanner.java @@ -1623,7 +1623,7 @@ public class MediaScanner if (line.startsWith("File")) { int equals = line.indexOf('='); if (equals > 0) { - cachePlaylistEntry(line, playListDirectory); + cachePlaylistEntry(line.substring(equals + 1), playListDirectory); } } line = reader.readLine(); diff --git a/packages/InputDevices/AndroidManifest.xml b/packages/InputDevices/AndroidManifest.xml index 6831a74..f0e4abc 100644 --- a/packages/InputDevices/AndroidManifest.xml +++ b/packages/InputDevices/AndroidManifest.xml @@ -8,7 +8,8 @@ android:label="@string/app_label" android:process="system"> - <receiver android:name=".InputDeviceReceiver"> + <receiver android:name=".InputDeviceReceiver" + android:label="@string/keyboard_layouts_label"> <intent-filter> <action android:name="android.hardware.input.action.QUERY_KEYBOARD_LAYOUTS" /> </intent-filter> diff --git a/packages/InputDevices/res/values/strings.xml b/packages/InputDevices/res/values/strings.xml index 140c7d4..c13e606 100644 --- a/packages/InputDevices/res/values/strings.xml +++ b/packages/InputDevices/res/values/strings.xml @@ -3,6 +3,9 @@ <!-- Name of the application. [CHAR LIMIT=35] --> <string name="app_label">Input Devices</string> + <!-- Keyboard layouts label, used to describe the set of all built-in layouts in the UI. [CHAR LIMIT=35] --> + <string name="keyboard_layouts_label">Android keyboard</string> + <!-- US English keyboard layout label. [CHAR LIMIT=35] --> <string name="keyboard_layout_english_us_label">English (US)</string> diff --git a/packages/SystemUI/res/drawable-hdpi/stat_sys_signal_null.png b/packages/SystemUI/res/drawable-hdpi/stat_sys_signal_null.png Binary files differindex daf18c7..d9ec745 100644 --- a/packages/SystemUI/res/drawable-hdpi/stat_sys_signal_null.png +++ b/packages/SystemUI/res/drawable-hdpi/stat_sys_signal_null.png diff --git a/packages/SystemUI/res/drawable-hdpi/stat_sys_wifi_signal_null.png b/packages/SystemUI/res/drawable-hdpi/stat_sys_wifi_signal_null.png Binary files differnew file mode 100644 index 0000000..117cf19 --- /dev/null +++ b/packages/SystemUI/res/drawable-hdpi/stat_sys_wifi_signal_null.png diff --git a/packages/SystemUI/res/drawable-mdpi/stat_sys_signal_null.png b/packages/SystemUI/res/drawable-mdpi/stat_sys_signal_null.png Binary files differindex 5292998..2cebe85 100644 --- a/packages/SystemUI/res/drawable-mdpi/stat_sys_signal_null.png +++ b/packages/SystemUI/res/drawable-mdpi/stat_sys_signal_null.png diff --git a/packages/SystemUI/res/drawable-mdpi/stat_sys_wifi_signal_null.png b/packages/SystemUI/res/drawable-mdpi/stat_sys_wifi_signal_null.png Binary files differnew file mode 100644 index 0000000..7c60bea --- /dev/null +++ b/packages/SystemUI/res/drawable-mdpi/stat_sys_wifi_signal_null.png diff --git a/packages/SystemUI/res/drawable-sw600dp-hdpi/stat_sys_signal_null.png b/packages/SystemUI/res/drawable-sw600dp-hdpi/stat_sys_signal_null.png Binary files differnew file mode 100644 index 0000000..9e6323c --- /dev/null +++ b/packages/SystemUI/res/drawable-sw600dp-hdpi/stat_sys_signal_null.png diff --git a/packages/SystemUI/res/drawable-sw600dp-hdpi/stat_sys_wifi_signal_null.png b/packages/SystemUI/res/drawable-sw600dp-hdpi/stat_sys_wifi_signal_null.png Binary files differnew file mode 100644 index 0000000..09b35b3 --- /dev/null +++ b/packages/SystemUI/res/drawable-sw600dp-hdpi/stat_sys_wifi_signal_null.png diff --git a/packages/SystemUI/res/drawable-sw600dp-mdpi/stat_sys_signal_null.png b/packages/SystemUI/res/drawable-sw600dp-mdpi/stat_sys_signal_null.png Binary files differnew file mode 100644 index 0000000..2220c73 --- /dev/null +++ b/packages/SystemUI/res/drawable-sw600dp-mdpi/stat_sys_signal_null.png diff --git a/packages/SystemUI/res/drawable-sw600dp-mdpi/stat_sys_wifi_signal_null.png b/packages/SystemUI/res/drawable-sw600dp-mdpi/stat_sys_wifi_signal_null.png Binary files differnew file mode 100644 index 0000000..ea987f1 --- /dev/null +++ b/packages/SystemUI/res/drawable-sw600dp-mdpi/stat_sys_wifi_signal_null.png diff --git a/packages/SystemUI/res/drawable-sw600dp-xhdpi/stat_sys_signal_null.png b/packages/SystemUI/res/drawable-sw600dp-xhdpi/stat_sys_signal_null.png Binary files differnew file mode 100644 index 0000000..21db6a2 --- /dev/null +++ b/packages/SystemUI/res/drawable-sw600dp-xhdpi/stat_sys_signal_null.png diff --git a/packages/SystemUI/res/drawable-sw600dp-xhdpi/stat_sys_wifi_signal_null.png b/packages/SystemUI/res/drawable-sw600dp-xhdpi/stat_sys_wifi_signal_null.png Binary files differnew file mode 100644 index 0000000..5e8baa4 --- /dev/null +++ b/packages/SystemUI/res/drawable-sw600dp-xhdpi/stat_sys_wifi_signal_null.png diff --git a/packages/SystemUI/res/drawable-xhdpi/stat_sys_signal_null.png b/packages/SystemUI/res/drawable-xhdpi/stat_sys_signal_null.png Binary files differindex 3e7fefd..90b8c84 100644 --- a/packages/SystemUI/res/drawable-xhdpi/stat_sys_signal_null.png +++ b/packages/SystemUI/res/drawable-xhdpi/stat_sys_signal_null.png diff --git a/packages/SystemUI/res/drawable-xhdpi/stat_sys_wifi_signal_null.png b/packages/SystemUI/res/drawable-xhdpi/stat_sys_wifi_signal_null.png Binary files differnew file mode 100644 index 0000000..5881402 --- /dev/null +++ b/packages/SystemUI/res/drawable-xhdpi/stat_sys_wifi_signal_null.png diff --git a/packages/SystemUI/res/layout-sw600dp/super_status_bar.xml b/packages/SystemUI/res/layout-sw600dp/super_status_bar.xml index b9af3a9..f93dd33 100644 --- a/packages/SystemUI/res/layout-sw600dp/super_status_bar.xml +++ b/packages/SystemUI/res/layout-sw600dp/super_status_bar.xml @@ -26,15 +26,16 @@ android:fitsSystemWindows="true" > + <include layout="@layout/status_bar" + android:layout_width="match_parent" + android:layout_height="@*android:dimen/status_bar_height" + /> + <include layout="@layout/status_bar_expanded" android:layout_width="@dimen/notification_panel_width" android:layout_height="match_parent" android:layout_gravity="center_horizontal|top" - /> - - <include layout="@layout/status_bar" - android:layout_width="match_parent" - android:layout_height="@*android:dimen/status_bar_height" + android:visibility="invisible" /> </com.android.systemui.statusbar.phone.StatusBarWindowView> diff --git a/packages/SystemUI/res/layout/super_status_bar.xml b/packages/SystemUI/res/layout/super_status_bar.xml index 6c31ff4..b85686f 100644 --- a/packages/SystemUI/res/layout/super_status_bar.xml +++ b/packages/SystemUI/res/layout/super_status_bar.xml @@ -26,14 +26,15 @@ android:fitsSystemWindows="true" > - <include layout="@layout/status_bar_expanded" + <include layout="@layout/status_bar" android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="@*android:dimen/status_bar_height" /> - <include layout="@layout/status_bar" + <include layout="@layout/status_bar_expanded" android:layout_width="match_parent" - android:layout_height="@*android:dimen/status_bar_height" + android:layout_height="match_parent" + android:visibility="invisible" /> </com.android.systemui.statusbar.phone.StatusBarWindowView> diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 07d55f1..be59075 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -33,9 +33,6 @@ <!-- Height of search panel including navigation bar height --> <dimen name="navbar_search_panel_height">300dip</dimen> - <!-- Extra space above the clock in the panel; on this device, zero --> - <dimen name="notification_panel_header_padding_top">0dp</dimen> - <!-- Size of application thumbnail --> <dimen name="status_bar_recents_thumbnail_width">200dp</dimen> <dimen name="status_bar_recents_thumbnail_height">177dp</dimen> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 0d79a9b..b1611d1 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -132,10 +132,10 @@ <!-- Height of the notification panel header bar --> <dimen name="notification_panel_header_height">48dp</dimen> - <!-- Height of the notification panel header bar --> - <dimen name="notification_panel_padding_top">@*android:dimen/status_bar_height</dimen> + <!-- Extra space above the panel --> + <dimen name="notification_panel_padding_top">4dp</dimen> - <!-- Extra space above the clock in the panel; half of (notification_panel_header_height - 32) --> + <!-- Extra space above the clock in the panel --> <dimen name="notification_panel_header_padding_top">0dp</dimen> <!-- Layout parameters for the notification panel --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index af77a30..a23fe12 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -59,7 +59,7 @@ <style name="TextAppearance.StatusBar.Expanded.Date"> <item name="android:textSize">12dp</item> <item name="android:textStyle">normal</item> - <item name="android:textColor">#666666</item> + <item name="android:textColor">#cccccc</item> <item name="android:textAllCaps">true</item> </style> diff --git a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java index c60c806..96f83c6 100644 --- a/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java +++ b/packages/SystemUI/src/com/android/systemui/ImageWallpaper.java @@ -281,9 +281,16 @@ public class ImageWallpaper extends WallpaperService { return; } - if (mBackgroundWidth < 0 || mBackgroundHeight < 0) { - // If we don't yet know the size of the wallpaper bitmap, - // we need to get it now. + // If we don't yet know the size of the wallpaper bitmap, + // we need to get it now. + boolean updateWallpaper = mBackgroundWidth < 0 || mBackgroundHeight < 0 ; + + // If we somehow got to this point after we have last flushed + // the wallpaper, well we really need it to draw again. So + // seems like we need to reload it. Ouch. + updateWallpaper = updateWallpaper || mBackground == null; + + if (updateWallpaper) { updateWallpaperLocked(); } @@ -308,12 +315,6 @@ public class ImageWallpaper extends WallpaperService { mLastXTranslation = xPixels; mLastYTranslation = yPixels; - if (mBackground == null) { - // If we somehow got to this point after we have last flushed - // the wallpaper, well we really need it to draw again. So - // seems like we need to reload it. Ouch. - updateWallpaperLocked(); - } if (mIsHwAccelerated) { if (!drawWallpaperWithOpenGL(sh, availw, availh, xPixels, yPixels)) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java index 2f02d23..1321ade 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/SignalClusterView.java @@ -21,9 +21,9 @@ import android.util.AttributeSet; import android.util.Slog; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; import android.widget.ImageView; import android.widget.LinearLayout; -import android.widget.TextView; import com.android.systemui.statusbar.policy.NetworkController; @@ -31,12 +31,12 @@ import com.android.systemui.R; // Intimately tied to the design of res/layout/signal_cluster_view.xml public class SignalClusterView - extends LinearLayout + extends LinearLayout implements NetworkController.SignalCluster { static final boolean DEBUG = false; static final String TAG = "SignalClusterView"; - + NetworkController mNC; private boolean mWifiVisible = false; @@ -132,6 +132,17 @@ public class SignalClusterView apply(); } + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Standard group layout onPopulateAccessibilityEvent() implementations + // ignore content description, so populate manually + if (mWifiVisible && mWifiGroup.getContentDescription() != null) + event.getText().add(mWifiGroup.getContentDescription()); + if (mMobileVisible && mMobileGroup.getContentDescription() != null) + event.getText().add(mMobileGroup.getContentDescription()); + return super.dispatchPopulateAccessibilityEvent(event); + } + // Run after each indicator change. private void apply() { if (mWifiGroup == null) return; 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 4e6857e..69d2e73 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -301,6 +301,9 @@ public class PhoneStatusBar extends BaseStatusBar { return true; } }); + mNotificationPanel.setSystemUiVisibility( + View.STATUS_BAR_DISABLE_NOTIFICATION_TICKER + | View.STATUS_BAR_DISABLE_SYSTEM_INFO); if (!ActivityManager.isHighEndGfx(mDisplay)) { mStatusBarWindow.setBackground(null); @@ -336,7 +339,6 @@ public class PhoneStatusBar extends BaseStatusBar { mPixelFormat = PixelFormat.OPAQUE; mStatusIcons = (LinearLayout)mStatusBarView.findViewById(R.id.statusIcons); mNotificationIcons = (IconMerger)mStatusBarView.findViewById(R.id.notificationIcons); - mMoreIcon = mStatusBarView.findViewById(R.id.moreIcon); mNotificationIcons.setOverflowIndicator(mMoreIcon); mIcons = (LinearLayout)mStatusBarView.findViewById(R.id.icons); mTickerView = mStatusBarView.findViewById(R.id.ticker); @@ -884,6 +886,15 @@ public class PhoneStatusBar extends BaseStatusBar { flagdbg.append(((diff & StatusBarManager.DISABLE_CLOCK) != 0) ? "* " : " "); flagdbg.append(">"); Slog.d(TAG, flagdbg.toString()); + + if ((diff & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) { + mIcons.animate().cancel(); + if ((state & StatusBarManager.DISABLE_SYSTEM_INFO) != 0) { + mIcons.animate().alpha(0f).setStartDelay(100).setDuration(200).start(); + } else { + mIcons.animate().alpha(1f).setStartDelay(0).setDuration(300).start(); + } + } if ((diff & StatusBarManager.DISABLE_CLOCK) != 0) { boolean show = (state & StatusBarManager.DISABLE_CLOCK) == 0; @@ -985,6 +996,7 @@ public class PhoneStatusBar extends BaseStatusBar { } mExpandedVisible = true; + mNotificationPanel.setVisibility(View.VISIBLE); updateExpandedViewPos(EXPANDED_LEAVE_ALONE); @@ -1078,7 +1090,7 @@ public class PhoneStatusBar extends BaseStatusBar { } mExpandedVisible = false; visibilityChanged(false); - //mNotificationPanel.setVisibility(View.GONE); + mNotificationPanel.setVisibility(View.INVISIBLE); // Shrink the window to the size of the status bar only WindowManager.LayoutParams lp = (WindowManager.LayoutParams) mStatusBarWindow.getLayoutParams(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java index a05fcc1..c65f581 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NetworkController.java @@ -282,7 +282,8 @@ public class NetworkController extends BroadcastReceiver { public void refreshSignalCluster(SignalCluster cluster) { cluster.setWifiIndicators( - mWifiConnected, // only show wifi in the cluster if connected + // only show wifi in the cluster if connected or if wifi-only + mWifiEnabled && (mWifiConnected || !mHasMobileDataFeature), mWifiIconId, mWifiActivityIconId, mContentDescriptionWifi); @@ -786,7 +787,7 @@ public class NetworkController extends BroadcastReceiver { if (mDataAndWifiStacked) { mWifiIconId = 0; } else { - mWifiIconId = mWifiEnabled ? WifiIcons.WIFI_SIGNAL_STRENGTH[0][0] : 0; + mWifiIconId = mWifiEnabled ? R.drawable.stat_sys_wifi_signal_null : 0; } mContentDescriptionWifi = mContext.getString(R.string.accessibility_no_wifi); } diff --git a/policy/src/com/android/internal/policy/impl/FaceUnlock.java b/policy/src/com/android/internal/policy/impl/FaceUnlock.java index c46b94a..737ea47 100644 --- a/policy/src/com/android/internal/policy/impl/FaceUnlock.java +++ b/policy/src/com/android/internal/policy/impl/FaceUnlock.java @@ -300,7 +300,18 @@ public class FaceUnlock implements BiometricSensorUnlock, Handler.Callback { * onServiceConnected() callback is received. */ void handleServiceConnected() { - if (DEBUG) Log.d(TAG, "handleServiceConnected()"); + Log.d(TAG, "handleServiceConnected()"); + + // It is possible that an unbind has occurred in the time between the bind and when this + // function is reached. If an unbind has already occurred, proceeding on to call startUi() + // can result in a fatal error. Note that the onServiceConnected() callback is + // asynchronous, so this possibility would still exist if we executed this directly in + // onServiceConnected() rather than using a handler. + if (!mBoundToService) { + Log.d(TAG, "Dropping startUi() in handleServiceConnected() because no longer bound"); + return; + } + try { mService.registerCallback(mFaceUnlockCallback); } catch (RemoteException e) { @@ -452,25 +463,12 @@ public class FaceUnlock implements BiometricSensorUnlock, Handler.Callback { * Tells the Face Unlock service to start displaying its UI and start processing. */ private void startUi(IBinder windowToken, int x, int y, int w, int h) { - Log.d(TAG, "startUi()"); + if (DEBUG) Log.d(TAG, "startUi()"); synchronized (mServiceRunningLock) { if (!mServiceRunning) { - if (DEBUG) Log.d(TAG, "Starting Face Unlock"); + Log.d(TAG, "Starting Face Unlock"); try { - // TODO: these checks and logs are for tracking down bug 6409767 and can be - // removed when that bug is fixed. - if (mService == null) { - Log.d(TAG, "mService is null"); - } - if (windowToken == null) { - Log.d(TAG, "windowToken is null"); - } - if (mLockPatternUtils == null) { - Log.d(TAG, "mLockPatternUtils is null"); - } - Log.d(TAG, "x,y,w,h,live: " + x + "," + y + "," + w + "," + h + ", no"); mService.startUi(windowToken, x, y, w, h, false); - Log.d(TAG, "mService.startUi() called"); } catch (RemoteException e) { Log.e(TAG, "Caught exception starting Face Unlock: " + e.toString()); return; @@ -492,7 +490,7 @@ public class FaceUnlock implements BiometricSensorUnlock, Handler.Callback { // screen is turned off. That's why we check. synchronized (mServiceRunningLock) { if (mServiceRunning) { - if (DEBUG) Log.d(TAG, "Stopping Face Unlock"); + Log.d(TAG, "Stopping Face Unlock"); try { mService.stopUi(); } catch (RemoteException e) { diff --git a/policy/src/com/android/internal/policy/impl/KeyguardViewManager.java b/policy/src/com/android/internal/policy/impl/KeyguardViewManager.java index 7f432bf..504bb63 100644 --- a/policy/src/com/android/internal/policy/impl/KeyguardViewManager.java +++ b/policy/src/com/android/internal/policy/impl/KeyguardViewManager.java @@ -117,7 +117,6 @@ public class KeyguardViewManager implements KeyguardWindowController { final int stretch = ViewGroup.LayoutParams.MATCH_PARENT; int flags = WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN | WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER - | WindowManager.LayoutParams.FLAG_KEEP_SURFACE_WHILE_ANIMATING | WindowManager.LayoutParams.FLAG_SLIPPERY /*| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR*/ ; diff --git a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java index 8ab148e..cee01ac 100755 --- a/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java +++ b/policy/src/com/android/internal/policy/impl/PhoneWindowManager.java @@ -158,7 +158,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { static final boolean DEBUG = false; static final boolean localLOGV = false; static final boolean DEBUG_LAYOUT = false; - static final boolean DEBUG_FALLBACK = false; + static final boolean DEBUG_INPUT = false; static final boolean SHOW_STARTING_ANIMATIONS = true; static final boolean SHOW_PROCESSES_ON_ALT_MENU = false; @@ -410,6 +410,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { int mSystemLeft, mSystemTop, mSystemRight, mSystemBottom; // For applications requesting stable content insets, these are them. int mStableLeft, mStableTop, mStableRight, mStableBottom; + // For applications requesting stable content insets but have also set the + // fullscreen window flag, these are the stable dimensions without the status bar. + int mStableFullscreenLeft, mStableFullscreenTop; + int mStableFullscreenRight, mStableFullscreenBottom; // During layout, the current screen borders with all outer decoration // (status bar, input method dock) accounted for. int mCurLeft, mCurTop, mCurRight, mCurBottom; @@ -500,6 +504,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { ShortcutManager mShortcutManager; PowerManager.WakeLock mBroadcastWakeLock; + boolean mHavePendingMediaKeyRepeatWithWakeLock; // Fallback actions by key code. private final SparseArray<KeyCharacterMap.FallbackAction> mFallbackActions = @@ -507,6 +512,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { private static final int MSG_ENABLE_POINTER_LOCATION = 1; private static final int MSG_DISABLE_POINTER_LOCATION = 2; + private static final int MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK = 3; + private static final int MSG_DISPATCH_MEDIA_KEY_REPEAT_WITH_WAKE_LOCK = 4; private class PolicyHandler extends Handler { @Override @@ -518,6 +525,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { case MSG_DISABLE_POINTER_LOCATION: disablePointerLocation(); break; + case MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK: + dispatchMediaKeyWithWakeLock((KeyEvent)msg.obj); + break; + case MSG_DISPATCH_MEDIA_KEY_REPEAT_WITH_WAKE_LOCK: + dispatchMediaKeyRepeatWithWakeLock((KeyEvent)msg.obj); + break; } } } @@ -1688,7 +1701,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; final boolean canceled = event.isCanceled(); - if (false) { + if (DEBUG_INPUT) { Log.d(TAG, "interceptKeyTi keyCode=" + keyCode + " down=" + down + " repeatCount=" + repeatCount + " keyguardOn=" + keyguardOn + " mHomePressed=" + mHomePressed + " canceled=" + canceled); @@ -1938,7 +1951,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override public KeyEvent dispatchUnhandledKey(WindowState win, KeyEvent event, int policyFlags) { // Note: This method is only called if the initial down was unhandled. - if (DEBUG_FALLBACK) { + if (DEBUG_INPUT) { Slog.d(TAG, "Unhandled key: win=" + win + ", action=" + event.getAction() + ", flags=" + event.getFlags() + ", keyCode=" + event.getKeyCode() @@ -1965,7 +1978,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } if (fallbackAction != null) { - if (DEBUG_FALLBACK) { + if (DEBUG_INPUT) { Slog.d(TAG, "Fallback: keyCode=" + fallbackAction.keyCode + " metaState=" + Integer.toHexString(fallbackAction.metaState)); } @@ -1992,7 +2005,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } - if (DEBUG_FALLBACK) { + if (DEBUG_INPUT) { if (fallbackEvent == null) { Slog.d(TAG, "No fallback."); } else { @@ -2143,22 +2156,31 @@ public class PhoneWindowManager implements WindowManagerPolicy { public void getContentInsetHintLw(WindowManager.LayoutParams attrs, Rect contentInset) { final int fl = attrs.flags; + final int systemUiVisibility = (attrs.systemUiVisibility|attrs.subtreeSystemUiVisibility); - if ((fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_FULLSCREEN | FLAG_LAYOUT_INSET_DECOR)) + if ((fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) == (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) { int availRight, availBottom; if (mCanHideNavigationBar && - (attrs.systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0) { + (systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0) { availRight = mUnrestrictedScreenLeft + mUnrestrictedScreenWidth; availBottom = mUnrestrictedScreenTop + mUnrestrictedScreenHeight; } else { availRight = mRestrictedScreenLeft + mRestrictedScreenWidth; availBottom = mRestrictedScreenTop + mRestrictedScreenHeight; } - if ((attrs.systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { - contentInset.set(mStableLeft, mStableTop, - availRight - mStableRight, availBottom - mStableBottom); - } else if ((attrs.systemUiVisibility & (View.SYSTEM_UI_FLAG_FULLSCREEN + if ((systemUiVisibility & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { + if ((fl & FLAG_FULLSCREEN) != 0) { + contentInset.set(mStableFullscreenLeft, mStableFullscreenTop, + availRight - mStableFullscreenRight, + availBottom - mStableFullscreenBottom); + } else { + contentInset.set(mStableLeft, mStableTop, + availRight - mStableRight, availBottom - mStableBottom); + } + } else if ((fl & FLAG_FULLSCREEN) != 0) { + contentInset.setEmpty(); + } else if ((systemUiVisibility & (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)) == 0) { contentInset.set(mCurLeft, mCurTop, availRight - mCurRight, availBottom - mCurBottom); @@ -2179,10 +2201,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { mRestrictedScreenLeft = mRestrictedScreenTop = 0; mRestrictedScreenWidth = displayWidth; mRestrictedScreenHeight = displayHeight; - mDockLeft = mContentLeft = mStableLeft = mSystemLeft = mCurLeft = 0; - mDockTop = mContentTop = mStableTop = mSystemTop = mCurTop = 0; - mDockRight = mContentRight = mStableRight = mSystemRight = mCurRight = displayWidth; - mDockBottom = mContentBottom = mStableBottom = mSystemBottom = mCurBottom = displayHeight; + mDockLeft = mContentLeft = mStableLeft = mStableFullscreenLeft + = mSystemLeft = mCurLeft = 0; + mDockTop = mContentTop = mStableTop = mStableFullscreenTop + = mSystemTop = mCurTop = 0; + mDockRight = mContentRight = mStableRight = mStableFullscreenRight + = mSystemRight = mCurRight = displayWidth; + mDockBottom = mContentBottom = mStableBottom = mStableFullscreenBottom + = mSystemBottom = mCurBottom = displayHeight; mDockLayer = 0x10000000; mStatusBarLayer = -1; @@ -2235,7 +2261,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } mTmpNavigationFrame.set(0, top, displayWidth, displayHeight); - mStableBottom = mTmpNavigationFrame.top; + mStableBottom = mStableFullscreenBottom = mTmpNavigationFrame.top; if (navVisible) { mNavigationBar.showLw(true); mDockBottom = mTmpNavigationFrame.top; @@ -2259,7 +2285,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } } mTmpNavigationFrame.set(left, 0, displayWidth, displayHeight); - mStableRight = mTmpNavigationFrame.left; + mStableRight = mStableFullscreenRight = mTmpNavigationFrame.left; if (navVisible) { mNavigationBar.showLw(true); mDockRight = mTmpNavigationFrame.left; @@ -2397,7 +2423,25 @@ public class PhoneWindowManager implements WindowManagerPolicy { pf.set((fl & FLAG_LAYOUT_IN_SCREEN) == 0 ? attached.getFrameLw() : df); } - + + private void applyStableConstraints(int sysui, int fl, Rect r) { + if ((sysui & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { + // If app is requesting a stable layout, don't let the + // content insets go below the stable values. + if ((fl & FLAG_FULLSCREEN) != 0) { + if (r.left < mStableFullscreenLeft) r.left = mStableFullscreenLeft; + if (r.top < mStableFullscreenTop) r.top = mStableFullscreenTop; + if (r.right > mStableFullscreenRight) r.right = mStableFullscreenRight; + if (r.bottom > mStableFullscreenBottom) r.bottom = mStableFullscreenBottom; + } else { + if (r.left < mStableLeft) r.left = mStableLeft; + if (r.top < mStableTop) r.top = mStableTop; + if (r.right > mStableRight) r.right = mStableRight; + if (r.bottom > mStableBottom) r.bottom = mStableBottom; + } + } + } + /** {@inheritDoc} */ public void layoutWindowLw(WindowState win, WindowManager.LayoutParams attrs, WindowState attached) { @@ -2504,14 +2548,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { cf.right = mContentRight; cf.bottom = mContentBottom; } - if ((sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { - // If app is requesting a stable layout, don't let the - // content insets go below the stable values. - if (cf.left < mStableLeft) cf.left = mStableLeft; - if (cf.top < mStableTop) cf.top = mStableTop; - if (cf.right > mStableRight) cf.right = mStableRight; - if (cf.bottom > mStableBottom) cf.bottom = mStableBottom; - } + applyStableConstraints(sysUiFl, fl, cf); if (adjust != SOFT_INPUT_ADJUST_NOTHING) { vf.left = mCurLeft; vf.top = mCurTop; @@ -2593,14 +2630,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { pf.bottom = df.bottom = cf.bottom = mRestrictedScreenTop+mRestrictedScreenHeight; } - if ((sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0) { - // If app is requesting a stable layout, don't let the - // content insets go below the stable values. - if (cf.left < mStableLeft) cf.left = mStableLeft; - if (cf.top < mStableTop) cf.top = mStableTop; - if (cf.right > mStableRight) cf.right = mStableRight; - if (cf.bottom > mStableBottom) cf.bottom = mStableBottom; - } + applyStableConstraints(sysUiFl, fl, cf); if (adjust != SOFT_INPUT_ADJUST_NOTHING) { vf.left = mCurLeft; vf.top = mCurTop; @@ -3068,7 +3098,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { return 0; } - if (false) { + if (DEBUG_INPUT) { Log.d(TAG, "interceptKeyTq keycode=" + keyCode + " screenIsOn=" + isScreenOn + " keyguardActive=" + keyguardActive); } @@ -3290,8 +3320,13 @@ public class PhoneWindowManager implements WindowManagerPolicy { // Only do this if we would otherwise not pass it to the user. In that // case, the PhoneWindow class will do the same thing, except it will // only do it if the showing app doesn't process the key on its own. + // Note that we need to make a copy of the key event here because the + // original key event will be recycled when we return. mBroadcastWakeLock.acquire(); - mHandler.post(new PassHeadsetKey(new KeyEvent(event))); + Message msg = mHandler.obtainMessage(MSG_DISPATCH_MEDIA_KEY_WITH_WAKE_LOCK, + new KeyEvent(event)); + msg.setAsynchronous(true); + msg.sendToTarget(); } break; } @@ -3340,24 +3375,58 @@ public class PhoneWindowManager implements WindowManagerPolicy { return result; } - class PassHeadsetKey implements Runnable { - KeyEvent mKeyEvent; + void dispatchMediaKeyWithWakeLock(KeyEvent event) { + if (DEBUG_INPUT) { + Slog.d(TAG, "dispatchMediaKeyWithWakeLock: " + event); + } + + if (mHavePendingMediaKeyRepeatWithWakeLock) { + if (DEBUG_INPUT) { + Slog.d(TAG, "dispatchMediaKeyWithWakeLock: canceled repeat"); + } - PassHeadsetKey(KeyEvent keyEvent) { - mKeyEvent = keyEvent; + mHandler.removeMessages(MSG_DISPATCH_MEDIA_KEY_REPEAT_WITH_WAKE_LOCK); + mHavePendingMediaKeyRepeatWithWakeLock = false; + mBroadcastWakeLock.release(); // pending repeat was holding onto the wake lock } - public void run() { - if (ActivityManagerNative.isSystemReady()) { - IAudioService audioService = getAudioService(); - if (audioService != null) { - try { - audioService.dispatchMediaKeyEventUnderWakelock(mKeyEvent); - } catch (RemoteException e) { - Log.e(TAG, "dispatchMediaKeyEvent threw exception " + e); - } + dispatchMediaKeyWithWakeLockToAudioService(event); + + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getRepeatCount() == 0) { + mHavePendingMediaKeyRepeatWithWakeLock = true; + + Message msg = mHandler.obtainMessage( + MSG_DISPATCH_MEDIA_KEY_REPEAT_WITH_WAKE_LOCK, event); + msg.setAsynchronous(true); + mHandler.sendMessageDelayed(msg, ViewConfiguration.getKeyRepeatTimeout()); + } else { + mBroadcastWakeLock.release(); + } + } + + void dispatchMediaKeyRepeatWithWakeLock(KeyEvent event) { + mHavePendingMediaKeyRepeatWithWakeLock = false; + + KeyEvent repeatEvent = KeyEvent.changeTimeRepeat(event, + SystemClock.uptimeMillis(), 1, event.getFlags() | KeyEvent.FLAG_LONG_PRESS); + if (DEBUG_INPUT) { + Slog.d(TAG, "dispatchMediaKeyRepeatWithWakeLock: " + repeatEvent); + } + + dispatchMediaKeyWithWakeLockToAudioService(repeatEvent); + mBroadcastWakeLock.release(); + } + + void dispatchMediaKeyWithWakeLockToAudioService(KeyEvent event) { + if (ActivityManagerNative.isSystemReady()) { + IAudioService audioService = getAudioService(); + if (audioService != null) { + try { + audioService.dispatchMediaKeyEventUnderWakelock(event); + } catch (RemoteException e) { + Log.e(TAG, "dispatchMediaKeyEvent threw exception " + e); } - mBroadcastWakeLock.release(); } } } @@ -4248,6 +4317,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { pw.print(","); pw.print(mRestrictedScreenTop); pw.print(") "); pw.print(mRestrictedScreenWidth); pw.print("x"); pw.println(mRestrictedScreenHeight); + pw.print(prefix); pw.print("mStableFullscreen=("); pw.print(mStableFullscreenLeft); + pw.print(","); pw.print(mStableFullscreenTop); + pw.print(")-("); pw.print(mStableFullscreenRight); + pw.print(","); pw.print(mStableFullscreenBottom); pw.println(")"); pw.print(prefix); pw.print("mStable=("); pw.print(mStableLeft); pw.print(","); pw.print(mStableTop); pw.print(")-("); pw.print(mStableRight); diff --git a/services/java/com/android/server/BackupManagerService.java b/services/java/com/android/server/BackupManagerService.java index a3768c6..2167c49 100644 --- a/services/java/com/android/server/BackupManagerService.java +++ b/services/java/com/android/server/BackupManagerService.java @@ -48,6 +48,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import android.content.pm.PackageManager.NameNotFoundException; +import android.database.ContentObserver; import android.net.Uri; import android.os.Binder; import android.os.Build; @@ -252,6 +253,34 @@ class BackupManagerService extends IBackupManager.Stub { IBackupTransport mLocalTransport, mGoogleTransport; ActiveRestoreSession mActiveRestoreSession; + // Watch the device provisioning operation during setup + ContentObserver mProvisionedObserver; + + class ProvisionedObserver extends ContentObserver { + public ProvisionedObserver(Handler handler) { + super(handler); + } + + public void onChange(boolean selfChange) { + final boolean wasProvisioned = mProvisioned; + final boolean isProvisioned = deviceIsProvisioned(); + // latch: never unprovision + mProvisioned = wasProvisioned || isProvisioned; + if (MORE_DEBUG) { + Slog.d(TAG, "Provisioning change: was=" + wasProvisioned + + " is=" + isProvisioned + " now=" + mProvisioned); + } + + synchronized (mQueueLock) { + if (mProvisioned && !wasProvisioned && mEnabled) { + // we're now good to go, so start the backup alarms + if (MORE_DEBUG) Slog.d(TAG, "Now provisioned, so starting backups"); + startBackupAlarmsLocked(FIRST_BACKUP_INTERVAL); + } + } + } + } + class RestoreGetSetsParams { public IBackupTransport transport; public ActiveRestoreSession session; @@ -695,12 +724,19 @@ class BackupManagerService extends IBackupManager.Stub { mBackupHandler = new BackupHandler(mHandlerThread.getLooper()); // Set up our bookkeeping - boolean areEnabled = Settings.Secure.getInt(context.getContentResolver(), + final ContentResolver resolver = context.getContentResolver(); + boolean areEnabled = Settings.Secure.getInt(resolver, Settings.Secure.BACKUP_ENABLED, 0) != 0; - mProvisioned = Settings.Secure.getInt(context.getContentResolver(), - Settings.Secure.BACKUP_PROVISIONED, 0) != 0; - mAutoRestore = Settings.Secure.getInt(context.getContentResolver(), + mProvisioned = Settings.Secure.getInt(resolver, + Settings.Secure.DEVICE_PROVISIONED, 0) != 0; + mAutoRestore = Settings.Secure.getInt(resolver, Settings.Secure.BACKUP_AUTO_RESTORE, 1) != 0; + + mProvisionedObserver = new ProvisionedObserver(mBackupHandler); + resolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.DEVICE_PROVISIONED), + false, mProvisionedObserver); + // If Encrypted file systems is enabled or disabled, this call will return the // correct directory. mBaseStateDir = new File(Environment.getSecureDataDirectory(), "backup"); @@ -5172,24 +5208,9 @@ class BackupManagerService extends IBackupManager.Stub { public void setBackupProvisioned(boolean available) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP, "setBackupProvisioned"); - - boolean wasProvisioned = mProvisioned; - synchronized (this) { - Settings.Secure.putInt(mContext.getContentResolver(), - Settings.Secure.BACKUP_PROVISIONED, available ? 1 : 0); - mProvisioned = available; - } - - synchronized (mQueueLock) { - if (available && !wasProvisioned && mEnabled) { - // we're now good to go, so start the backup alarms - startBackupAlarmsLocked(FIRST_BACKUP_INTERVAL); - } else if (!available) { - // No longer enabled, so stop running backups - Slog.w(TAG, "Backup service no longer provisioned"); - mAlarmManager.cancel(mRunBackupIntent); - } - } + /* + * This is now a no-op; provisioning is simply the device's own setup state. + */ } private void startBackupAlarmsLocked(long delayBeforeFirstBackup) { diff --git a/services/java/com/android/server/MountService.java b/services/java/com/android/server/MountService.java index d6606f6..13ab586 100644 --- a/services/java/com/android/server/MountService.java +++ b/services/java/com/android/server/MountService.java @@ -79,6 +79,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import java.util.Set; import javax.crypto.SecretKey; @@ -178,7 +180,8 @@ class MountService extends IMountService.Stub final private ArrayList<MountServiceBinderListener> mListeners = new ArrayList<MountServiceBinderListener>(); private boolean mBooted = false; - private boolean mReady = false; + private CountDownLatch mConnectedSignal = new CountDownLatch(1); + private CountDownLatch mAsecsScanned = new CountDownLatch(1); private boolean mSendUmsConnectedOnBoot = false; // true if we should fake MEDIA_MOUNTED state for external storage private boolean mEmulateExternalStorage = false; @@ -446,15 +449,30 @@ class MountService extends IMountService.Stub final private HandlerThread mHandlerThread; final private Handler mHandler; + void waitForAsecScan() { + waitForLatch(mAsecsScanned); + } + private void waitForReady() { - while (mReady == false) { - for (int retries = 5; retries > 0; retries--) { - if (mReady) { + waitForLatch(mConnectedSignal); + } + + private void waitForLatch(CountDownLatch latch) { + if (latch == null) { + return; + } + + for (;;) { + try { + if (latch.await(5000, TimeUnit.MILLISECONDS)) { return; + } else { + Slog.w(TAG, "Thread " + Thread.currentThread().getName() + + " still waiting for MountService ready..."); } - SystemClock.sleep(1000); + } catch (InterruptedException e) { + Slog.w(TAG, "Interrupt while waiting for MountService to be ready."); } - Slog.w(TAG, "Waiting too long for mReady!"); } } @@ -627,7 +645,7 @@ class MountService extends IMountService.Stub * Since we'll be calling back into the NativeDaemonConnector, * we need to do our work in a new thread. */ - new Thread() { + new Thread("MountService#onDaemonConnected") { @Override public void run() { /** @@ -668,14 +686,19 @@ class MountService extends IMountService.Stub updatePublicVolumeState(mExternalStoragePath, Environment.MEDIA_REMOVED); } - // Let package manager load internal ASECs. - mPms.updateExternalMediaStatus(true, false); - /* * Now that we've done our initialization, release * the hounds! */ - mReady = true; + mConnectedSignal.countDown(); + mConnectedSignal = null; + + // Let package manager load internal ASECs. + mPms.scanAvailableAsecs(); + + // Notify people waiting for ASECs to be scanned that it's done. + mAsecsScanned.countDown(); + mAsecsScanned = null; } }.start(); } @@ -1159,22 +1182,12 @@ class MountService extends IMountService.Stub mObbActionHandler = new ObbActionHandler(mHandlerThread.getLooper()); /* - * Vold does not run in the simulator, so pretend the connector thread - * ran and did its thing. - */ - if ("simulator".equals(SystemProperties.get("ro.product.device"))) { - mReady = true; - mUmsEnabling = true; - return; - } - - /* * Create the connection to vold with a maximum queue of twice the * amount of containers we'd ever expect to have. This keeps an * "asec list" from blocking a thread repeatedly. */ mConnector = new NativeDaemonConnector(this, "vold", MAX_CONTAINERS * 2, VOLD_TAG, 25); - mReady = false; + Thread thread = new Thread(mConnector, VOLD_TAG); thread.start(); diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index bb10358..eaecd4c 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -320,6 +320,21 @@ class ServerThread extends Thread { } if (factoryTest != SystemServer.FACTORY_TEST_LOW_LEVEL) { + MountService mountService = null; + if (!"0".equals(SystemProperties.get("system_init.startmountservice"))) { + try { + /* + * NotificationManagerService is dependant on MountService, + * (for media / usb notifications) so we must start MountService first. + */ + Slog.i(TAG, "Mount Service"); + mountService = new MountService(context); + ServiceManager.addService("mount", mountService); + } catch (Throwable e) { + reportWtf("starting Mount Service", e); + } + } + try { Slog.i(TAG, "LockSettingsService"); lockSettings = new LockSettingsService(context); @@ -441,17 +456,13 @@ class ServerThread extends Thread { reportWtf("starting UpdateLockService", e); } - if (!"0".equals(SystemProperties.get("system_init.startmountservice"))) { - try { - /* - * NotificationManagerService is dependant on MountService, - * (for media / usb notifications) so we must start MountService first. - */ - Slog.i(TAG, "Mount Service"); - ServiceManager.addService("mount", new MountService(context)); - } catch (Throwable e) { - reportWtf("starting Mount Service", e); - } + /* + * MountService has a few dependencies: Notification Manager and + * AppWidget Provider. Make sure MountService is completely started + * first before continuing. + */ + if (mountService != null) { + mountService.waitForAsecScan(); } try { diff --git a/services/java/com/android/server/WifiService.java b/services/java/com/android/server/WifiService.java index b1558c7..1f03d17 100644 --- a/services/java/com/android/server/WifiService.java +++ b/services/java/com/android/server/WifiService.java @@ -376,11 +376,7 @@ public class WifiService extends IWifiManager.Stub { @Override public void onReceive(Context context, Intent intent) { mAirplaneModeOn.set(isAirplaneModeOn()); - /* On airplane mode disable, restore wifi state if necessary */ - if (!mAirplaneModeOn.get() && (testAndClearWifiSavedState() || - mPersistWifiState.get() == WIFI_ENABLED_AIRPLANE_OVERRIDE)) { - persistWifiState(true); - } + handleAirplaneModeToggled(mAirplaneModeOn.get()); updateWifiState(); } }, @@ -447,7 +443,10 @@ public class WifiService extends IWifiManager.Stub { boolean wifiEnabled = shouldWifiBeEnabled() || testAndClearWifiSavedState(); Slog.i(TAG, "WifiService starting up with Wi-Fi " + (wifiEnabled ? "enabled" : "disabled")); - setWifiEnabled(wifiEnabled); + + // If we are already disabled (could be due to airplane mode), avoid changing persist + // state here + if (wifiEnabled) setWifiEnabled(wifiEnabled); mWifiWatchdogStateMachine = WifiWatchdogStateMachine. makeWifiWatchdogStateMachine(mContext); @@ -485,26 +484,43 @@ public class WifiService extends IWifiManager.Stub { } } - private void persistWifiState(boolean enabled) { - final ContentResolver cr = mContext.getContentResolver(); - boolean airplane = mAirplaneModeOn.get() && isAirplaneToggleable(); - if (enabled) { - if (airplane) { - mPersistWifiState.set(WIFI_ENABLED_AIRPLANE_OVERRIDE); + private void handleWifiToggled(boolean wifiEnabled) { + boolean airplaneEnabled = mAirplaneModeOn.get() && isAirplaneToggleable(); + if (wifiEnabled) { + if (airplaneEnabled) { + persistWifiState(WIFI_ENABLED_AIRPLANE_OVERRIDE); } else { - mPersistWifiState.set(WIFI_ENABLED); + persistWifiState(WIFI_ENABLED); } } else { - if (airplane) { - mPersistWifiState.set(WIFI_DISABLED_AIRPLANE_ON); - } else { - mPersistWifiState.set(WIFI_DISABLED); - } + // When wifi state is disabled, we do not care + // if airplane mode is on or not. The scenario of + // wifi being disabled due to airplane mode being turned on + // is handled handleAirplaneModeToggled() + persistWifiState(WIFI_DISABLED); } + } - Settings.Secure.putInt(cr, Settings.Secure.WIFI_ON, mPersistWifiState.get()); + private void handleAirplaneModeToggled(boolean airplaneEnabled) { + if (airplaneEnabled) { + // Wifi disabled due to airplane on + if (mWifiEnabled) { + persistWifiState(WIFI_DISABLED_AIRPLANE_ON); + } + } else { + /* On airplane mode disable, restore wifi state if necessary */ + if (testAndClearWifiSavedState() || + mPersistWifiState.get() == WIFI_ENABLED_AIRPLANE_OVERRIDE) { + persistWifiState(WIFI_ENABLED); + } + } } + private void persistWifiState(int state) { + final ContentResolver cr = mContext.getContentResolver(); + mPersistWifiState.set(state); + Settings.Secure.putInt(cr, Settings.Secure.WIFI_ON, state); + } /** * see {@link android.net.wifi.WifiManager#pingSupplicant()} @@ -578,12 +594,9 @@ public class WifiService extends IWifiManager.Stub { * only CHANGE_WIFI_STATE is enforced */ - /* Avoids overriding of airplane state when wifi is already in the expected state */ - if (enable != mWifiEnabled) { - long ident = Binder.clearCallingIdentity(); - persistWifiState(enable); - Binder.restoreCallingIdentity(ident); - } + long ident = Binder.clearCallingIdentity(); + handleWifiToggled(enable); + Binder.restoreCallingIdentity(ident); if (enable) { if (!mIsReceiverRegistered) { diff --git a/services/java/com/android/server/input/InputManagerService.java b/services/java/com/android/server/input/InputManagerService.java index 9e94b52..4f1f76f 100644 --- a/services/java/com/android/server/input/InputManagerService.java +++ b/services/java/com/android/server/input/InputManagerService.java @@ -37,7 +37,6 @@ import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.Resources.NotFoundException; import android.content.res.TypedArray; @@ -597,8 +596,8 @@ public class InputManagerService extends IInputManager.Stub implements Watchdog. visitAllKeyboardLayouts(new KeyboardLayoutVisitor() { @Override public void visitKeyboardLayout(Resources resources, - String descriptor, String label, int keyboardLayoutResId) { - list.add(new KeyboardLayout(descriptor, label)); + String descriptor, String label, String collection, int keyboardLayoutResId) { + list.add(new KeyboardLayout(descriptor, label, collection)); } }); return list.toArray(new KeyboardLayout[list.size()]); @@ -614,8 +613,8 @@ public class InputManagerService extends IInputManager.Stub implements Watchdog. visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() { @Override public void visitKeyboardLayout(Resources resources, - String descriptor, String label, int keyboardLayoutResId) { - result[0] = new KeyboardLayout(descriptor, label); + String descriptor, String label, String collection, int keyboardLayoutResId) { + result[0] = new KeyboardLayout(descriptor, label, collection); } }); if (result[0] == null) { @@ -663,6 +662,9 @@ public class InputManagerService extends IInputManager.Stub implements Watchdog. return; } + CharSequence receiverLabel = receiver.loadLabel(pm); + String collection = receiverLabel != null ? receiverLabel.toString() : ""; + try { Resources resources = pm.getResourcesForApplication(receiver.applicationInfo); XmlResourceParser parser = resources.getXml(configResId); @@ -696,7 +698,7 @@ public class InputManagerService extends IInputManager.Stub implements Watchdog. receiver.packageName, receiver.name, name); if (keyboardName == null || name.equals(keyboardName)) { visitor.visitKeyboardLayout(resources, descriptor, - label, keyboardLayoutResId); + label, collection, keyboardLayoutResId); } } } finally { @@ -1139,7 +1141,7 @@ public class InputManagerService extends IInputManager.Stub implements Watchdog. visitKeyboardLayout(keyboardLayoutDescriptor, new KeyboardLayoutVisitor() { @Override public void visitKeyboardLayout(Resources resources, - String descriptor, String label, int keyboardLayoutResId) { + String descriptor, String label, String collection, int keyboardLayoutResId) { try { result[0] = descriptor; result[1] = Streams.readFully(new InputStreamReader( @@ -1262,7 +1264,7 @@ public class InputManagerService extends IInputManager.Stub implements Watchdog. private interface KeyboardLayoutVisitor { void visitKeyboardLayout(Resources resources, - String descriptor, String label, int keyboardLayoutResId); + String descriptor, String label, String collection, int keyboardLayoutResId); } private final class InputDevicesChangedListenerRecord implements DeathRecipient { diff --git a/services/java/com/android/server/pm/PackageManagerService.java b/services/java/com/android/server/pm/PackageManagerService.java index 3936c18..77c3e78 100644 --- a/services/java/com/android/server/pm/PackageManagerService.java +++ b/services/java/com/android/server/pm/PackageManagerService.java @@ -240,6 +240,9 @@ public class PackageManagerService extends IPackageManager.Stub { // This is where all application persistent data goes for secondary users. final File mUserAppDataDir; + /** The location for ASEC container files on internal storage. */ + final String mAsecInternalPath; + // This is the object monitoring the framework dir. final FileObserver mFrameworkInstallObserver; @@ -907,6 +910,7 @@ public class PackageManagerService extends IPackageManager.Stub { File dataDir = Environment.getDataDirectory(); mAppDataDir = new File(dataDir, "data"); + mAsecInternalPath = new File(dataDir, "app-asec").getPath(); mUserAppDataDir = new File(dataDir, "user"); mDrmAppPrivateInstallDir = new File(dataDir, "app-private"); @@ -1043,7 +1047,7 @@ public class PackageManagerService extends IPackageManager.Stub { scanDirLI(mFrameworkDir, PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR, scanMode | SCAN_NO_DEX, 0); - + // Collect all system packages. mSystemAppDir = new File(Environment.getRootDirectory(), "app"); mSystemInstallObserver = new AppDirObserver( @@ -6479,6 +6483,11 @@ public class PackageManagerService extends IPackageManager.Stub { } } + private boolean isAsecExternal(String cid) { + final String asecPath = PackageHelper.getSdFilesystem(cid); + return !asecPath.startsWith(mAsecInternalPath); + } + /** * Extract the MountService "container ID" from the full code path of an * .apk. @@ -6517,7 +6526,7 @@ public class PackageManagerService extends IPackageManager.Stub { } AsecInstallArgs(String cid) { - super(null, null, 0, null, null); + super(null, null, isAsecExternal(cid) ? PackageManager.INSTALL_EXTERNAL : 0, null, null); this.cid = cid; setCachePath(PackageHelper.getSdDir(cid)); } @@ -8659,6 +8668,14 @@ public class PackageManagerService extends IPackageManager.Stub { }); } + /** + * Called by MountService when the initial ASECs to scan are available. + * Should block until all the ASEC containers are finished being scanned. + */ + public void scanAvailableAsecs() { + updateExternalMediaStatusInner(true, false); + } + /* * Collect information of applications on external media, map them against * existing containers and update information based on current mount status. @@ -8793,7 +8810,11 @@ public class PackageManagerService extends IPackageManager.Stub { continue; } // Parse package - int parseFlags = PackageParser.PARSE_ON_SDCARD | mDefParseFlags; + int parseFlags = mDefParseFlags; + if (args.isExternal()) { + parseFlags |= PackageParser.PARSE_ON_SDCARD; + } + doGc = true; synchronized (mInstallLock) { final PackageParser.Package pkg = scanPackageLI(new File(codePath), parseFlags, diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java index b3ac6f1..f460f9b 100755 --- a/services/java/com/android/server/wm/WindowManagerService.java +++ b/services/java/com/android/server/wm/WindowManagerService.java @@ -1173,8 +1173,7 @@ public class WindowManagerService extends IWindowManager.Stub if (DEBUG_INPUT_METHOD) { Slog.i(TAG, "isVisibleOrAdding " + w + ": " + w.isVisibleOrAdding()); if (!w.isVisibleOrAdding()) { - Slog.i(TAG, " mSurface=" + w.mWinAnimator.mSurface + " reportDestroy=" - + w.mWinAnimator.mReportDestroySurface + Slog.i(TAG, " mSurface=" + w.mWinAnimator.mSurface + " relayoutCalled=" + w.mRelayoutCalled + " viewVis=" + w.mViewVisibility + " policyVis=" + w.mPolicyVisibility + " attachHid=" + w.mAttachedHidden + " exiting=" + w.mExiting + " destroying=" + w.mDestroying); @@ -2651,7 +2650,7 @@ public class WindowManagerService extends IWindowManager.Stub int requestedHeight, int viewVisibility, int flags, Rect outFrame, Rect outContentInsets, Rect outVisibleInsets, Configuration outConfig, Surface outSurface) { - boolean displayed = false; + boolean toBeDisplayed = false; boolean inTouchMode; boolean configChanged; boolean surfaceChanged = false; @@ -2754,7 +2753,7 @@ public class WindowManagerService extends IWindowManager.Stub } if (viewVisibility == View.VISIBLE && (win.mAppToken == null || !win.mAppToken.clientHidden)) { - displayed = !win.isVisibleLw(); + toBeDisplayed = !win.isVisibleLw(); if (win.mExiting) { winAnimator.cancelExitAnimationForNextAnimationLocked(); win.mExiting = false; @@ -2766,7 +2765,7 @@ public class WindowManagerService extends IWindowManager.Stub if (oldVisibility == View.GONE) { winAnimator.mEnterAnimationPending = true; } - if (displayed) { + if (toBeDisplayed) { if (win.isDrawnLw() && okToDisplay()) { winAnimator.applyEnterAnimationLocked(); } @@ -2792,7 +2791,7 @@ public class WindowManagerService extends IWindowManager.Stub if ((attrChanges&WindowManager.LayoutParams.FORMAT_CHANGED) != 0) { // To change the format, we need to re-build the surface. winAnimator.destroySurfaceLocked(); - displayed = true; + toBeDisplayed = true; surfaceChanged = true; } try { @@ -2802,8 +2801,6 @@ public class WindowManagerService extends IWindowManager.Stub Surface surface = winAnimator.createSurfaceLocked(); if (surface != null) { outSurface.copyFrom(surface); - winAnimator.mReportDestroySurface = false; - winAnimator.mSurfacePendingDestroy = false; if (SHOW_TRANSACTIONS) Slog.i(TAG, " OUT SURFACE " + outSurface + ": copied"); } else { @@ -2820,7 +2817,7 @@ public class WindowManagerService extends IWindowManager.Stub Binder.restoreCallingIdentity(origId); return 0; } - if (displayed) { + if (toBeDisplayed) { focusMayChange = true; } if (win.mAttrs.type == TYPE_INPUT_METHOD @@ -2845,11 +2842,10 @@ public class WindowManagerService extends IWindowManager.Stub winAnimator.mEnterAnimationPending = false; if (winAnimator.mSurface != null) { if (DEBUG_VISIBILITY) Slog.i(TAG, "Relayout invis " + win - + ": mExiting=" + win.mExiting - + " mSurfacePendingDestroy=" + winAnimator.mSurfacePendingDestroy); + + ": mExiting=" + win.mExiting); // If we are not currently running the exit animation, we // need to see about starting one. - if (!win.mExiting || winAnimator.mSurfacePendingDestroy) { + if (!win.mExiting) { surfaceChanged = true; // Try starting an animation; if there isn't one, we // can destroy the surface right away. @@ -2857,7 +2853,7 @@ public class WindowManagerService extends IWindowManager.Stub if (win.mAttrs.type == TYPE_APPLICATION_STARTING) { transit = WindowManagerPolicy.TRANSIT_PREVIEW_DONE; } - if (!winAnimator.mSurfacePendingDestroy && win.isWinVisibleLw() && + if (win.isWinVisibleLw() && winAnimator.applyAnimationLocked(transit, false)) { focusMayChange = true; win.mExiting = true; @@ -2880,22 +2876,8 @@ public class WindowManagerService extends IWindowManager.Stub } } - if (winAnimator.mSurface == null || (win.getAttrs().flags - & WindowManager.LayoutParams.FLAG_KEEP_SURFACE_WHILE_ANIMATING) == 0 - || winAnimator.mSurfacePendingDestroy) { - // We could be called from a local process, which - // means outSurface holds its current surface. Ensure the - // surface object is cleared, but we don't necessarily want - // it actually destroyed at this point. - winAnimator.mSurfacePendingDestroy = false; - outSurface.release(); - if (DEBUG_VISIBILITY) Slog.i(TAG, "Releasing surface in: " + win); - } else if (winAnimator.mSurface != null) { - if (DEBUG_VISIBILITY) Slog.i(TAG, - "Keeping surface, will report destroy: " + win); - winAnimator.mReportDestroySurface = true; - outSurface.copyFrom(winAnimator.mSurface); - } + outSurface.release(); + if (DEBUG_VISIBILITY) Slog.i(TAG, "Releasing surface in: " + win); } if (focusMayChange) { @@ -2912,7 +2894,7 @@ public class WindowManagerService extends IWindowManager.Stub boolean assignLayers = false; if (imMayMove) { - if (moveInputMethodWindowsIfNeededLocked(false) || displayed) { + if (moveInputMethodWindowsIfNeededLocked(false) || toBeDisplayed) { // Little hack here -- we -should- be able to rely on the // function to return true if the IME has moved and needs // its layer recomputed. However, if the IME was hidden @@ -2934,7 +2916,7 @@ public class WindowManagerService extends IWindowManager.Stub } configChanged = updateOrientationFromAppTokensLocked(false); performLayoutAndPlaceSurfacesLocked(); - if (displayed && win.mIsWallpaper) { + if (toBeDisplayed && win.mIsWallpaper) { updateWallpaperOffsetLocked(win, mAppDisplayWidth, mAppDisplayHeight, false); } if (win.mAppToken != null) { @@ -2970,7 +2952,7 @@ public class WindowManagerService extends IWindowManager.Stub Binder.restoreCallingIdentity(origId); return (inTouchMode ? WindowManagerImpl.RELAYOUT_RES_IN_TOUCH_MODE : 0) - | (displayed ? WindowManagerImpl.RELAYOUT_RES_FIRST_TIME : 0) + | (toBeDisplayed ? WindowManagerImpl.RELAYOUT_RES_FIRST_TIME : 0) | (surfaceChanged ? WindowManagerImpl.RELAYOUT_RES_SURFACE_CHANGED : 0) | (animating ? WindowManagerImpl.RELAYOUT_RES_ANIMATING : 0); } diff --git a/services/java/com/android/server/wm/WindowState.java b/services/java/com/android/server/wm/WindowState.java index 1fd80c2..e2a904f 100644 --- a/services/java/com/android/server/wm/WindowState.java +++ b/services/java/com/android/server/wm/WindowState.java @@ -679,8 +679,7 @@ final class WindowState implements WindowManagerPolicy.WindowState { */ boolean isVisibleOrAdding() { final AppWindowToken atoken = mAppToken; - return ((mHasSurface && !mWinAnimator.mReportDestroySurface) - || (!mRelayoutCalled && mViewVisibility == View.VISIBLE)) + return (mHasSurface || (!mRelayoutCalled && mViewVisibility == View.VISIBLE)) && mPolicyVisibility && !mAttachedHidden && (atoken == null || !atoken.hiddenRequested) && !mExiting && !mDestroying; diff --git a/services/java/com/android/server/wm/WindowStateAnimator.java b/services/java/com/android/server/wm/WindowStateAnimator.java index 5516dea..355db6e 100644 --- a/services/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/java/com/android/server/wm/WindowStateAnimator.java @@ -71,8 +71,6 @@ class WindowStateAnimator { Surface mSurface; Surface mPendingDestroySurface; - boolean mReportDestroySurface; - boolean mSurfacePendingDestroy; /** * Set when we have changed the size of the surface, to know that @@ -561,8 +559,6 @@ class WindowStateAnimator { Surface createSurfaceLocked() { if (mSurface == null) { - mReportDestroySurface = false; - mSurfacePendingDestroy = false; if (DEBUG_ANIM || DEBUG_ORIENTATION) Slog.i(TAG, "createSurface " + this + ": mDrawState=DRAW_PENDING"); mDrawState = DRAW_PENDING; @@ -694,7 +690,6 @@ class WindowStateAnimator { mWin.mAppToken.startingDisplayed = false; } - mDrawState = NO_SURFACE; if (mSurface != null) { int i = mWin.mChildWindows.size(); @@ -704,17 +699,6 @@ class WindowStateAnimator { c.mAttachedHidden = true; } - if (mReportDestroySurface) { - mReportDestroySurface = false; - mSurfacePendingDestroy = true; - try { - mWin.mClient.dispatchGetNewSurface(); - // We'll really destroy on the next time around. - return; - } catch (RemoteException e) { - } - } - try { if (DEBUG_VISIBILITY) { RuntimeException e = null; @@ -760,6 +744,7 @@ class WindowStateAnimator { mSurfaceShown = false; mSurface = null; mWin.mHasSurface =false; + mDrawState = NO_SURFACE; } } @@ -1147,7 +1132,7 @@ class WindowStateAnimator { } } else { if (DEBUG_ANIM) { - Slog.v(TAG, "prepareSurface: No changes in animation for " + mWin); + // Slog.v(TAG, "prepareSurface: No changes in animation for " + mWin); } displayed = true; } diff --git a/tests/HwAccelerationTest/src/com/android/test/hwui/GLTextureViewActivity.java b/tests/HwAccelerationTest/src/com/android/test/hwui/GLTextureViewActivity.java index 414ae0d..0e75b80 100644 --- a/tests/HwAccelerationTest/src/com/android/test/hwui/GLTextureViewActivity.java +++ b/tests/HwAccelerationTest/src/com/android/test/hwui/GLTextureViewActivity.java @@ -210,28 +210,31 @@ public class GLTextureViewActivity extends Activity implements TextureView.Surfa glEnableVertexAttribArray(attribTexCoords); checkGlError(); - glUniform1i(uniformTexture, texture); + glUniform1i(uniformTexture, 0); + checkGlError(); + + // drawQuad + triangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); + glVertexAttribPointer(attribPosition, 3, GL_FLOAT, false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, triangleVertices); + checkGlError(); + + triangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); + glVertexAttribPointer(attribTexCoords, 3, GL_FLOAT, false, + TRIANGLE_VERTICES_DATA_STRIDE_BYTES, triangleVertices); + checkGlError(); + + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); checkGlError(); while (!mFinished) { checkCurrent(); - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - checkGlError(); - glClear(GL_COLOR_BUFFER_BIT); checkGlError(); - // drawQuad - triangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); - glVertexAttribPointer(attribPosition, 3, GL_FLOAT, false, - TRIANGLE_VERTICES_DATA_STRIDE_BYTES, triangleVertices); - - triangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); - glVertexAttribPointer(attribTexCoords, 3, GL_FLOAT, false, - TRIANGLE_VERTICES_DATA_STRIDE_BYTES, triangleVertices); - glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + checkGlError(); if (!mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) { throw new RuntimeException("Cannot swap buffers"); |