diff options
Diffstat (limited to 'core/java')
33 files changed, 1371 insertions, 269 deletions
diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index b390aa1..a2bb78c 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -702,6 +702,10 @@ public final class BluetoothGatt implements BluetoothProfile { * @param start Start or stop advertising */ /*package*/ void listen(boolean start) { + if (mContext == null || !mContext.getResources(). + getBoolean(com.android.internal.R.bool.config_bluetooth_le_peripheral_mode_supported)) { + throw new UnsupportedOperationException("BluetoothGatt#listen is blocked"); + } if (DBG) Log.d(TAG, "listen() - start: " + start); if (mService == null || mClientIf == 0) return; @@ -728,6 +732,10 @@ public final class BluetoothGatt implements BluetoothProfile { /*package*/ void setAdvData(boolean advData, boolean includeName, boolean includeTxPower, Integer minInterval, Integer maxInterval, Integer appearance, Byte[] manufacturerData) { + if (mContext == null || !mContext.getResources(). + getBoolean(com.android.internal.R.bool.config_bluetooth_le_peripheral_mode_supported)) { + throw new UnsupportedOperationException("BluetoothGatt#setAdvData is blocked"); + } if (DBG) Log.d(TAG, "setAdvData()"); if (mService == null || mClientIf == 0) return; diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index d7ca915..dfc0412 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -3300,8 +3300,10 @@ public class Intent implements Parcelable, Cloneable { /** * Optional extra for {@link #ACTION_SHUTDOWN} that allows the sender to qualify that * this shutdown is only for the user space of the system, not a complete shutdown. - * Hardware should not be shut down when this is true. The default if not supplied - * is false. + * When this is true, hardware devices can use this information to determine that + * they shouldn't do a complete shutdown of their device since this is not a + * complete shutdown down to the kernel, but only user space restarting. + * The default if not supplied is false. */ public static final String EXTRA_SHUTDOWN_USERSPACE_ONLY = "android.intent.extra.SHUTDOWN_USERSPACE_ONLY"; diff --git a/core/java/android/hardware/FlushCompleteListener.java b/core/java/android/hardware/FlushCompleteListener.java new file mode 100644 index 0000000..cb5b9e3 --- /dev/null +++ b/core/java/android/hardware/FlushCompleteListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2008 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.hardware; + +/** + * Used for receiving a notification when a flush() has been successfully completed. + * @hide + */ +public interface FlushCompleteListener { + /** + * Called after flush() is completed. This flush() could have been initiated by this application + * or some other application. All the events in the batch at the point when the flush was called + * have been delivered to the applications registered for those sensor events. + * <p> + * + * @param sensor The {@link android.hardware.Sensor Sensor} on which flush was called. + * + * @see android.hardware.SensorManager#flush(Sensor) + */ + public void onFlushCompleted(Sensor sensor); +} diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index 9bffdbe..bbede57 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -319,6 +319,8 @@ public final class Sensor { private float mResolution; private float mPower; private int mMinDelay; + private int mFifoReservedEventCount; + private int mFifoMaxEventCount; Sensor() { } @@ -381,6 +383,26 @@ public final class Sensor { return mMinDelay; } + /** + * @return Number of events reserved for this sensor in the batch mode FIFO. This gives a + * guarantee on the minimum number of events that can be batched + * @hide + */ + public int getFifoReservedEventCount() { + return mFifoReservedEventCount; + } + + /** + * @return Maximum number of events of this sensor that could be batched. If this value is zero + * it indicates that batch mode is not supported for this sensor. If other applications + * registered to batched sensors, the actual number of events that can be batched might be + * smaller because the hardware FiFo will be partially used to batch the other sensors. + * @hide + */ + public int getFifoMaxEventCount() { + return mFifoMaxEventCount; + } + /** @hide */ public int getHandle() { return mHandle; diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index 30118f9..b6ca62a 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -608,8 +608,72 @@ public abstract class SensorManager { } /** - * Registers a {@link android.hardware.SensorEventListener - * SensorEventListener} for the given sensor. + * Enables batch mode for a sensor with the given rate and maxBatchReportLatency. If the + * underlying hardware does not support batch mode, this defaults to + * {@link #registerListener(SensorEventListener, Sensor, int)} and other parameters are are + * ignored. In non-batch mode, all sensor events must be reported as soon as they are detected. + * While in batch mode, sensor events do not need to be reported as soon as they are detected. + * They can be temporarily stored in batches and reported in batches, as long as no event is + * delayed by more than "maxBatchReportLatency" microseconds. That is, all events since the + * previous batch are recorded and returned all at once. This allows to reduce the amount of + * interrupts sent to the SoC, and allows the SoC to switch to a lower power state (Idle) while + * the sensor is capturing and batching data. + * <p> + * Registering to a sensor in batch mode will not prevent the SoC from going to suspend mode. In + * this case, the sensor will continue to gather events and store it in a hardware FIFO. If the + * FIFO gets full before the AP wakes up again, some events will be lost, as the older events + * get overwritten by new events in the hardware FIFO. This can be avoided by holding a wake + * lock. If the application holds a wake lock, the SoC will not go to suspend mode, so no events + * will be lost, as the events will be reported before the FIFO gets full. + * </p> + * <p> + * Batching is always best effort. If a different application requests updates in continuous + * mode, this application will also get events in continuous mode. Batch mode updates can be + * unregistered by calling {@link #unregisterListener(SensorEventListener)}. + * </p> + * <p class="note"> + * </p> + * Note: Don't use this method with a one shot trigger sensor such as + * {@link Sensor#TYPE_SIGNIFICANT_MOTION}. Use + * {@link #requestTriggerSensor(TriggerEventListener, Sensor)} instead. </p> + * + * @param listener A {@link android.hardware.SensorEventListener SensorEventListener} object + * that will receive the sensor events. + * @param sensor The {@link android.hardware.Sensor Sensor} to register to. + * @param rate The desired delay between two consecutive events in microseconds. This is only a + * hint to the system. Events may be received faster or slower than the specified + * rate. Usually events are received faster. Can be one of + * {@link #SENSOR_DELAY_NORMAL}, {@link #SENSOR_DELAY_UI}, + * {@link #SENSOR_DELAY_GAME}, {@link #SENSOR_DELAY_FASTEST} or the delay in + * microseconds. + * @param maxBatchReportLatency An event in the batch can be delayed by at most + * maxBatchReportLatency microseconds. More events can be batched if this value is + * large. If this is set to zero, batch mode is disabled and events are delivered in + * continuous mode as soon as they are available which is equivalent to calling + * {@link #registerListener(SensorEventListener, Sensor, int)}. + * @param reservedFlags Always set to Zero. + * @param flushCompleteListener A {@link android.hardware.FlushCompleteListener + * FlushCompleteListener} object which is called when any application calls flush() + * on this sensor and all the events in the batch at the time of calling flush() are + * successfully delivered to the listeners. + * @return true if batch mode is successfully enabled for this sensor, false otherwise. + * @see #registerListener(SensorEventListener, Sensor, int) + * @see #unregisterListener(SensorEventListener) + * @see #flush(Sensor) + * @throws IllegalArgumentException when sensor or listener is null or a trigger sensor. + * @hide + */ + public boolean registerListener(SensorEventListener listener, Sensor sensor, int rateUs, + int maxBatchReportLatencyUs, int reservedFlags, + FlushCompleteListener flushCompleteListener) { + int delay = getDelay(rateUs); + return registerListenerImpl(listener, sensor, delay, null, maxBatchReportLatencyUs, + reservedFlags, flushCompleteListener); + } + + /** + * Registers a {@link android.hardware.SensorEventListener SensorEventListener} for the given + * sensor. Events are delivered in continuous mode as soon as they are available. * * <p class="note"></p> * Note: Don't use this method with a one shot trigger sensor such as @@ -655,31 +719,55 @@ public abstract class SensorManager { return false; } - int delay = -1; - switch (rate) { - case SENSOR_DELAY_FASTEST: - delay = 0; - break; - case SENSOR_DELAY_GAME: - delay = 20000; - break; - case SENSOR_DELAY_UI: - delay = 66667; - break; - case SENSOR_DELAY_NORMAL: - delay = 200000; - break; - default: - delay = rate; - break; - } + int delay = getDelay(rate); + return registerListenerImpl(listener, sensor, delay, handler, 0, 0, null); + } - return registerListenerImpl(listener, sensor, delay, handler); + /** + * Enables batch mode for a sensor with the given rate and maxBatchReportLatency. + * @param handler + * The {@link android.os.Handler Handler} the + * {@link android.hardware.SensorEvent sensor events} will be + * delivered to. + * + * @see #registerListener(SensorEventListener, Sensor, int, int, int, FlushCompleteListener) + * @hide + */ + public boolean registerListener(SensorEventListener listener, Sensor sensor, int rateUs, + int maxBatchReportLatencyUs, int reservedFlags, Handler handler, + FlushCompleteListener flushCompleteListener) { + int delayUs = getDelay(rateUs); + return registerListenerImpl(listener, sensor, delayUs, handler, maxBatchReportLatencyUs, + reservedFlags, flushCompleteListener); } /** @hide */ protected abstract boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, - int delay, Handler handler); + int delayUs, Handler handler, int maxBatchReportLatencyUs, int reservedFlags, + FlushCompleteListener flushCompleteListener); + + + /** + * Flushes the batch FIFO of the given sensor. If there are events in the FIFO of this sensor, + * they are returned as if the batch timeout has expired. Events are returned in the + * usual way through the SensorEventListener. This call doesn't effect the batch timeout for + * this sensor. This call is asynchronous and returns immediately. FlushCompleteListener is + * called after all the events in the batch at the time of calling this method have been + * delivered successfully. + * @param sensor + * The {@link android.hardware.Sensor Sensor} to flush. + * @return true if the flush is initiated successfully. false if the sensor isn't active + * i.e no application is registered for updates from this sensor. + * @see #registerListener(SensorEventListener, Sensor, int, int, int, FlushCompleteListener) + * @throws IllegalArgumentException when sensor is null or a trigger sensor. + * @hide + */ + public boolean flush(Sensor sensor) { + return flushImpl(sensor); + } + + /** @hide */ + protected abstract boolean flushImpl(Sensor sensor); /** * <p> @@ -1079,15 +1167,15 @@ public abstract class SensorManager { * <p> * All three angles above are in <b>radians</b> and <b>positive</b> in the * <b>counter-clockwise</b> direction. - * + * * @param R * rotation matrix see {@link #getRotationMatrix}. - * + * * @param values * an array of 3 floats to hold the result. - * + * * @return The array values passed as argument. - * + * * @see #getRotationMatrix(float[], float[], float[], float[]) * @see GeomagneticField */ @@ -1407,4 +1495,26 @@ public abstract class SensorManager { return mLegacySensorManager; } } + + private static int getDelay(int rate) { + int delay = -1; + switch (rate) { + case SENSOR_DELAY_FASTEST: + delay = 0; + break; + case SENSOR_DELAY_GAME: + delay = 20000; + break; + case SENSOR_DELAY_UI: + delay = 66667; + break; + case SENSOR_DELAY_NORMAL: + delay = 200000; + break; + default: + delay = rate; + break; + } + return delay; + } } diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index 852cf4a..9747f0d 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -93,30 +93,35 @@ public class SystemSensorManager extends SensorManager { /** @hide */ @Override protected boolean registerListenerImpl(SensorEventListener listener, Sensor sensor, - int delay, Handler handler) - { + int delayUs, Handler handler, int maxBatchReportLatencyUs, int reservedFlags, + FlushCompleteListener flushCompleteListener) { + if (sensor == null) throw new IllegalArgumentException("sensor cannot be null"); + if (listener == null) throw new IllegalArgumentException("listener cannot be null"); + if (reservedFlags != 0) throw new IllegalArgumentException("reservedFlags should be zero"); + if (delayUs < 0) throw new IllegalArgumentException("rateUs should be positive"); + if (maxBatchReportLatencyUs < 0) + throw new IllegalArgumentException("maxBatchReportLatencyUs should be positive"); + // Trigger Sensors should use the requestTriggerSensor call. + if (Sensor.getReportingMode(sensor) == Sensor.REPORTING_MODE_ONE_SHOT) + throw new IllegalArgumentException("Trigger Sensors cannot use registerListener"); + // Invariants to preserve: // - one Looper per SensorEventListener // - one Looper per SensorEventQueue // We map SensorEventListener to a SensorEventQueue, which holds the looper - if (sensor == null) throw new IllegalArgumentException("sensor cannot be null"); - - // Trigger Sensors should use the requestTriggerSensor call. - if (Sensor.getReportingMode(sensor) == Sensor.REPORTING_MODE_ONE_SHOT) return false; - synchronized (mSensorListeners) { SensorEventQueue queue = mSensorListeners.get(listener); if (queue == null) { Looper looper = (handler != null) ? handler.getLooper() : mMainLooper; - queue = new SensorEventQueue(listener, looper, this); - if (!queue.addSensor(sensor, delay)) { + queue = new SensorEventQueue(listener, looper, this, flushCompleteListener); + if (!queue.addSensor(sensor, delayUs, maxBatchReportLatencyUs, reservedFlags)) { queue.dispose(); return false; } mSensorListeners.put(listener, queue); return true; } else { - return queue.addSensor(sensor, delay); + return queue.addSensor(sensor, delayUs, maxBatchReportLatencyUs, reservedFlags); } } } @@ -157,14 +162,14 @@ public class SystemSensorManager extends SensorManager { TriggerEventQueue queue = mTriggerListeners.get(listener); if (queue == null) { queue = new TriggerEventQueue(listener, mMainLooper, this); - if (!queue.addSensor(sensor, 0)) { + if (!queue.addSensor(sensor, 0, 0, 0)) { queue.dispose(); return false; } mTriggerListeners.put(listener, queue); return true; } else { - return queue.addSensor(sensor, 0); + return queue.addSensor(sensor, 0, 0, 0); } } } @@ -195,6 +200,18 @@ public class SystemSensorManager extends SensorManager { } } + protected boolean flushImpl(Sensor sensor) { + if (sensor == null) throw new IllegalArgumentException("sensor cannot be null"); + if(Sensor.getReportingMode(sensor) == Sensor.REPORTING_MODE_ONE_SHOT) + throw new IllegalArgumentException("Trigger Sensors cannot call flush"); + + FlushEventQueue queue = new FlushEventQueue(mMainLooper, this); + if (queue.flushSensor(sensor) != 0) { + return false; + } + return true; + } + /* * BaseEventQueue is the communication channel with the sensor service, * SensorEventQueue, TriggerEventQueue are subclases and there is one-to-one mapping between @@ -202,11 +219,12 @@ public class SystemSensorManager extends SensorManager { */ private static abstract class BaseEventQueue { private native int nativeInitBaseEventQueue(BaseEventQueue eventQ, MessageQueue msgQ, - float[] scratch); - private static native int nativeEnableSensor(int eventQ, int handle, int us); + private static native int nativeEnableSensor(int eventQ, int handle, int rateUs, + int maxBatchReportLatencyUs, int reservedFlags); private static native int nativeDisableSensor(int eventQ, int handle); private static native void nativeDestroySensorEventQueue(int eventQ); + private static native int nativeFlushSensor(int eventQ, int handle); private int nSensorEventQueue; private final SparseBooleanArray mActiveSensors = new SparseBooleanArray(); protected final SparseIntArray mSensorAccuracies = new SparseIntArray(); @@ -225,7 +243,8 @@ public class SystemSensorManager extends SensorManager { dispose(false); } - public boolean addSensor(Sensor sensor, int delay) { + public boolean addSensor( + Sensor sensor, int delayUs, int maxBatchReportLatencyUs, int reservedFlags) { // Check if already present. int handle = sensor.getHandle(); if (mActiveSensors.get(handle)) return false; @@ -233,9 +252,13 @@ public class SystemSensorManager extends SensorManager { // Get ready to receive events before calling enable. mActiveSensors.put(handle, true); addSensorEvent(sensor); - if (enableSensor(sensor, delay) != 0) { - removeSensor(sensor, false); - return false; + if (enableSensor(sensor, delayUs, maxBatchReportLatencyUs, reservedFlags) != 0) { + // Try continuous mode if batching fails. + if (maxBatchReportLatencyUs == 0 || + maxBatchReportLatencyUs > 0 && enableSensor(sensor, delayUs, 0, 0) != 0) { + removeSensor(sensor, false); + return false; + } } return true; } @@ -268,6 +291,12 @@ public class SystemSensorManager extends SensorManager { return false; } + public int flushSensor(Sensor sensor) { + if (nSensorEventQueue == 0) throw new NullPointerException(); + if (sensor == null) throw new NullPointerException(); + return nativeFlushSensor(nSensorEventQueue, sensor.getHandle()); + } + public boolean hasSensors() { // no more sensors are set return mActiveSensors.indexOfValue(true) >= 0; @@ -295,11 +324,14 @@ public class SystemSensorManager extends SensorManager { } } - private int enableSensor(Sensor sensor, int us) { + private int enableSensor( + Sensor sensor, int rateUs, int maxBatchReportLatencyUs, int reservedFlags) { if (nSensorEventQueue == 0) throw new NullPointerException(); if (sensor == null) throw new NullPointerException(); - return nativeEnableSensor(nSensorEventQueue, sensor.getHandle(), us); + return nativeEnableSensor(nSensorEventQueue, sensor.getHandle(), rateUs, + maxBatchReportLatencyUs, reservedFlags); } + private int disableSensor(Sensor sensor) { if (nSensorEventQueue == 0) throw new NullPointerException(); if (sensor == null) throw new NullPointerException(); @@ -307,6 +339,7 @@ public class SystemSensorManager extends SensorManager { } protected abstract void dispatchSensorEvent(int handle, float[] values, int accuracy, long timestamp); + protected abstract void dispatchFlushCompleteEvent(int handle); protected abstract void addSensorEvent(Sensor sensor); protected abstract void removeSensorEvent(Sensor sensor); @@ -314,12 +347,14 @@ public class SystemSensorManager extends SensorManager { static final class SensorEventQueue extends BaseEventQueue { private final SensorEventListener mListener; + private final FlushCompleteListener mFlushCompleteListener; private final SparseArray<SensorEvent> mSensorsEvents = new SparseArray<SensorEvent>(); public SensorEventQueue(SensorEventListener listener, Looper looper, - SystemSensorManager manager) { + SystemSensorManager manager, FlushCompleteListener flushCompleteListener) { super(looper, manager); mListener = listener; + mFlushCompleteListener = flushCompleteListener; } public void addSensorEvent(Sensor sensor) { @@ -370,6 +405,15 @@ public class SystemSensorManager extends SensorManager { } mListener.onSensorChanged(t); } + + @SuppressWarnings("unused") + protected void dispatchFlushCompleteEvent(int handle) { + final Sensor sensor = sHandleToSensor.get(handle); + if (mFlushCompleteListener != null) { + mFlushCompleteListener.onFlushCompleted(sensor); + } + return; + } } static final class TriggerEventQueue extends BaseEventQueue { @@ -415,5 +459,35 @@ public class SystemSensorManager extends SensorManager { mListener.onTrigger(t); } + + @SuppressWarnings("unused") + protected void dispatchFlushCompleteEvent(int handle) { + } + } + + static final class FlushEventQueue extends BaseEventQueue { + public FlushEventQueue(Looper looper, SystemSensorManager manager) { + super(looper, manager); + } + + @SuppressWarnings("unused") + @Override + protected void dispatchSensorEvent(int handle, float[] values, int accuracy, + long timestamp) { + } + + @Override + @SuppressWarnings("unused") + protected void addSensorEvent(Sensor sensor) { + } + + @Override + @SuppressWarnings("unused") + protected void removeSensorEvent(Sensor sensor) { + } + + @SuppressWarnings("unused") + protected void dispatchFlushCompleteEvent(int handle) { + } } } diff --git a/core/java/android/hardware/camera2/impl/CameraDevice.java b/core/java/android/hardware/camera2/impl/CameraDevice.java index 64e4dc9..86a073f 100644 --- a/core/java/android/hardware/camera2/impl/CameraDevice.java +++ b/core/java/android/hardware/camera2/impl/CameraDevice.java @@ -301,7 +301,9 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { synchronized (mLock) { try { - mRemoteDevice.disconnect(); + if (mRemoteDevice != null) { + mRemoteDevice.disconnect(); + } } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { diff --git a/core/java/android/nfc/INdefPushCallback.aidl b/core/java/android/nfc/IAppCallback.aidl index 16771dc..9599308 100644 --- a/core/java/android/nfc/INdefPushCallback.aidl +++ b/core/java/android/nfc/IAppCallback.aidl @@ -17,12 +17,14 @@ package android.nfc; import android.nfc.BeamShareData; +import android.nfc.Tag; /** * @hide */ -interface INdefPushCallback +interface IAppCallback { BeamShareData createBeamShareData(); void onNdefPushComplete(); + void onTagDiscovered(in Tag tag); } diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 15d0475..8414738 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -21,10 +21,11 @@ import android.content.IntentFilter; import android.nfc.NdefMessage; import android.nfc.Tag; import android.nfc.TechListParcel; -import android.nfc.INdefPushCallback; +import android.nfc.IAppCallback; import android.nfc.INfcAdapterExtras; import android.nfc.INfcTag; import android.nfc.INfcCardEmulation; +import android.os.Bundle; /** * @hide @@ -44,10 +45,10 @@ interface INfcAdapter void setForegroundDispatch(in PendingIntent intent, in IntentFilter[] filters, in TechListParcel techLists); - void setNdefPushCallback(in INdefPushCallback callback); + void setAppCallback(in IAppCallback callback); void dispatch(in Tag tag); - void setReaderMode (IBinder b, int flags); + void setReaderMode (IBinder b, IAppCallback callback, int flags, in Bundle extras); void setP2pModes(int initatorModes, int targetModes); } diff --git a/core/java/android/nfc/NfcActivityManager.java b/core/java/android/nfc/NfcActivityManager.java index d0d943c..77c0234 100644 --- a/core/java/android/nfc/NfcActivityManager.java +++ b/core/java/android/nfc/NfcActivityManager.java @@ -19,6 +19,7 @@ package android.nfc; import android.app.Activity; import android.app.Application; import android.net.Uri; +import android.nfc.NfcAdapter.ReaderCallback; import android.os.Binder; import android.os.Bundle; import android.os.RemoteException; @@ -36,7 +37,7 @@ import java.util.List; * * @hide */ -public final class NfcActivityManager extends INdefPushCallback.Stub +public final class NfcActivityManager extends IAppCallback.Stub implements Application.ActivityLifecycleCallbacks { static final String TAG = NfcAdapter.TAG; static final Boolean DBG = false; @@ -113,6 +114,8 @@ public final class NfcActivityManager extends INdefPushCallback.Stub Uri[] uris = null; int flags = 0; int readerModeFlags = 0; + NfcAdapter.ReaderCallback readerCallback = null; + Bundle readerModeExtras = null; Binder token; public NfcActivityState(Activity activity) { @@ -197,17 +200,20 @@ public final class NfcActivityManager extends INdefPushCallback.Stub mDefaultEvent = new NfcEvent(mAdapter); } - public void enableReaderMode(Activity activity, int flags) { + public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, + Bundle extras) { boolean isResumed; Binder token; synchronized (NfcActivityManager.this) { NfcActivityState state = getActivityState(activity); + state.readerCallback = callback; state.readerModeFlags = flags; + state.readerModeExtras = extras; token = state.token; isResumed = state.resumed; } if (isResumed) { - setReaderMode(token, flags); + setReaderMode(token, flags, extras); } } @@ -216,20 +222,22 @@ public final class NfcActivityManager extends INdefPushCallback.Stub Binder token; synchronized (NfcActivityManager.this) { NfcActivityState state = getActivityState(activity); + state.readerCallback = null; state.readerModeFlags = 0; + state.readerModeExtras = null; token = state.token; isResumed = state.resumed; } if (isResumed) { - setReaderMode(token, 0); + setReaderMode(token, 0, null); } } - public void setReaderMode(Binder token, int flags) { + public void setReaderMode(Binder token, int flags, Bundle extras) { if (DBG) Log.d(TAG, "Setting reader mode"); try { - NfcAdapter.sService.setReaderMode(token, flags); + NfcAdapter.sService.setReaderMode(token, this, flags, extras); } catch (RemoteException e) { mAdapter.attemptDeadServiceRecovery(e); } @@ -302,12 +310,12 @@ public final class NfcActivityManager extends INdefPushCallback.Stub } /** - * Request or unrequest NFC service callbacks for NDEF push. + * Request or unrequest NFC service callbacks. * Makes IPC call - do not hold lock. */ void requestNfcServiceCallback() { try { - NfcAdapter.sService.setNdefPushCallback(this); + NfcAdapter.sService.setAppCallback(this); } catch (RemoteException e) { mAdapter.attemptDeadServiceRecovery(e); } @@ -375,6 +383,22 @@ public final class NfcActivityManager extends INdefPushCallback.Stub } } + @Override + public void onTagDiscovered(Tag tag) throws RemoteException { + NfcAdapter.ReaderCallback callback; + synchronized (NfcActivityManager.this) { + NfcActivityState state = findResumedActivityState(); + if (state == null) return; + + callback = state.readerCallback; + } + + // Make callback without lock + if (callback != null) { + callback.onTagDiscovered(tag); + } + + } /** Callback from Activity life-cycle, on main thread */ @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { /* NO-OP */ } @@ -387,6 +411,7 @@ public final class NfcActivityManager extends INdefPushCallback.Stub @Override public void onActivityResumed(Activity activity) { int readerModeFlags = 0; + Bundle readerModeExtras = null; Binder token; synchronized (NfcActivityManager.this) { NfcActivityState state = findActivityState(activity); @@ -395,9 +420,10 @@ public final class NfcActivityManager extends INdefPushCallback.Stub state.resumed = true; token = state.token; readerModeFlags = state.readerModeFlags; + readerModeExtras = state.readerModeExtras; } if (readerModeFlags != 0) { - setReaderMode(token, readerModeFlags); + setReaderMode(token, readerModeFlags, readerModeExtras); } requestNfcServiceCallback(); } @@ -417,7 +443,7 @@ public final class NfcActivityManager extends INdefPushCallback.Stub } if (readerModeFlagsSet) { // Restore default p2p modes - setReaderMode(token, 0); + setReaderMode(token, 0, null); } } @@ -441,4 +467,5 @@ public final class NfcActivityManager extends INdefPushCallback.Stub } } } + } diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index fa0c1f6..2a18900 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -33,6 +33,7 @@ import android.nfc.tech.MifareClassic; import android.nfc.tech.Ndef; import android.nfc.tech.NfcA; import android.nfc.tech.NfcF; +import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; import android.os.ServiceManager; @@ -196,42 +197,42 @@ public final class NfcAdapter { public static final int STATE_TURNING_OFF = 4; /** - * Flag for use with {@link #enableReaderMode(Activity, int)}. + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. * <p> * Setting this flag enables polling for Nfc-A technology. */ public static final int FLAG_READER_NFC_A = 0x1; /** - * Flag for use with {@link #enableReaderMode(Activity, int)}. + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. * <p> * Setting this flag enables polling for Nfc-B technology. */ public static final int FLAG_READER_NFC_B = 0x2; /** - * Flag for use with {@link #enableReaderMode(Activity, int)}. + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. * <p> * Setting this flag enables polling for Nfc-F technology. */ public static final int FLAG_READER_NFC_F = 0x4; /** - * Flag for use with {@link #enableReaderMode(Activity, int)}. + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. * <p> * Setting this flag enables polling for Nfc-V (ISO15693) technology. */ public static final int FLAG_READER_NFC_V = 0x8; /** - * Flag for use with {@link #enableReaderMode(Activity, int)}. + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. * <p> * Setting this flag enables polling for Kovio technology. */ public static final int FLAG_READER_KOVIO = 0x10; /** - * Flag for use with {@link #enableReaderMode(Activity, int)}. + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. * <p> * Setting this flag allows the caller to prevent the * platform from performing an NDEF check on the tags it @@ -239,6 +240,23 @@ public final class NfcAdapter { */ public static final int FLAG_READER_SKIP_NDEF_CHECK = 0x80; + /** + * Flag for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + * <p> + * Setting this flag allows the caller to prevent the + * platform from playing sounds when it discovers a tag. + */ + public static final int FLAG_READER_NO_PLATFORM_SOUNDS = 0x100; + + /** + * Int Extra for use with {@link #enableReaderMode(Activity, ReaderCallback, int, Bundle)}. + * <p> + * Setting this integer extra allows the calling application to specify + * the delay that the platform will use for performing presence checks + * on any discovered tag. + */ + public static final String EXTRA_READER_PRESENCE_CHECK_DELAY = "presence"; + /** @hide */ public static final int FLAG_NDEF_PUSH_NO_CONFIRM = 0x1; @@ -291,6 +309,14 @@ public final class NfcAdapter { final Context mContext; /** + * A callback to be invoked when the system has found a tag in + * reader mode. + */ + public interface ReaderCallback { + public void onTagDiscovered(Tag tag); + } + + /** * A callback to be invoked when the system successfully delivers your {@link NdefMessage} * to another device. * @see #setOnNdefPushCompleteCallback @@ -1167,19 +1193,18 @@ public final class NfcAdapter { * {@link Ndef} tag technology from being enumerated on the tag, and that * NDEF-based tag dispatch will not be functional. * - * <p>It is recommended to combine this method with - * {@link #enableForegroundDispatch(Activity, PendingIntent, IntentFilter[], String[][]) - * to ensure that tags are delivered to this activity. - * * <p>For interacting with tags that are emulated on another Android device * using Android's host-based card-emulation, the recommended flags are * {@link #FLAG_READER_NFC_A} and {@link #FLAG_READER_SKIP_NDEF_CHECK}. * * @param activity the Activity that requests the adapter to be in reader mode + * @param callback the callback to be called when a tag is discovered * @param flags Flags indicating poll technologies and other optional parameters + * @param extras Additional extras for configuring reader mode. */ - public void enableReaderMode(Activity activity, int flags) { - mNfcActivityManager.enableReaderMode(activity, flags); + public void enableReaderMode(Activity activity, ReaderCallback callback, int flags, + Bundle extras) { + mNfcActivityManager.enableReaderMode(activity, callback, flags, extras); } /** diff --git a/core/java/android/nfc/cardemulation/ApduServiceInfo.java b/core/java/android/nfc/cardemulation/ApduServiceInfo.java index b83911a..41c6603 100644 --- a/core/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/core/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -105,8 +105,12 @@ public final class ApduServiceInfo implements Parcelable { if (onHost) { parser = si.loadXmlMetaData(pm, HostApduService.SERVICE_META_DATA); if (parser == null) { - throw new XmlPullParserException("No " + HostApduService.SERVICE_META_DATA + - " meta-data"); + Log.d(TAG, "Didn't find service meta-data, trying legacy."); + parser = si.loadXmlMetaData(pm, HostApduService.OLD_SERVICE_META_DATA); + if (parser == null) { + throw new XmlPullParserException("No " + HostApduService.SERVICE_META_DATA + + " meta-data"); + } } } else { parser = si.loadXmlMetaData(pm, OffHostApduService.SERVICE_META_DATA); @@ -170,12 +174,12 @@ public final class ApduServiceInfo implements Parcelable { com.android.internal.R.styleable.AidGroup_description); String groupCategory = groupAttrs.getString( com.android.internal.R.styleable.AidGroup_category); - if (!CardEmulationManager.CATEGORY_PAYMENT.equals(groupCategory)) { - groupCategory = CardEmulationManager.CATEGORY_OTHER; + if (!CardEmulation.CATEGORY_PAYMENT.equals(groupCategory)) { + groupCategory = CardEmulation.CATEGORY_OTHER; } currentGroup = mCategoryToGroup.get(groupCategory); if (currentGroup != null) { - if (!CardEmulationManager.CATEGORY_OTHER.equals(groupCategory)) { + if (!CardEmulation.CATEGORY_OTHER.equals(groupCategory)) { Log.e(TAG, "Not allowing multiple aid-groups in the " + groupCategory + " category"); currentGroup = null; diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java new file mode 100644 index 0000000..3cd7863 --- /dev/null +++ b/core/java/android/nfc/cardemulation/CardEmulation.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.nfc.cardemulation; + +import android.annotation.SdkConstant; +import android.annotation.SdkConstant.SdkConstantType; +import android.app.ActivityThread; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.nfc.INfcCardEmulation; +import android.nfc.NfcAdapter; +import android.os.RemoteException; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; + +import java.util.HashMap; +import java.util.List; + +public final class CardEmulation { + static final String TAG = "CardEmulation"; + + /** + * Activity action: ask the user to change the default + * card emulation service for a certain category. This will + * show a dialog that asks the user whether he wants to + * replace the current default service with the service + * identified with the ComponentName specified in + * {@link #EXTRA_SERVICE_COMPONENT}, for the category + * specified in {@link #EXTRA_CATEGORY} + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_CHANGE_DEFAULT = + "android.nfc.cardemulation.action.ACTION_CHANGE_DEFAULT"; + + /** + * The category extra for {@link #ACTION_CHANGE_DEFAULT} + * + * @see #ACTION_CHANGE_DEFAULT + */ + public static final String EXTRA_CATEGORY = "category"; + + /** + * The ComponentName object passed in as a parcelable + * extra for {@link #ACTION_CHANGE_DEFAULT} + * + * @see #ACTION_CHANGE_DEFAULT + */ + public static final String EXTRA_SERVICE_COMPONENT = "component"; + + /** + * The payment category can be used to indicate that an AID + * represents a payment application. + */ + public static final String CATEGORY_PAYMENT = "payment"; + + /** + * If an AID group does not contain a category, or the + * specified category is not defined by the platform version + * that is parsing the AID group, all AIDs in the group will + * automatically be categorized under the {@link #CATEGORY_OTHER} + * category. + */ + public static final String CATEGORY_OTHER = "other"; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, the user has set a default service for this + * AID category. If a remote reader selects any of the AIDs + * that the default service has registered in this category, + * that service will automatically be bound to to handle + * the transaction. + * + * <p>There are still cases where a service that is + * not the default for a category can selected: + * <p> + * If a remote reader selects an AID in this category + * that is not handled by the default service, and there is a set + * of other services {S} that do handle this AID, the + * user is asked if he wants to use any of the services in + * {S} instead. + * <p> + * As a special case, if the size of {S} is one, containing a single service X, + * and all AIDs X has registered in this category are not + * registered by any other service, then X will be + * selected automatically without asking the user. + * <p>Example: + * <ul> + * <li>Service A registers AIDs "1", "2" and "3" in the category + * <li>Service B registers AIDs "3" and "4" in the category + * <li>Service C registers AIDs "5" and "6" in the category + * </ul> + * In this case, the following will happen when service A + * is the default: + * <ul> + * <li>Reader selects AID "1", "2" or "3": service A is invoked automatically + * <li>Reader selects AID "4": the user is asked to confirm he + * wants to use service B, because its AIDs overlap with service A. + * <li>Reader selects AID "5" or "6": service C is invoked automatically, + * because all AIDs it has asked for are only registered by C, + * and there is no overlap. + * </ul> + * + */ + public static final int SELECTION_MODE_PREFER_DEFAULT = 0; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, whenever an AID of this category is selected, + * the user is asked which service he wants to use to handle + * the transaction, even if there is only one matching service. + */ + public static final int SELECTION_MODE_ALWAYS_ASK = 1; + + /** + * Return value for {@link #getSelectionModeForCategory(String)}. + * + * <p>In this mode, the user will only be asked to select a service + * if the selected AID has been registered by multiple applications. + */ + public static final int SELECTION_MODE_ASK_IF_CONFLICT = 2; + + static boolean sIsInitialized = false; + static HashMap<Context, CardEmulation> sCardEmus = new HashMap(); + static INfcCardEmulation sService; + + final Context mContext; + + private CardEmulation(Context context, INfcCardEmulation service) { + mContext = context.getApplicationContext(); + sService = service; + } + + public static synchronized CardEmulation getInstance(NfcAdapter adapter) { + if (adapter == null) throw new NullPointerException("NfcAdapter is null"); + Context context = adapter.getContext(); + if (context == null) { + Log.e(TAG, "NfcAdapter context is null."); + throw new UnsupportedOperationException(); + } + if (!sIsInitialized) { + IPackageManager pm = ActivityThread.getPackageManager(); + if (pm == null) { + Log.e(TAG, "Cannot get PackageManager"); + throw new UnsupportedOperationException(); + } + try { + if (!pm.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) { + Log.e(TAG, "This device does not support card emulation"); + throw new UnsupportedOperationException(); + } + } catch (RemoteException e) { + Log.e(TAG, "PackageManager query failed."); + throw new UnsupportedOperationException(); + } + sIsInitialized = true; + } + CardEmulation manager = sCardEmus.get(context); + if (manager == null) { + // Get card emu service + INfcCardEmulation service = adapter.getCardEmulationService(); + manager = new CardEmulation(context, service); + sCardEmus.put(context, manager); + } + return manager; + } + + /** + * Allows an application to query whether a service is currently + * the default service to handle a card emulation category. + * + * <p>Note that if {@link #getSelectionModeForCategory(String)} + * returns {@link #SELECTION_MODE_ALWAYS_ASK}, this method will always + * return false. + * + * @param service The ComponentName of the service + * @param category The category + * @return whether service is currently the default service for the category. + */ + public boolean isDefaultServiceForCategory(ComponentName service, String category) { + try { + return sService.isDefaultServiceForCategory(UserHandle.myUserId(), service, category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.isDefaultServiceForCategory(UserHandle.myUserId(), service, + category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + } + } + + /** + * + * Allows an application to query whether a service is currently + * the default handler for a specified ISO7816-4 Application ID. + * + * @param service The ComponentName of the service + * @param aid The ISO7816-4 Application ID + * @return + */ + public boolean isDefaultServiceForAid(ComponentName service, String aid) { + try { + return sService.isDefaultServiceForAid(UserHandle.myUserId(), service, aid); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.isDefaultServiceForAid(UserHandle.myUserId(), service, aid); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * Returns the application selection mode for the passed in category. + * Valid return values are: + * <p>{@link #SELECTION_MODE_PREFER_DEFAULT} the user has requested a default + * application for this category, which will be preferred. + * <p>{@link #SELECTION_MODE_ALWAYS_ASK} the user has requested to be asked + * every time what app he would like to use in this category. + * <p>{@link #SELECTION_MODE_ASK_IF_CONFLICT} the user will only be asked + * to pick a service if there is a conflict. + * @param category The category, for example {@link #CATEGORY_PAYMENT} + * @return + */ + public int getSelectionModeForCategory(String category) { + if (CATEGORY_PAYMENT.equals(category)) { + String defaultComponent = Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.NFC_PAYMENT_DEFAULT_COMPONENT); + if (defaultComponent != null) { + return SELECTION_MODE_PREFER_DEFAULT; + } else { + return SELECTION_MODE_ALWAYS_ASK; + } + } else { + // All other categories are in "only ask if conflict" mode + return SELECTION_MODE_ASK_IF_CONFLICT; + } + } + + /** + * @hide + */ + public boolean setDefaultServiceForCategory(ComponentName service, String category) { + try { + return sService.setDefaultServiceForCategory(UserHandle.myUserId(), service, category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setDefaultServiceForCategory(UserHandle.myUserId(), service, + category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + + /** + * @hide + */ + public boolean setDefaultForNextTap(ComponentName service) { + try { + return sService.setDefaultForNextTap(UserHandle.myUserId(), service); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return false; + } + try { + return sService.setDefaultForNextTap(UserHandle.myUserId(), service); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return false; + } + } + } + /** + * @hide + */ + public List<ApduServiceInfo> getServices(String category) { + try { + return sService.getServices(UserHandle.myUserId(), category); + } catch (RemoteException e) { + // Try one more time + recoverService(); + if (sService == null) { + Log.e(TAG, "Failed to recover CardEmulationService."); + return null; + } + try { + return sService.getServices(UserHandle.myUserId(), category); + } catch (RemoteException ee) { + Log.e(TAG, "Failed to reach CardEmulationService."); + return null; + } + } + } + + void recoverService() { + NfcAdapter adapter = NfcAdapter.getDefaultAdapter(mContext); + sService = adapter.getCardEmulationService(); + } +} diff --git a/core/java/android/nfc/cardemulation/CardEmulationManager.java b/core/java/android/nfc/cardemulation/CardEmulationManager.java index 9d60c73..124ea1c 100644 --- a/core/java/android/nfc/cardemulation/CardEmulationManager.java +++ b/core/java/android/nfc/cardemulation/CardEmulationManager.java @@ -33,6 +33,10 @@ import android.util.Log; import java.util.HashMap; import java.util.List; +/** + * TODO Remove when calling .apks are upgraded + * @hide + */ public final class CardEmulationManager { static final String TAG = "CardEmulationManager"; diff --git a/core/java/android/nfc/cardemulation/HostApduService.java b/core/java/android/nfc/cardemulation/HostApduService.java index ae94b2f..174acc0 100644 --- a/core/java/android/nfc/cardemulation/HostApduService.java +++ b/core/java/android/nfc/cardemulation/HostApduService.java @@ -40,13 +40,31 @@ public abstract class HostApduService extends Service { */ @SdkConstant(SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = + "android.nfc.cardemulation.action.HOST_APDU_SERVICE"; + + /** + * The name of the meta-data element that contains + * more information about this service. + */ + public static final String SERVICE_META_DATA = + "android.nfc.cardemulation.host_apdu_service"; + + /** + * The {@link Intent} that must be declared as handled by the service. + * TODO Remove + * @hide + */ + public static final String OLD_SERVICE_INTERFACE = "android.nfc.HostApduService"; /** * The name of the meta-data element that contains * more information about this service. + * + * TODO Remove + * @hide */ - public static final String SERVICE_META_DATA = "android.nfc.HostApduService"; + public static final String OLD_SERVICE_META_DATA = "android.nfc.HostApduService"; /** * Reason for {@link #onDeactivated(int)}. @@ -63,7 +81,7 @@ public abstract class HostApduService extends Service { * currently active on the logical channel). * * <p>Note that this next AID may still be resolved to this - * service, in which case {@link #processCommandApdu(byte[], int)} + * service, in which case {@link #processCommandApdu(byte[], Bundle)} * will be called again. */ public static final int DEACTIVATION_DESELECTED = 1; @@ -131,7 +149,7 @@ public abstract class HostApduService extends Service { byte[] apdu = dataBundle.getByteArray(KEY_DATA); if (apdu != null) { - byte[] responseApdu = processCommandApdu(apdu, 0); + byte[] responseApdu = processCommandApdu(apdu, null); if (responseApdu != null) { if (mNfcService == null) { Log.e(TAG, "Response not sent; service was deactivated."); @@ -230,7 +248,7 @@ public abstract class HostApduService extends Service { * transaction. * * <p>Note: this method may be called anywhere between - * the first {@link #processCommandApdu(byte[], int)} + * the first {@link #processCommandApdu(byte[], Bundle)} * call and a {@link #onDeactivated(int)} call. */ public final void notifyUnhandled() { @@ -290,8 +308,11 @@ public abstract class HostApduService extends Service { * @param flags * @return a byte-array containing the response APDU, or null if no * response APDU can be sent at this point. + * @hide */ - public abstract byte[] processCommandApdu(byte[] commandApdu, int flags); + public byte[] processCommandApdu(byte[] commandApdu, int flags) { + return null; + } /** * This method will be called in two possible scenarios: diff --git a/core/java/android/nfc/cardemulation/OffHostApduService.java b/core/java/android/nfc/cardemulation/OffHostApduService.java index 79599db..15f63f9 100644 --- a/core/java/android/nfc/cardemulation/OffHostApduService.java +++ b/core/java/android/nfc/cardemulation/OffHostApduService.java @@ -42,13 +42,14 @@ public abstract class OffHostApduService extends Service { */ @SdkConstant(SdkConstantType.SERVICE_ACTION) public static final String SERVICE_INTERFACE = - "android.nfc.OffHostApduService"; + "android.nfc.cardemulation.action.OFF_HOST_APDU_SERVICE"; /** * The name of the meta-data element that contains * more information about this service. */ - public static final String SERVICE_META_DATA = "android.nfc.OffHostApduService"; + public static final String SERVICE_META_DATA = + "android.nfc.cardemulation.off_host_apdu_service"; /** * The Android platform itself will not bind to this service, diff --git a/core/java/android/printservice/PrinterDiscoverySession.java b/core/java/android/printservice/PrinterDiscoverySession.java index 1f86ecc..6464cc1 100644 --- a/core/java/android/printservice/PrinterDiscoverySession.java +++ b/core/java/android/printservice/PrinterDiscoverySession.java @@ -80,7 +80,7 @@ import java.util.List; public abstract class PrinterDiscoverySession { private static final String LOG_TAG = "PrinterDiscoverySession"; - private static final int MAX_ITEMS_PER_CALLBACK = 100; + private static final int MAX_ITEMS_PER_CALLBACK = 50; private static int sIdCounter = 0; diff --git a/core/java/android/security/IKeystoreService.java b/core/java/android/security/IKeystoreService.java index 3d75dc8..bf8d4e5 100644 --- a/core/java/android/security/IKeystoreService.java +++ b/core/java/android/security/IKeystoreService.java @@ -244,7 +244,8 @@ public interface IKeystoreService extends IInterface { return _result; } - public int generate(String name, int uid, int flags) throws RemoteException { + public int generate(String name, int uid, int keyType, int keySize, int flags, + byte[][] args) throws RemoteException { Parcel _data = Parcel.obtain(); Parcel _reply = Parcel.obtain(); int _result; @@ -252,7 +253,17 @@ public interface IKeystoreService extends IInterface { _data.writeInterfaceToken(DESCRIPTOR); _data.writeString(name); _data.writeInt(uid); + _data.writeInt(keyType); + _data.writeInt(keySize); _data.writeInt(flags); + if (args == null) { + _data.writeInt(0); + } else { + _data.writeInt(args.length); + for (int i = 0; i < args.length; i++) { + _data.writeByteArray(args[i]); + } + } mRemote.transact(Stub.TRANSACTION_generate, _data, _reply, 0); _reply.readException(); _result = _reply.readInt(); @@ -560,7 +571,8 @@ public interface IKeystoreService extends IInterface { public int zero() throws RemoteException; - public int generate(String name, int uid, int flags) throws RemoteException; + public int generate(String name, int uid, int keyType, int keySize, int flags, byte[][] args) + throws RemoteException; public int import_key(String name, byte[] data, int uid, int flags) throws RemoteException; diff --git a/core/java/android/view/ScaleGestureDetector.java b/core/java/android/view/ScaleGestureDetector.java index 51c5c7b..0bebc04 100644 --- a/core/java/android/view/ScaleGestureDetector.java +++ b/core/java/android/view/ScaleGestureDetector.java @@ -18,6 +18,8 @@ package android.view; import android.content.Context; import android.content.res.Resources; +import android.os.Build; +import android.os.Handler; import android.os.SystemClock; import android.util.FloatMath; @@ -128,6 +130,8 @@ public class ScaleGestureDetector { private float mFocusX; private float mFocusY; + private boolean mDoubleTapScales; + private float mCurrSpan; private float mPrevSpan; private float mInitialSpan; @@ -148,9 +152,14 @@ public class ScaleGestureDetector { private int mTouchHistoryDirection; private long mTouchHistoryLastAcceptedTime; private int mTouchMinMajor; + private MotionEvent mDoubleTapEvent; + private int mDoubleTapMode = DOUBLE_TAP_MODE_NONE; + private final Handler mHandler; private static final long TOUCH_STABILIZE_TIME = 128; // ms - private static final int TOUCH_MIN_MAJOR = 48; // dp + private static final int DOUBLE_TAP_MODE_NONE = 0; + private static final int DOUBLE_TAP_MODE_IN_PROGRESS = 1; + /** * Consistency verifier for debugging purposes. @@ -158,8 +167,37 @@ public class ScaleGestureDetector { private final InputEventConsistencyVerifier mInputEventConsistencyVerifier = InputEventConsistencyVerifier.isInstrumentationEnabled() ? new InputEventConsistencyVerifier(this, 0) : null; + private GestureDetector mGestureDetector; + + private boolean mEventBeforeOrAboveStartingGestureEvent; + /** + * Creates a ScaleGestureDetector with the supplied listener. + * You may only use this constructor from a {@link android.os.Looper Looper} thread. + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * + * @throws NullPointerException if {@code listener} is null. + */ public ScaleGestureDetector(Context context, OnScaleGestureListener listener) { + this(context, listener, null); + } + + /** + * Creates a ScaleGestureDetector with the supplied listener. + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler to use for running deferred listener events. + * + * @throws NullPointerException if {@code listener} is null. + */ + public ScaleGestureDetector(Context context, OnScaleGestureListener listener, + Handler handler) { mContext = context; mListener = listener; mSpanSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 2; @@ -167,8 +205,12 @@ public class ScaleGestureDetector { final Resources res = context.getResources(); mTouchMinMajor = res.getDimensionPixelSize( com.android.internal.R.dimen.config_minScalingTouchMajor); - mMinSpan = res.getDimensionPixelSize( - com.android.internal.R.dimen.config_minScalingSpan); + mMinSpan = res.getDimensionPixelSize(com.android.internal.R.dimen.config_minScalingSpan); + mHandler = handler; + // Quick scale is enabled by default after JB_MR2 + if (context.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.JELLY_BEAN_MR2) { + setQuickScaleEnabled(true); + } } /** @@ -263,8 +305,14 @@ public class ScaleGestureDetector { final int action = event.getActionMasked(); + // Forward the event to check for double tap gesture + if (mDoubleTapScales) { + mGestureDetector.onTouchEvent(event); + } + final boolean streamComplete = action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL; + if (action == MotionEvent.ACTION_DOWN || streamComplete) { // Reset any scale in progress with the listener. // If it's an ACTION_DOWN we're beginning a new event stream. @@ -273,6 +321,7 @@ public class ScaleGestureDetector { mListener.onScaleEnd(this); mInProgress = false; mInitialSpan = 0; + mDoubleTapMode = DOUBLE_TAP_MODE_NONE; } if (streamComplete) { @@ -284,21 +333,37 @@ public class ScaleGestureDetector { final boolean configChanged = action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN; + + final boolean pointerUp = action == MotionEvent.ACTION_POINTER_UP; final int skipIndex = pointerUp ? event.getActionIndex() : -1; // Determine focal point float sumX = 0, sumY = 0; final int count = event.getPointerCount(); - for (int i = 0; i < count; i++) { - if (skipIndex == i) continue; - sumX += event.getX(i); - sumY += event.getY(i); - } final int div = pointerUp ? count - 1 : count; - final float focusX = sumX / div; - final float focusY = sumY / div; + final float focusX; + final float focusY; + if (mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS) { + // In double tap mode, the focal pt is always where the double tap + // gesture started + focusX = mDoubleTapEvent.getX(); + focusY = mDoubleTapEvent.getY(); + if (event.getY() < focusY) { + mEventBeforeOrAboveStartingGestureEvent = true; + } else { + mEventBeforeOrAboveStartingGestureEvent = false; + } + } else { + for (int i = 0; i < count; i++) { + if (skipIndex == i) continue; + sumX += event.getX(i); + sumY += event.getY(i); + } + focusX = sumX / div; + focusY = sumY / div; + } addTouchHistory(event); @@ -320,7 +385,12 @@ public class ScaleGestureDetector { // the focal point. final float spanX = devX * 2; final float spanY = devY * 2; - final float span = FloatMath.sqrt(spanX * spanX + spanY * spanY); + final float span; + if (inDoubleTapMode()) { + span = spanY; + } else { + span = FloatMath.sqrt(spanX * spanX + spanY * spanY); + } // Dispatch begin/end events as needed. // If the configuration changes, notify the app to reset its current state by beginning @@ -328,10 +398,11 @@ public class ScaleGestureDetector { final boolean wasInProgress = mInProgress; mFocusX = focusX; mFocusY = focusY; - if (mInProgress && (span < mMinSpan || configChanged)) { + if (!inDoubleTapMode() && mInProgress && (span < mMinSpan || configChanged)) { mListener.onScaleEnd(this); mInProgress = false; mInitialSpan = span; + mDoubleTapMode = DOUBLE_TAP_MODE_NONE; } if (configChanged) { mPrevSpanX = mCurrSpanX = spanX; @@ -354,6 +425,7 @@ public class ScaleGestureDetector { mCurrSpan = span; boolean updatePrev = true; + if (mInProgress) { updatePrev = mListener.onScale(this); } @@ -369,6 +441,34 @@ public class ScaleGestureDetector { return true; } + + private boolean inDoubleTapMode() { + return mDoubleTapMode == DOUBLE_TAP_MODE_IN_PROGRESS; + } + + /** + * Set whether the associated {@link OnScaleGestureListener} should receive onScale callbacks + * when the user performs a doubleTap followed by a swipe. Note that this is enabled by default + * if the app targets API 19 and newer. + * @param scales true to enable quick scaling, false to disable + */ + public void setQuickScaleEnabled(boolean scales) { + mDoubleTapScales = scales; + if (mDoubleTapScales && mGestureDetector == null) { + GestureDetector.SimpleOnGestureListener gestureListener = + new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDoubleTap(MotionEvent e) { + // Double tap: start watching for a swipe + mDoubleTapEvent = e; + mDoubleTapMode = DOUBLE_TAP_MODE_IN_PROGRESS; + return true; + } + }; + mGestureDetector = new GestureDetector(mContext, gestureListener, mHandler); + } + } + /** * Returns {@code true} if a scale gesture is in progress. */ @@ -472,6 +572,12 @@ public class ScaleGestureDetector { * @return The current scaling factor. */ public float getScaleFactor() { + if (inDoubleTapMode() && mEventBeforeOrAboveStartingGestureEvent) { + // Drag is moving up; the further away from the gesture + // start, the smaller the span should be, the closer, + // the larger the span, and therefore the larger the scale + return (1 / mCurrSpan) / (1 / mPrevSpan); + } return mPrevSpan > 0 ? mCurrSpan / mPrevSpan : 1; } @@ -493,4 +599,4 @@ public class ScaleGestureDetector { public long getEventTime() { return mCurrTime; } -} +}
\ No newline at end of file diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 5b279ec..907290b 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -13256,14 +13256,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, // Keep the DRAWING_CACHE_QUALITY_LOW flag just in case switch (mViewFlags & DRAWING_CACHE_QUALITY_MASK) { case DRAWING_CACHE_QUALITY_AUTO: - quality = Bitmap.Config.ARGB_8888; - break; case DRAWING_CACHE_QUALITY_LOW: - quality = Bitmap.Config.ARGB_8888; - break; case DRAWING_CACHE_QUALITY_HIGH: - quality = Bitmap.Config.ARGB_8888; - break; default: quality = Bitmap.Config.ARGB_8888; break; diff --git a/core/java/android/webkit/CallbackProxy.java b/core/java/android/webkit/CallbackProxy.java index fea6be6..7707392 100644 --- a/core/java/android/webkit/CallbackProxy.java +++ b/core/java/android/webkit/CallbackProxy.java @@ -50,7 +50,9 @@ import java.util.Map; */ class CallbackProxy extends Handler { // Logging tag - private static final String LOGTAG = "CallbackProxy"; + static final String LOGTAG = "WebViewCallback"; + // Enables API callback tracing + private static final boolean TRACE = DebugFlags.TRACE_CALLBACK; // Instance of WebViewClient that is the client callback. private volatile WebViewClient mWebViewClient; // Instance of WebChromeClient for handling all chrome functions. @@ -258,6 +260,7 @@ class CallbackProxy extends Handler { } boolean override = false; if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "shouldOverrideUrlLoading=" + overrideUrl); override = mWebViewClient.shouldOverrideUrlLoading(mWebView.getWebView(), overrideUrl); } else { @@ -307,6 +310,7 @@ class CallbackProxy extends Handler { String startedUrl = msg.getData().getString("url"); mWebView.onPageStarted(startedUrl); if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onPageStarted=" + startedUrl); mWebViewClient.onPageStarted(mWebView.getWebView(), startedUrl, (Bitmap) msg.obj); } @@ -316,18 +320,21 @@ class CallbackProxy extends Handler { String finishedUrl = (String) msg.obj; mWebView.onPageFinished(finishedUrl); if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onPageFinished=" + finishedUrl); mWebViewClient.onPageFinished(mWebView.getWebView(), finishedUrl); } break; case RECEIVED_ICON: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onReceivedIcon"); mWebChromeClient.onReceivedIcon(mWebView.getWebView(), (Bitmap) msg.obj); } break; case RECEIVED_TOUCH_ICON_URL: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onReceivedTouchIconUrl"); mWebChromeClient.onReceivedTouchIconUrl(mWebView.getWebView(), (String) msg.obj, msg.arg1 == 1); } @@ -335,6 +342,7 @@ class CallbackProxy extends Handler { case RECEIVED_TITLE: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onReceivedTitle"); mWebChromeClient.onReceivedTitle(mWebView.getWebView(), (String) msg.obj); } @@ -345,6 +353,7 @@ class CallbackProxy extends Handler { int reasonCode = msg.arg1; final String description = msg.getData().getString("description"); final String failUrl = msg.getData().getString("failingUrl"); + if (TRACE) Log.d(LOGTAG, "onReceivedError=" + failUrl); mWebViewClient.onReceivedError(mWebView.getWebView(), reasonCode, description, failUrl); } @@ -356,6 +365,7 @@ class CallbackProxy extends Handler { Message dontResend = (Message) msg.getData().getParcelable("dontResend"); if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onFormResubmission"); mWebViewClient.onFormResubmission(mWebView.getWebView(), dontResend, resend); } else { @@ -379,6 +389,7 @@ class CallbackProxy extends Handler { HttpAuthHandler handler = (HttpAuthHandler) msg.obj; String host = msg.getData().getString("host"); String realm = msg.getData().getString("realm"); + if (TRACE) Log.d(LOGTAG, "onReceivedHttpAuthRequest"); mWebViewClient.onReceivedHttpAuthRequest(mWebView.getWebView(), handler, host, realm); } @@ -388,6 +399,7 @@ class CallbackProxy extends Handler { if (mWebViewClient != null) { HashMap<String, Object> map = (HashMap<String, Object>) msg.obj; + if (TRACE) Log.d(LOGTAG, "onReceivedSslError"); mWebViewClient.onReceivedSslError(mWebView.getWebView(), (SslErrorHandler) map.get("handler"), (SslError) map.get("error")); @@ -396,6 +408,7 @@ class CallbackProxy extends Handler { case PROCEEDED_AFTER_SSL_ERROR: if (mWebViewClient != null && mWebViewClient instanceof WebViewClientClassicExt) { + if (TRACE) Log.d(LOGTAG, "onProceededAfterSslError"); ((WebViewClientClassicExt) mWebViewClient).onProceededAfterSslError( mWebView.getWebView(), (SslError) msg.obj); @@ -404,6 +417,7 @@ class CallbackProxy extends Handler { case CLIENT_CERT_REQUEST: if (mWebViewClient != null && mWebViewClient instanceof WebViewClientClassicExt) { + if (TRACE) Log.d(LOGTAG, "onReceivedClientCertRequest"); HashMap<String, Object> map = (HashMap<String, Object>) msg.obj; ((WebViewClientClassicExt) mWebViewClient).onReceivedClientCertRequest( mWebView.getWebView(), @@ -418,6 +432,7 @@ class CallbackProxy extends Handler { // changed. synchronized (this) { if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onProgressChanged=" + mLatestProgress); mWebChromeClient.onProgressChanged(mWebView.getWebView(), mLatestProgress); } @@ -427,14 +442,18 @@ class CallbackProxy extends Handler { case UPDATE_VISITED: if (mWebViewClient != null) { + String url = (String) msg.obj; + if (TRACE) Log.d(LOGTAG, "doUpdateVisitedHistory=" + url); mWebViewClient.doUpdateVisitedHistory(mWebView.getWebView(), - (String) msg.obj, msg.arg1 != 0); + url, msg.arg1 != 0); } break; case LOAD_RESOURCE: if (mWebViewClient != null) { - mWebViewClient.onLoadResource(mWebView.getWebView(), (String) msg.obj); + String url = (String) msg.obj; + if (TRACE) Log.d(LOGTAG, "onLoadResource=" + url); + mWebViewClient.onLoadResource(mWebView.getWebView(), url); } break; @@ -448,6 +467,7 @@ class CallbackProxy extends Handler { String referer = msg.getData().getString("referer"); Long contentLength = msg.getData().getLong("contentLength"); + if (TRACE) Log.d(LOGTAG, "onDownloadStart"); if (mDownloadListener instanceof BrowserDownloadListener) { ((BrowserDownloadListener) mDownloadListener).onDownloadStart(url, userAgent, contentDisposition, mimetype, referer, contentLength); @@ -460,6 +480,7 @@ class CallbackProxy extends Handler { case CREATE_WINDOW: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onCreateWindow"); if (!mWebChromeClient.onCreateWindow(mWebView.getWebView(), msg.arg1 == 1, msg.arg2 == 1, (Message) msg.obj)) { @@ -473,12 +494,14 @@ class CallbackProxy extends Handler { case REQUEST_FOCUS: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onRequestFocus"); mWebChromeClient.onRequestFocus(mWebView.getWebView()); } break; case CLOSE_WINDOW: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onCloseWindow"); mWebChromeClient.onCloseWindow(((WebViewClassic) msg.obj).getWebView()); } break; @@ -500,6 +523,7 @@ class CallbackProxy extends Handler { case ASYNC_KEYEVENTS: if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onUnhandledKeyEvent"); mWebViewClient.onUnhandledKeyEvent(mWebView.getWebView(), (KeyEvent) msg.obj); } @@ -521,6 +545,7 @@ class CallbackProxy extends Handler { WebStorage.QuotaUpdater quotaUpdater = (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + if (TRACE) Log.d(LOGTAG, "onExceededDatabaseQuota"); mWebChromeClient.onExceededDatabaseQuota(url, databaseIdentifier, quota, estimatedDatabaseSize, totalQuota, quotaUpdater); @@ -538,6 +563,7 @@ class CallbackProxy extends Handler { WebStorage.QuotaUpdater quotaUpdater = (WebStorage.QuotaUpdater) map.get("quotaUpdater"); + if (TRACE) Log.d(LOGTAG, "onReachedMaxAppCacheSize"); mWebChromeClient.onReachedMaxAppCacheSize(requiredStorage, quota, quotaUpdater); } @@ -551,6 +577,7 @@ class CallbackProxy extends Handler { GeolocationPermissions.Callback callback = (GeolocationPermissions.Callback) map.get("callback"); + if (TRACE) Log.d(LOGTAG, "onGeolocationPermissionsShowPrompt"); mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); } @@ -558,6 +585,7 @@ class CallbackProxy extends Handler { case GEOLOCATION_PERMISSIONS_HIDE_PROMPT: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "onGeolocationPermissionsHidePrompt"); mWebChromeClient.onGeolocationPermissionsHidePrompt(); } break; @@ -566,6 +594,7 @@ class CallbackProxy extends Handler { if (mWebChromeClient != null) { final JsResultReceiver receiver = (JsResultReceiver) msg.obj; JsDialogHelper helper = new JsDialogHelper(receiver.mJsResult, msg); + if (TRACE) Log.d(LOGTAG, "onJsAlert"); if (!helper.invokeCallback(mWebChromeClient, mWebView.getWebView())) { helper.showDialog(mContext); } @@ -577,6 +606,7 @@ class CallbackProxy extends Handler { if(mWebChromeClient != null) { final JsResultReceiver receiver = (JsResultReceiver) msg.obj; final JsResult res = receiver.mJsResult; + if (TRACE) Log.d(LOGTAG, "onJsTimeout"); if (mWebChromeClient.onJsTimeout()) { res.confirm(); } else { @@ -598,6 +628,7 @@ class CallbackProxy extends Handler { case SCALE_CHANGED: if (mWebViewClient != null) { + if (TRACE) Log.d(LOGTAG, "onScaleChanged"); mWebViewClient.onScaleChanged(mWebView.getWebView(), msg.getData() .getFloat("old"), msg.getData().getFloat("new")); } @@ -624,6 +655,7 @@ class CallbackProxy extends Handler { ConsoleMessage.MessageLevel messageLevel = ConsoleMessage.MessageLevel.values()[msgLevel]; + if (TRACE) Log.d(LOGTAG, "onConsoleMessage"); if (!mWebChromeClient.onConsoleMessage(new ConsoleMessage(message, sourceID, lineNumber, messageLevel))) { // If false was returned the user did not provide their own console function so @@ -654,12 +686,14 @@ class CallbackProxy extends Handler { case GET_VISITED_HISTORY: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "getVisitedHistory"); mWebChromeClient.getVisitedHistory((ValueCallback<String[]>)msg.obj); } break; case OPEN_FILE_CHOOSER: if (mWebChromeClient != null) { + if (TRACE) Log.d(LOGTAG, "openFileChooser"); UploadFileMessageData data = (UploadFileMessageData)msg.obj; mWebChromeClient.openFileChooser(data.getUploadFile(), data.getAcceptType(), data.getCapture()); @@ -668,6 +702,7 @@ class CallbackProxy extends Handler { case ADD_HISTORY_ITEM: if (mWebBackForwardListClient != null) { + if (TRACE) Log.d(LOGTAG, "onNewHistoryItem"); mWebBackForwardListClient.onNewHistoryItem( (WebHistoryItem) msg.obj); } @@ -693,6 +728,7 @@ class CallbackProxy extends Handler { String realm = msg.getData().getString("realm"); String account = msg.getData().getString("account"); String args = msg.getData().getString("args"); + if (TRACE) Log.d(LOGTAG, "onReceivedLoginRequest"); mWebViewClient.onReceivedLoginRequest(mWebView.getWebView(), realm, account, args); } @@ -910,6 +946,7 @@ class CallbackProxy extends Handler { return null; } // Note: This method does _not_ send a message. + if (TRACE) Log.d(LOGTAG, "shouldInterceptRequest=" + url); WebResourceResponse r = mWebViewClient.shouldInterceptRequest(mWebView.getWebView(), url); if (r == null) { diff --git a/core/java/android/webkit/DebugFlags.java b/core/java/android/webkit/DebugFlags.java index 349113e..524f610 100644 --- a/core/java/android/webkit/DebugFlags.java +++ b/core/java/android/webkit/DebugFlags.java @@ -24,25 +24,33 @@ package android.webkit; * The name of each flags maps directly to the name of the class in which that * flag is used. * + * @hide Only used by WebView implementations. */ -class DebugFlags { +public class DebugFlags { + public static final boolean COOKIE_SYNC_MANAGER = false; + public static final boolean TRACE_API = false; + public static final boolean TRACE_CALLBACK = false; + public static final boolean TRACE_JAVASCRIPT_BRIDGE = false; + public static final boolean URL_UTIL = false; + public static final boolean WEB_SYNC_MANAGER = false; + + // TODO: Delete these when WebViewClassic is moved public static final boolean BROWSER_FRAME = false; public static final boolean CACHE_MANAGER = false; public static final boolean CALLBACK_PROXY = false; public static final boolean COOKIE_MANAGER = false; - public static final boolean COOKIE_SYNC_MANAGER = false; public static final boolean FRAME_LOADER = false; public static final boolean J_WEB_CORE_JAVA_BRIDGE = false;// HIGHLY VERBOSE public static final boolean LOAD_LISTENER = false; + public static final boolean MEASURE_PAGE_SWAP_FPS = false; public static final boolean NETWORK = false; public static final boolean SSL_ERROR_HANDLER = false; public static final boolean STREAM_LOADER = false; - public static final boolean URL_UTIL = false; public static final boolean WEB_BACK_FORWARD_LIST = false; public static final boolean WEB_SETTINGS = false; - public static final boolean WEB_SYNC_MANAGER = false; public static final boolean WEB_VIEW = false; public static final boolean WEB_VIEW_CORE = false; - public static final boolean MEASURE_PAGE_SWAP_FPS = false; + + } diff --git a/core/java/android/webkit/HTML5VideoFullScreen.java b/core/java/android/webkit/HTML5VideoFullScreen.java index b52218d..6fb32c8 100644 --- a/core/java/android/webkit/HTML5VideoFullScreen.java +++ b/core/java/android/webkit/HTML5VideoFullScreen.java @@ -19,6 +19,7 @@ package android.webkit; import android.content.Context; import android.media.MediaPlayer; import android.media.Metadata; +import android.util.Log; import android.view.Gravity; import android.view.MotionEvent; import android.view.SurfaceHolder; @@ -293,12 +294,16 @@ public class HTML5VideoFullScreen extends HTML5VideoView mLayout.setVisibility(View.VISIBLE); WebChromeClient client = webView.getWebChromeClient(); if (client != null) { + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onShowCustomView"); client.onShowCustomView(mLayout, mCallback); // Plugins like Flash will draw over the video so hide // them while we're playing. if (webView.getViewManager() != null) webView.getViewManager().hideAll(); + if (DebugFlags.TRACE_CALLBACK) { + Log.d(CallbackProxy.LOGTAG, "getVideoLoadingProgressView"); + } mProgressView = client.getVideoLoadingProgressView(); if (mProgressView != null) { mLayout.addView(mProgressView, layoutParams); diff --git a/core/java/android/webkit/HTML5VideoViewProxy.java b/core/java/android/webkit/HTML5VideoViewProxy.java index a3d62ae..e8538f6 100644 --- a/core/java/android/webkit/HTML5VideoViewProxy.java +++ b/core/java/android/webkit/HTML5VideoViewProxy.java @@ -180,6 +180,7 @@ class HTML5VideoViewProxy extends Handler if (!mHTML5VideoView.fullScreenExited() && mHTML5VideoView.isFullScreenMode()) { WebChromeClient client = webView.getWebChromeClient(); if (client != null) { + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onHideCustomView"); client.onHideCustomView(); } } @@ -405,6 +406,7 @@ class HTML5VideoViewProxy extends Handler case ERROR: { WebChromeClient client = mWebView.getWebChromeClient(); if (client != null) { + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onHideCustomView"); client.onHideCustomView(); } break; @@ -412,6 +414,9 @@ class HTML5VideoViewProxy extends Handler case LOAD_DEFAULT_POSTER: { WebChromeClient client = mWebView.getWebChromeClient(); if (client != null) { + if (DebugFlags.TRACE_CALLBACK) { + Log.d(CallbackProxy.LOGTAG, "getDefaultVideoPoster"); + } doSetPoster(client.getDefaultVideoPoster()); } break; diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java index eded438..f0e8c4f 100644 --- a/core/java/android/webkit/WebView.java +++ b/core/java/android/webkit/WebView.java @@ -244,7 +244,7 @@ public class WebView extends AbsoluteLayout implements ViewTreeObserver.OnGlobalFocusChangeListener, ViewGroup.OnHierarchyChangeListener, ViewDebug.HierarchyHandler { - private static final String LOGTAG = "webview_proxy"; + private static final String LOGTAG = "WebView"; // Throwing an exception for incorrect thread usage if the // build target is JB MR2 or newer. Defaults to false, and is @@ -496,6 +496,7 @@ public class WebView extends AbsoluteLayout sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2; checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "WebView<init>"); ensureProviderCreated(); mProvider.init(javaScriptInterfaces, privateBrowsing); @@ -510,6 +511,7 @@ public class WebView extends AbsoluteLayout */ public void setHorizontalScrollbarOverlay(boolean overlay) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setHorizontalScrollbarOverlay=" + overlay); mProvider.setHorizontalScrollbarOverlay(overlay); } @@ -520,6 +522,7 @@ public class WebView extends AbsoluteLayout */ public void setVerticalScrollbarOverlay(boolean overlay) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setVerticalScrollbarOverlay=" + overlay); mProvider.setVerticalScrollbarOverlay(overlay); } @@ -574,6 +577,7 @@ public class WebView extends AbsoluteLayout @Deprecated public void setCertificate(SslCertificate certificate) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setCertificate=" + certificate); mProvider.setCertificate(certificate); } @@ -597,6 +601,7 @@ public class WebView extends AbsoluteLayout @Deprecated public void savePassword(String host, String username, String password) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "savePassword=" + host); mProvider.savePassword(host, username, password); } @@ -616,6 +621,7 @@ public class WebView extends AbsoluteLayout public void setHttpAuthUsernamePassword(String host, String realm, String username, String password) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setHttpAuthUsernamePassword=" + host); mProvider.setHttpAuthUsernamePassword(host, realm, username, password); } @@ -645,6 +651,7 @@ public class WebView extends AbsoluteLayout */ public void destroy() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "destroy"); mProvider.destroy(); } @@ -683,6 +690,7 @@ public class WebView extends AbsoluteLayout */ public void setNetworkAvailable(boolean networkUp) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setNetworkAvailable=" + networkUp); mProvider.setNetworkAvailable(networkUp); } @@ -699,6 +707,7 @@ public class WebView extends AbsoluteLayout */ public WebBackForwardList saveState(Bundle outState) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "saveState"); return mProvider.saveState(outState); } @@ -715,6 +724,7 @@ public class WebView extends AbsoluteLayout @Deprecated public boolean savePicture(Bundle b, final File dest) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "savePicture=" + dest.getName()); return mProvider.savePicture(b, dest); } @@ -732,6 +742,7 @@ public class WebView extends AbsoluteLayout @Deprecated public boolean restorePicture(Bundle b, File src) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "restorePicture=" + src.getName()); return mProvider.restorePicture(b, src); } @@ -749,6 +760,7 @@ public class WebView extends AbsoluteLayout */ public WebBackForwardList restoreState(Bundle inState) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "restoreState"); return mProvider.restoreState(inState); } @@ -765,6 +777,7 @@ public class WebView extends AbsoluteLayout */ public void loadUrl(String url, Map<String, String> additionalHttpHeaders) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadUrl(extra headers)=" + url); mProvider.loadUrl(url, additionalHttpHeaders); } @@ -775,6 +788,7 @@ public class WebView extends AbsoluteLayout */ public void loadUrl(String url) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadUrl=" + url); mProvider.loadUrl(url); } @@ -789,6 +803,7 @@ public class WebView extends AbsoluteLayout */ public void postUrl(String url, byte[] postData) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "postUrl=" + url); mProvider.postUrl(url, postData); } @@ -823,6 +838,7 @@ public class WebView extends AbsoluteLayout */ public void loadData(String data, String mimeType, String encoding) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadData"); mProvider.loadData(data, mimeType, encoding); } @@ -855,6 +871,7 @@ public class WebView extends AbsoluteLayout public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadDataWithBaseURL=" + baseUrl); mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl); } @@ -871,6 +888,7 @@ public class WebView extends AbsoluteLayout */ public void evaluateJavascript(String script, ValueCallback<String> resultCallback) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "evaluateJavascript=" + script); mProvider.evaluateJavaScript(script, resultCallback); } @@ -881,6 +899,7 @@ public class WebView extends AbsoluteLayout */ public void saveWebArchive(String filename) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "saveWebArchive=" + filename); mProvider.saveWebArchive(filename); } @@ -898,6 +917,7 @@ public class WebView extends AbsoluteLayout */ public void saveWebArchive(String basename, boolean autoname, ValueCallback<String> callback) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "saveWebArchive(auto)=" + basename); mProvider.saveWebArchive(basename, autoname, callback); } @@ -906,6 +926,7 @@ public class WebView extends AbsoluteLayout */ public void stopLoading() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "stopLoading"); mProvider.stopLoading(); } @@ -914,6 +935,7 @@ public class WebView extends AbsoluteLayout */ public void reload() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "reload"); mProvider.reload(); } @@ -932,6 +954,7 @@ public class WebView extends AbsoluteLayout */ public void goBack() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "goBack"); mProvider.goBack(); } @@ -950,6 +973,7 @@ public class WebView extends AbsoluteLayout */ public void goForward() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "goForward"); mProvider.goForward(); } @@ -975,6 +999,7 @@ public class WebView extends AbsoluteLayout */ public void goBackOrForward(int steps) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "goBackOrForwad=" + steps); mProvider.goBackOrForward(steps); } @@ -994,6 +1019,7 @@ public class WebView extends AbsoluteLayout */ public boolean pageUp(boolean top) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "pageUp"); return mProvider.pageUp(top); } @@ -1005,6 +1031,7 @@ public class WebView extends AbsoluteLayout */ public boolean pageDown(boolean bottom) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "pageDown"); return mProvider.pageDown(bottom); } @@ -1017,6 +1044,7 @@ public class WebView extends AbsoluteLayout @Deprecated public void clearView() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearView"); mProvider.clearView(); } @@ -1036,6 +1064,7 @@ public class WebView extends AbsoluteLayout */ public Picture capturePicture() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "capturePicture"); return mProvider.capturePicture(); } @@ -1073,6 +1102,7 @@ public class WebView extends AbsoluteLayout ValueCallback<Boolean> resultCallback, CancellationSignal cancellationSignal) throws java.io.IOException { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "exportToPdf"); mProvider.exportToPdf(fd, attributes, resultCallback, cancellationSignal); } @@ -1104,6 +1134,7 @@ public class WebView extends AbsoluteLayout */ public void setInitialScale(int scaleInPercent) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setInitialScale=" + scaleInPercent); mProvider.setInitialScale(scaleInPercent); } @@ -1114,6 +1145,7 @@ public class WebView extends AbsoluteLayout */ public void invokeZoomPicker() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "invokeZoomPicker"); mProvider.invokeZoomPicker(); } @@ -1137,6 +1169,7 @@ public class WebView extends AbsoluteLayout */ public HitTestResult getHitTestResult() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "getHitTestResult"); return mProvider.getHitTestResult(); } @@ -1155,6 +1188,7 @@ public class WebView extends AbsoluteLayout */ public void requestFocusNodeHref(Message hrefMsg) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "requestFocusNodeHref"); mProvider.requestFocusNodeHref(hrefMsg); } @@ -1167,6 +1201,7 @@ public class WebView extends AbsoluteLayout */ public void requestImageRef(Message msg) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "requestImageRef"); mProvider.requestImageRef(msg); } @@ -1271,6 +1306,7 @@ public class WebView extends AbsoluteLayout */ public void pauseTimers() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "pauseTimers"); mProvider.pauseTimers(); } @@ -1280,6 +1316,7 @@ public class WebView extends AbsoluteLayout */ public void resumeTimers() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "resumeTimers"); mProvider.resumeTimers(); } @@ -1292,6 +1329,7 @@ public class WebView extends AbsoluteLayout */ public void onPause() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "onPause"); mProvider.onPause(); } @@ -1300,6 +1338,7 @@ public class WebView extends AbsoluteLayout */ public void onResume() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "onResume"); mProvider.onResume(); } @@ -1319,6 +1358,7 @@ public class WebView extends AbsoluteLayout */ public void freeMemory() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "freeMemory"); mProvider.freeMemory(); } @@ -1330,6 +1370,7 @@ public class WebView extends AbsoluteLayout */ public void clearCache(boolean includeDiskFiles) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearCache"); mProvider.clearCache(includeDiskFiles); } @@ -1341,6 +1382,7 @@ public class WebView extends AbsoluteLayout */ public void clearFormData() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearFormData"); mProvider.clearFormData(); } @@ -1349,6 +1391,7 @@ public class WebView extends AbsoluteLayout */ public void clearHistory() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearHistory"); mProvider.clearHistory(); } @@ -1358,6 +1401,7 @@ public class WebView extends AbsoluteLayout */ public void clearSslPreferences() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearSslPreferences"); mProvider.clearSslPreferences(); } @@ -1399,6 +1443,7 @@ public class WebView extends AbsoluteLayout */ public void findNext(boolean forward) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "findNext"); mProvider.findNext(forward); } @@ -1414,6 +1459,7 @@ public class WebView extends AbsoluteLayout @Deprecated public int findAll(String find) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "findAll"); StrictMode.noteSlowCall("findAll blocks UI: prefer findAllAsync"); return mProvider.findAll(find); } @@ -1428,6 +1474,7 @@ public class WebView extends AbsoluteLayout */ public void findAllAsync(String find) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "findAllAsync"); mProvider.findAllAsync(find); } @@ -1448,6 +1495,7 @@ public class WebView extends AbsoluteLayout @Deprecated public boolean showFindDialog(String text, boolean showIme) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "showFindDialog"); return mProvider.showFindDialog(text, showIme); } @@ -1483,6 +1531,7 @@ public class WebView extends AbsoluteLayout */ public void clearMatches() { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "clearMatches"); mProvider.clearMatches(); } @@ -1543,6 +1592,7 @@ public class WebView extends AbsoluteLayout @Deprecated public void setPictureListener(PictureListener listener) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "setPictureListener=" + listener); mProvider.setPictureListener(listener); } @@ -1592,6 +1642,7 @@ public class WebView extends AbsoluteLayout */ public void addJavascriptInterface(Object object, String name) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "addJavascriptInterface=" + name); mProvider.addJavascriptInterface(object, name); } @@ -1604,6 +1655,7 @@ public class WebView extends AbsoluteLayout */ public void removeJavascriptInterface(String name) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "removeJavascriptInterface=" + name); mProvider.removeJavascriptInterface(name); } @@ -1693,6 +1745,7 @@ public class WebView extends AbsoluteLayout public void flingScroll(int vx, int vy) { checkThread(); + if (DebugFlags.TRACE_API) Log.d(LOGTAG, "flingScroll"); mProvider.flingScroll(vx, vy); } diff --git a/core/java/android/webkit/WebViewClassic.java b/core/java/android/webkit/WebViewClassic.java index b1a7878..3f22d53 100644 --- a/core/java/android/webkit/WebViewClassic.java +++ b/core/java/android/webkit/WebViewClassic.java @@ -7951,6 +7951,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // triggered in setNewPicture Picture picture = mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2 ? capturePicture() : null; + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onNewPicture"); mPictureListener.onNewPicture(getWebView(), picture); } } @@ -8038,6 +8039,7 @@ public final class WebViewClassic implements WebViewProvider, WebViewProvider.Sc // triggered in pageSwapCallback Picture picture = mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.JELLY_BEAN_MR2 ? capturePicture() : null; + if (DebugFlags.TRACE_CALLBACK) Log.d(CallbackProxy.LOGTAG, "onNewPicture"); mPictureListener.onNewPicture(getWebView(), picture); } } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index be47bf0..c308024 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -4924,11 +4924,37 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * Scrolls the list items within the view by a specified number of pixels. * * @param y the amount of pixels to scroll by vertically - * @return true if the list is able to scroll, or false if the list is - * already at the beginning/end and unable to scroll any more. + * @see #canScrollList(int) */ - public boolean scrollListBy(int y) { - return !trackMotionScroll(-y, -y); + public void scrollListBy(int y) { + trackMotionScroll(-y, -y); + } + + /** + * Check if the items in the list can be scrolled in a certain direction. + * + * @param direction Negative to check scrolling up, positive to check + * scrolling down. + * @return true if the list can be scrolled in the specified direction, + * false otherwise. + * @see #scrollListBy(int) + */ + public boolean canScrollList(int direction) { + final int childCount = getChildCount(); + if (childCount == 0) { + return false; + } + + final int firstPosition = mFirstPosition; + final Rect listPadding = mListPadding; + if (direction > 0) { + final int lastBottom = getChildAt(childCount - 1).getBottom(); + final int lastPosition = firstPosition + childCount; + return lastPosition < mItemCount || lastBottom > getHeight() - listPadding.bottom; + } else { + final int firstTop = getChildAt(0).getTop(); + return firstPosition > 0 || firstTop < listPadding.top; + } } /** diff --git a/core/java/android/widget/ListPopupWindow.java b/core/java/android/widget/ListPopupWindow.java index f2da765..b7e1fdd 100644 --- a/core/java/android/widget/ListPopupWindow.java +++ b/core/java/android/widget/ListPopupWindow.java @@ -974,10 +974,12 @@ public class ListPopupWindow { * currently touched list item. * <p> * Example usage: - * <pre>ListPopupWindow myPopup = new ListPopupWindow(context); + * <pre> + * ListPopupWindow myPopup = new ListPopupWindow(context); * myPopup.setAnchor(myAnchor); * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor); - * myAnchor.setOnTouchListener(dragListener);</pre> + * myAnchor.setOnTouchListener(dragListener); + * </pre> * * @param src the view on which the resulting listener will be set * @return a touch listener that controls drag-to-open behavior diff --git a/core/java/android/widget/PopupMenu.java b/core/java/android/widget/PopupMenu.java index e5344c6..603db70 100644 --- a/core/java/android/widget/PopupMenu.java +++ b/core/java/android/widget/PopupMenu.java @@ -82,8 +82,10 @@ public class PopupMenu implements MenuBuilder.Callback, MenuPresenter.Callback { * currently touched list item. * <p> * Example usage: - * <pre>PopupMenu myPopup = new PopupMenu(context, myAnchor); - * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener());</pre> + * <pre> + * PopupMenu myPopup = new PopupMenu(context, myAnchor); + * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener()); + * </pre> * * @return a touch listener that controls drag-to-open behavior */ diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index ebf9fe0..0ddc131 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -21,12 +21,14 @@ import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.res.Resources; +import android.graphics.Canvas; import android.media.AudioManager; +import android.media.MediaFormat; import android.media.MediaPlayer; -import android.media.Metadata; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnInfoListener; +import android.media.Metadata; import android.net.Uri; import android.util.AttributeSet; import android.util.Log; @@ -40,7 +42,10 @@ import android.view.accessibility.AccessibilityNodeInfo; import android.widget.MediaController.MediaPlayerControl; import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; import java.util.Map; +import java.util.Vector; /** * Displays a video file. The VideoView class @@ -91,6 +96,15 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { private boolean mCanSeekBack; private boolean mCanSeekForward; + /** List of views overlaid on top of the video. */ + private ArrayList<View> mOverlays; + + /** + * Listener for overlay layout changes. Invalidates the video view to ensure + * that captions are redrawn whenever their layout changes. + */ + private OnLayoutChangeListener mOverlayLayoutListener; + public VideoView(Context context) { super(context); initVideoView(); @@ -194,6 +208,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { setFocusable(true); setFocusableInTouchMode(true); requestFocus(); + mPendingSubtitleTracks = 0; mCurrentState = STATE_IDLE; mTargetState = STATE_IDLE; } @@ -218,6 +233,47 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { invalidate(); } + /** + * Adds an external subtitle source file (from the provided input stream.) + * + * Note that a single external subtitle source may contain multiple or no + * supported tracks in it. If the source contained at least one track in + * it, one will receive an {@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE} + * info message. Otherwise, if reading the source takes excessive time, + * one will receive a {@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT} + * message. If the source contained no supported track (including an empty + * source file or null input stream), one will receive a {@link + * MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE} message. One can find the + * total number of available tracks using {@link MediaPlayer#getTrackInfo()} + * to see what additional tracks become available after this method call. + * + * @param is input stream containing the subtitle data. It will be + * closed by the media framework. + * @param format the format of the subtitle track(s). Must contain at least + * the mime type ({@link MediaFormat#KEY_MIME}) and the + * language ({@link MediaFormat#KEY_LANGUAGE}) of the file. + * If the file itself contains the language information, + * specify "und" for the language. + */ + public void addSubtitleSource(InputStream is, MediaFormat format) { + // always signal unsupported message for now + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + } + + if (mMediaPlayer == null) { + ++mPendingSubtitleTracks; + } else { + mInfoListener.onInfo( + mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); + } + } + + private int mPendingSubtitleTracks; + public void stopPlayback() { if (mMediaPlayer != null) { mMediaPlayer.stop(); @@ -253,7 +309,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener); mMediaPlayer.setOnCompletionListener(mCompletionListener); mMediaPlayer.setOnErrorListener(mErrorListener); - mMediaPlayer.setOnInfoListener(mOnInfoListener); + mMediaPlayer.setOnInfoListener(mInfoListener); mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener); mCurrentBufferPercentage = 0; mMediaPlayer.setDataSource(mContext, mUri, mHeaders); @@ -261,6 +317,12 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mMediaPlayer.setScreenOnWhilePlaying(true); mMediaPlayer.prepareAsync(); + + for (int ix = 0; ix < mPendingSubtitleTracks; ix++) { + mInfoListener.onInfo( + mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0); + } + // we don't set the target state here either, but preserve the // target state that was there before. mCurrentState = STATE_PREPARING; @@ -277,6 +339,8 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mTargetState = STATE_ERROR; mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); return; + } finally { + mPendingSubtitleTracks = 0; } } @@ -386,6 +450,16 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { } }; + private MediaPlayer.OnInfoListener mInfoListener = + new MediaPlayer.OnInfoListener() { + public boolean onInfo(MediaPlayer mp, int arg1, int arg2) { + if (mOnInfoListener != null) { + mOnInfoListener.onInfo(mp, arg1, arg2); + } + return true; + } + }; + private MediaPlayer.OnErrorListener mErrorListener = new MediaPlayer.OnErrorListener() { public boolean onError(MediaPlayer mp, int framework_err, int impl_err) { @@ -530,6 +604,7 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { mMediaPlayer.reset(); mMediaPlayer.release(); mMediaPlayer = null; + mPendingSubtitleTracks = 0; mCurrentState = STATE_IDLE; if (cleartargetstate) { mTargetState = STATE_IDLE; @@ -702,4 +777,101 @@ public class VideoView extends SurfaceView implements MediaPlayerControl { } return mAudioSession; } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + + // Layout overlay views, if necessary. + if (changed && mOverlays != null && !mOverlays.isEmpty()) { + measureAndLayoutOverlays(); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + final int count = mOverlays.size(); + for (int i = 0; i < count; i++) { + final View overlay = mOverlays.get(i); + overlay.draw(canvas); + } + } + + /** + * Adds a view to be overlaid on top of this video view. During layout, the + * view will be forced to match the bounds, less padding, of the video view. + * <p> + * Overlays are drawn in the order they are added. The last added overlay + * will be drawn on top. + * + * @param overlay the view to overlay + * @see #removeOverlay(View) + */ + private void addOverlay(View overlay) { + if (mOverlays == null) { + mOverlays = new ArrayList<View>(1); + } + + if (mOverlayLayoutListener == null) { + mOverlayLayoutListener = new OnLayoutChangeListener() { + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { + invalidate(); + } + }; + } + + if (mOverlays.isEmpty()) { + setWillNotDraw(false); + } + + mOverlays.add(overlay); + overlay.addOnLayoutChangeListener(mOverlayLayoutListener); + measureAndLayoutOverlays(); + } + + /** + * Removes a view previously added using {@link #addOverlay}. + * + * @param overlay the view to remove + * @see #addOverlay(View) + */ + private void removeOverlay(View overlay) { + if (mOverlays == null) { + return; + } + + overlay.removeOnLayoutChangeListener(mOverlayLayoutListener); + mOverlays.remove(overlay); + + if (mOverlays.isEmpty()) { + setWillNotDraw(true); + } + + invalidate(); + } + + /** + * Forces a measurement and layout pass for all overlaid views. + * + * @see #addOverlay(View) + */ + private void measureAndLayoutOverlays() { + final int left = getPaddingLeft(); + final int top = getPaddingTop(); + final int right = getWidth() - left - getPaddingRight(); + final int bottom = getHeight() - top - getPaddingBottom(); + final int widthSpec = MeasureSpec.makeMeasureSpec(right - left, MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec(bottom - top, MeasureSpec.EXACTLY); + + final int count = mOverlays.size(); + for (int i = 0; i < count; i++) { + final View overlay = mOverlays.get(i); + overlay.measure(widthSpec, heightSpec); + overlay.layout(left, top, right, bottom); + } + } } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index ab81a37..8819237 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -287,102 +287,104 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte } protected void onIntentSelected(ResolveInfo ri, Intent intent, boolean alwaysCheck) { - // Build a reasonable intent filter, based on what matched. - IntentFilter filter = new IntentFilter(); + if (mAlwaysUseOption) { + // Build a reasonable intent filter, based on what matched. + IntentFilter filter = new IntentFilter(); - if (intent.getAction() != null) { - filter.addAction(intent.getAction()); - } - Set<String> categories = intent.getCategories(); - if (categories != null) { - for (String cat : categories) { - filter.addCategory(cat); + if (intent.getAction() != null) { + filter.addAction(intent.getAction()); } - } - filter.addCategory(Intent.CATEGORY_DEFAULT); - - int cat = ri.match&IntentFilter.MATCH_CATEGORY_MASK; - Uri data = intent.getData(); - if (cat == IntentFilter.MATCH_CATEGORY_TYPE) { - String mimeType = intent.resolveType(this); - if (mimeType != null) { - try { - filter.addDataType(mimeType); - } catch (IntentFilter.MalformedMimeTypeException e) { - Log.w("ResolverActivity", e); - filter = null; + Set<String> categories = intent.getCategories(); + if (categories != null) { + for (String cat : categories) { + filter.addCategory(cat); } } - } - if (data != null && data.getScheme() != null) { - // We need the data specification if there was no type, - // OR if the scheme is not one of our magical "file:" - // or "content:" schemes (see IntentFilter for the reason). - if (cat != IntentFilter.MATCH_CATEGORY_TYPE - || (!"file".equals(data.getScheme()) - && !"content".equals(data.getScheme()))) { - filter.addDataScheme(data.getScheme()); - - // Look through the resolved filter to determine which part - // of it matched the original Intent. - Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator(); - if (pIt != null) { - String ssp = data.getSchemeSpecificPart(); - while (ssp != null && pIt.hasNext()) { - PatternMatcher p = pIt.next(); - if (p.match(ssp)) { - filter.addDataSchemeSpecificPart(p.getPath(), p.getType()); - break; - } + filter.addCategory(Intent.CATEGORY_DEFAULT); + + int cat = ri.match&IntentFilter.MATCH_CATEGORY_MASK; + Uri data = intent.getData(); + if (cat == IntentFilter.MATCH_CATEGORY_TYPE) { + String mimeType = intent.resolveType(this); + if (mimeType != null) { + try { + filter.addDataType(mimeType); + } catch (IntentFilter.MalformedMimeTypeException e) { + Log.w("ResolverActivity", e); + filter = null; } } - Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator(); - if (aIt != null) { - while (aIt.hasNext()) { - IntentFilter.AuthorityEntry a = aIt.next(); - if (a.match(data) >= 0) { - int port = a.getPort(); - filter.addDataAuthority(a.getHost(), - port >= 0 ? Integer.toString(port) : null); - break; + } + if (data != null && data.getScheme() != null) { + // We need the data specification if there was no type, + // OR if the scheme is not one of our magical "file:" + // or "content:" schemes (see IntentFilter for the reason). + if (cat != IntentFilter.MATCH_CATEGORY_TYPE + || (!"file".equals(data.getScheme()) + && !"content".equals(data.getScheme()))) { + filter.addDataScheme(data.getScheme()); + + // Look through the resolved filter to determine which part + // of it matched the original Intent. + Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator(); + if (pIt != null) { + String ssp = data.getSchemeSpecificPart(); + while (ssp != null && pIt.hasNext()) { + PatternMatcher p = pIt.next(); + if (p.match(ssp)) { + filter.addDataSchemeSpecificPart(p.getPath(), p.getType()); + break; + } } } - } - pIt = ri.filter.pathsIterator(); - if (pIt != null) { - String path = data.getPath(); - while (path != null && pIt.hasNext()) { - PatternMatcher p = pIt.next(); - if (p.match(path)) { - filter.addDataPath(p.getPath(), p.getType()); - break; + Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator(); + if (aIt != null) { + while (aIt.hasNext()) { + IntentFilter.AuthorityEntry a = aIt.next(); + if (a.match(data) >= 0) { + int port = a.getPort(); + filter.addDataAuthority(a.getHost(), + port >= 0 ? Integer.toString(port) : null); + break; + } + } + } + pIt = ri.filter.pathsIterator(); + if (pIt != null) { + String path = data.getPath(); + while (path != null && pIt.hasNext()) { + PatternMatcher p = pIt.next(); + if (p.match(path)) { + filter.addDataPath(p.getPath(), p.getType()); + break; + } } } } } - } - if (filter != null) { - final int N = mAdapter.mList.size(); - ComponentName[] set = new ComponentName[N]; - int bestMatch = 0; - for (int i=0; i<N; i++) { - ResolveInfo r = mAdapter.mList.get(i).ri; - set[i] = new ComponentName(r.activityInfo.packageName, - r.activityInfo.name); - if (r.match > bestMatch) bestMatch = r.match; - } - if (alwaysCheck) { - getPackageManager().addPreferredActivity(filter, bestMatch, set, - intent.getComponent()); - } else { - try { - AppGlobals.getPackageManager().setLastChosenActivity(intent, - intent.resolveTypeIfNeeded(getContentResolver()), - PackageManager.MATCH_DEFAULT_ONLY, - filter, bestMatch, intent.getComponent()); - } catch (RemoteException re) { - Log.d(TAG, "Error calling setLastChosenActivity\n" + re); + if (filter != null) { + final int N = mAdapter.mList.size(); + ComponentName[] set = new ComponentName[N]; + int bestMatch = 0; + for (int i=0; i<N; i++) { + ResolveInfo r = mAdapter.mList.get(i).ri; + set[i] = new ComponentName(r.activityInfo.packageName, + r.activityInfo.name); + if (r.match > bestMatch) bestMatch = r.match; + } + if (alwaysCheck) { + getPackageManager().addPreferredActivity(filter, bestMatch, set, + intent.getComponent()); + } else { + try { + AppGlobals.getPackageManager().setLastChosenActivity(intent, + intent.resolveTypeIfNeeded(getContentResolver()), + PackageManager.MATCH_DEFAULT_ONLY, + filter, bestMatch, intent.getComponent()); + } catch (RemoteException re) { + Log.d(TAG, "Error calling setLastChosenActivity\n" + re); + } } } } diff --git a/core/java/com/android/internal/app/RestrictionsPinActivity.java b/core/java/com/android/internal/app/RestrictionsPinActivity.java index f8ce108..2112474 100644 --- a/core/java/com/android/internal/app/RestrictionsPinActivity.java +++ b/core/java/com/android/internal/app/RestrictionsPinActivity.java @@ -16,9 +16,7 @@ package com.android.internal.app; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.os.Bundle; import android.os.UserManager; import android.text.Editable; @@ -26,7 +24,8 @@ import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; -import android.view.WindowManager; +import android.view.View.OnClickListener; +import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; @@ -38,14 +37,15 @@ import com.android.internal.R; * challenge for an existing PIN. The PIN is maintained by UserManager. */ public class RestrictionsPinActivity extends AlertActivity - implements DialogInterface.OnClickListener, TextWatcher, OnEditorActionListener { + implements OnClickListener, TextWatcher, OnEditorActionListener { protected UserManager mUserManager; protected boolean mHasRestrictionsPin; protected EditText mPinText; protected TextView mPinErrorMessage; - protected TextView mPinMessage; + private Button mOkButton; + private Button mCancelButton; @Override public void onCreate(Bundle icicle) { @@ -59,19 +59,20 @@ public class RestrictionsPinActivity extends AlertActivity protected void initUi() { AlertController.AlertParams ap = mAlertParams; - ap.mTitle = getString(R.string.restr_pin_enter_pin); - ap.mPositiveButtonText = getString(R.string.ok); - ap.mNegativeButtonText = getString(R.string.cancel); - ap.mPositiveButtonListener = this; - ap.mNegativeButtonListener = this; + ap.mTitle = getString(R.string.restr_pin_enter_admin_pin); LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); ap.mView = inflater.inflate(R.layout.restrictions_pin_challenge, null); - mPinMessage = (TextView) ap.mView.findViewById(R.id.pin_message); - mPinText = (EditText) ap.mView.findViewById(R.id.pin_text); mPinErrorMessage = (TextView) ap.mView.findViewById(R.id.pin_error_message); + mPinText = (EditText) ap.mView.findViewById(R.id.pin_text); + mOkButton = (Button) ap.mView.findViewById(R.id.pin_ok_button); + mCancelButton = (Button) ap.mView.findViewById(R.id.pin_cancel_button); + mPinText.addTextChangedListener(this); + + mOkButton.setOnClickListener(this); + mCancelButton.setOnClickListener(this); } protected boolean verifyingPin() { @@ -84,8 +85,7 @@ public class RestrictionsPinActivity extends AlertActivity setPositiveButtonState(false); boolean hasPin = mUserManager.hasRestrictionsPin(); if (hasPin) { - mPinMessage.setVisibility(View.GONE); - mPinErrorMessage.setVisibility(View.GONE); + mPinErrorMessage.setVisibility(View.INVISIBLE); mPinText.setOnEditorActionListener(this); updatePinTimer(-1); } else if (verifyingPin()) { @@ -94,39 +94,37 @@ public class RestrictionsPinActivity extends AlertActivity } } - private void setPositiveButtonState(boolean enabled) { - mAlert.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enabled); + protected void setPositiveButtonState(boolean enabled) { + mOkButton.setEnabled(enabled); } - private void updatePinTimer(int pinTimerMs) { + private boolean updatePinTimer(int pinTimerMs) { if (pinTimerMs < 0) { pinTimerMs = mUserManager.checkRestrictionsPin(null); } + boolean enableInput; if (pinTimerMs >= 200) { - final int seconds = (pinTimerMs + 200) / 1000; - final String formatString = getResources().getQuantityString( - R.plurals.restr_pin_countdown, - seconds); - mPinErrorMessage.setText(String.format(formatString, seconds)); + // Do the count down timer for less than a minute, otherwise just say try again later. + if (pinTimerMs <= 60000) { + final int seconds = (pinTimerMs + 200) / 1000; + final String formatString = getResources().getQuantityString( + R.plurals.restr_pin_countdown, + seconds); + mPinErrorMessage.setText(String.format(formatString, seconds)); + } else { + mPinErrorMessage.setText(R.string.restr_pin_try_later); + } + enableInput = false; mPinErrorMessage.setVisibility(View.VISIBLE); - mPinText.setEnabled(false); mPinText.setText(""); - setPositiveButtonState(false); mPinText.postDelayed(mCountdownRunnable, Math.min(1000, pinTimerMs)); } else { - mPinErrorMessage.setVisibility(View.INVISIBLE); - mPinText.setEnabled(true); - mPinText.setText(""); - } - } - - public void onClick(DialogInterface dialog, int which) { - setResult(RESULT_CANCELED); - if (which == AlertDialog.BUTTON_POSITIVE) { - performPositiveButtonAction(); - } else if (which == AlertDialog.BUTTON_NEGATIVE) { - finish(); + enableInput = true; + mPinErrorMessage.setText(R.string.restr_pin_incorrect); } + mPinText.setEnabled(enableInput); + setPositiveButtonState(enableInput); + return enableInput; } protected void performPositiveButtonAction() { @@ -135,7 +133,10 @@ public class RestrictionsPinActivity extends AlertActivity setResult(RESULT_OK); finish(); } else if (result >= 0) { + mPinErrorMessage.setText(R.string.restr_pin_incorrect); + mPinErrorMessage.setVisibility(View.VISIBLE); updatePinTimer(result); + mPinText.setText(""); } } @@ -161,7 +162,20 @@ public class RestrictionsPinActivity extends AlertActivity private Runnable mCountdownRunnable = new Runnable() { public void run() { - updatePinTimer(-1); + if (updatePinTimer(-1)) { + // If we are no longer counting down, clear the message. + mPinErrorMessage.setVisibility(View.INVISIBLE); + } } }; + + @Override + public void onClick(View v) { + if (v == mOkButton) { + performPositiveButtonAction(); + } else if (v == mCancelButton) { + setResult(RESULT_CANCELED); + finish(); + } + } } diff --git a/core/java/com/android/internal/app/RestrictionsPinSetupActivity.java b/core/java/com/android/internal/app/RestrictionsPinSetupActivity.java index 1d09292..f7fc6c6 100644 --- a/core/java/com/android/internal/app/RestrictionsPinSetupActivity.java +++ b/core/java/com/android/internal/app/RestrictionsPinSetupActivity.java @@ -16,9 +16,7 @@ package com.android.internal.app; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.os.UserManager; import android.text.Editable; import android.text.TextUtils; @@ -44,17 +42,13 @@ public class RestrictionsPinSetupActivity extends RestrictionsPinActivity { ap.mTitle = getString(R.string.restr_pin_enter_pin); ap.mPositiveButtonText = getString(R.string.ok); ap.mNegativeButtonText = getString(R.string.cancel); - ap.mPositiveButtonListener = this; - ap.mNegativeButtonListener = this; LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); ap.mView = inflater.inflate(R.layout.restrictions_pin_setup, null); mPinText = (EditText) ap.mView.findViewById(R.id.pin_text); - mPinMessage = (TextView) ap.mView.findViewById(R.id.pin_message); mNewPinText = (EditText) ap.mView.findViewById(R.id.pin_new_text); mConfirmPinText = (EditText) ap.mView.findViewById(R.id.pin_confirm_text); - mPinErrorMessage = (TextView) ap.mView.findViewById(R.id.pin_error_message); mNewPinText.addTextChangedListener(this); mConfirmPinText.addTextChangedListener(this); @@ -72,19 +66,7 @@ public class RestrictionsPinSetupActivity extends RestrictionsPinActivity { return false; } - private void setPositiveButtonState(boolean enabled) { - mAlert.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enabled); - } - - public void onClick(DialogInterface dialog, int which) { - setResult(RESULT_CANCELED); - if (which == AlertDialog.BUTTON_POSITIVE) { - performPositiveButtonAction(); - } else if (which == AlertDialog.BUTTON_NEGATIVE) { - finish(); - } - } - + @Override protected void performPositiveButtonAction() { if (mHasRestrictionsPin) { int result = mUserManager.checkRestrictionsPin(mPinText.getText().toString()); @@ -115,7 +97,6 @@ public class RestrictionsPinSetupActivity extends RestrictionsPinActivity { boolean showError = !TextUtils.isEmpty(pin1) && !TextUtils.isEmpty(pin2); // TODO: Check recovery email address as well setPositiveButtonState(match); - mPinErrorMessage.setVisibility((match || !showError) ? View.INVISIBLE : View.VISIBLE); } @Override |
